Kotlin 泛型 VS Java 泛型
Posted 码农乐园
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin 泛型 VS Java 泛型相关的知识,希望对你有一定的参考价值。
和 Java 泛型一样,Kotlin 泛型也是 Kotlin 语言中较难理解的一个部分。Kotlin 泛型的本质也是参数化类型,并且提供了编译时强类型检查,实际上也是伪泛型,和 Java 泛型类型一样。这篇文章将介绍 Kotlin 泛型里中的重要概念,以及与 Java 泛型的对比。
1. 泛型类型与泛型函数
Kotlin 下泛型类型与泛型函数的写法,与 Java 差不多,直接看下面的例子:
|
|
Kotlin 中泛型的类型参数如果可以推断出来,例如从构造函数的参数或者其他途径,允许省略类型参数:
|
|
通过 Tools -> Kotlin -> Show Kotlin Bytecode, 然后点击字节码上面的 Decompile 出 Java 代码可以看出与 Java 泛型的原理是一样的,都进行了类型擦除。
2. 泛型约束
Java 中可以通过有界类型参数来限制参数类型的边界,Kotlin 下泛型约束也可以限制参数类型的上界:
|
|
默认的上界是Any?
,是可空类型,如果确定为非空类型的话,应该使用<T: Any>
。
泛型约束中的尖括号中只能指定一个上界,如果需要多个上界,需要一个单独的 where 子句:
|
|
3. 使用处型变:类型投影
在 Java 泛型的通配符中有一个“Producer Extends, Consumer Super”原则,简称 PECS 原则:只读类型使用上界通配符? extends T
,只写类型使用下界通配符? super T
。Kotlin 中提供了类似功能的两个操作符out
和in
,分别生产和消费。
先看_Collection.kt
中一个扩展函数:
|
|
所以Array<out T>
相当于 Java 中的Array<? extends T>
,而Array<in T>
相当于 Java 中的Array<? super T>
,out 表示生产,用于只读类型,in 表示消费,用于只写类型。
类型投影和 Java 的上界通配符和下界通配符一样,只能用于参数、属性、局部变量或返回值的类型,但是不能用于泛型类型和泛型函数声明的类型,所以称之使用处型变。
4. 声明处型变
与 Java 有界通配符不能用于泛型声明时使用不同的是,Kotlin 中out
和in
两个型变注解还可以用于泛型声明时,更加灵活。下面通过 Java 和 Kotlin 中对 Collection 的定义来分析:
|
|
但是 Kotlin 中可以通过声明处型变(型变的概念在后面会详细解释)定义只读的集合:
|
|
Collection<out E>
使得 Collection 里面的元素是只读的,也使得 Collection<Number>
是 Collection<Int>
的父类,在 Kotlin 中称 Collection 的元素类型是协变的。对于协变的类型,通常不允许泛型类型作为函数的传入参数。
in
型变注解可以使得元素类型是逆变的,只能被消费,与协变相反,通常不允许泛型类型作为函数的返回值,看Comparable
的定义:
|
|
4.1 UnsafeVariance 注解
上面提到过对于协变的类型,通常不允许泛型类型作为函数的传入参数,对于逆变类型,通常不允许泛型类型作为函数的返回值,但是有时我们可以通过@UnsafeVariance
注解告诉 Kotlin 编译器:“我保证不会干坏事”,例如 Collection 的 contains 函数:
|
|
5. 星投影
使用泛型的过程中,如果参数类型未知时,在 Java 中可以使用原始类型(Raw Types),但是 Java 的原始类型是类型不安全的:
|
|
而在 Kotlin 中,在参数类型未知时,可以用星投影来安全的使用泛型:
|
|
对于ArrayList<*>
来说,因为不知道具体的参数类型,对于add(e E)
这种不安全的操作,Kotlin 编译器会直接报错,比 Java 的原始类型更安全。
Kotlin 中具体的星投影语法如下:
对于
Foo<out T>
,其中T
是一个具有上界TUpper
的协变类型参数,Foo<*>
等价于Foo<out TUpper>
。这意味着当T
未知时,你可以安全地从Foo<*>
读取TUpper
的值。对于
Foo<in T>
,其中T
是一个逆变类型参数,Foo<*>
等价于Foo<in Nothing>
。这意味着当T
未知时,没有什么可以以安全的方式写入Foo<*>
。对于
Foo<T>
,其中T
是一个具有TUpper
的不型变类型参数,Foo<*>
对于读取值时等价于Foo<out TUpper>
,而对于写值时等价于Foo<in Nothing>
。
如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影。例如,如果类型被声明为interface Function<in T, out U>
,我们可以想象以下星投影:
Function<*, String>
表示Function<in Nothing, String>
Function<Int, *>
表示Function<Int, out Any?>
Function<*, *>
表示Function<in Nothing, out Any?>
6. 型变的概念
在上面提到过使用处型变和声明处型变,那具体型变指什么呢?型变:是否允许对参数类型进行子类型转换。例如在 Java 中List<Integer>
与List<Number>
没有直接的类型关系,就是说 Java 中的泛型是不可以直接型变的。
为了提高代码的灵活性,Java 中可以通配符在使用时实现型变,例如void addNumbers(List<? super Number> list)
方法中可以传List<Integer>
,List<Integer>
是List<? super Number>
的子类型,而 Integer 也是 Number 的子类型,这也称之为协变。另外,List<Number>
是List<? super Integer>
的子类型,和 Integer 与 Number 之间的类型关系相反,称之为逆变。
Kotlin 中out
和in
操作符可以更简洁地实现 Java 的使用处型变,而且还支持声明处型变,这也使得 Kotlin 中的泛型是可以直接型变的。
Kotlin 下协变:interface List<out E>
,List<Int>
是`List<Number>
的子类型。
Kotlin 下逆变:interface Comparable<in T>
,Comparable<Double>
是Comparable<Number>
的父类型。
7. 具体化的类型参数
Kotlin 与 Java 中泛型都会进行类型擦除,泛型的具体类型在运行时是未知的,例如在解析 json 字符串时:
|
|
还必须传泛型的 Class 类型,不能直接使用T.class
获取类型,除非使用反射。
而在 Kotlin 中可以使用reified
修饰符将内联函数的泛型类型当作具体的类型来使用,不需要再额外传一个 class 对象:
|
|
对于具体化的类型参数,可以当做一个普通的类型一样,as
和!as
操作符也可以使用。因为 Kotlin 编译器会把内联函数的代码插入到调用者的地方,所以可以在编译期就确定泛型的类型。需要注意的是,Kotlin 中的reified
的内联函数不能被 Java 代码调用。
8. 小结
回顾 Kotlin 和 Java 中的泛型,Kotlin 泛型扩展了 Java 中的泛型,添加了使用处型变和更安全的星投影,还支持具体化的类型参数。我整理了下面表格对比两者:
Java 泛型 | Java 中代码示例 | Kotlin 中代码示例 | Kotlin 泛型 |
---|---|---|---|
泛型类型 | class Box<T> | class Box<T> | 泛型类型 |
泛型方法 | <K, V> boolean method(Pair<K, V> p) | fun <K, V> function(p: Pair<K, V>) | 泛型函数 |
有界类型参数 | class Box<T extends Comparable<T> | class Box<T : Comparable<T>> | 泛型约束 |
上界通配符 | void sumOfList(List<? extends Number> list) | fun sumOfList(list: List<out Number>) | 使用处协变 |
下界通配符 | void addNumbers(List<? super Integer> list) | fun addNumbers(list: List<in Int>) | 使用处逆变 |
无 | 无 | interface Collection<out E> : Iterable<E> | 声明处协变 |
无 | 无 | interface Comparable<in T> | 声明处逆变 |
原始类型 | ArrayList unkownList = new ArrayList<String>(5) | val unkownList: ArrayList<*> = ArrayList<Int>(5) | 星投影 |
总的来说,Kotlin 泛型更加简洁安全,但是和 Java 一样都是有类型擦除的,都属于编译时泛型。
以上是关于Kotlin 泛型 VS Java 泛型的主要内容,如果未能解决你的问题,请参考以下文章