你真的懂Java泛型嗎

來源:互聯網
上載者:User

標籤:tle   base   article   應用   情況   not   gui   apple   運行   

泛型實現參數化型別的概念,使代碼可以應用於多種類型,解除類或方法與所使用的類型之間的約束。在JDK 1.5開始引入了泛型,但Java實現泛型的方式與C++或C#差異很大。在平常寫代碼用到泛型時,彷彿一切都來得如此理所當然。但其實Java泛型還是有挺多tricky的東西的,編譯器在背後為我們做了很多事。下面我們來看看有關Java泛型容易忽視的點。

泛型不支援協變

什麼是協變?舉個例子。

class Fruit{}class Apple extends Fruit{}Fruit[] fruit = new Apple[10]; // OK

子類數組可以賦給父類數組的引用。但泛型是不支援這種協變的。

ArrayList<Fruit> flist = new ArrayList<Apple>(); // 無法通過編譯

但我們可以使用萬用字元來解決

ArrayList<? extends Fruit> flist = new ArrayList<Apple>();// 使用萬用字元解決協變問題
萬用字元上界萬用字元
        List<? extends Fruit> flist = Arrays.asList(new Apple());        Apple a = (Apple)flist.get(0); // No warning        flist.contains(new Apple()); // Argument is ‘Object’        flist.indexOf(new Apple()); // Argument is ‘Object’        //flist.add(new Apple());   無法編譯

List<? extends Fruit> 表示某種特定類型 ( Fruit 或者其子類 ) 的 List,但是編譯器並不關心(不知道)這個實際的具體類型到底是什麼。值得注意的是,這並不意味著這個List可以持有Fruit的任意類型!
由於List的具體類型是並不確定的,而且Java泛型是不支援協變的,因此帶有泛型型別參數的方法都無法正常調用。比如add(T item);,即使是傳Object也無法通過編譯。
但對於傳回型別是泛型的方法,比如T get(int index);,傳回值類型與上界類型一樣。如上面範例程式碼調用的flist.get(0)傳回值就是Fruit類型的。

下界萬用字元
    static void add(List<? super Apple> list) {//        list.add(new Fruit()); // 無法編譯        Object object = list.get(0);// pass    }

代碼中的
List<? super Apple> list表明list持有的類型是Apple的父類類型,但與上界萬用字元類似,這並不意味list可以持有Apple任意的子類類型的對象,編譯器並不知道list具體的類型是什麼。因此,list.add(new Fruit());就不能編譯了。

無界萬用字元

List<?> list 表示 list 是持有某種特定類型的 List,但是不知道具體是哪種類型。而單獨的 List list ,也就是沒有傳入泛型參數,表示這個 list 持有的元素的類型是 Object

所有泛型資訊都被擦除了嗎

所謂的擦除,僅僅是對方法的Code屬性中的位元組碼(也就是方法內的邏輯代碼)進行擦除,實際上中繼資料(類和介面的聲明,類欄位的聲明)中還是保留了泛型資訊。
引用R大的話就是:
位於聲明一側的,源碼裡寫了什麼到運行時就能看到什麼;
位於使用一側的,源碼裡寫什麼到運行時都沒了。

public class GenericClass<T> {                // 1      private List<T> list;                     // 2      private Map<String, T> map;               // 3      public <U> U genericMethod(Map<T, U> m) { // 4          List<String> list = new ArrayList<>(); // 5        return null;    }}  

上面的代碼中,注釋1到注釋4的T和U是保留在Class檔案當中的,源碼是什麼,那麼通過反射擷取得到的就是什麼。也就是說,在運行時,是無法擷取到具體的T和U是什麼類型的。

但運行時,在方法內部的局部變數的泛型資訊是被全部擦除的。如上的注釋5中的list的具體類型是無法在運行時擷取到的。

真的無法擷取到泛型型別嗎

當時今日頭條的面試官問過我這個問題,我當時對泛型的認識比較淺薄,以為編譯器會將所有的泛型資訊擦除,那麼運行時也就無能擷取到具體的泛型型別了。但其實並不是這樣,如上面介紹到,JDK1.5之後,Class的格式有變化,編譯器會將聲明的類,介面,方法的泛型資訊保留到位元組碼當中。那麼通過反射,這些資訊還是可以擷取到的。但要擷取到具體的泛型型別,一般也只能擷取到繼承父類所使用的泛型型別。
比如:

public class SubClass extends Base<String> { }

那麼Base所綁定的泛型型別可以被擷取到的。對SubClass.class調用getGenericSuperclass可以擷取到T所綁定的類型。

        Type type = SubClass.class.getGenericSuperclass();        Type targ = ((ParameterizedType) type).getActualTypeArguments()[0];        System.out.println(type); // SubClass<java.lang.String>        System.out.println(targ); // class java.lang.String

具體的用法可以參考Gson和Guice的源碼:

  • https://github.com/google/guice/blob/abc78c361d9018da211690b673accb580a52abf2/core/src/com/google/inject/TypeLiteral.java#L94

  • https://github.com/google/gson/blob/master/gson/src/main/java/com/google/gson/internal/%24Gson%24Types.java

橋方法

為了使Java的泛型方法產生的位元組碼與1.5以前的位元組碼相相容,由編譯期自己產生的方法。顧名思義,橋方法是一座橋,溝通著泛型與多態。

可以通過Method.isBridge()方法來判斷一個方法是否是橋接方法,在位元組碼中橋接方法會被標記為ACC_BRIDGEACC_SYNTHETIC

public class Fruit<T> {    T value;    public T getValue() {        return value;    }}public class Apple extends Fruit<String> {    @Override    public String getValue() {        return "foo was call";    }}

反編譯產生的位元組碼:

public class Apple extends Fruit<java.lang.String> {  public Apple();    Code:       0: aload_0       1: invokespecial #1                  // Method Fruit."<init>":()V       4: return  public java.lang.String getValue();    Code:       0: ldc           #2                  // String calling       2: areturn  public java.lang.Object getValue();    Code:       0: aload_0       1: invokevirtual #3                  // Method getValue:()Ljava/lang/String;       4: areturn}

編譯器為我們自動產生了有一個橋方法,這個橋方法傳回型別為Object,內部調用了我們自訂的另一個getValue方法。

在Java代碼中,方法的特徵簽名只包括方法名稱,參數順序和參數類型,而位元組碼中的特徵簽名還包括方法傳回值和受查異常表。因此,橋方法public Object getValue()public String getValue()是可以被JVM區分而在同一個Class檔案中共存的。

由於編譯期泛型擦除機制,在父類中帶泛型參數的方法會被替換成Object類型。要讓子類重寫父類帶泛型參數的方法,需要通過橋方法直接複寫父類的方法,然後橋方法再調用子類自訂的方法,就以上面作為例子,子類Apple中的橋方法public Object getValue()直接override父類Fruit的public Object getValue(),然後橋方法內部再調用子類Apple的public String getValue()。因此,Java利用橋方法在保證多態機制不被破壞情況下實現了泛型。

參考
  • 1190000005337789
  • http://rednaxelafx.iteye.com/blog/586212
  • http://blog.csdn.net/mhmyqn/article/details/47342577#
  • http://blog.csdn.net/lonelyroamer/article/details/7868820

你真的懂Java泛型嗎

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.