目錄
第一問:什么是泛型?有什么好處?
第二問:泛型是如何實現的呢?
第三問:類型擦除的缺點有哪些?
第四問:泛型中上下界限定符extends和super有什么區別?
第五問:List、List、List之間的區別?
第六問:如何在泛型為Integer的ArrayList中存放一個String類型的對象?
我們用問面試題的方式來講解集合的泛型概念,這樣方便讀者理解。
第一問:什么是泛型?有什么好處?
Java泛型(generics) 是JDK5中引入的一個新特性,允許在定義類和接口的時候使用類型參數(type parameter)。聲明的類型參數在使用時用具體的類型來替換。泛型最主要的應用是在JDK5中的新集合類框架中。
泛型的好處有兩個:
- 方便:可以提高代碼的復用性。以List接口為例,我們可以將String、Integer等類型放入List中,如不用泛型,存放String類型要寫一個List接口,存放Integer要寫另外一個List接口,泛型可以很好的解決這個問題。
- 安全:在泛型出現之前,通過Object實現的類型轉換需要在運行時檢查,如果類型轉換出錯,程序直接GG,可能會帶來毀滅性打擊。而泛型的作用就是在編譯時做類型檢查,這無疑增加程序的安全性。
第二問:泛型是如何實現的呢?
Java中的泛型通過類型擦除的方式來實現,通俗點理解,就是通過語法糖的形式,在java->.class轉換的階段,將List<String>擦除調轉為List的手段。換句話說,Java的泛型只在編譯期,jvm是不會感知到泛型的。
比如Java的編譯器在編譯以下代碼時:
public class Foo<T> {T bar;void doSth(T param) {}
};Foo<String> f1;
Foo<Integer> f2;
在編譯后的字節碼文件中,會把泛型的信息擦除掉:
public class Foo {Object bar;void doSth(Object param) {}
};
也就是說,在代碼中的Foo<String> 和 Foo<Integer>使用的類,經過編譯后都是同一個類。
所以說泛型技術實際上是Java語言的一顆語法糖,因為泛型經過編譯器處理之后就被擦除了。
這種擦除的過程,被稱之為——類型擦除。所以類型擦除指的是通過類型參數名T,將泛型類型實例關聯到同一份字節碼上。編譯器只為泛型類型生成一份字節碼,并將其實例關聯到這份字節碼上。類型擦除的關鍵在于從泛型類型中清除類型變量的相關信息,并在僅需要的時候插入類型標記和類型轉換的方法。
第三問:類型擦除的缺點有哪些?
- 泛型不可以重載
- 泛型異常類不可以多次catch
- 泛型類中的靜態變量也只有一份,不會有多份
第四問:泛型中上下界限定符extends和super有什么區別?
<? extends T> 表示類型的上界,表示參數化類型可能是T或是T的子類。
// 定義一個泛型方法,接受任何繼承自Number的類型
public <T extends Number> void processNumber(T number) {
// 在這個方法中,可以安全地調用Number的方法
double value = number.doubleValue();
// 其他操作...
}
<? super T> 表示類型下界(Java Core中叫超類型限定),表示參數化類型是此類型的超類型(父類型),直至Object。
// 定義一個泛型方法,接受任何類型的List,并向其中添加元素
public <T> void addElements(List<? super T> list, T element) {
list.add(element);
// 其他操作...
}
在使用限定通配符的時候,需要遵守PECS原則,即Producer Extends, Consumer Super:上界生產,下界消費。
如果是從集合中讀取類型T的數據,并且不能寫入,可以使用<? extends 通配符; (Producer Extends),如上面的processNumber方法。
如果是從集合中寫入類型T的數據,并且不需要讀取,可以使用<? super 通配符; (Consumer Super),如上面的addElements方法。
如果既要存又要取,那就不要用任何通配符。
第五問:List<?>、List<Object>、List之間的區別?
-
List<?> 是一個未知類型的List,而List<Object> 其實是任意類型的List。可以把List<String>、List<Integer>賦值給List<?>,卻不能把List<String>賦值給List<Object>。
-
可以把任何帶參數的類型傳遞給原始類型List,但卻不能把List<String>賦值給List<Object>,因為會產生編譯錯誤(不支持協變)。
-
List<?>由于不確定列表中元素的具體類型,因此只能從這種列表中讀取數據,而不能往里面添加除了null之外的任何元素。
public class Test {public static void main(String[] args) {List<String> list = new ArrayList<>();test(list);// 編譯出錯test1(list);test2(list);test3(list);}public static void test(List list) {list.add("CLAY");list.add(666);}public static void test1(List<Object> list) {list.add(new Test());}public static void test2(List<?> list) {// 編譯出錯list.add("CLAY");// 編譯正常list.get(0);}public static void test3(List<String> list) {list.add("CLAY");}
}
第六問:如何在泛型為Integer的ArrayList中存放一個String類型的對象?
通過反射可以實現:
List<Integer> list = new ArrayList<>();
Method method = list.getClass().getMethod("add", Object.class);
method.invoke(list, "Java反射機制實例");
System.out.println(list.get(0));