Java隨筆-Java泛型的一點學習

Java泛型

Java泛型(generics)是JDK 5中引入的一個新特性,允許在定義類和接口的時候使用類型參數(type parameter)。聲明的類型參數在使用時用具體的類型來替換。泛型最主要的應用是在JDK 5中的新集合類框架中。泛型的引入可以解決JDK5之前的集合類框架在使用過程中較爲容出現的運行時類型轉換異常,因爲編譯器可以在編譯時通過類型檢查,規避掉一些潛在的風險。

在JDK5之前,使用集合框架時,是沒有類型信息的,統一使用Object,我找了一段JDK4 List接口的方法簽名如下是JDK5開始引入泛型,List接口的改動,新的方法簽名,引入了類型參數。

boolean add(E e);

在JDK5之前,使用集合類時,可以往其中添加任意元素,因爲其中的類型是Object,在取出的階段做強制轉換,由此可能引發很多意向不到的運行時強制轉換錯誤,比如以下代碼。

public class Test1 {
    public static void main(String[] args) {
        List a = new ArrayList();
        a.add("123");
        a.add(1);             
        // 以上代碼可以正常通過編譯,其中同時含有了Integer類型和String類型
        for (int i = 0 ; i < a.size(); i++) {
            int result = (Integer)a.get(i);    // 在取出時需要對Object進行強制轉型
            System.out.println(result);
        }
    }}

如上代碼就會在運行時階段帶來強轉異常,在編譯時間不能夠排查出潛在風險。如果使用泛型機制,可以在編譯期間就檢查出List的類型插入的有問題,進行規避,如下代碼。

public class Test1 {
    public static void main(String[] args) {
        List<Integer> a = new ArrayList();
        a.add("123");          // 編譯不通過
        a.add(1);
    }}

引入泛型後,編譯器會在編譯時先根據類型參數進行類型檢查,杜絕掉一些潛在風險。爲何說是在編譯時檢查,因爲在運行時仍然是可以通過反射,將不符合類型參數的數據插入至list中,如下代碼所示。

public class Test1 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        List<Integer> a = new ArrayList();
        List b = new ArrayList();
        a.getClass().getMethod("add",Object.class).invoke(a,"abc");    
        // 以上代碼編譯通過,運行通過
    }}

引入泛型的同時,也爲了兼容JDK5之前的類庫,JDK5開始引入的其實是僞泛型,在生成的Java字節碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數,會在編譯器在編譯的時候去掉。這個過程就稱爲類型擦除。如在代碼中定義的List等類型,在編譯後都會變成List,也就自然兼容了JDK5之前的代碼。Java的泛型機制和C++等的泛型機制實現不同,Java的泛型靠的還是類型擦除,目標代碼只會生成一份,犧牲的是運行速度。C++的模板會對針對不同的模板參數靜態實例化,目標代碼體積會稍大一些,運行速度會快很多。

進行類型擦除後,類型參數原始類型(raw type)就是擦除去了泛型信息,最後在字節碼中的類型變量的真正類型。無論何時定義一個泛型類型,相應的原始類型都會被自動地提供。類型變量被擦除,並使用其限定類型(無限定的變量用Object)替換。

class Pair<T> {
    private T value;
    public T getValue() {
        return value;
    }
    public void setValue(T  value) {
        this.value = value;
    }} 
  Pair<T>的原始類型爲:class Pair {
    private Object value;
    public Object getValue() {
        return value;
    }
    public void setValue(Object  value) {
        this.value = value;
    }}

在Pair中,類型擦除,使用Object,其結果就是一個普通的類,如同泛型加入java編程語言之前已經實現的那樣。在程序中可以包含不同類型的Pair,如Pair或Pair,但是,擦除類型後它們就成爲原始的Pair類型了,原始類型都是Object。ArrayList被擦除類型後,原始類型也變成了Object,通過反射我們就可以存儲字符串了。

在調用泛型方法的時候,可以指定泛型,也可以不指定泛型。在不指定泛型的情況下,泛型變量的類型爲 該方法中的幾種類型的同一個父類的最小級,直到Object。在指定泛型的時候,該方法中的幾種類型必須是該泛型實例類型或者其子類。

public class Test1 {
 
   public static void main(String[] args) {
 
      /** 不指定泛型的時候 */
      int i = Test1.add(1, 2); // 這兩個參數都是Integer,所以T爲Integer類型
      Number f = Test1.add(1, 1.2);// 這兩個參數一個是Integer,以風格是Float,所以取同一父類的最小級,爲Number
      Object o = Test1.add(1, "asd");// 這兩個參數一個是Integer,以風格是Float,所以取同一父類的最小級,爲Object
 
      /** 指定泛型的時候 */
      int a = Test1.<Integer> add(1, 2);// 指定了Integer,所以只能爲Integer類型或者其子類
      int b = Test1.<Integer> add(1, 2.2);// 編譯錯誤,指定了Integer,不能爲Float
      Number c = Test1.<Number> add(1, 2.2); // 指定爲Number,所以可以爲Integer和Float
   }
 
   // 這是一個簡單的泛型方法
   public static <T> T add(T x, T y) {
      return y;
   }}

因爲類型擦除的問題,所有的泛型類型變量最後都會被替換爲原始類型,但在泛型的使用中,我們不需要對取出的數據做強制轉換。

public class Test1 {
 
    public static void main(String[] args) {
        List<Integer> a = new ArrayList();
        a.add(1);
 
        for (int i = 0 ; i < a.size(); i++) {
            int result = a.get(i);
            System.out.println(result);
        }
    }}

我們從字節碼的角度來探索一下。

public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: iconst_1
      10: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      13: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      18: pop
      19: iconst_0
      20: istore_2
      21: iload_2
      22: aload_1
      23: invokeinterface #6,  1            // InterfaceMethod java/util/List.size:()I
      28: if_icmpge     58
      31: aload_1
      32: iload_2
      33: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
      38: checkcast     #8                  // class java/lang/Integer       這裏JVM做了強轉
      41: invokevirtual #9                  // Method java/lang/Integer.intValue:()I
      44: istore_3
      45: getstatic     #10                 // Field java/lang/System.out:Ljava/io/PrintStream;
      48: iload_3
      49: invokevirtual #11                 // Method java/io/PrintStream.println:(I)V
      52: iinc          2, 1
      55: goto          21
      58: return

在偏移量38的位置可以看到,JVM使用了checkcast指令,說明雖然在編譯時進行了類型擦除,但是JVM中仍然保留了類型參數的元信息,在取出時自動進行了強轉,這也算是使用泛型的方便之處吧。

在別人的例子有看到說類型擦除和多態的衝突,舉了一個例子。

public class Test1 {
 
   public static void main(String[] args) {
      DateInter dateInter = new DateInter();
      dateInter.setValue(new Date());
      dateInter.setValue(new Object());// 編譯錯誤
   }}
 class Pair<T> {
   private T value;
 
   public T getValue() {
      return value;
   }
 
   public void setValue(T value) {
      this.value = value;
   }}
 class DateInter extends Pair<Date> {
   @Override
   public Date getValue() {
      return super.getValue();
   }
 
   @Override
   public void setValue(Date value) {
      super.setValue(value);
   }}

因爲在類型擦除後,父類也就變成了一個普通的類,如下所示

class Pair {
   private Object value;
 
   public Object getValue() {
      return value;
   }
 
   public void setValue(Object value) {
      this.value = value;
   }}

但這樣setValue就從重寫變成了重載,顯然打破了想達到的目的,那麼JVM是如何幫助解決這個衝突的呢?答案是 JVM幫我們搭了一個橋,具體我們從字節碼的角度再來看看。

class DateInter extends Pair<java.util.Date> {
  DateInter();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method Pair."<init>":()V
       4: return
 
  public java.util.Date getValue();
    Code:
       0: aload_0
       1: invokespecial #2                  // Method Pair.getValue:()Ljava/lang/Object;
       4: checkcast     #3                  // class java/util/Date
       7: areturn
 
  public void setValue(java.util.Date);
    Code:
       0: aload_0
       1: aload_1
       2: invokespecial #4                  // Method Pair.setValue:(Ljava/lang/Object;)V
       5: return
 
  public void setValue(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #3                  // class java/util/Date
       5: invokevirtual #5                  // Method setValue:(Ljava/util/Date;)V
       8: return
 
  public java.lang.Object getValue();
    Code:
       0: aload_0
       1: invokevirtual #6                  // Method getValue:()Ljava/util/Date;
       4: areturn}

從編譯的結果來看,我們本意重寫setValue和getValue方法的子類,有4個方法,最後的兩個方法,就是編譯器自己生成的橋接方法。可以看到橋方法的參數類型都是Object,也就是說,子類中真正覆蓋父類兩個方法的就是這兩個我們看不到的橋方法,打在我們自己定義的setvalue和getValue方法上面的@Oveerride只不過是假象。而橋方法的內部實現,就只是去調用我們自己重寫的那兩個方法。所以,虛擬機巧妙的使用了巧方法,來解決了類型擦除和多態的衝突。

最後附上最近在瀏覽一些別人經驗時得到一些tips。

  1. 使用JSON串反序列化對象集合時,記得標註對象的class類型,不然會得到一個只有原始類型也就是Object的集合,可能引起類型轉換錯誤,尤其是在服務調用的這種場景下。

  2. 重視編譯器提出的警告信息。

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