Kotlin 泛型之類型擦除

一. Java 泛型的優點

泛型是 Java 5 的重要特性之一。泛型的本質是參數化類型,也就是說所操作的數據類型被指定爲一個參數。這種參數類型可以用在類、接口和方法的創建中,分別稱爲泛型類、泛型接口、泛型方法。

Java 泛型的優點包括:

  • 類型安全
  • 消除強制類型轉換
  • 避免了不必要的裝箱、拆箱操作,提高程序性能
  • 提高代碼的重用性

下面,以我的緩存框架 RxCache 中 Memory 接口爲例:

package com.safframework.rxcache.memory;

import com.safframework.rxcache.domain.CacheStatistics;
import com.safframework.rxcache.domain.Record;

import java.util.Set;

/**
 * Created by tony on 2018/9/29.
 */
public interface Memory {

    <T> Record<T> getIfPresent(String key);

    <T> void put(String key, T value);

    <T> void put(String key, T value, long expireTime);

    Set<String> keySet();

    boolean containsKey(String key);

    void evict(String key);

    void evictAll();

    CacheStatistics getCacheStatistics();
}

通過該接口的定義,能夠看到使用泛型提高了代碼的重用性。

二. Kotlin 泛型

Kotlin 基於 Java 6,因此 Kotlin 天生支持泛型。但是 Kotlin 的泛型有自己的特點。

例如,對於擴展函數涉及到泛型的類,需要指定泛型參數:

fun <T : View> T.longClick(block: (T) -> Boolean) = setOnLongClickListener { block(it as T) }

三. Java 通過類型擦除支持泛型

Java 爲了兼容性的考慮,採用泛型擦除的機制來支持泛型。

泛型信息只存在於代碼編譯階段,在進入 JVM 之前,與泛型相關的信息會被擦除掉,這個過程被稱爲類型擦除。

3.1 類型擦除

由於類型擦除,List<String> 和 List<Integer> 在編譯後都會變成 List<Object>。

例如:

        List<String> list1 = new ArrayList<>();
        list1.add("kotlin");

        List<Integer> list2 = new ArrayList<>();
        list2.add(1);

通過 javap -c 命令對代碼進行反彙編:

    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #4                  // String kotlin
      11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      16: pop
      17: new           #2                  // class java/util/ArrayList
      20: dup
      21: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
      24: astore_2
      25: aload_2
      26: iconst_1
      27: invokestatic  #6                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      30: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      35: pop
      36: return

再對上述代碼分別打印 list1、list2 的類型,再次驗證一下類型擦除。

        System.out.println(list1.getClass());
        System.out.println(list2.getClass());

執行結果:

class java.util.ArrayList
class java.util.ArrayList

因此,List <T> 在運行時並不知道泛型參數的類型。

3.2 Java 數組並沒有受到類型擦除的影響

例如:

        String[] array1 = new String[5];
        array1[0] = "kotlin";

        Integer[] array2 = new Integer[5];
        array2[0] = 1;

通過 javap -c 命令對代碼進行反彙編:

    Code:
       0: iconst_5
       1: anewarray     #2                  // class java/lang/String
       4: astore_1
       5: aload_1
       6: iconst_0
       7: ldc           #3                  // String kotlin
       9: aastore
      10: iconst_5
      11: anewarray     #4                  // class java/lang/Integer
      14: astore_2
      15: aload_2
      16: iconst_0
      17: iconst_1
      18: invokestatic  #5                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      21: aastore
      22: return

從反彙編的角度可以看出,Java 數組並沒有受到類型擦除的影響。

再對上述代碼分別打印 array1、array2 的類型,

        System.out.println(array1.getClass());
        System.out.println(array2.getClass());

執行結果:

class [Ljava.lang.String;
class [Ljava.lang.Integer;

由此可見,數組在運行時可以獲得它的類型。因爲,Java 數組是協變的,所以 Java 數組不支持泛型。

協變是在計算機科學中,描述具有父/子型別關係的多個型別通過型別構造器、構造出的多個複雜型別之間是否有父/子型別關係的用語。

四. Kotlin 如何獲得聲明的泛型類型

跟 Java 一樣,Kotlin 也是通過類型擦除支持泛型。

但是 Kotlin 的數組支持泛型,因此它們並不會協變。例如:

    val array1 = arrayOf<Int>(1, 2, 3, 4)
    val array2 = arrayOf<String>("1", "2", "3", "4")

Kotlin的泛型在運行時被擦除了,會帶來一些影響。如何解決 Kotlin 類型擦除帶來的影響呢?辦法肯定是有的。

4.1 聲明內聯函數,使其類型不被擦除

剛纔定義的兩個數組使用了 arrayOf ,通過查看 arrayOf 的源碼:

/**
 * Returns an array containing the specified elements.
 */
public inline fun <reified @PureReifiable T> arrayOf(vararg elements: T): Array<T>

它使用inline, 並且使用reified標記類型參數。

打印 array1、array2 的類型:

    println(array1.javaClass)
    println(array2.javaClass)

執行結果:

class [Ljava.lang.Integer;
class [Ljava.lang.String;

4.2 實例化類型參數代替類引用

再舉一個 Kotlin 使用 Gson 的反序列化的例子,可以使用實例化類型參數 T::class.java

inline fun <reified T : Any> Gson.fromJson(json: String): T = Gson().fromJson(json, T::class.java)

總結:

本文介紹了 Java 和 Kotlin 的泛型以及類型擦除,並介紹瞭如何獲得聲明的泛型類型。

Kotlin 的泛型遠不止這些,後續的文章會進一步介紹泛型的協變和逆變。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章