聊聊Java泛型擦除那些事

版權申明】非商業目的註明出處可自由轉載
博文地址:https://blog.csdn.net/ShuSheng0007/article/details/89789849
出自:shusheng007

相關文章:
秒懂Java泛型
秒懂Java類型(Type)系統

概述

曾幾何時你是否在簡歷中寫道:精通Java、精通xx、精通oo 等等的字眼,說出來不怕丟人,本人也幹過。現在想想真是驚歎於當時的“勇氣”,或者是自大?亦或是無知? 如果爲了找工作,寫上這些還是情有可原的,畢竟大部分篩選簡歷的是那些毛都不懂的HR,但是你內心中還是要對精通二字懷有敬畏之情!不爲別的,就爲了你可以虛懷若谷,勇往直前,如果你都認爲自己精通了,估計就會坐井觀天,止步不前了。

今天讓我們聊聊Java泛型擦除那些事,就當做自己的學習筆記吧。

泛型

概念及使用方法不是本文的重點,如果對泛型的概念和使用有疑問請出門左轉查看 秒懂Java泛型。本文也不想爭論Java泛型實現機制的優劣,因爲研究不夠深入,不敢妄言。我們就現在Java泛型的實現方式聊聊其中的一些事。

泛型信息運行時擦除

Java 泛型在運行時擦除這一點應該大部分開發者都有一定了的解,那麼什麼叫擦除了呢?就是泛型信息在源代碼裏面有,而在生成的字節碼裏面就沒有了,之所以這麼幹主要是爲了向下兼容老的java版本。那問題就很明瞭了,字節碼中都不存在泛型的信息了,而我們的運行時對象是JVM從字節碼裝載進來的,那自然也就沒有泛型信息了。看下面代碼

 ArrayList<String> arrayString=new ArrayList<String>();     
 ArrayList<Integer> arrayInteger=new ArrayList<Integer>();     
 System.out.println(arrayString.getClass()==arrayInteger.getClass()); 

上面代碼輸入結果爲 true,可見通過運行時獲取的類信息是完全一致的,泛型類型被擦除了!

至此,我以爲我精通了Java 泛型,直到有一天我寫了 秒懂Java類型(Type)系統 這篇文章,我變的很困惑。不是說Java的泛型是運行時擦除的嗎,爲什麼我還能通過反射獲取到泛型信息,例如我有如下代碼

public class TypeTest<T>{
      public List<T> list;
}

我通過反射可以獲取到這list的聲明類型 java.util.List<T>

Field list = TypeTest.class.getField("list");
Type genericType1 = list.getGenericType();
System.out.println("參數類型1:" + genericType1.getTypeName()); 

輸出

參數類型1:java.util.List<T>

是不是很奇怪,不是被擦除了嗎?這是爲什麼?

泛型信息聲明時保留

通過仔細思考上面對泛型擦除的原理的理解:泛型擦除是泛型信息在源代碼裏面有,而在生成的字節碼裏面就沒有了,我們會提出疑問。難道這種情況下,泛型的信息被保留到了字節碼裏面去了嗎?如果你對一個問題有疑問那就去親自試一下。

第一:定義一個非泛型類,然後查看其生成的字節碼文件

public class FruitsContainer{
    public Number mNum;
}

我用 Bytecode Viewer 這個工具來查看生成的字節碼文件FruitsContainer.class,如下圖所示:第一部分是源代碼,第二部分是從class文件中反編譯出的代碼,第三部分是字節碼文件內容。
在這裏插入圖片描述
我們其實要重點關注的是字節碼的那部分,看與其泛型版本的異同

第二:定義一個泛型類,然後查看其生成的字節碼文件

public class Fruit {
}

public class FruitsContainer<T extends Fruit>{
    public T t;
}

如下圖所示:
在這裏插入圖片描述
我們先看最右側的原始的字節碼文件有何異同。可以看到,在第三行多了&<T:Ladvanced/Fruit:>,下面還有幾處帶<>的地方,這些就是被保留了的泛型信息。其實從第二部分反編譯後的源碼更容易看出,多了很多Signature.

可見,在這種情況下,泛型的信息被保留到了字節碼文件中。

結論

關於Java泛型有兩種請求:

  1. 聲明側泛型信息保留
    例如聲明泛型接口,泛型類,泛型方法時的泛型信息
  2. 使用側泛型信息擦除
    例如方法的局部變量等。

獲取並使用泛型

下面舉一個如何獲取並使用泛型信息的例子,接着上面的代碼,我們定義如下兩個類

public class Apple extends Fruit {
}

public class AppleContainer extends FruitsContainer<Apple> {
    public AppleContainer appleContainer;
}

其中AppleContainer繼承至FruitsContainer 泛型類.我們可以通過如下代碼獲得AppleContainer帶泛型信息的父類FruitsContainer<advanced.Apple>

    private static void genericTest(){
        Type superType=(AppleContainer.class.getGenericSuperclass());
        System.out.println(superType);

        if (superType instanceof ParameterizedType){
            ParameterizedType paramType=(ParameterizedType)superType;
            System.out.println(Arrays.toString(paramType.getActualTypeArguments()));
        }
    }

輸出:

advanced.FruitsContainer<advanced.Apple>
[class advanced.Apple]

可見,我們是有能力在運行時獲取聲明類型的泛型信息的。

使用場景

那麼在運行時獲取泛型信息有什麼用呢?我現在遇到過兩個使用場景。

  1. 對泛型類型進行校驗
    Retrofit2這個網絡請求庫中大量使用,例如其要求定義的方法返回類型必須是泛型的,例如Call<T>,而不能是Call。它還會校驗T是否爲合法的類型.
  2. 序列化和反序列化
    例如使用Gson時,我們需要將·json·轉換爲Java對象,假設這個對象是帶泛型信息,就得告訴框架要轉換的那個java對象的泛型類型。
List<CommentBean>= fromJson("json...",
                                new TypeToken<ArrayList<CommentBean>>(){}.getType());

上面那段反序列化的代碼就是要告訴Gson框架泛型參數爲ArrayList<CommentBean>>。這裏面又有一個很有趣的點,就是 new TypeToken<ArrayList<CommentBean>>(){} 而不是 new TypeToken<ArrayList<CommentBean>>()
加上一對{}就表示是匿名類,那樣通過編譯器編譯後才能正確獲取到其泛型信息。

例如我們有如下代碼,那麼大家想一想,輸出是什麼呢?

  System.out.println(new FruitsContainer<Apple>().getClass().getGenericSuperclass());
  System.out.println(new FruitsContainer<Apple>(){}.getClass().getGenericSuperclass());

輸出:

class java.lang.Object
advanced.FruitsContainer<advanced.Apple>

留個思考題,爲什麼會這樣?有興趣的可以在評論區討論,其實上面已經給出了提示。

總結

如果你仍然在做技術,那就努力提高你的技術,雖然真理無窮。但怕什麼真理無窮,進一寸有一寸的歡喜!

最後,求關注,求點贊!有任何疑問可以評論留言,我會盡力回覆的。

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