天天看點

2023年了面試官仍然在考泛型,你确定不了解下麼前言What-泛型&類型擦除是什麼?Why-為什麼泛型是由類型擦除來實作的?How-具體是如何擦除泛型的?最後的面經分享參考資料最後

前言

前幾天了解到 Java 中的泛型是僞泛型,實際上是通過類型擦除實作的。

然後呢,我去了解了類型擦除的概念。發現很多文章,說明了他是啥,優缺點,但是很少講明白具體的工作過程以及内部原理,是以有了這篇文章的誕生。

如果你有和我一樣的困惑,并且非常期望能得到解答,那就繼續看下去吧。

What-泛型&類型擦除是什麼?

分析原理之前,先複習下泛型&類型擦除的知識,溫故而知新嘛~

泛型是什麼

泛型的本質是參數化類型(Parameterized Type)的應用,也就是說把所操作的資料類型指定為一個參數。這個參數類型可以用在類、接口、方法的建立中,分别稱為泛型類、泛型接口、泛型方法。

這個概念,其實并不陌生,咱們常見的 List,Map 都使用了泛型,無形之中我們都有在使用。一般常用的泛型類型定義是有

T

E

K

V

啥的。

為什麼要出現了泛型

還沒引進泛型的時候,類似的需求,就是通過2個技術點來實作類型泛化。

  • java.lang.Object

    是所有類型的父類,是以轉型成任何對象都是有可能的。
  • 類型支援強制轉換

但是呢,這種轉換關系,隻能在運作期間才能被驗證是否正确,

ClassCastException

異常出現的機率就大大提高,而且隻能通過人力避免。顯然這種機制是有風險且很難改善的。是以就需要一個在編譯期間就能保證類型安全的機制出現,是以就出現了泛型機制。

類型擦除是什麼 類型擦除的本質就是将原有的類型參數替換成即非泛化的上界 (這個後面會詳細介紹)

在編譯成.class檔案的時機,編譯器擦除了泛型類型相關的資訊,是以在運作時不存在任泛型類型相關的資訊。這樣Java就不需要産生新的類型位元組碼, 所有的泛型類型最終都是一種原始類型,在Java運作時根本就不存在泛型資訊,是以說是以泛型技術實際上是java語言的一顆文法糖,Java實作的是一種 僞泛型機制,

例如:

//編譯的時候,變為List<Object>
List<String> strList = new ArrayList<>();

//編譯的時候,變為List<Object>
List<Integer> intList = new ArrayList<>();

           

Why-為什麼泛型是由類型擦除來實作的?

這個涉及到泛型引入的曆史。Java的泛型是在 jdk 1.5 引入的,在此之前已經過去了 10年,已經存在大量沒有使用泛型的代碼,是以為了能夠讓這些代碼和泛型互用,是以采取了這種方式。總結來說,就是新功能考慮到移植相容性,所做的決定。

這樣做有缺點嗎? 當然是有的呀,擦除之後失去了很多特性。 例如:

  • 不支援基本類型。 原因是由于泛型類型擦除後,變成了java.lang.Object類型,這種方式對于基本類型如int/long/float等八種基本類型來說,就比較麻煩,因為Java無法實作基本類型到Object類型的強制轉換。
  • 運作期間無法擷取泛型實際類型
    • 無法通過

      instanceof

      判斷
    • 無法通過

      getClass

      判斷實際類型
  • 為了保證類型安全,不能使用泛型類型參數建立執行個體 例如:

    T object=new T() ;

    不合法
  • 不能聲明泛型執行個體數組,這會導緻運作錯誤 例如:

    T[] numbers= new T[capcity];

    不合法
  • 無法重載泛型參數的方法
  • 在靜态的環境下不允許參數類型是泛型類型的 由于泛型類的所有執行個體都有相同的運作時類,是以泛型類的靜态變量和方法是被它的所有執行個體所共享的。既然是共享的你就沒有必要再重新定義一樣的泛型類型,那如果你不定義一樣的泛型類型,又達不到共享(或者說是一緻性),更沒有必要讓這種情況通過。是以,在靜态環境了類的參數被設定成泛型是非法的。
  • 泛型類對象無法被抛出或捕獲 因為泛型類不能繼承或實作Throwable接口及其子類。

How-具體是如何擦除泛型的?

該怎麼探索這一切?

首先可以明确的是時機!

  • 擦除時機:

    .java

    ->

    .class

2023年了面試官仍然在考泛型,你确定不了解下麼前言What-泛型&amp;類型擦除是什麼?Why-為什麼泛型是由類型擦除來實作的?How-具體是如何擦除泛型的?最後的面經分享參考資料最後

這個工作,是編譯器的工作,具體的執行方式一般是使用 javac指令是将源代碼程式設計成class位元組碼檔案

javac xxx.java
           

.class 檔案是二進制格式,打開是下面這個樣式的,正常人都隻能看懂前面的的

cafe babe

cafe babe 0000 0039 001b 0a00 0200 0307
0004 0c00 0500 0601 0010 6a61 7661 2f6c
616e 672f 4f62 6a65 6374 0100 063c 696e
6974 3e01 0003 2829 5609 0008 0009 0700
//等等.....
           

是以還需要借助

javap

指令。

  • javap :JDK自帶的反彙編器,可以檢視java編譯器為我們生成的位元組碼。通過它,我們可以對照源代碼和位元組碼,進而了解很多編譯器内部的工作

我們用到的指令是

//反編譯class檔案,并輸出Java位元組碼,還有豐富的中繼資料
javap -c -s -p -l -verbose xxx.class

           

但是,有人會嫌這太麻煩了是不是!!! 如果你像我一樣,用的IDE是Intellij idea, 會有更友善的方法。

  1. 標明要檢視的 Java檔案
  2. 确認已經在項目的

    out

    目錄下生成了對應的 class 檔案 (我的方式,是運作一下包下的随便一個可執行java類就OK了,肯定有其他方法吧但我不知道)
  3. View->Show Bytecode 就可以展示位元組碼啦
2023年了面試官仍然在考泛型,你确定不了解下麼前言What-泛型&amp;類型擦除是什麼?Why-為什麼泛型是由類型擦除來實作的?How-具體是如何擦除泛型的?最後的面經分享參考資料最後

泛型類型被擦除成什麼了?

看我們一個 Java 檔案的例子實作

public class DRCommonGeneric<T> {
    T t;
}
           

檢視位元組碼,發現泛型 T 被編譯後,類型擦除為 Object

2023年了面試官仍然在考泛型,你确定不了解下麼前言What-泛型&amp;類型擦除是什麼?Why-為什麼泛型是由類型擦除來實作的?How-具體是如何擦除泛型的?最後的面經分享參考資料最後

Java 源代碼中的 List 就是使用的這種非限定泛型實作,

還記得上文說的麼

類型擦除的本質就是将原有的類型參數替換成即非泛化的上界

類型擦除也是有規則的,非泛化的上界的意思是說,如果使用了泛型限定符

<T extends XClass>

,會擦除為

XClass

。多個的場景下

<T extends AClass & BClass>

,會預設擦除為第一個。

例如

public class DRGeneric<T extends Number> {
    T t;
}
           

擦除為:

2023年了面試官仍然在考泛型,你确定不了解下麼前言What-泛型&amp;類型擦除是什麼?Why-為什麼泛型是由類型擦除來實作的?How-具體是如何擦除泛型的?最後的面經分享參考資料最後

也許你會問,那如果限定符

<T super XClass>

呢,哈哈,泛型類是不允許使用這種

<T super XClass>

修飾符的。

原因是隻限定了下限,是以上限最高還是

java.lang.Object

, 類型擦除以上限為準,

Object.class

又是所有類型的父類型,所有類型就都可以作為

T

,等于沒限定,是以是沒有意義的事情。

類型擦除是如果保證類型安全的

還是拿

List<T>

舉例,T 被擦除為 java.lang.Object,那代碼邏輯中真正執行個體化的類型是怎麼限制的呢?

例如:

List<String> list = new ArrayList();
list.add("aaaa");
System.out.println(list.get(0)); //list.get(0)得到是 Object,還是 String 呢?
list.add(1);//會報錯,為什麼呢?

           

看看位元組碼一探究竟

2023年了面試官仍然在考泛型,你确定不了解下麼前言What-泛型&amp;類型擦除是什麼?Why-為什麼泛型是由類型擦除來實作的?How-具體是如何擦除泛型的?最後的面經分享參考資料最後

注意看圖中标注的地方,編譯器除了類型擦除,在必要之處,還插入類型轉換以保持類型安全(

checkCast()

方法)

另外除了類型強轉,如果涉及到使用泛型類型作為參數的方法,子類做了重寫,編譯器還會自動生成橋方法以在擴充時保持多态性(

Bridge

關鍵字)

例如:

//父類
public class DRCommonGeneric<T> {
    T t;

    public void setT(T t) {
        this.t = t;
    }
}

//子類繼承泛型類,将泛型執行個體化為String
public class DRString extends DRCommonGeneric<String> {

    //同時重寫了setT
    @Override
    public void setT(String s) {
        super.setT(s);
    }
}

           
2023年了面試官仍然在考泛型,你确定不了解下麼前言What-泛型&amp;類型擦除是什麼?Why-為什麼泛型是由類型擦除來實作的?How-具體是如何擦除泛型的?最後的面經分享參考資料最後

泛型真的全部被擦除了嗎?

為啥有如此疑問,是因為泛型實際上是支援反射的!!!

拿上文中的

DRCommonGeneric.setT(T t)

舉例,雖然擦除為 object, 但是通過反射還是能拿到

T

這個我們定義的泛型類型,說明 T 這個參數類型也是在的。

try {
        Method method = DRCommonGeneric.class.getMethod("setT", Object.class);
        Type[] types = method.getGenericParameterTypes();
        for (Type type : types) {
            System.out.println("參數類型:"+type);

        }
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    }
           

輸出:

另外如果泛型聲明的時候指定了具體的類型,也是可以拿到使用時定義的真實類型。 例如:

// Test.class 中聲名了這個變量,T 被明切指定為String
DRCommonGeneric<String> dr = new DRCommonGeneric<>();

//反射擷取 Test.class 中的 dr 變量
Field field = Test.class.getDeclaredField("dr");
//擷取泛型參數真實類型
ParameterizedType pType = (ParameterizedType) field.getGenericType();
System.out.println(pType);
           

輸出:

2023年了面試官仍然在考泛型,你确定不了解下麼前言What-泛型&amp;類型擦除是什麼?Why-為什麼泛型是由類型擦除來實作的?How-具體是如何擦除泛型的?最後的面經分享參考資料最後

弱小無助的我,在對天呐喊:不是全部擦除了麼?反射那是怎麼拿到的!!!反射難道是牛到可以從無到有麼?!!!

反射表示這個鍋他不背。那咱們還是再仔細看看泛型的位元組碼實作。

通過搜尋,我發現位元組碼檔案中确實有泛型類型 [圖檔上傳失敗…(image-70cdd3-1677116394697)]

都是

signature

declaration

在用。原先看到添加了

//

注釋,我就給忽略了,看來并非這麼簡單,再次觸及知識盲區,我還是淺薄了啊。

截圖中它放入注釋中,實際上是 IDE 工具的優化,目的友善集中在 code 上去看了,實際上flags/signature都是屬于

中繼資料

。(換個“反彙編”工具發現的這一點…),declaration 沒有查詢到,應該也是這個IDE的自我發揮,不用管,主要核心還是看 signature 的邏輯。

signature 是什麼

在JVM中,所有的字段、方法、類都有屬于自己的簽名。比如方法簽名由方法名、參數、通路修飾符等構成,用于确定唯一的方法。而類的簽名主要是記錄一些JVM類型系統以外的額外的類型資訊,比如泛型的類型資訊,JVM不支援泛型,但是提供了class signature來存儲類泛型的類型資訊。

在代碼層面,實際上它是個屬性,

JDK的源碼中可以找到相關證據

  • java.lang.reflect.Field
  • java.lang.reflect.Method
  • java.lang.reflect.Constructor

其中都有一個成員變量

signature

,注釋都寫的一摸一樣,支援泛型和注解。

// Generics and annotations support
    private transient String    signature;
           

可以看到

signature

就是個字元串,是以不同的簽名有一定的解析規則,主要的邏輯在

sun.reflect.generics.parser.SignatureParser

類中。

簽名的實作有兩個,分别是方法簽名

MethodTypeSignature

和類簽名

ClassSignature

,變量雖然也有簽名,但是相對來說比較簡單,沒有專門的解析類。

為了友善了解,咱們還用上文例子,進行介紹

方法簽名: 方法聲明:public void setT(T t) 簽名字元串:(TT;)V

解析出來分為四個部分:

  • 泛型定義:無 (泛型方法才會有)
  • 參數類型: T
  • 傳回值類型:V 無傳回值
  • 異常類型:無

類簽名: 類聲明:public class DRCommonGeneric 簽名字元串:<T:Ljava/lang/Object;>Ljava/lang/Object; 解析出來分為三個部分:

  • 泛型定義:<T:Ljava/lang/Object;>
  • 繼承的類:無
  • 實作的借口:無

簽名的建立和儲存?

編譯時就會生成簽名。 這個就是例子的 class signature :

2023年了面試官仍然在考泛型,你确定不了解下麼前言What-泛型&amp;類型擦除是什麼?Why-為什麼泛型是由類型擦除來實作的?How-具體是如何擦除泛型的?最後的面經分享參考資料最後

儲存在常量池中

簽名的解析時機?

咱們通過反射調用泛型有關的API時,例如

method.getGenericParameterTypes()

就會觸發到簽名的解析,以拿到正确的參數類型。

例如反射調用

setT(T t)

方法, 主要流程就是:

  1. 通過

    hasGenericInformation

    判斷方法簽名中

    (TT;)V

    是否有存在泛型
  2. 通過

    getGenericInfo()

    解析方法簽名字元串,填充到對應的變量

    tree

    中 。
  3. computeParameterTypes

    方法中,将解析好的方法簽名

    tree

    ,通過

    getParameterTypes

    擷取到方法參數類型數組,也就是 [

    T

    ]。
  4. 此時還不知道

    T

    的實作類是什麼,是以再通過解析類簽名

    <T:Ljava/lang/Object;>Ljava/lang/Object;

    ,擷取

    T

    的實際類型

    java/lang/Object

  5. 放入parameterTypes 數組中傳回

以上就是

T

這個類型參數的儲存和解釋,上面的例子中其實還舉例了

DRCommonGeneric<String>

反射可以拿到

String

而不是

Object

。也是一樣的道理,将泛型資訊放入了

signature

中,進而得到了儲存。

2023年了面試官仍然在考泛型,你确定不了解下麼前言What-泛型&amp;類型擦除是什麼?Why-為什麼泛型是由類型擦除來實作的?How-具體是如何擦除泛型的?最後的面經分享參考資料最後

GSON 針對類型擦除的處理設計

著名的json解析工具 Gson,在反序列化的時候,也是使用同樣的原理來支援泛型的序列化。

例如泛序列化

List<T>

這類泛型集合:

List<xxx> resultList = gson.fromJson(json, new TypeToken<List<xxx>>() {
        }.getType());
           

new TypeToken<List<xxx>>() {}.getType()

這行代碼,重點是要注意到大括号

{}

,這是說明它不是一個執行個體化了一個對象,而是建立了一個匿名内部類,而且繼承于TypeToken

2023年了面試官仍然在考泛型,你确定不了解下麼前言What-泛型&amp;類型擦除是什麼?Why-為什麼泛型是由類型擦除來實作的?How-具體是如何擦除泛型的?最後的面經分享參考資料最後

TypeToken 内部也是使用泛型反射的API,差別在于使用了

getClass().getGenericSuperclass()

,先獲得帶有泛型的父類,然後再去拿泛型。

2023年了面試官仍然在考泛型,你确定不了解下麼前言What-泛型&amp;類型擦除是什麼?Why-為什麼泛型是由類型擦除來實作的?How-具體是如何擦除泛型的?最後的面經分享參考資料最後

這裡特殊設計的地方是使用了匿名内部類的方式,為什麼要這麼設計呢?

目的是為了實作完整的泛型聲明。

  • 使用匿名内部類實作:

    TypeToken<List<xxx>> typeToken = new TypeToken的匿名子類();

    聲明了泛型
  • 無匿名内部類實作:

    new TypeToken<List<xxx>>

    未聲明泛型

需要明确的是,編譯器隻會将聲明類型的泛型資訊進行儲存,放在 signature 屬性中。

//這是一個完整的泛型使用方式
List<xxx> list = new ArrayList<>();
           

相對比而言,下面就沒有聲明泛型,用的是原始類型List,編譯器不認為這個是個泛型需要特别處理

//左側沒有聲明泛型類型<xxx>
List list = new ArrayList<xxx>();
           

位元組碼驗證如下:

2023年了面試官仍然在考泛型,你确定不了解下麼前言What-泛型&amp;類型擦除是什麼?Why-為什麼泛型是由類型擦除來實作的?How-具體是如何擦除泛型的?最後的面經分享參考資料最後

是不是又get到了一些小細節~

總結

類型擦除的工作過程為:

  1. 檢查泛型類型,擷取目标類型
  2. 擦除類型變量,并替換為限定類型
    • 如果泛型類型的類型變量沒有限定(),則用Object作為原始類型
    • 如果有限定(),則用XClass作為原始類型
    • 如果有多個限定(T extends XClass1&XClass2),則使用第一個邊界XClass1作為原始類
  3. 在必要時,插入類型轉換以保持類型安全
  4. 在必要時,為泛型類的子類,生成橋方法以在擴充時保持多态性
  5. 編譯位元組碼中新增了Signature屬性記錄泛型資訊,儲存在常量池中,來解決泛型參數識别問題,是以通過反射手段可以擷取到參數化類型

另外需要注意,編譯器隻會将聲明類型的泛型資訊進行儲存。開源工具 Gson 處理泛型識别的時候借助匿名内部類的方式解決了這個細節問題。

最後的面經分享

泛型是日常開發中經常接觸到的知識,是以算是面試必問題吧。而且有些大廠考的很細(特别是位元組)。 一面的時候會通過代碼改錯的方式,考慮你對泛型的了解,避免你照本宣科背書。

例如:

//編輯報錯
Set<Bean> set =  new HashSet()

//應該明确泛型的實作類型
Set<Bean> set =  new HashSet<>()
           
List<String> strList =  new ArrayList<>()
List<Bean>  list= new ArrayList<>()

//說出result的值
boolean result = strList.getClass() == list.getClass()

//實際上類型擦除後,都是List
//就算問反射擷取List内部的 elementData ,結果也是一樣,因為都被擦除為 java.lang.Object
           

以上比較經典的面試題,當然還有其他的變種,等我看到值得參考的再補充吧,其實呢,原理都是一樣的。咱們隻有真正了解了,才能融會貫通,舉一反三,任它東南西北風。

與君共勉~

參考資料

Java 泛型,你了解類型擦除嗎?

作業5:Java編譯原理

泛型(泛型擦除、泛型可以反射、泛型的限制和問題)

Java泛型-4(類型擦除後如何擷取泛型參數)

IDE中 代碼提示與跳轉的原理是什麼?

Java泛型的類型擦除始末,找回被擦除的類型

如何了解ByteCode、IL、彙編等底層語言與上層語言的對應關系?

java裡JSON使用中TypeToken為什麼要用匿名内部類建立的真正原因(泛型擦除)

作者:段淺淺兒

連結:https://juejin.cn/post/7202560190750294075

最後

如果想要成為架構師或想突破20~30K薪資範疇,那就不要局限在編碼,業務,要會選型、擴充,提升程式設計思維。此外,良好的職業規劃也很重要,學習的習慣很重要,但是最重要的還是要能持之以恒,任何不能堅持落實的計劃都是空談。

如果你沒有方向,這裡給大家分享一套由阿裡進階架構師編寫的《Android八大子產品進階筆記》,幫大家将雜亂、零散、碎片化的知識進行體系化的整理,讓大家系統而高效地掌握Android開發的各個知識點。

2023年了面試官仍然在考泛型,你确定不了解下麼前言What-泛型&amp;類型擦除是什麼?Why-為什麼泛型是由類型擦除來實作的?How-具體是如何擦除泛型的?最後的面經分享參考資料最後

相對于我們平時看的碎片化内容,這份筆記的知識點更系統化,更容易了解和記憶,是嚴格按照知識體系編排的。

全套視訊資料:

一、面試合集

2023年了面試官仍然在考泛型,你确定不了解下麼前言What-泛型&amp;類型擦除是什麼?Why-為什麼泛型是由類型擦除來實作的?How-具體是如何擦除泛型的?最後的面經分享參考資料最後

二、源碼解析合集

2023年了面試官仍然在考泛型,你确定不了解下麼前言What-泛型&amp;類型擦除是什麼?Why-為什麼泛型是由類型擦除來實作的?How-具體是如何擦除泛型的?最後的面經分享參考資料最後

三、開源架構合集

2023年了面試官仍然在考泛型,你确定不了解下麼前言What-泛型&amp;類型擦除是什麼?Why-為什麼泛型是由類型擦除來實作的?How-具體是如何擦除泛型的?最後的面經分享參考資料最後

歡迎大家一鍵三連支援,若需要文中資料,直接點選文末CSDN官方認證微信卡片免費領取↓↓↓

2023年了面試官仍然在考泛型,你确定不了解下麼前言What-泛型&amp;類型擦除是什麼?Why-為什麼泛型是由類型擦除來實作的?How-具體是如何擦除泛型的?最後的面經分享參考資料最後