場景
解決方案
在 JDK 1.5 之前
在 JDK 1.5 之前是沒有泛型的,最好的辦法是開發一個能夠存儲和檢索 Object
類型本身的容器,然後再將該對象用於各種類型時進行類型轉換。
public class ObjectContainer {
public Object obj;
}
雖然這個容器會達到預期效果,但就我們的目的而言,它並不是最合適的解決方案。它不是類型安全的(Java 的編譯器對於類型轉換的錯誤是檢測不到的,在運行時執行到 checkcast
這個字節碼指令時,如果類型轉換錯誤纔會拋出 ClassCastException
),並且要求在檢索封裝對象時使用顯式類型轉換(向下轉型),因此有可能引發運行時異常。
測試方法
public static void main(String[] args) {
ObjectContainer myObj = new ObjectContainer();
// 這裏發生向上轉型
myObj.obj = "Test";
// 檢索封裝對象,發生向下轉型
String myStr = (String) myObj.obj;
System.out.println("myStr: " + myStr);
}
注意:一個面試點,發生向下轉型的必要條件是先發生向上轉型。
從 JDK 1.5 開始
從 JDK 1.5 開始出現了泛型,使用泛型可以很好的解決我們的場景需求。在實例化時爲所使用的容器分配一個類型,也稱泛型類型,這樣就可以創建一個對象來存儲所分配類型的對象。泛型類型可以看成一種弱類型(類似於 js 中的 var,定義的時候你可以隨便定義,使用的時候就需要給出具體類型),這意味着可以通過執行泛型類型調用分配一個類型,將用分配的具體類型替換泛型類型。然後,所分配的類型將用於限制容器內使用的值,這樣就無需進行類型轉換,還可以在編譯時提供更強的類型檢查。
// 根據這個代碼來看,泛型類型就是 T。泛型類型也可以稱爲泛型形參。
public class ObjectContainer<T>{
public Object obj;
}
測試方法
代碼裏的註釋很重要!很重要!很重要!建議多看幾遍。
public static void main(String[] args) {
// 執行泛型類型調用分配 String 這個具體類型,String 也可以稱爲泛型實參。
ObjectContainer<String> myObj = new ObjectContainer<String>();
myObj.obj = "ypf";
// 這裏不需要類型轉型,因爲通過執行泛型類型調用分配了 String 這個類型,編譯器會幫我們去生成一個 checkcast 字節碼指令來做類型轉換。也就是說我們以前需要手動去做的事(類型轉型),現在編譯器幫我們做了。其實泛型也可以看成是 Java 的一種語法糖。
String myStr = myObj.obj;
System.out.println("myStr: " + myStr);
}
從上面的類中我們已經知道了泛型類型就是 T。在這個測試方法中執行泛型類型調用對應的代碼就是第 2 行,可以看到我們泛型類型 T 在執行時分配了 String 這個類型。其實執行泛型類型調用就是在寫類的時候把 ClassName<T> 寫成 ClassName<具體的類型>,也就是說把泛型類型替換成具體的類型。
優點
更強的類型檢查,避開運行時可能引發的
ClassCastException
,可以節省時間。消除了類型轉換。
開發泛型算法。(可以多去看看 Java 集合中是怎麼利用泛型的)
怎麼用
泛型類
public class GenericClass<T>{
// key 這個成員變量的類型爲 T,T 的類型由外部使用時指定。
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
泛型接口
定義一個泛型接口
public interface Generator<T> {
public T next();
}
定義實現泛型接口的類
// 未傳入泛型實參時,與泛型類的定義相同,在聲明類的時候,需將泛型的聲明也一起加到類中。
public class FruitGenerator<T> implements Generator<T>{
// 使用泛型類型 T。
public T next() {
return null;
}
}
// 在實現類實現泛型接口時,如已將泛型類型傳入實參類型,則所有使用泛型的地方都要替換成傳入的實參類型。
public class FruitGenerator implements Generator<String> {
private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
// 泛型類型 T 被替換成分配的類型 String
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}
泛型方法
當我們只想在某個方法中使用泛型而並不需要給整個類加個泛型時,可以使用泛型方法來指定一個泛型類型。泛型方法的泛型類型完全獨立於類,也就是說可以與泛型類中聲明的 T 不是同一種類型。通過下面的代碼來驗證這個結論。
public class GenericTest<T> {
public T t;
public static <T> T genericMethod(Class<T> clazz)throws InstantiationException ,IllegalAccessException{
T instance = clazz.newInstance();
return instance;
}
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
GenericTest g = new GenericTest();
g.t = "666";
System.out.println(g.t);
GenericTest g2 = genericMethod(GenericTest.class);
System.out.println(g2);
}
}
輸出結果
從輸出結果可以看出 GenericTest 類的泛型類型 T 爲 String,genericMethod 方法的泛型類型 T 爲 GenericTest 。
泛型方法和可變參數靈活使用
通過泛型方法和可變參數,我們可以 new 出任何類型的數組。這樣我就很方便創建一個數組,其實在底層實現上是編譯器幫我們去 new 數組這個操作了。
public class GenericTest<T> {
// 巧妙利用語言的特性。多去了解一下語言的特性,利用起來。
public static <T> T[] of(T... array){
return array;
}
public static void main(String[] args) {
Integer[] numArr = of(1,2,3,4);
String[] strArr = of("y", "p", "f");
System.out.println(Arrays.toString(numArr));
System.out.println(Arrays.toString(strArr));
}
}
泛型通配符
泛型類型是沒有繼承關係的。
public class GenericTest<T> {
public T t;
public GenericTest(T t) {
this.t = t;
}
public static void showKeyValue(GenericTest<Number> obj){
System.out.println(obj.toString());
}
public static void main(String[] args) {
GenericTest<Integer> gInteger = new GenericTest<Integer>(123);
GenericTest<Number> gNumber = new GenericTest<Number>(456);
showKeyValue(gNumber);
// 下面這個會報錯,編譯不通過。因爲泛型沒有繼承這個說法。
// GenericTest<Integer> 和 GenericTest<Number> 經過泛型擦除後都變爲 GenericTest。
// showKeyValue(gInteger);
}
}
解決方案
當操作類型時,不需要使用類型的具體功能時,只使用 Object 類中的功能。那麼可以用 ?
通配符來表未知類型。?
和 Number、String、Integer 一樣都是一種被分配的具體類型,可以把?
看成所有類型的父類來理解(也可以把這個看成 Java 語言的一種規範)。
public class GenericTest<T> {
public T t;
public GenericTest(T t) {
this.t = t;
}
// 使用泛型通配符,可以接收任意類型的泛型類。
public static void showKeyValue(GenericTest<?> obj){
System.out.println(obj.toString());
}
public static void main(String[] args) {
GenericTest<Integer> gInteger = new GenericTest<Integer>(123);
GenericTest<Number> gNumber = new GenericTest<Number>(456);
showKeyValue(gNumber);
showKeyValue(gInteger);
}
}
泛型上下邊界
在使用泛型的時候,我們還可以爲傳入的泛型類型實參進行上下邊界的限制,如:類型實參只准傳入某種類型的父類或某種類型的子類。
爲泛型類型添加上邊界,即傳入的類型實參必須是指定類型的子類型。
爲泛型類型添加下邊界,即傳入的類型實參必須是指定類型的父類型。
泛型上下邊界這塊具體怎麼使用在下次分析時介紹。
注意事項
泛型類型不可以是基本類型,只能是類。
泛型類型沒有繼承關係。
不能對確切的泛型類型使用 instanceof 操作。
不可以創建一個確切的泛型類型的數組,但是可以聲明泛型數組。
底層實現
下次分析時從字節碼角度來講解。
參考鏈接:
現在的喜歡,其實不是真正的喜歡,只是因爲不瞭解。真正的喜歡,是建立在非常瞭解的基礎之上的。
本文分享自微信公衆號 - Java知其所以然(gh_37a1335e2608)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。