Java 泛型(上)

點擊上方 “藍字” 可以關注我哦!

爲什麼需要

場景

開發一個能夠存儲各種類型對象(比如:String 對象、Integer 對象等)的容器(容器就是存儲數據的,可以是對象,可以是數組等等)。

解決方案

在 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。
   @Override
   public T next() {
       return null;
   }
}
// 在實現類實現泛型接口時,如已將泛型類型傳入實參類型,則所有使用泛型的地方都要替換成傳入的實參類型。
public class FruitGenerator implements Generator<String> {

   private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

   // 泛型類型 T 被替換成分配的類型 String
   @Override
   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 操作。

不可以創建一個確切的泛型類型的數組,但是可以聲明泛型數組。

底層實現

下次分析時從字節碼角度來講解。

參考鏈接:https://blog.csdn.net/s10461/article/details/53941091


         


現在的喜歡,其實不是真正的喜歡,只是因爲不瞭解。真正的喜歡,是建立在非常瞭解的基礎之上的。 

本文分享自微信公衆號 - Java知其所以然(gh_37a1335e2608)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章