Kotlin 和 Java 泛型的侷限性、泛型擦除、星投影

語言: CN / TW / HK

Hi 大家好,我是 DHL。公眾號:ByteCode ,專注分享有趣硬核原創內容,Kotlin、Jetpack、效能優化、系統原始碼、演算法及資料結構、動畫、大廠面經

全文分為 影片版文字版

  • 文字版: 文字側重細節和深度,有些知識點,影片不好表達,文字描述的更加準確
  • 影片版: 影片會更加的直觀,看完文字版,在看影片,知識點會更加清楚

影片版 bilibili 地址: http://b23.tv/fGXnKn1

在之前的文章 Kotlin 和 Java 泛型的缺陷和應用場景 中介紹了:

  • 為什麼要有泛型
  • Kotlin 和 Java 的協變和逆變的區別和應用場景,
  • Java 陣列協變的缺陷
  • 萬用字元 <? extends><? super><out><in> 的區別和應用場景

而今天這篇文章我們主要介紹泛型擦除和它的侷限性,所以通過這篇文章你將學習到以下內容:

  • 能直接例項化泛型嗎?
  • 泛型被擦除之後,一定會編譯成 Object 型別嗎?
  • 為什麼無法獲取泛型 Class 型別?
  • 為什麼要擦除掉泛型?
  • 泛型資訊被擦除了之後,泛型資訊真的不存在了嗎?
  • 迷惑的萬用字元 ? 號和星投影

泛型擦除我相信對於每個開發者並不陌生,先寫一段示例程式碼,例項化泛型 <T>,我們花三秒鐘思考一下,下面的程式碼是否可以正常編譯。

```kotlin // Java public class GenericPersonJava { GenericPersonJava() { T t = new T() } }

//Kotlin class GenericPersonKt { init { val t: T = T() } } ```

編譯會出錯,因為 <T> 被擦除之後,會將 <T> 編譯成 Object, JVM 指令如下圖所示。

如果傳入的泛型引數是 String, 那麼泛型被擦除之後,會編譯成 Object 型別,型別顯然不對,無論是 Java 還是 Kotlin 在編譯階段都無法確定泛型引數的型別,所以為了防止型別問題,編譯器不支援直接對泛型例項化,這也是泛型的侷限性,不能直接對泛型例項化

如果想例項化泛型,Java 和 Kotlin 通用解決方案,通過 Class 反射的方式來例項化泛型 <T>,我們修改一下上面的程式碼,即可正常編譯執行。

```kotlin // Java public class GenericPersonJava { GenericPersonJava(Class classes) { T t = classes.newInstance(); } }

// kotlin class GenericPersonKt(classes: Class) {
init {
val t: T = classes.newInstance()
}
} ```

我們在思考一下泛型 <T> 被擦除之後,一定會編譯成 Object 型別嗎? 修改一下上面的程式碼,給泛型新增上界,在花 3 秒鐘思考一下編譯後的泛型 <T> 是什麼型別。

```kotlin // Java public class GenericPersonJava {

}

// Kotlin class GenericPersonKt() {

} ```

修改後的程式碼,給泛型添加了上界,即泛型 <T> 繼承自 String,泛型被擦除之後,會被編譯成 String 型別,如下圖所示。

因此我們可以得出一個結論,無論是 Java 還是 Kotin:

  • 如果泛型沒有指定上界 <T>,泛型被擦除之後,會被編譯成 Object 型別
  • 如果泛型指定了上界,例如 <T : String>,泛型被擦除之後,會被編譯成 String 型別

無法獲取泛型 Class 型別

我們在來介紹一下泛型另外一個侷限性無法獲取泛型 Class 型別,先寫一段程式碼,我們在花 3 秒鐘思考一下,以下程式碼輸出的結果是什麼。

```kotlin // Java public static void main(String... args) { List p1 = new ArrayList(); List p2 = new ArrayList(); System.out.println(p1.getClass() == p2.getClass()); }

// Kotlin fun main() { val p1: List = ArrayList() val p2: List = ArrayList() println(p1.javaClass == p2.javaClass) } ```

上面的程式碼 Java 和 Kotlin 輸出的結果都是 true,因為泛型被擦除了之後,無論 ArrayList<Integer> 還是 ArrayList<Double> 獲取 class 的時候,獲取到的都是同一個 ArrayList class

所以對於一個泛型 ArrayList<T> 無論 <T> 是什麼型別,編譯完了之後都會被擦除掉,最後獲取到的都是 ArrayList class 而不是 ArrayList<T> class

為什麼要擦除掉泛型?

泛型是 Java 1.5 之後引入的,在之前的版本是沒有泛型這個概念,所以為了相容之前的版本,因此在生成位元組碼的時候,將泛型資訊擦除掉了。

泛型資訊被擦除,真的不存在了嗎

我們在思考一下泛型資訊被擦除了之後,泛型資訊真的不存在了嗎?我們先寫一段程式碼,看一下編譯後的 JVM 指令。

kotlin public class GenericJava { public static void main(String... args) { ArrayList<Integer> p1 = new ArrayList<Integer>(); } }

生成的 JVM 指令如下圖所示。

標記 1 執行 New 命令建立了 ArrayList 物件,而不是 ArrayList<Integer>,由此可見泛型資訊被擦除了。我們繼續往下看有個 LocalVariableTableLocalVariableTypeTable

  • LocalVariableTable 就是我們常說的區域性變量表,儲存方法引數列表和方法內的區域性變數,按照宣告的順序儲存,它以陣列的形式展示,更多關於區域性變量表運算元棧的知識點,歡迎前往檢視另外一個篇文章 CPU 如何記錄函式呼叫過程和返回過程

  • LocalVariableTypeTable 它的資料結構和 LocalVariableTable 是一樣的,只不過它是用來儲存泛型資訊

正如圖中 標記 2 所示,我們在程式碼中編寫的泛型 ArrayList<Integer>,泛型資訊被擦除掉之後,會儲存到 LocalVariableTypeTable 中,所以並沒有真正意義上的將泛型相關的資訊抹除掉。正因為泛型資訊被儲存了下來,所以我們在執行時,可以通過反射獲取到泛型相關的資訊。

無論使用 Java 還是 Kotlin, 定義在型別的上的泛型、定義在方法引數的泛型,定義在方法返回值的泛型、定義在區域性變數的泛型、還是定義在全域性變數中的泛型,它們的泛型資訊都會儲存下來。如下圖所示。

? 號和星投影

Koltin 中的星投影,即泛型 <*>,其實等效於 Java 中的 <?> 號,用於表示不確定泛型是什麼型別的資訊。我們來看一段程式碼。

```kotlin // Java List<?> list;

// Kotlin val list: List<*> ```

上面的程式碼其實等效於下面的程式碼。

```kotlin // Java List<? extends Object> list;

// Koltin val list: List ```

  • Java 中的萬用字元 <?> 號等效於 <? extends Object>
  • Kotlin 中的萬用字元 <*> 號等效於 <out Any>

在 Java 中 Object 類是所有類的父類,在 Kotlin 中分為非空可空兩種型別,因此 Any 是所有非空型別的父類,而 Any? 是所有可空型別的父類。因此我們可以用 ObjectAny 來表示。

在 Java 中用萬用字元 ? extends 表示協變,而在 Kotlin 中關鍵字 out 表示協變,關於協變和逆變更多的知識點,可以前往檢視 90%的人都不知道的知識點,Kotlin 和 Java 的協變和逆變


我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿


全文到這裡就結束了,感謝你的閱讀,堅持原創不易,歡迎在看、點贊、分享給身邊的小夥伴,我會持續分享原創乾貨!!!

真誠推薦你關注我,公眾號:ByteCode ,持續分享硬核原創內容,Kotlin、Jetpack、效能優化、系統原始碼、演算法及資料結構、動畫、大廠面經。



近期必讀熱門文章