Overview of Content
本文深入探討 Kotlin 與 Java 中泛型的各個方面,從泛型擦除到如何取得泛型信息,再到通配符的型變和泛型約束,以及 Kotlin 中的投影概念。
首先,我們深入研究 Java 泛型的擦除機制,看看在泛型類型與陣列類型的擦除機制差異。
接著,探討 Kotlin 如何取得泛型信息,包括使用 Class 對象獲取具體泛型和 Kotlin 特有的 inline
/ reified
關鍵字。在型變部分,我們深入了解 Kotlin 泛型通配符的協變(Covariance
)和逆變(Contravariance
),以及如何在泛型中設置約束。
最後,我們將焦點轉向 Kotlin 的投影概念,涵蓋了類型投影和星號投影。閱讀本文,將有助於讀者深入理解泛型在 Kotlin 與 Java 中的精隨,並應用於實際開發場景。
以下參考,Kotlin 進階實戰,如有引用參考本文章請詳註出處,感謝 🙂
Java 泛型可以參考 Java 泛型篇
Kotlin、Java 泛型擦除
Java 的泛型是 偽泛型,這是 Java 因為兼容性的考慮,泛型只會存在於 .java
階段,在進入 JVM 之前會將泛型轉為 Object 類(也就是在 JVM 運行中,是不會有泛型的)
Kotlin 同樣使用泛型擦除機制,所以以下舉例的 Java 例子也可以使用在 Kotlin 中
Java 泛型:擦除
● 當泛型指定為基礎類別(Integer
, Long
, String
... 等等),在編譯成 .class
之後就會被擦除,指定的泛型類別會轉為 Object 類(這個特點);範例如下…
public class JavaGeneral_1 {
public static void main(String[] args) {
// 指定泛型類為 String
List<String> list1 = new ArrayList<>();
list1.add("Alien");
// 指定泛型類為 Integer
List<Integer> list2 = new ArrayList<>();
list2.add(123);
System.out.println(list1.getClass());
System.out.println(list2.getClass());
}
}
透過反編譯後,我們可以看到 List
#add
方法的接收參數轉為 Object,而不是在源碼中的泛型指定類
Java 數組類型:不擦除
● 我們看另外一個擦除與否的案例「數組, Array
」;以結論來講 Java 在編譯時 不會將數組類型擦除,也就是數組類型會保留到運行時!
● Java 不支援泛型數組,所以要使用原生類型的數組
Java 數組類型範例如下
public class JavaGeneral_2 {
public static void main(String[] args) {
String[] strArray = new String[10];
Integer[] intArray = new Integer[10];
System.out.println(strArray.getClass());
System.out.println(intArray.getClass());
}
}
我們將編譯過後的 .class
檔反編譯,會發現 Java 簽名會呈現為 Array 的形式,並不會擦除(仍保留指定類型)
JVM 識別泛型:Class 常量池
● 前我們知道在經過編譯後 Java 會將泛型擦除(轉為 Object 類),那 JVM 如何知道該 Object 類指定的泛型是哪個呢?明明已經被擦除了呀?
Java 在運行時使用泛型時已經被擦除,但編譯完畢的 .class
文件中還是會保存了泛型相關的訊息 (保存在 class 的常量池中)
● 使用泛型的程式在編譯後會產生簽名 (
signature
) 字段,簽名會指向在常量池中實際的類型,之後就可以透過該類型強制轉型
Kotlin 取得泛型類型
Java/Kotlin 類:Class 獲得具體泛型
● 不管是匿名類,或是實體類都可以透過 Class 物件來獲取泛型的資訊
取得泛型資訊的函數 | 簡介 |
---|---|
Class#getGenericSuperclass | 取得類的類型(包含泛型資訊) |
ParameterizedType#getActualTypeArguments | ParameterizedType 是 Type 的子類,表示 參數的類型 (可取得泛型的具體類型) |
A. Java 匿名類取得泛型資訊:範例如下
class Generic_1 {
private static class GenericClz<T, R> { }
public static void main(String[] args) {
// 匿名類
GenericClz<Integer, String> genericClz = new GenericClz<>() { };
// 取得類的泛型的類型
Type genericSuperclass = genericClz.getClass().getGenericSuperclass();
System.out.println("Generic class: " + genericSuperclass);
if(genericSuperclass instanceof ParameterizedType) {
// 轉譯為 ParameterizedType
// 取得實際的泛型類型
Type[] actualTypeArguments = ((ParameterizedType) genericSuperclass).getActualTypeArguments();
for(Type t: actualTypeArguments) {
// 取得泛型類型 Generic type
System.out.println("actualTypeArguments class: " + t);
}
}
}
}
使用 Kotlin 匿名類,也可以達到同樣的效果,範例如下
object Generic_1_kt {
open class GenericClz<T, S>
@JvmStatic
fun main(args: Array<String>) {
// 匿名類
val genericClz = object : GenericClz<Int, String>() { }
val typeClz = genericClz.javaClass.genericSuperclass
println(typeClz)
when(typeClz) {
is ParameterizedType -> {
for (i in typeClz.actualTypeArguments) {
println(i)
}
}
}
}
}
B. Java 繼承泛型:當有一個類繼承於泛型類,並且有指定泛型類型時,也可以透過相同的 API 來取得泛型資訊;範例如下
class Generic_2 {
// 基礎泛型
private static class GenericClz_2<T, R> { }
// 繼承泛型
private static class ChildGeneric extends GenericClz_2<String, Long> { }
public static void main(String[] args) {
ChildGeneric genericClz = new ChildGeneric();
Type genericSuperclass = genericClz.getClass().getGenericSuperclass();
System.out.println("Generic class: " + genericSuperclass);
if(genericSuperclass instanceof ParameterizedType) {
Type[] actualTypeArguments = ((ParameterizedType) genericSuperclass).getActualTypeArguments();
for(Type t: actualTypeArguments) {
// 取得 Generic type
System.out.println("actualTypeArguments class: " + t);
}
}
}
}
● 這也就證明了 Class 類會保存泛型的相關資訊
使用 Kotlin 也可以達到同樣的效果,範例如下
object Generic_2_kt {
// 基礎泛型
open class GenericClz_2<T, S>
// 繼承泛型
class ChildGenericClz : GenericClz_2<String, Long>()
@JvmStatic
fun main(args: Array<String>) {
val genericClz = ChildGenericClz()
val typeClz = genericClz.javaClass.genericSuperclass
println(typeClz)
when(typeClz) {
is ParameterizedType -> {
for (i in typeClz.actualTypeArguments) {
println(i)
}
}
}
}
}
Kotlin 泛型數組
● 我們知道 Java 不支援泛型數組(上面小節有說到),不過 Kotlin 支持泛性數組!!
fun main() {
// 創建數組, java/kotlin 都保留簽名
val array1 = arrayOf(1, 2, 3, 4, 5)
val array2 = arrayOf("A", "B", "C", "D", "E")
println(array1.javaClass)
println(array2.javaClass)
// 創建泛型數組, java 不能這樣寫!不過 kotlin 可以!
val list1 = listOf<Int>(1, 2, 3, 4, 5)
val list2 = listOf<String>("A", "B", "C", "D", "E")
println(list1.javaClass)
println(list2.javaClass)
}
如同 Java,數組會保持簽名
● 從上面可以發現
arrayOf
沒有擦除, 而listOf
被擦除,而它們有以下差異
函數 是否使用 inline
是否使用 reified
(具體化)arrayOf
Y Y listOf
N N 可以看到
arrayOf
有使用inline
、reified
關鍵字,而這兩個關鍵字就可以用來保留泛型類型(以下小節會說明這兩個關鍵字)// arrayOf 原型 public inline fun <reified @PureReifiable T> arrayOf(vararg elements: T) : Array<T> // listOf 原型 public fun <T> listOf(vararg elements: T): List<T> = if (elements.size > 0) elements.asList() else emptyList()
Kotlin 泛型方法:inline / reified 關鍵字
● 如果要 保留類型不被擦除就需要使用 inline
+ reified
描述,這樣才能 不透過實例(instance
) 直接取得泛型 Class!
inline fun <reified T : Any> Gson.formJson(jsonStr: String) : T =
// 這樣才能直接取得泛型 Class
Gson().formJson(json, T::class.java)
● 再一個 inline
+ reified
的使用範例,加強印象
class Test {
inline fun <reified T> toClassArray(vararg elements: T) : List<Class<*>> {
val res = mutableListOf<Class<*>>()
// 直接取得泛型 T 的實際類型
val clz = T::class.java
for (i in elements) {
res.add(i!!::class.java)
}
return res.toList()
}
}
fun main() {
val list = Test().toClassArray<Int>(1, 2, 3, 4)
for(i in list) {
println(i)
}
}
● 沒有
reified
就無法透過 泛型 取得 class
Kotlin 通配符:型變
通配符以 Java 來說就是 ?
符號,可以用在讓 泛型產生繼承關係 的指定,加強靜態語言的檢查特性
Kotlin 中是指 類型 轉換後的繼承關係
類 Class & 類型 Type
● 在 Kotlin 中,類 & 類型是兩種不同的概念(或是說它強調了兩者的不同概念)
● 類型(Type
):具有相同特性的物件,強調了「特性」的概念,在程式中的表達方式通常可以使用 interface
、abstract
描述特性
● 類(Class
):類是類型的實現,提供「具體」的數據結構、方法集合,提供共同方法、操作,它說明了如何使用物件
● 所以子類(
SubClass
) & 子類型(SubType
)有很大的區別所有出現 Type 的地方可被另一個 SubType 取代;它們有相同的特性。在程式中,任何出現類型的地方都可以被另一個子類型取代
以 Java 泛型來舉例:
List<? extends Hello>
,可使用取代?
符號的類型都是 Hello 或是 Hello 的子類
● 接著我們再來看 Kotlin 的「可空類型, Nullable Type
」與類之間的關係
Kotlin 的「可空類型」代表的是一種類型(Type
),它說明的是該類型是「可空的特性」,可空類型本身並不代表一個具體的類,而是類型的一種特性,但這個 可空類型並非代表一個類!!如下表所示
\ | Class 類 | Type 類型 |
---|---|---|
String | Y | Y |
String? | N | Y |
List | Y | Y |
List<String> | N | Y |
Kotlin 泛型通配符:Covariance
協變 out
● 先來看看 Java 的「通配符上限, extends
」;如果沒有通配符限制,每個類型都是單獨個體,不會有任何關係
public class JavaType {
// 父類
private static class Animal { }
// 子類
private static class Dog extends Animal { }
public static void main(String[] args) {
List<Animal> animalList = new ArrayList<>();
List<Dog> dogList = new ArrayList<>();
// Error
animalList = dogList;
}
}
如下圖,我們可以看到,類有關係(上面案例中是繼承關係)不代表類型有關係!
要讓「類 & 類型」產生關係就要使用 Java 的通配符 ?
,這樣就可以建立「類」與「類型」的產生關係
public class JavaType {
// 父類
private static class Animal { }
// 子類
private static class Dog extends Animal { }
public static void main(String[] args) {
// 使用通配符!
List<? extends Animal> animalList = new ArrayList<>();
List<Dog> dogList = new ArrayList<>();
// OK
animalList = dogList;
}
}
● Kotlin 中實現同樣的效果則是使用 out
關鍵字;像是 List<out E>
就有使用到 out
關鍵字
Kotlin 的 List 源碼:可以看到 <out E>
關鍵字,它與 Java 的 <? extends E>
有相同的功能,它代表了可以 安全取值!
// List 源碼
public interface List<out E> : Collection<E> {
// Query Operations
override val size: Int
override fun isEmpty(): Boolean
override fun contains(element: @UnsafeVariance E): Boolean
override fun iterator(): Iterator<E>
// Bulk Operations
override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
public operator fun get(index: Int): E
public fun indexOf(element: @UnsafeVariance E): Int
... 省略部分
}
●
@UnsafeVariance
是幹嘛的?主要因為這個操作可能違反規則,用來告訴編譯器這個操作由自己負責
違反規則的操作:像是我可以在
contains
函數中修改原來元素的內容,這就不安全
同上面 Java 的程式,但我們現在用 Kotlin 來撰寫,可以看到編譯結果是完全正常不會報錯的!
fun main() {
open class Animal
class Dog : Animal()
// 由於協變
// 所以 List<Animal> 相當於寫了 List<? extends Animal>
var animalList : List<Animal> = ArrayList()
val dogList = ArrayList<Dog>()
// Okay, Pass ~
animalList = dogList
}
這是因為在 Kotlin 中,對於擁有不同類型參數的泛型類型,如果 泛型參數的類型是協變的(out
),則泛型類型本身也是協變的
// 由於協變的關係,Kotlin 會讓 List<Animal>、 List<Dog> 自動產生關係
List<Animal>
List<Dog>
換句話說
List<Dog>
是List<Animal>
的子類型,因為 List 在 Kotlin 中被聲明為<out E>
的形式,這也就是為什麼我們可以將 dogList 分配給 animalList,而不需要使用通配符
● 協變(
Covariance
)結論:如果 A 是 B 的子類(Class 關係),則在 Kotlin 中,協變使用
out
關鍵字表示泛型;這意味著,將List<A>
視為List<B>
的子類型(Type 關係)是合法的
Kotlin 泛型通配符:Contravariance
逆變 in
● 一樣先來看看 Java 的「通配符下限, super
」;如果沒有通配符限制,則每個類型都是單獨個體,不會有任何關係;我們先來看看 Java 範例…
class JavaType_2 {
private static class Animal { }
private static class Dog extends Animal { }
public static void main(String[] args) {
List<? extends Animal> animalList = new ArrayList<>();
// 無法安全的設定
animalList.add(new Dog()); // Error
}
}
為甚麽 add
會被編譯器警告?這是 因為不知道加入 AnimalList
的具體子類型,所以無法加入到 List 列表
使用 <? surper E>
修正就可正常設定進列表;這保證了設定進列表中的元素一定是 E
or E 的父類
! 這相當於 OOP 類設計原則中的 里式原則的一個展現
class JavaType_2 {
private static class Animal { }
private static class Dog extends Animal { }
public static void main(String[] args) {
// 修正
List<? super Animal> animalList = new ArrayList<>();
animalList.add(new Dog()); // Ok
}
}
● 從這邊也可以看出
<? extends>
、<? super>
是相互對應(正交)關係●
<? extends>
用來安全取值●
<? super>
用來安全的設定值
● Kotlin 也可以使用 in
關鍵字達到上述 super
的效果
interface IPrint<in E> {
fun print(e: E)
}
class AddInfoPrint : IPrint<String> {
override fun print(e: String) {
println("Hello: $e")
}
}
class CountPrint : IPrint<Int> {
override fun print(e: Int) {
for (i in 0 until e) {
println("times: ${i + 1}")
}
}
}
fun main() {
val info = AddInfoPrint()
info.print("123")
val count = CountPrint()
count.print(3)
}
● 如果改成
out
就不能安全取值,編譯就會出錯
Kotlin 泛型約束
上面我們提到的是泛型的通配符(主要是讓類型 Type 產生繼承關係),而這裡要講的則是泛型的 約束
Java 中使用 <T extends E>
來約束能接收的參數只能是 E 或 E 的子類;在 Kotlin 中使用 :
符號來進行約束
● 不能使用 super 限制,只能使用 extends
Kotlin 泛型限制範例
● Kotlin 使用 :
符號限制傳入的類型(就像是 extends
)
fun <T : Number> sum(vararg param: T) = param.sumOf {
it.toDouble()
}
fun main() {
val res = sum(1, 2, 3, 0.1) // 所有傳入的參數都是 Number 的子類
println(res)
}
Kotlin 投影
投影是 Kotlin 特殊的概念,可以用來 限制泛型類型,而不影響原先的類型!
限制類型:類型投影
● Kotlin 的 MutalbleList<T>
是泛型類,並且沒有用通配符(<in>
、<out>
)限制;這時我們可以透過對泛型 <T>
限制來達到泛型限制
fun main() {
val list1 : MutableList<String> = mutableListOf()
list1.add("Hello")
list1.add("World")
// 可安全的取值
val list2 : MutableList<out String> = mutableListOf()
list2.add("111") // Error
list2.add("831") // Error
// 可安全的設定值
val list3 : MutableList<in String> = mutableListOf()
list3.add("555")
list3.add("666")
lateinit var list4 : MutableList<String>
list4 = list3 // Error
}
A. 由於 list2
使用 <out>
來描述,所以不可以設定值
B. 進行投影過限制過的屬性不等於原先類型
● 投影時必須直接指定類型,不可寫在指定屬性時
限制類型:星號投影
● 如果我們要在 Java 的通配符中不指定任何類型,我們就會使用 <?>
符號來表示,用來接收任何類型的泛型;範例如下
public class Java_Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("AAA");
list.add("BBB");
list.add("CCC");
showList(list);
List<Integer> list2 = new ArrayList<>();
list2.add(111);
list2.add(222);
list2.add(333);
showList(list2);
}
// 不限定類型
private static void showList(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
}
● Kotlin 中使用 <*>
符號來取代 <?>
符號
fun showList_1(list: MutableList<*>) {
for (i in list) {
print(i)
}
println()
}
// `<*>` 意思也等同於 `<out Any?>`
fun showList_2(list: MutableList<out Any?>) {
for (i in list) {
print(i)
}
println()
}
fun main() {
val intList = mutableListOf<Int>(1, 2, 3, 4, 5)
val strList = mutableListOf<String>("A", "B", "C", "D", "E")
showList_1(intList)
showList_1(strList)
showList_2(intList)
showList_2(strList)
}
像是這種不限定類型的投影,只可以用來取得,不可用在寫入 (
out
跟extends
的概念類似)
更多的 Kotlin 語言相關文章
在這裡,我們提供了一系列豐富且深入的 Kotlin 語言相關文章,涵蓋了從基礎到進階的各個方面。讓我們一起來探索這些精彩內容!
Kotlin 語言基礎
● Kotlin 語言基礎:想要建立堅實的 Kotlin 基礎?以下這些文章將帶你深入探索 Kotlin 的關鍵基礎和概念,幫你打造更堅固的 Kotlin 語言基礎
Kotlin 特性、特點
● Kotlin 特性、特點:探索 Kotlin 的獨特特性和功能,加深對 Kotlin 語言的理解,並增強對於語言特性的應用
Kotlin 進階:協程、響應式、異步
● Kotlin 進階:協程、響應式、異步:若想深入學習 Kotlin 的進階主題,包括協程應用、Channel 使用、以及 Flow 的探索,請查看以下文章