1. 程式人生 > 實用技巧 >泛型 & 反射

泛型 & 反射

參考文章:

https://cloud.tencent.com/developer/article/1033693什麼是反射?反射機制的應用場景有哪些?10 道 Java 泛型面試題(強推)Java 泛型進階(強推)Java中陣列為什麼不支援泛型,集合卻支援泛型?

反射常見面試題

簡單介紹一下反射

思路:概述 --- 獲取位元組碼物件的方式 -- 方法的使用 --- 優缺點 -- 使用場景

概述

JAVA反射機制是在執行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;利用class 位元組碼物件,可以使得對於任意一個物件,都能夠呼叫它的任意一個方法和屬性;這種動態獲取的資訊以及動態呼叫物件的方法的功能稱為java語言的反射機制。

獲取 Class 位元組碼物件的方法

  1. Object類的getClass()方法,判斷兩個物件是否是同一個位元組碼檔案
  2. 靜態屬性class,鎖物件
  3. Class類中靜態方法forName(),讀取配置檔案, 類名必須是完整類名

方法的使用:

通過呼叫相應的get()方法,我們可以獲取到相應的field,Method 和 Constructor ,如果是這些欄位或方法不是公有的,那麼還需要用setAccessible() 來修改訪問許可權,最後用某個例項物件作為引數即可呼叫我們想要執行的方法或修改某個欄位。

反射機制優缺點

優點:可擴充套件性高,在執行時進行型別的判斷,動態載入類,可以提高程式碼靈活度。缺點:1、因為
反射涉及了動態型別的解析,所以 JVM 無法對這些程式碼進行優化。因此,反射操作的效率要比那些非反射操作低得多。2、安全問題,讓我們可以動態操作改變類的屬性同時也增加了類的安全隱患。比如利用反射給型別為 Integer 的List 新增Sting 元素

使用場景:

  1. 動態代理
  2. 我們在使用JDBC連線資料庫時使用Class.forName()通過反射載入資料庫的驅動程式;
  3. Spring框架也用到很多反射機制,最經典的就是xml的配置模式。Spring 通過 XML 配置模式裝載 Bean 的過程:1) 將程式內所有 XML 或 Properties 配置檔案載入入記憶體中; 2)Java類裡面解析xml或properties裡面的內容,得到對應實體類的位元組碼字串以及相關的屬性資訊; 3)使用反射機制,根據這個字串獲得某個類的Class例項; 4)動態配置例項的屬性。
  4. 物件的反序列化

靜態編譯和動態編譯

  • 靜態編譯:在編譯時確定型別,繫結物件
  • 動態編譯:執行時確定型別,繫結物件

泛型常見面試題

1、Java中的泛型是什麼 ? 使用泛型的好處是什麼?

泛型就是在方法中或類中或集合中可以傳入不同的型別,提高類,集合,方法的通用性。沒有泛型的情況的下,通過對型別Object的引用來實現引數的“任意化”,但是在實際的使用中,會有型別轉換異常的問題。所以Java提供了泛型來解決這個安全問題。它提供了編譯期的型別安全,確保你只能把正確型別的物件放入集合中,避免了在執行時出現ClassCastException。

沒有泛型的情況的下,通過對型別Object的引用來實現引數的“任意化”,存資料的時候因為型別是Object, 所以任何物件都能存進去,但是使用的時候對取出來的元素進行強制型別轉換,“任意化”帶來的缺點是要做 顯式的強制型別轉換,而這種轉換是要求開發者對實際引數型別可以預知的情況下進行的。對於強制型別轉換錯誤的情況 ,編譯器可能不提示錯誤,在執行的時候才出現異常,這是一個安全隱患

2、Java的泛型是如何工作的 ? 什麼是型別擦除 ?

Java 中的泛型是偽泛型,為什麼說它是偽泛型呢?因為它採用的是型別擦除的方式來實現的。編譯器在編譯時擦除了所有型別相關的資訊,所以在執行時不存在任何型別相關的資訊。我們無法在執行時訪問到型別引數,因為編譯器已經把泛型型別轉換成了原始型別。Java 中的泛型僅用於編譯器做型別檢查,就是保證我們程式碼寫的不會出現型別錯誤。List<String>,List<Integer>編譯之後可以理解成統統變成List<Object>了,而之所以在使用內部元素的時候察覺不到泛型被擦除了,覺得這個ArrayList 正是按我們設定的泛型來使用元素,是因為編譯的時候凡是使用到了泛型的地方,編譯器自動加上了型別強轉,我們也察覺不到泛型被擦除了。又因為加上了強轉,所以擦除掉泛型也不會出問題。證明型別擦除的方式有三種,一種是定義兩個陣列列表,一個是引數為 String 的 ArrayList, l另一個是引數為 Integer 的ArrayList , 對這兩個物件分別呼叫getClass()方法然後做等等於的運算,會發現結果是返回 true

        ArrayList<String> arrayList1=new ArrayList<String>();
        arrayList1.add("abc");
        ArrayList<Integer> arrayList2=new ArrayList<Integer>();
        arrayList2.add(123);
        System.out.println(arrayList1.getClass()==arrayList2.getClass());

第二種是用反射獲取一個 泛型型別為 Integer 的 ArrayList 的 add 方法,給這add 方法傳參 Object.class, 然後呼叫 invoke() 存入一個字串,會發現存入成功,這也說明泛型在執行時不存在的。第三種是通過反編譯程式碼來證明

3、什麼是泛型中的限定萬用字元和非限定萬用字元 ?

  這是另一個非常流行的Java泛型面試題。限定萬用字元對型別進行了限制。有兩種限定萬用字元,一種是<? extends T>它通過確保型別必須是T的子類來設定型別的上界,另一種是<? super T>它通過確保型別必須是T的父類來設定型別的下界。泛型型別必須用限定內的型別來進行初始化,否則會導致編譯錯誤。另一方面<?>表示了非限定萬用字元,因為<?>可以用任意型別來替代。更多資訊請參閱我的文章泛型中限定萬用字元和非限定萬用字元之間的區別。

4、List<? extends T>和List <? super T>之間有什麼區別 ?

  這和上一個面試題有聯絡,有時面試官會用這個問題來評估你對泛型的理解,而不是直接問你什麼是限定萬用字元和非限定萬用字元。這兩個List的宣告都是限定萬用字元的例子,List<? extends T>可以接受任何繼承自T的型別的List,而List<? super T>可以接受任何T的父類構成的List。例如List<? extends Number>可以接受List<Integer>或List<Float>。在本段出現的連線中可以找到更多資訊。

5、如何編寫一個泛型方法,讓它能接受泛型引數並返回泛型型別?

  編寫泛型方法並不困難,你需要用泛型型別來替代原始型別,比如使用T, E or K,V等被廣泛認可的型別佔位符。泛型方法的例子請參閱Java集合類框架。最簡單的情況下,一個泛型方法可能會像這樣:

public V put(K key, V value) {
	return cache.put(key, value);
}

6、Java中如何使用泛型編寫帶有引數的類?

public class 類名<泛型型別1,…>

  這是上一道面試題的延伸。面試官可能會要求你用泛型編寫一個型別安全的類,而不是編寫一個泛型方法。關鍵仍然是使用泛型型別來代替原始型別,而且要使用JDK中採用的標準佔位符。

7、編寫一段泛型程式來實現LRU快取?

accessOrder 決定了順序,預設為 false,此時維護的是插入順序
LinkedHashMap 最重要的是以下用於維護順序的函式,它們會在 put、get 等方法中呼叫。
afterNodeAccess()
當一個節點被訪問時,如果 accessOrder 為 true,則會將該節點移到連結串列尾部。也就是說指定為 LRU 順序之後,在每次訪問一個節點時,會將這個節點移到連結串列尾部,保證連結串列尾部是最近訪問的節點,那麼連結串列首部就是最近最久未使用的節點。(類似於維護一個棧,每次把最新訪問過的資料放到棧底,最近最久未使用的元素始終呈現在棧頂)
afterNodeInsertion()
在 put 等操作之後執行,當 removeEldestEntry() 方法返回 true 時會移除最晚的節點,也就是連結串列首部節點 first。以下是使用 LinkedHashMap 實現的一個 LRU 快取:
  • 設定最大快取空間 MAX_ENTRIES 為 3;
  • 使用 LinkedHashMap 的建構函式將 accessOrder 設定為 true,開啟 LRU 順序;
  • 覆蓋 removeEldestEntry() 方法實現,在節點多於 MAX_ENTRIES 就會將最近最久未使用的資料移除。
class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private static final int MAX_ENTRIES = 3;
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
    }
    LRUCache() {
        super(MAX_ENTRIES, 0.75f, true);
    }
}
public static void main(String[] args) {
        LRUCache cache = new LRUCache();
        cache.put(1, "a");
        System.out.println(cache.keySet());
        cache.put(2, "b");
        System.out.println(cache.keySet());
        cache.put(3, "c");
        System.out.println(cache.keySet());
        cache.put(4, "d");
        System.out.println(cache.keySet());
        cache.get(1);
        System.out.println(cache.keySet());
        cache.put(5, "e");
        System.out.println(cache.keySet());
    }
[1]
[1, 2]
[1, 2, 3]
[1, 2, 3, 4]
[2, 3, 4, 1]
[3, 4, 1, 5]

8、可以把List<String>傳遞給一個接受List<Object>引數的方法嗎?

不行

  這樣做的話會導致編譯錯誤。因為List<Object>可以儲存任何型別的物件包括String, Integer等等,而List<String>卻只能用來儲存Strings。 

       List<Object> objectList;
       List<String> stringList;
       objectList = stringList;  //compilation error incompatible types

9、Array中可以用泛型嗎?

陣列就允許將子類陣列引用賦值給父類陣列,而我們的泛型要求不能這麼幹,所以導致無法在編譯期檢查陣列的型別安全問題,這個與泛型的初衷相違背,導致陣列無法使用泛型。所以泛型一般用在集合中。如果非要泛型陣列的話,可以定義一個帶泛型的類,在這個類中定義一個同類型的陣列,

10、泛型要注意哪些問題:

1. 泛型型別不能顯式地運用在執行時型別的操作當中,例如:轉型、instanceofnew。因為在執行時,所有引數的型別資訊都丟失了。使用帶有泛型型別引數的轉型或者instanceof不會有任何效果。因為他們在執行時都會被擦除到上邊界上。所以轉型的時候用的型別實際上是上邊界對應的型別。
不能new T()的原因有兩個,一是因為擦除,不能確定型別;而是無法確定T是否包含無參建構函式。
2.任何基本型別都不能作為型別引數3. 過載的兩個函式如果僅僅是引數型別不同,這兩個引數型別不能都是泛型型別,否則可能會報錯 both methods have same erasure。那是因為由於擦除的原因,過載方法將產生相同的型別簽名。避免這種問題的方法就是換個方法名。
class UseList{
    <W> void f(W  v){}
    <T> void f(T v){};
}

11、 <T extends Comparable<? super T>> 與 <T extends Comparable<T>>的區別

如果用的是 <T extends Comparable<? super T>> 那麼只要實參型別或實參型別的父類實現了 Comparable 介面,那 這個實參型別就可以用這個方法,

而如果 用的是 <T extends Comparable<T>> 宣告泛型的話,只有當實參型別實現了 Comparable 介面,他才可以使用這個方法,就算它的父類實現這個方法也不行。

<T extends Comparable<? Super T>> 相比於 <T extends Comparable<T>> 有更好的通用性。

// 實參必須是Number的子類,且實參或者實參的父類必須實現了 Comparable介面
    private static <T extends Number & Comparable<T>> T min(T[] values){
        if(values == null || values.length == 0){
            return null;
        }
        T min = values[0];
        for(int i = 1; i < values.length; i++){
            if(min.compareTo(values[i]) > 0){
                min = values[i];
            }
        }

        return min;
    }

小插曲:

extends關鍵字宣告中,有兩個要注意的地方:

  1. 類必須要寫在介面之前;
  2. 只能設定一個類做邊界,其它均為介面。