Java基礎——深入理解泛型

it_was發表於2020-09-17

Java的泛型是透過型別擦除實現的!即Java的泛型是偽泛型,在編譯期間,所有的泛型資訊都會被擦除掉。所以Java編譯器會在編譯時儘可能的發現可能出錯的地方,但是仍然無法在執行時刻出現的型別轉換異常的情況。

型別擦除也是Java的泛型與C++模板機制實現方式之間的重要區別。:boom:

2.1 Java泛型擦除的證明

  • 型別資訊被擦除,原始型別相同 下面兩個list容器的引數型別分別是String和Integer,但在執行期間兩者的型別資訊被擦除,只保留原始型別Object
    public class reflect {
      public static void main(String[] args){
          List<String> list1 = new ArrayList<>(); 
          List<Integer> list2 = new ArrayList<>();
          list1.add("12");
          list2.add(12);
          System.out.println(list1.getClass() == list2.getClass());//true
      }
    }
  • 透過反射新增其他型別也可以證明泛型的型別資訊被擦除,留下原始型別!
    public class reflect {
      public static void main(String[] args) throws Exception{
          List<Integer> list1 = new ArrayList<>();
          list1.add(13);
          list1.getClass().getMethod("add",Object.class).invoke(list1,"a123");
          System.out.println(list1.toString());
      }
    }
    輸出結果
    [13, a123]

2.2 型別擦除後的原始型別

  • 原始型別 就是擦除去了泛型資訊,最後在位元組碼中的型別變數的真正型別,無論何時定義一個泛型,相應的原始型別都會被自動提供,型別變數擦除,並使用其限定型別(無限定的變數用Object)替換:boom:

    class info<T>{
      //無限定,原始型別就是Object
    }
    class message<T extends Comparable>{
      //有限定,即Comparable為邊界,那麼就用這個限定型別
    }
  • 所以在呼叫泛型類或者方法的時候,可以指定泛型,也可以不指定泛型。:boom:

    • 在不指定泛型的情況下,泛型變數的型別為該方法中的幾種型別的同一父類的最小級,直到Object
    • 在指定泛型的情況下,該方法的幾種型別必須是該泛型的例項的型別或者其子類

因為種種原因,Java不能實現真正的泛型,只能使用型別擦除來實現偽泛型,這樣雖然不會有型別膨脹問題,但是也引起來許多新問題,所以,SUN對這些問題做出了種種限制,避免我們發生各種錯誤

3.1 編譯前檢查

:raising_hand:: 既然說型別變數會在編譯的時候擦除掉,那為什麼我們往 ArrayList< Integer > 建立的物件中新增整數會報錯呢?不是說泛型變數String會在編譯的時候變為Object型別嗎?為什麼不能存別的型別呢?既然型別擦除了,如何保證我們只能使用泛型變數限定的型別呢?

ArrayList<String> list1 = new ArrayList(); 
list1.add("1"); //編譯透過 
list1.add(1); //編譯錯誤 

:information_desk_person:Java編譯器是透過先檢查程式碼中泛型的型別,然後在進行型別擦除,再進行編譯。

3.2 引用傳遞

討論一下情況:

ArrayList<String> list1 = new ArrayList(); //第一種 情況
ArrayList list2 = new ArrayList<String>(); //第二種 情況

list1.add("1"); //編譯透過 
list1.add(1); //編譯錯誤 
String str1 = list1.get(0); //返回型別就是String

list2.add("1"); //編譯透過 
list2.add(1); //編譯透過
Object object = list2.get(0); //返回型別就是Object

我們可以發現,型別的檢查是依賴於它的引用的,而無關於它指向物件的型別。即引用限定了什麼型別,編譯器就用限定的型別進行檢查,沒限定則泛型變數的型別為該方法中的幾種型別的同一父類的最小級,直到Object:boom::boom::boom:

再來看一種情況:

ArrayList<Object> list2 = new ArrayList<>(); 
list2.add(new Object());
ArrayList<String> list22 = list2;//編譯錯誤

以下為對上面程式碼個人理解
首先已經限定了list2的型別為Object,則新增的元素都為Object,此時將其指向ArrayList< String > 的引用,則就像向下轉型一樣,是不允許的!

3.3 自動型別轉換

因為型別擦除的問題,所以所有的泛型型別變數最後都會被替換為原始型別。既然都被替換為原始型別,那麼為什麼我們在獲取的時候,不需要進行強制型別轉換呢?

看下ArrayList.get()方法:

public E get(int index) {  

    RangeCheck(index);  

    return (E) elementData[index];  

}

可以看到,在return之前,會根據泛型變數進行強轉。假設泛型型別變數為Date,雖然泛型資訊會被擦除掉,但是會將(E) elementData[index],編譯為(Date)elementData[index]。所以我們不用自己進行強轉。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章