1. 程式人生 > >深入理解系列之JAVA泛型機制

深入理解系列之JAVA泛型機制

泛型是指在宣告(類,方法,屬性)的時候採用一個“標誌符”來代替,而只有在呼叫的時候才傳入真正的型別,我們最常見的泛型例項就是前面講述的集合類,集合類在宣告的時候都是通過泛型方式來宣告的,只有在呼叫(例項化)時我們才確定傳入的是Integer亦或是String等等!

注:本文著重敘述泛型實現的原理,而忽略一些泛型應用時的注意事項,詳細應用時的注意事項請參看其他博文

問題一、為什麼要採用泛型?

泛型機制是JDK1.5出現的。拿ArrayList舉例,在JDK1.5出現之前,為了解決儲存不同引數型別資料的問題,ArrayList宣告的時候傳入引數定義為Object,因為Object是所有型別的父類,這樣在取出的時候再通過手動的強制轉換為實際的型別。大概的實現是這樣的(原理性描述):

class ArrayList_Before_JDK5{
  private Object[] elements=new Object[10];
  public Object get(int i){
    return elements[i];
  }

 public void add(Object o){
    if(elements.length > 1)
      elements[1] = o;
    elements[0] = o;
  }
}

這樣我們在使用ArrayList的時候,將會這樣用:

public static void main(String[] args) {

    ArrayList_Before_JDK5 arrayList_before_jdk5 = new
ArrayList_Before_JDK5(); arrayList_before_jdk5.add("123"); arrayList_before_jdk5.add(123); String string = (String)arrayList_before_jdk5.get(0); Integer integer = (Integer) arrayList_before_jdk5.get(1); }

我們這裡發現兩個問題:
1、當我們獲取某一個值的時候必須手動強轉;
2、假設我們想把這個例項全部存入String型別的,由於底層的引數是Object,所以程式並不會阻止我們傳入Integer型別的引數,這時如果我們仍然使用:

String string = (String) arrayList_before_jdk5.get(1);

獲取位置1的資料(注意此時我們認為存入的是string,所以我們都使用String強轉符,但是實際上位置1被我們傳入了Interger型別)時,程式在編譯期間不會出現任何錯誤,但是執行的時候卻會出現異常:
Exception in thread “main” java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String at Fxing.main(Fxing.java:10)
能不能通過一種機制在編譯的時候就(或者說在IDE語法提示的時候)就能提前檢測出錯誤,避免不必要的執行異常呢?答案是肯定的,就是泛型!採用泛型設計的ArrayList將會是這樣的(JDK8原始碼,但是剔除了不必要的原始碼):

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
 transient Object[] elementData; 

public boolean add(E e) {
        elementData[size++] = e;
        return true;
    }

    public E get(int index) {
        return elementData(index);
    }

這裡的E就是在ArrayList中的“泛型標誌符”,我們在建立例項的時候就會依照自己的需求傳入String、Integer等等,那麼在底層這個E就變成了String、Integer等等,所以傳入引數的時候就必須按照相應的型別來寫入了!

問題二、泛型的底層原理是什麼?

我們上面說過,當依照自己的需求傳入實際的型別引數的時候,E將會變成實際的型別引數——泛型追求的效果就是這樣的,但是在JVM編譯後其實並不是這樣的,我們稱之為“泛型擦除”!也就是說,編譯過後的位元組碼將會還原回原始型別——和JDK5之前的一樣。所以,我們也稱JAVA的泛型為偽泛型!為了證明這一點,我們通過兩種反編譯方法來驗證,以下面程式碼為例:

public class Fxing {
  public static void main(String[] args) {
    ArrayList<String> arrayList_string = new ArrayList<>();
    ArrayList<Integer> arrayList_integer = new ArrayList<>();
    arrayList_integer.add(1);
    arrayList_string.add("1");
    System.out.println(arrayList_integer.get(0));
    System.out.println(arrayList_string.get(0));
  }
}

第一:IDE反編譯位元組碼
如果你使用IDE反編譯位元組碼,你會發現下面的情形:

public class Fxing {
  public Fxing() {
  }

  public static void main(String[] args) {
    ArrayList<String> arrayList_string = new ArrayList();
    ArrayList<Integer> arrayList_integer = new ArrayList();
    arrayList_integer.add(Integer.valueOf(1));
    arrayList_string.add("1");
    System.out.println(arrayList_integer.get(0));
    System.out.println((String)arrayList_string.get(0));
  }
}

不是說編譯位元組碼的時候會發生“泛型擦除”迴歸到原始型別嗎?為什麼我們可以看到String、Integer型別的呢?這個問題在部落格 關於java泛型擦除反編譯後泛型會出現問題也同樣出現了,同時在《深入理解JVM虛擬機器》中也找到了佐證:

JCP組織對虛擬機器規範做出了相應的修改,引入了諸如Signature、Loca-lVariableTypeTable等新的屬性用於解決伴隨泛型而來的引數型別的識別問題,Signature是其中最重要的一項屬性,它的作用就是儲存一個方法在位元組碼層面的特徵簽名,這個屬性中儲存的引數型別並不是原生型別 ,而是包括了引數化型別的資訊。修改後的虛擬機器規範要求所有能識別49.0以上版本的Class檔案的虛擬機器都要能正確地識別Signature引數。

通俗點將就是,JCP組織要求class檔案儲存實際型別資訊,所以IDE可以由特殊欄位獲取實際型別,從而智慧的反編譯!所以事實上,反編譯後的程式碼是這樣的:

public class Fxing {
  public Fxing() {
  }

  public static void main(String[] args) {
    ArrayList arrayList_string = new ArrayList();
    ArrayList arrayList_integer = new ArrayList();
    arrayList_integer.add(Integer.valueOf(1));
    arrayList_string.add("1");
    System.out.println(arrayList_integer.get(0));
    System.out.println((String)arrayList_string.get(0));
  }
}

方法二:javap反編譯
如果上述還是不能使你信服,我們可以通過javap(javap -c XXX.class)編譯出位元組碼指令來看看到底發生了什麼:

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: new           #2                  // class java/util/ArrayList
      11: dup
      12: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
      15: astore_2
      16: aload_2
      17: iconst_1
      18: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      21: invokevirtual #5                  // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
      24: pop
      25: aload_1
      26: ldc           #6                  // String 1
      28: invokevirtual #5                  // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
      31: pop
      32: aload_2
      33: iconst_0
      34: invokevirtual #7                  // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
      37: pop
      38: aload_1
      39: iconst_0
      40: invokevirtual #7                  // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
      43: pop
      44: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
      47: aload_2
      48: iconst_0
      49: invokevirtual #7                  // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
      52: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      55: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
      58: aload_1
      59: iconst_0
      60: invokevirtual #7                  // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
      63: checkcast     #10                 // class java/lang/String
      66: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      69: return
}

我們可以看到(4、12)(21、28、34、40),初始化的時候並沒有攜帶實際型別資訊,add、get的時候使用的都是object!同時當需要獲取最終資料的時候,JVM虛擬機器將自動強制轉型(63、66),所以我們需要注意的是:

泛型是偽泛型,即使泛型物件是傳入不同的型別引數泛型物件,因為在編譯階段會被擦除,所以實際上該泛型物件屬於同一個類,進而在函式過載的時候不會因為泛型引數不同而“過載成功”!

類似還用其他類似引用傳遞問題、萬用字元問題請移步部落格Java泛型深入理解檢視!