深入探究 Kotlin 與 Java 泛型:擦除、取得泛型類型、型變、投影 | 協變、逆變

深入探究 Kotlin 與 Java 泛型:擦除、取得泛型類型、型變、投影 | 協變、逆變

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#getActualTypeArgumentsParameterizedType 是 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
listOf

可以看到 arrayOf 有使用 inlinereified 關鍵字,而這兩個關鍵字就可以用來保留泛型類型(以下小節會說明這兩個關鍵字)


// 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:具有相同特性的物件,強調了「特性」的概念,在程式中的表達方式通常可以使用 interfaceabstract 描述特性

類(Class:類是類型的實現,提供「具體」的數據結構、方法集合,提供共同方法、操作,它說明了如何使用物件

所以子類(SubClass) & 子類型(SubType)有很大的區別

所有出現 Type 的地方可被另一個 SubType 取代;它們有相同的特性。在程式中,任何出現類型的地方都可以被另一個子類型取代

以 Java 泛型來舉例:List<? extends Hello>,可使用取代 ? 符號的類型都是 Hello 或是 Hello 的子類

接著我們再來看 Kotlin 的「可空類型, Nullable Type」與類之間的關係

Kotlin 的「可空類型」代表的是一種類型(Type),它說明的是該類型是「可空的特性」,可空類型本身並不代表一個具體的類,而是類型的一種特性,但這個 可空類型並非代表一個類!!如下表所示

Class 類Type 類型
StringYY
String?NY
ListYY
List<String>NY

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)
}

像是這種不限定類型的投影,只可以用來取得,不可用在寫入 (outextends 的概念類似)


更多的 Kotlin 語言相關文章

在這裡,我們提供了一系列豐富且深入的 Kotlin 語言相關文章,涵蓋了從基礎到進階的各個方面。讓我們一起來探索這些精彩內容!

Kotlin 特性、特點

Kotlin 特性、特點:探索 Kotlin 的獨特特性和功能,加深對 Kotlin 語言的理解,並增強對於語言特性的應用

Kotlin 進階:協程、響應式、異步

Kotlin 進階:協程、響應式、異步:若想深入學習 Kotlin 的進階主題,包括協程應用、Channel 使用、以及 Flow 的探索,請查看以下文章

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響