一. Java 泛型的优点
泛型是 Java 5 的重要特性之一。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
Java 泛型的优点包括:
- 类型安全
- 消除强制类型转换
- 避免了不必要的装箱、拆箱操作,提高程序性能
- 提高代码的重用性
下面,以我的缓存框架 RxCache 中 Memory 接口为例:
代码语言:javascript复制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 的泛型有自己的特点。
例如,对于扩展函数涉及到泛型的类,需要指定泛型参数:
代码语言:javascript复制fun <T : View> T.longClick(block: (T) -> Boolean) = setOnLongClickListener { block(it as T) }
三. Java 通过类型擦除支持泛型
Java 为了兼容性的考虑,采用泛型擦除的机制来支持泛型。
泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,这个过程被称为类型擦除。
3.1 类型擦除
由于类型擦除,List<String> 和 List<Integer> 在编译后都会变成 List<Object>。
例如:
代码语言:javascript复制 List<String> list1 = new ArrayList<>();
list1.add("kotlin");
List<Integer> list2 = new ArrayList<>();
list2.add(1);
通过 javap -c 命令对代码进行反汇编:
代码语言:javascript复制 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 的类型,再次验证一下类型擦除。
代码语言:javascript复制 System.out.println(list1.getClass());
System.out.println(list2.getClass());
执行结果:
代码语言:javascript复制class java.util.ArrayList
class java.util.ArrayList
因此,List <T> 在运行时并不知道泛型参数的类型。
3.2 Java 数组并没有受到类型擦除的影响
例如:
代码语言:javascript复制 String[] array1 = new String[5];
array1[0] = "kotlin";
Integer[] array2 = new Integer[5];
array2[0] = 1;
通过 javap -c 命令对代码进行反汇编:
代码语言:javascript复制 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 的类型,
代码语言:javascript复制 System.out.println(array1.getClass());
System.out.println(array2.getClass());
执行结果:
代码语言:javascript复制class [Ljava.lang.String;
class [Ljava.lang.Integer;
由此可见,数组在运行时可以获得它的类型。因为,Java 数组是协变
的,所以 Java 数组不支持泛型。
协变是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。
四. Kotlin 如何获得声明的泛型类型
跟 Java 一样,Kotlin 也是通过类型擦除支持泛型。
但是 Kotlin 的数组支持泛型,因此它们并不会协变。例如:
代码语言:javascript复制 val array1 = arrayOf<Int>(1, 2, 3, 4)
val array2 = arrayOf<String>("1", "2", "3", "4")
Kotlin的泛型在运行时被擦除了,会带来一些影响。如何解决 Kotlin 类型擦除带来的影响呢?办法肯定是有的。
4.1 声明内联函数,使其类型不被擦除
刚才定义的两个数组使用了 arrayOf ,通过查看 arrayOf 的源码:
代码语言:javascript复制/**
* Returns an array containing the specified elements.
*/
public inline fun <reified @PureReifiable T> arrayOf(vararg elements: T): Array<T>
它使用inline
, 并且使用reified
标记类型参数。
打印 array1、array2 的类型:
代码语言:javascript复制 println(array1.javaClass)
println(array2.javaClass)
执行结果:
代码语言:javascript复制class [Ljava.lang.Integer;
class [Ljava.lang.String;
4.2 实例化类型参数代替类引用
再举一个 Kotlin 使用 Gson 的反序列化的例子,可以使用实例化类型参数 T::class.java
代码语言:javascript复制inline fun <reified T : Any> Gson.fromJson(json: String): T = Gson().fromJson(json, T::class.java)
总结:
本文介绍了 Java 和 Kotlin 的泛型以及类型擦除,并介绍了如何获得声明的泛型类型。
Kotlin 的泛型远不止这些,后续的文章会进一步介绍泛型的协变和逆变。