泛型是Java SE 1.5的新特性,泛型的本質是參數化類型,也就是說所操作的數據類型被指定為一個參數。這種參數類型可以用在類、接口和方法的創建中,分別稱為泛型類、泛型接口、泛型方法。 Java語言引入泛型的好處是安全簡單。
在Java SE 1.5之前,沒有泛型的情況的下,通過對類型Object的引用來實現參數的“任意化”,“任意化”帶來的缺點是要做顯式的強制類型轉換,而這種轉換是要求開發者對實際參數類型可以預知的情況下進行的。對於強制類型轉換錯誤的情況,編譯器可能不提示錯誤,在運行的時候才出現異常,這是一個安全隱患。
泛型的好處是在編譯的時候檢查類型安全,並且所有的強制轉換都是自動和隱式的,提高代碼的重用率。
1、泛型的類型參數只能是類類型(包括自定義類),不能是簡單類型。
2、同一種泛型可以對應多個版本(因為參數類型是不確定的),不同版本的泛型類實例是不兼容的。
3、泛型的類型參數可以有多個。
4、泛型的參數類型可以使用extends語句,例如<T extends superclass>。習慣上稱為“有界類型”。
5、泛型的參數類型還可以是通配符類型。例如Class<?> classType = Class.forName("java.lang.String");
泛型擦除以及相關的概念
Java中的泛型基本上都是在編譯器這個層次來實現的。在生成的Java字節碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數,會在編譯器在編譯的時候去掉。這個過程就稱為類型擦除。
類型擦除引起的問題及解決方法
1、先檢查,在編譯,以及檢查編譯的對象和引用傳遞的問題
2、自動類型轉換
3、類型擦除與多態的沖突和解決方法
4、泛型類型變量不能是基本數據類型
5、運行時類型查詢
6、異常中使用泛型的問題
7、數組(這個不屬於類型擦除引起的問題)
9、類型擦除后的沖突
10、泛型在靜態方法和靜態類中的問題
1. 問:Java 的泛型是什么?有什么好處和優點?JDK 不同版本的泛型有什么區別?
答:泛型是 Java SE 1.5 的新特性,泛型的本質是參數化類型,這種參數類型可以用在類、接口和方法的創建中,分別稱為泛型類、泛型接口、泛型方法。在 Java SE 1.5 之前沒有泛型的情況的下只能通過對類型 Object 的引用來實現參數的任意化,其帶來的缺點是要做顯式強制類型轉換,而這種強制轉換編譯期是不做檢查的,容易把問題留到運行時,所以 泛型的好處是在編譯時檢查類型安全,並且所有的強制轉換都是自動和隱式的,提高了代碼的重用率,避免在運行時出現 ClassCastException。
JDK 1.5 引入了泛型來允許強類型在編譯時進行類型檢查;JDK 1.7 泛型實例化類型具備了自動推斷能力,譬如 List<String> list = new ArrayList<String>(); 可以寫成 List<String> llist = new ArrayList<>(); 了,JDK 具備自動推斷能力。下面幾種寫法可以說是不同版本的兼容性了:
//JDK 1.5 推薦使用的寫法 List<String> list = new ArrayList<String>(); //JDK 1.7 推薦使用的寫法 List<String> llist = new ArrayList<>(); //可以使用,但不推薦,是為了兼容老版本,IDE 會提示警告,可以通過注解屏蔽警告 List<String> list = new ArrayList(); //可以使用,但不推薦,是為了兼容老版本,IDE 會提示警告,可以通過注解屏蔽警告 List list = new ArrayList<String>();
2. 問:Java 泛型是如何工作的?什么是類型擦除?
答:泛型是通過類型擦除來實現的,編譯器在編譯時擦除了所有泛型類型相關的信息,所以在運行時不存在任何泛型類型相關的信息,譬如 List<Integer> 在運行時僅用一個 List 來表示,這樣做的目的是為了和 Java 1.5 之前版本進行兼容。泛型擦除具體來說就是在編譯成字節碼時首先進行類型檢查,接着進行類型擦除(即所有類型參數都用他們的限定類型替換,包括類、變量和方法),接着如果類型擦除和多態性發生沖突時就在子類中生成橋方法解決,接着如果調用泛型方法的返回類型被擦除則在調用該方法時插入強制類型轉換。
3. 問:Java 泛型類、泛型接口、泛型方法有什么區別?
答:泛型類是在實例化類的對象時才能確定的類型,其定義譬如 class Test<T> {},在實例化該類時必須指明泛型 T 的具體類型。
泛型接口與泛型類一樣,其定義譬如 interface Generator<E> { E dunc(E e); }。
泛型方法所在的類可以是泛型類也可以是非泛型類,是否擁有泛型方法與所在的類無關,所以在我們應用中應該盡可能使用泛型方法,不要放大作用空間,尤其是在 static 方法時 static 方法無法訪問泛型類的類型參數,所以更應該使用泛型的 static 方法(聲明泛型一定要寫在 static 后返回值類型前)。泛型方法的定義譬如 <T> void func(T val) {}。
4. 問:Java 如何優雅的實現元組?
答:元組其實是關系數據庫中的一個學術名詞,一條記錄就是一個元組,一個表就是一個關系,紀錄組成表,元組生成關系,這就是關系數據庫的核心理念。很多語言天生支持元組,譬如 Python 等,在語法本身支持元組的語言中元組是用括號表示的,如 (int, bool, string) 就是一個三元組類型,不過在 Java、C 等語言中就比較坑爹,語言語法本身不具備這個特性,所以在 Java 中我們如果想優雅實現元組就可以借助泛型類實現,如下是一個三元組類型的實現:
Triplet<A, B, C> { private A a; private B a; private C a; public Triplet(A a, B b, C c) { this.a = a; this.b = b; this.c = c; } }
5. 問:下面程序塊的運行結果是什么,為什么?
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<String>().getClass();
System.out.println(c1 == c2);
答:上面代碼段結果為 true,解釋如下。
因為 load 的是同一個 class 文件,存在 ArrayList.class 文件但是不存在 ArrayList<String>.class 文件,即便是通過 class.getTypeParameters() 方法獲取類型信息也只能獲取到 [T] 一樣的泛型參數占位符。泛型是通過擦除來實現的,所以編譯后任何具體的泛型類型都被擦除了(替換為非泛型上邊界,如果沒有指定邊界則為 Object 類型),泛型類型只有在靜態類型檢查期間才出現,上面都被擦除成了 ArrayList 類型,所以運行時加載的是同一個 class 文件。
6. 問:為什么 Java 泛型要通過擦除來實現?擦除有什么壞處或者說代價?
答:可以說 Java 泛型的存在就是一個不得已的妥協,正因為這種妥協導致了 Java 泛型的混亂,甚至說是 JDK 泛型設計的失敗。Java 之所以要通過擦除來實現泛型機制其實是為了兼容性考慮,只有這樣才能讓非泛化代碼到泛化代碼的轉變過程建立在不破壞現有類庫的實現上。正是因為這種兼容也帶來了一些代價,譬如泛型不能顯式地引用運行時類型的操作之中(如向上向下轉型、instanceof 操作等),因為所有關於參數的信息都丟失了,所以任何時候使用泛型都要提醒自己背后的真實擦除類型到底是什么;此外擦除和兼容性導致了使用泛型並不是強制的(如 List<String> list = new ArrayList(); 等寫法);其次擦除會導致我們在編寫代碼時十分謹慎(如不想被擦除為 Object 類型時不要忘了添加上邊界操作等)。
7. 問:下面三個 funcX 方法有問題嗎,為什么?
class Product<T> { private void func1(Object arg) { if (arg instanceof T) {} } private void func2() { T var = new T(); } private void func3() { T[] vars = new T[3]; } }
答:func1、func2、func3 三個方法均無法編譯通過。
因為泛型擦除丟失了在泛型代碼中執行某些操作的能力,任何在運行時需要知道確切類型信息的操作都將無法工作。
8. 問:下面代碼段有問題嗎,運行效果是什么,為什么?
ArrayList<Integer> arrayList = new ArrayList<Integer>();
arrayList.add(1);
arrayList.getClass().getMethod("add", Object.class).invoke(arrayList, "abc");
for (int i=0; i<arrayList.size(); i++) {
System.out.println(arrayList.get(i));
}
答:由於在程序中定義的 ArrayList 泛型類型實例化為 Integer 的對象,如果直接調用 add 方法則只能存儲整形數據,不過當我們利用反射調用 add 方法時就可以存儲字符串,因為 Integer 泛型實例在編譯之后被擦除了,只保留了原始類型 Object,所以自然可以插入。
9. 問:請比較深入的談談你對 Java 泛型擦除的理解和帶來的問題認識?
答:Java 的泛型是偽泛型,因為在編譯期間所有的泛型信息都會被擦除掉,譬如 List<Integer> 在運行時僅用一個 List 來表示(所以我們可以通過反射 add 方法來向 Integer 的泛型列表添加字符串,因為編譯后都成了 Object),這樣做的目的是為了和 Java 1.5 之前版本進行兼容。泛型擦除具體來說就是在編譯成字節碼時首先進行類型檢查,接着進行類型擦除(即所有類型參數都用他們的限定類型替換,包括類、變量和方法,如果類型變量有限定則原始類型就用第一個邊界的類型來替換,譬如 class Prd<T extends Comparable & Serializable> {} 的原始類型就是 Comparable),接着如果類型擦除和多態性發生沖突時就在子類中生成橋方法解決,接着如果調用泛型方法的返回類型被擦除則在調用該方法時插入強制類型轉換。
先檢查再擦除的類型檢查是針對引用的,用引用調用泛型方法就會對這個引用調用的方法進行類型檢測而無關它真正引用的對象。可以說這是為了兼容帶來的問題,如下:
ArrayList<String> arrayList1 = new ArrayList<String>(); arrayList1.add("123"); //編譯通過 arrayList1.add(123); //編譯錯誤 String str1 = arrayList1.get(0); //返回類型是 String ArrayList<String> arrayList2 = new ArrayList(); arrayList2.add("123"); //編譯通過 arrayList2.add(123); //編譯錯誤 String object2 = arrayList2.get(0); //返回類型是 String ArrayList arrayList3 = new ArrayList<String>(); arrayList3.add("123"); //編譯通過 arrayList3.add(123); //編譯通過 Object object3 = arrayList3.get(0); //返回類型是 Object
所以說擦除前的類型檢查是針對引用的,用這個引用調用泛型方法就會對這個引用調用的方法進行類型檢測而無關它真正引用的對象。
先檢查再擦除帶來的另一個問題就是泛型中參數化類型無法支持繼承關系,因為泛型的設計初衷就是為了解決 Object 類型轉換的弊端而存在,如果泛型中參數化類型支持繼承操作就違背了設計的初衷而繼續回到原始的 Object 類型轉換弊端。也同樣可以說這是為了兼容帶來的問題,如下:
ArrayList<Object> arrayList1 = new ArrayList<Object>(); arrayList1.add(new Object()); arrayList1.add(new Object()); ArrayList<String> arrayList2 = arrayList1; //編譯錯誤 ArrayList<String> arrayList3 = new ArrayList<String>(); arrayList3.add("abc"); arrayList3.add(new String()); ArrayList<Object> arrayList4 = arrayList3; //編譯錯誤 ArrayList<String> arrayList5 = new ArrayList<Object>(); //編譯錯誤 ArrayList<Object> arrayList6 = new ArrayList<String>(); //編譯錯誤
之所以這樣我們可以從反面來論證,假設編譯不報錯則當通過 arrayList2 調用 get() 方法取值時返回的是 String 類型的對象(因為類型檢測是根據引用來決定的),而實際上存放的是 Object 類型的對象,這樣 get 出來就會 ClassCastException 了,所以這違背了泛型的初衷。對於 arrayList4 同樣假設編譯不報錯,當調用 arrayList4 的 get() 方法取出來的 String 變成了 Object 雖然不會出現 ClassCastException,但是依然沒有意義啊,泛型出現的原因就是為了解決類型轉換的問題,其次如果我們通過 arrayList4 的 add() 方法繼續添加對象則可以添加任意類型對象實例,這就會導致我們 get() 時更加懵逼不知道加的是什么類型了,所以怎么說都是個死循環。
擦除帶來的另一個問題就是泛型與多態的沖突,其通過子類中生成橋方法解決了多態沖突問題,這個問題的驗證也很簡單,可以通過下面的例子說明:
class Creater<T> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } } class StringCreater extends Creater<String> { @Override public void setValue(String value) { super.setValue(value); } @Override public String getValue() { return super.getValue(); } } StringCreater stringCreater =new StringCreater(); stringCreater.setValue("abc"); stringCreater.setValue(new Object()); //編譯錯誤
上面代碼段的運行情況很詫異吧,按理來說 Creater 類被編譯擦除后 setValue 方法的參數應該是 Object 類型了,子類 StringCreater 的 setValue 方法參數類型為 String,看起來父子類的這組方法應該是重載關系,所以調用子類的 setValue 方法添加字符串和 Object 類型參數應該都是合法才對,然而從編譯來看子類根本沒有繼承自父類參數為 Object 類型的 setValue 方法,所以說子類的 setValue 方法是對父類的重寫而不是重載(從子類添加 @Override 注解沒報錯也能說明是重寫關系)。關於出現上面現象的原理其實我們通過 javap 看下兩個類編譯后的本質即可:
class StringCreater extends Creater<java.lang.String> { ...... public void setValue(java.lang.String); //重寫的setValue方法 Code: 0: aload_0 1: aload_1 2: invokespecial #2 // Method Creater.setValue:(Ljava/lang/Object;)V 5: return
public java.lang.String getValue(); //重寫的getValue方法 Code: 0: aload_0 1: invokespecial #3 // Method Creater.getValue:()Ljava/lang/Object; 4: checkcast #4 // class java/lang/String 7: areturn public void setValue(java.lang.Object); //編譯器生成的橋方法,調用我們重寫的setValue方法 Code: 0: aload_0 1: aload_1 2: checkcast #4 // class java/lang/String 5: invokevirtual #5 // Method setValue:(Ljava/lang/String;)V 8: return public java.lang.Object getValue(); //編譯器生成的橋方法,調用我們重寫的getValue方法 Code: 0: aload_0 1: invokevirtual #6 // Method getValue:()Ljava/lang/String; 4: areturn }
通過編譯后的字節碼我們可以看到 Creater 泛型類在編譯后類型被擦除為 Object,而我們子類的本意是進行重寫實現多態,可類型擦除后子類就和多態產生了沖突,所以編譯后的字節碼里就出現了橋方法來實現多態。可以看到橋方法的參數類型都是 Object,也就是說子類中真正覆蓋父類方法的是橋方法,而子類 String 參數 setValue、getValue 方法上的 @Oveerride 注解只是個假象,橋方法的內部實現是直接調用了我們自己重寫的那兩個方法;不過上面的 setValue 方法是為了解決類型擦除與多態之間的沖突生成的橋方法,而 getValue 是一種協變,之所以子類中 Object getValue() 和 String getValue() 方法可以同時存在是虛擬機內部的一種區分(我們自己寫的代碼是不允許這樣的),因為虛擬機內部是通過參數類型和返回類型來確定一個方法簽名的,所以編譯器為了實現泛型的多態允許自己做這個看起來不合法的實現,實質還是交給了虛擬機去區別。
先檢查再擦除帶來的另一個問題就是泛型讀取時會進行自動類型轉換問題,所以如果調用泛型方法的返回類型被擦除則在調用該方法時插入強制類型轉換。
關於這個可以通過 javap 去查看使用 List 的 add、get 方法后的字節碼指令,你會發現 checkcast 指令不是在 get 方法里面強轉的(雖然 get 方法里面返回值在代碼里面是被轉換成了 T,實際編譯擦除了),而是在調用處強轉的。
擦除帶來的另一個問題是泛型類型參數不能是基本類型,比如 ArrayList<int> 是不合法的,只有 ArrayList<Integer> 是合法的,因為當類型擦除后 ArrayList 的原始類型是 Object,而 Object 是引用類型而不是基本類型。
擦除帶來的另一個問題是無法進行具體泛型參數類型的運行時類型檢查,譬如 arrayList instanceof ArrayList<String> 是非法的,Java 對於泛型運行時檢查的支持僅限於 arrayList instanceof ArrayList<?> 方式。
擦除帶來的另一個問題是我們不能拋出也不能捕獲泛型類的對象,因為異常是在運行時捕獲和拋出的,而在編譯時泛型信息會被擦除掉,擦除后兩個 catch 會變成一樣的東西。也不能在 catch 子句中使用泛型變量,因為泛型信息在編譯時已經替換為原始類型(譬如 catch(T) 在限定符情況下會變為原始類型 Throwable),如果可以在 catch 子句中使用則違背了異常的捕獲優先級順序。
這一個題目能說明白和全面泛型基本就掌握百分之九十了。
10. 問:為什么 Java 的泛型數組不能采用具體的泛型類型進行初始化?
答:這個問題可以通過一個例子來說明。
List<String>[] lsa = new List<String>[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error: ClassCastException.
由於 JVM 泛型的擦除機制,所以上面代碼可以給 oa[1] 賦值為 ArrayList 也不會出現異常,但是在取出數據的時候卻要做一次類型轉換,所以就會出現 ClassCastException,如果可以進行泛型數組的聲明則上面說的這種情況在編譯期不會出現任何警告和錯誤,只有在運行時才會出錯,但是泛型的出現就是為了消滅 ClassCastException,所以如果 Java 支持泛型數組初始化操作就是搬起石頭砸自己的腳。而對於下面的代碼來說是成立的:
List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Correct. Integer i = (Integer) lsa[1].get(0); // OK
所以說采用通配符的方式初始化泛型數組是允許的,因為對於通配符的方式最后取出數據是要做顯式類型轉換的,符合預期邏輯。綜述就是說 Java 的泛型數組初始化時數組類型不能是具體的泛型類型,只能是通配符的形式,因為具體類型會導致可存入任意類型對象,在取出時會發生類型轉換異常,會與泛型的設計思想沖突,而通配符形式本來就需要自己強轉,符合預期。
關於這道題的答案其 Oracle 官方文檔給出了原因:https://docs.oracle.com/javase/tutorial/extra/generics/fineprint.html
11. 問:下面語句哪些是有問題,哪些沒有問題?
List<String>[] list1 = new ArrayList<String>[10]; //編譯錯誤,非法創建 List<String>[] list2 = new ArrayList<?>[10]; //編譯錯誤,需要強轉類型 List<String>[] list3 = (List<String>[]) new ArrayList<?>[10]; //OK,但是會有警告 List<?>[] list4 = new ArrayList<String>[10]; //編譯錯誤,非法創建 List<?>[] list5 = new ArrayList<?>[10]; //OK List<String>[] list6 = new ArrayList[10]; //OK,但是會有警告
答:上面每個語句的問題注釋部分已經闡明了,因為在 Java 中是不能創建一個確切的泛型類型的數組的,除非是采用通配符的方式且要做顯式類型轉換才可以。
12. 問:如何正確的初始化泛型數組實例?
答:這個無論我們通過 new ArrayList[10] 的形式還是通過泛型通配符的形式初始化泛型數組實例都是存在警告的,也就是說僅僅語法合格,運行時潛在的風險需要我們自己來承擔,因此那些方式初始化泛型數組都不是最優雅的方式,我們在使用到泛型數組的場景下應該盡量使用列表集合替換,此外也可以通過使用 java.lang.reflect.Array.newInstance(Class<T> componentType, int length) 方法來創建一個具有指定類型和維度的數組,如下:
public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); } public void put(int index, T item) { array[index] = item; } public T get(int index) { return array[index]; }
public T[] create() { return array; } }
ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create();
所以使用反射來初始化泛型數組算是優雅實現,因為泛型類型 T 在運行時才能被確定下來,我們能創建泛型數組也必然是在 Java 運行時想辦法,而運行時能起作用的技術最好的就是反射了。
13. 問:Java 泛型對象能實例化 T t = new T() 嗎,為什么?
答:不能,因為在 Java 編譯期沒法確定泛型參數化類型,也就找不到對應的類字節碼文件,所以自然就不行了,此外由於 T 被擦除為 Object,如果可以 new T() 則就變成了 new Object(),失去了本意。如果要實例化一個泛型 T 則可以通過反射實現(實例化泛型數組也類似),如下:
static <T> T newTclass(Class<T> clazz) throws InstantiationException, IllegalAccessException { T obj = clazz.newInstance(); return obj; }
原因就不解釋了,姑且可以認為和上面泛型數組創建一個原因,至於本質深層次原因請關注后邊關於泛型反射面試題的推送。
14. 問:什么是 Java 泛型中的限定通配符和非限定通配符?有什么區別?
答:限定通配符對類型進行限制,泛型中有兩種限定通配符,一種是 <? extends T> 來保證泛型類型必須是 T 的子類來設定泛型類型的上邊界,另一種是 <? super T> 來保證泛型類型必須是 T 的父類來設定類型的下邊界,泛型類型必須用限定內的類型來進行初始化,否則會導致編譯錯誤。非限定通配符 <?> 表示可以用任意泛型類型來替代,可以在某種意義上來說是泛型向上轉型的語法格式,因為 List<String> 與 List<Object> 不存在繼承關系。
15. 問:簡單說說 List<Object> 與 List 原始類型之間的區別?
答:主要區別有兩點。
-
原始類型和帶泛型參數類型 <Object> 之間的主要區別是在編譯時編譯器不會對原始類型進行類型安全檢查,卻會對帶參數的類型進行檢查,通過使用 Object 作為類型可以告知編譯器該方法可以接受任何類型的對象(比如 String 或 Integer)。
-
我們可以把任何帶參數的類型傳遞給原始類型 List,但卻不能把 List<String> 傳遞給接受 List<Object> 的方法,因為會產生編譯錯誤。
16. 問:簡單說說 List<Object> 與 List<?> 類型之間的區別?
答:這道題跟上一道題看起來很像,實質上卻完全不同。List<?> 是一個未知類型的 List,而 List<Object> 其實是任意類型的 List,我們可以把 List<String>、List<Integer> 賦值給 List<?>,卻不能把 List<String> 賦值給 List<Object>。譬如:
List<?> listOfAnyType; List<Object> listOfObject = new ArrayList<Object>(); List<String> listOfString = new ArrayList<String>(); List<Integer> listOfInteger = new ArrayList<Integer>(); listOfAnyType = listOfString; //legal listOfAnyType = listOfInteger; //legal listOfObjectType = (List<Object>) listOfString; //compiler error
所以通配符形式都可以用類型參數的形式來替代,通配符能做的用類型參數都能做。 通配符形式可以減少類型參數,形式上往往更為簡單,可讀性也更好,所以能用通配符的就用通配符。 如果類型參數之間有依賴關系或者返回值依賴類型參數或者需要寫操作則只能用類型參數。
17. 問:List<? extends T>和List <? super T>之間有什么區別?
答:有時面試官會用這個問題來評估你對泛型的理解,而不是直接問你什么是限定通配符和非限定通配符,這兩個 List 的聲明都是限定通配符的例子,List<? extends T> 可以接受任何繼承自 T 的類型的 List,而 List<? super T> 可以接受任何 T 的父類構成的 List。例如 List<? extends Number> 可以接受 List<Integer> 或 List<Float>。Java 容器類的實現中有很多這種用法,比如 Collections 中就有如下一些方法:
public static <T extends Comparable<? super T>> void sort(List<T> list) public static <T> void sort(List<T> list, Comparator<? super T> c) public static <T> void copy(List<? super T> dest, List<? extends T> src) public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp)
18. 問:說說 <T extends E> 和 <? extends E> 有什么區別?
答:它們用的地方不一樣,<T extends E> 用於定義類型參數,聲明了一個類型參數 T,可放在泛型類定義中類名后面、接口后面、泛型方法返回值前面。 <? extends E> 用於實例化類型參數,用於實例化泛型變量中的類型參數,只是這個具體類型是未知的,只知道它是 E 或 E 的某個子類型。雖然它們不一樣,但兩種寫法經常可以達到相同的目的,譬如:
public void addAll(Bean<? extends E> c) public <T extends E> void addAll(Bean<T> c)
19. 問:說說 List<String> 與 List<Object> 的關系和區別?
答:這兩個東西沒有關系只有區別。
因為也許很多人認為 String 是 Object 的子類,所以 List<String> 應當可以用在需要 List<Object> 的地方,但是事實並非如此,泛型類型之間不具備泛型參數類型的繼承關系,所以 List<String> 和 List<Object> 沒有關系,無法轉換。
20. 問:下面兩個代碼片段有問題嗎,為什么?
//Part 1
List<Object> obj = new ArrayList<Long>(); obj.add("I love Android!");
//Part 2
Object[] objArray = new Long[1]; objArray[0] = "I love Android!";
答:上面 Part 1 編譯出錯,Part 2 編譯 OK,運行出錯。
因為 List<Object> 和 ArrayList<Long> 沒有繼承關系,而 Java 的數組是在運行時類型檢查的。
21. 問:如何把 int 值放入 ArrayList<String> list = new ArrayList<String>(); 的 list 列表中?
答:本題實質為泛型的擦除,不過答案比較多,常見的一種是通過兼容性,一種是通過反射的特性來處理。
通過泛型擦除兼容性實現如下:
ArrayList<String> list = new ArrayList<String>(); ArrayList list1 = list; list1.add(12); System.out.println(list1.get(0));
通過反射實現如下:
ArrayList<String> list = new ArrayList<String>(); Class clazz = list.getClass(); Method m = clazz.getMethod("add", Object.class); m.invoke(list, 100);
22. 問:泛型擦除到底擦除了哪些信息?
答:這道題就比較有意思和深度了,很多沒有深入了解泛型的人可能聽到這道題就覺得題出的有問題,因為在他們的認識里泛型信息都被擦除了,怎么還分擦除了哪些信息?難道還分情況?答案是確定的,泛型擦除其實是分情況擦除的,不是完全擦除,一定要消除這個誤區。
Java 在編譯時會在字節碼里指令集之外的地方保留部分泛型信息,泛型接口、類、方法定義上的所有泛型、成員變量聲明處的泛型都會被保留類型信息,其他地方的泛型信息都會被擦除。
感興趣的可以自己編寫各種場景的泛型代碼然后編譯后反編譯查看即可發現。
23. 問:既然泛型類型在編譯時就被擦除了,那類似 Gson 這種 json 解析框架是如何解析數據到泛型類型 Bean 結構的呢?
答:本題其實一箭雙雕,即考察了對於 Gson 框架是否熟悉又考察了 Java 泛型與反射的關系及泛型的實質。
由於在運行期間無法通過 getClass() 得知 T 的具體類型,所以 Gson 通過借助 TypeToken 類來解決這個問題,使用樣例如下:
ArrayList<String> list = new ArrayList<String>(); list.add("java"); Type type = new TypeToken<ArrayList<String>>(){}.getType(); String gStr = new Gson().toJson(list, type); ArrayList<String> gList = new Gson().fromJson(gStr, type);
可以看到 TypeToken 的使用非常簡單,只用將需要獲取類型的泛型類作為 TypeToken 的泛型參數構造一個匿名的子類就可以通過 getType() 方法獲取到我們使用的泛型類的泛型參數類型。
通過上面的使用樣例我們會發現使用 Gson 解析轉換的 Bean 不存在特殊的構造方法,因此可以排除在泛型類的構造方法中顯示地傳入泛型類的 Class 類型作為這個泛型類的私有屬性來保存泛型類的類型信息的實現方案,所以通過源碼分析發現 Gson 使用了另一種方式來獲取泛型的類型參數,其方法依賴 Java 的 Class 字節碼中存儲的泛型參數信息,Java 的泛型機制雖然在編譯期間進行了擦除,但是在編譯 Java 源代碼成 class 文件中還是保存了泛型相關的信息,這些信息被保存在 class 字節碼的常量池中,使用了泛型的代碼處會生成一個 signature 簽名字段,通過簽名 signature 字段指明這個常量池的地址,JDK 提供了方法去讀取這些泛型信息的方法,然后再借助反射就可以獲得泛型參數的具體類型,具體實現原理如下:
Type mySuperClass = new ArrayList<String>(){}.getClass().getGenericSuperclass(); Type type = ((ParameterizedType) mySuperClass).getActualTypeArguments()[0]; System.out.println(type);
所以獲取泛型參數類型的實質就是通過 Class 類的 getGenericSuperClass() 方法返回一個 ParameterizedType 對象(對於 Object、接口和原始類型返回 null,對於數組 class 返回 Object.class),ParameterizedType 表示帶有泛型參數類型的 Java 類型,JDK1.5 引入泛型后 Java 中所有的 Class 都實現了 Type 接口,ParameterizedType 繼承了 Type 接口,所有包含泛型的 Class 類都會自動實現這個接口。
關於 class 文件中存儲泛型參數類型的詳細信息可以參考:http://stackoverflow.com/questions/937933/where-are-generic-types-stored-in-java-class-files
24. 問:下面程序的輸出是什么?為什么?
public class Demo { public static void main(String[] args) throws Exception { ParameterizedType type = (ParameterizedType) Bar.class.getGenericSuperclass(); System.out.println(type.getActualTypeArguments()[0]); ParameterizedType fieldType = (ParameterizedType) Foo.class.getField("children").getGenericType(); System.out.println(fieldType.getActualTypeArguments()[0]); ParameterizedType paramType = (ParameterizedType) Foo.class.getMethod("foo", List.class).getGenericParameterTypes()[0]; System.out.println(paramType.getActualTypeArguments()[0]); System.out.println(Foo.class.getTypeParameters()[0].getBounds()[0]); } class Foo<T extends CharSequence> { public List<Bar> children = new ArrayList<Bar>(); public List<StringBuilder> foo(List<String> foo) { return null; } public void bar(List<? extends String> param) { //empty } } class Bar extends Foo<String> {} }
答:其運行結果如下。
class java.lang.String class Demo$Bar class java.lang.String interface java.lang.CharSequence
通過上面例子會發現泛型類型的每一個類型參數都被保留了,而且在運行期可以通過反射機制獲取到,因為泛型的擦除機制實際上擦除的是除結構化信息外的所有東西(結構化信息指與類結構相關的信息,而不是與程序執行流程有關的,即與類及其字段和方法的類型參數相關的元數據都會被保留下來通過反射獲取到)。
25. 問:請說說下面代碼片段中注釋行執行結果和原因?
DynamicArray<Integer> ints = new DynamicArray<>(); DynamicArray<? extends Number> numbers = ints; Integer a = 200; numbers.add(a); //這三行add現象? numbers.add((Number)a); numbers.add((Object)a); public void copyTo(DynamicArray<? super E> dest){ for(int i=0; i<size; i++){ dest.add(get(i)); //這行add現象? } }
答:上面代碼段注釋行執行情況解釋如下。
三個 add 方法都是非法的,無論是 Integer,還是 Number 或 Object,編譯器都會報錯。因為 ? 表示類型安全無知,? extends Number 表示是 Number 的某個子類型,但不知道具體子類型, 如果允許寫入,Java 就無法確保類型安全性,所以直接禁止。
最后方法的 add 是合法的,因為 <? super E> 形式與 <? extends E> 正好相反,超類型通配符表示 E 的某個父類型,有了它我們就可以更靈活的寫入了。
本題特別重要:一定要注意泛型類型聲明變量 ?時寫數據的規則。
26. 問:請說說下面代碼片段中注釋行執行結果和原因?
Vector<? extends Number> x1 = new Vector<Integer>(); //正確 Vector<? extends Number> x2 = new Vector<String>(); //編譯錯誤 Vector<? super Integer> y1 = new Vector<Number>(); //正確 Vector<? super Integer> y2 = new Vector<Byte>(); //編譯錯誤
答:上面代碼編譯運行情況如注釋所示,本題主要考察泛型中的 ? 通配符的上下邊界擴展問題。
通配符對於上邊界有如下限制:Vector<? extends 類型1> x = new Vector<類型2>(); 中的類型1指定一個數據類型,則類型2就只能是類型1或者是類型1的子類。
通配符對於下邊界有如下限制:Vector<? super 類型1> x = new Vector<類型2>(); 中的類型1指定一個數據類型,則類型2就只能是類型1或者是類型1的父類。
27. 問:下面程序合法嗎?
class Bean<T super Student> { //TODO }
答:編譯時報錯,因為 Java 類型參數限定只有 extends 形式,沒有 super 形式。
28. 問:下面程序有什么問題?該如何修復?
public class Test { public static void main(String[] args) throws Exception{ List<Integer> listInteger = new ArrayList<Integer>(); printCollection(listInteger); } public static void printCollection(Collection<Object> collection) { for(Object obj:collection){ System.out.println(obj); } } }
答:語句 printCollection(listInteger); 編譯報錯,因為泛型的參數是沒有繼承關系的。修復方式就是使用 ?通配符,printCollection(Collection<?> collection),因為在方法 printCollection(Collection<?> collection) 中不可以出現與參數類型有關的方法,譬如 collection.add(),因為程序調用這個方法的時候傳入的參數不知道是什么類型的,但是可以調用與參數類型無關的方法,譬如
collection.size()。
29. 問:請解釋下面程序片段的執行情況及原因?
public class Test{ public static <T> T add(T x, T y){ return y; } public static void main(String[] args) { int t0 = Test.add(10, 20.8); int t1 = Test.add(10, 20); Number t2 = Test.add(100, 22.2); Object t3 = Test.add(121, "abc"); int t4 = Test.<Integer>add(10, 20); int t5 = Test.<Integer>add(100, 22.2); Number t6 = Test.<Number>add(121, 22.2); } }
答:t0 編譯直接報錯,add 的兩個參數一個是 Integer,一個是 Float,所以取同一父類的最小級為 Number,故 T 為 Number 類型,而 t0 類型為 int,所以類型錯誤。
-
t1 執行賦值成功,add 的兩個參數都是 Integer,所以 T 為 Integer 類型。
-
t2 執行賦值成功,add 的兩個參數一個是 Integer,一個是 Float,所以取同一父類的最小級為 Number,故 T 為 Number 類型。
-
t3 執行賦值成功,add 的兩個參數一個是 Integer,一個是 Float,所以取同一父類的最小級為 Object,故 T 為 Object 類型。
-
t4 執行賦值成功,add 指定了泛型類型為 Integer,所以只能 add 為 Integer 類型或者其子類的參數。
-
t5 編譯直接報錯,add 指定了泛型類型為 Integer,所以只能 add 為 Integer 類型或者其子類的參數,不能為 Float。
-
t6 執行賦值成功,add 指定了泛型類型為 Number,所以只能 add 為 Number 類型或者其子類的參數,Integer 和 Float 均為其子類,所以可以 add 成功。
t0、t1、t2、t3 其實演示了調用泛型方法不指定泛型的幾種情況,t4、t5、t6 演示了調用泛型方法指定泛型的情況。 在調用泛型方法的時可以指定泛型,也可以不指定泛型;在不指定泛型時泛型變量的類型為該方法中的幾種類型的同一個父類的最小級(直到 Object),在指定泛型時該方法中的幾種類型必須是該泛型實例類型或者其子類。切記,java 編譯器是通過先檢查代碼中泛型的類型,然后再進行類型擦除,再進行編譯的。
30. 問:下面兩個方法有什么區別?為什么?
public static <T> T get1(T t1, T t2) { if(t1.compareTo(t2) >= 0); return t1; } public static <T extends Comparable> T get2(T t1, T t2) { if(t1.compareTo(t2) >= 0); return t1; }
答:get1 方法直接編譯錯誤,因為編譯器在編譯前首先進行了泛型檢查和泛型擦除才編譯,所以等到真正編譯時 T 由於沒有類型限定自動擦除為 Object 類型,所以只能調用 Object 的方法,而 Object 沒有 compareTo 方法。
get2 方法添加了泛型類型限定可以正常使用,因為限定類型為 Comparable 接口,其存在 compareTo 方法,所以 t1、t2 擦除后被強轉成功。所以類型限定在泛型類、泛型接口和泛型方法中都可以使用,不過不管該限定是類還是接口都使用 extends 和 & 符號,如果限定類型既有接口也有類則類必須只有一個且放在首位,如果泛型類型變量有多個限定則原始類型就用第一個邊界的類型變量來替換。