Kotlin 和 Java 泛型的侷限性、泛型擦除、星投影
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
//Kotlin
class GenericPersonKt
編譯會出錯,因為 <T>
被擦除之後,會將 <T>
編譯成 Object
, JVM 指令如下圖所示。
如果傳入的泛型引數是 String
, 那麼泛型被擦除之後,會編譯成 Object
型別,型別顯然不對,無論是 Java 還是 Kotlin 在編譯階段都無法確定泛型引數的型別,所以為了防止型別問題,編譯器不支援直接對泛型例項化,這也是泛型的侷限性,不能直接對泛型例項化。
如果想例項化泛型,Java 和 Kotlin 通用解決方案,通過 Class
反射的方式來例項化泛型 <T>
,我們修改一下上面的程式碼,即可正常編譯執行。
```kotlin
// Java
public class GenericPersonJava
// kotlin
class GenericPersonKt
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
// Kotlin
fun main() {
val p1: List
上面的程式碼 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>
,由此可見泛型資訊被擦除了。我們繼續往下看有個 LocalVariableTable
和 LocalVariableTypeTable
。
-
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?
是所有可空型別的父類。因此我們可以用 Object
和 Any
來表示。
在 Java 中用萬用字元 ? extends
表示協變,而在 Kotlin 中關鍵字 out
表示協變,關於協變和逆變更多的知識點,可以前往檢視 90%的人都不知道的知識點,Kotlin 和 Java 的協變和逆變。
我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿。
全文到這裡就結束了,感謝你的閱讀,堅持原創不易,歡迎在看、點贊、分享給身邊的小夥伴,我會持續分享原創乾貨!!!
真誠推薦你關注我,公眾號:ByteCode ,持續分享硬核原創內容,Kotlin、Jetpack、效能優化、系統原始碼、演算法及資料結構、動畫、大廠面經。
近期必讀熱門文章