一. 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 的泛型遠不止這些,後續的文章會進一步介紹泛型的協變和逆變。