Kotlin 中也有泛型的概念,和 Java 中的类似,但又不尽相同,一起来认识 Kotlin 中的泛型吧。
一、基本使用
通常我们会在类、接口、方法中声明泛型:
1、泛型类
class Animal<T> {}
2、泛型接口
interface IAnimal<T> {}
3、泛型方法
fun <T> initAnimal(param: T) {}
二、泛型约束
泛型约束表示我们可以指定泛型类型(T)的上界,即父类型,默认的上界为Any?
,如果只有一个上界可以这样指定:
fun <T : Animal<T>> initAnimal(param: T) {}
即Animal<T>
就是上界类型,这里使用了:
,在 Java 中对应extends
关键字,如果需要指定多个上界类型,就需要使用where
语句:
fun <T> initAnimal(param: T) where T : Animal<T>, T : IAnimal<T> {}
三、类型擦除
Kotlin 为泛型声明执行的类型安全检测仅在编译期进行, 运行时实例不保留关于泛型类型的任何信息。这一点在 Java 中也是类似的。
例如,Array<String>
、Array<Int>
的实例都会被擦除为Array<*>
,这样带来的好处是保存在内存中的类型信息也就减少了。
由于运行时泛型信息被擦除,所以在运行时无法检测一个实例是否是带有某个类型参数的泛型类型,所以下面的代码是无法通过编译的(Cannot check for instance of erased type: Array<Int>)
:
fun isArray(a: Any) {
if (a is Array<Int>) {
println("is array")
}
}
但我们可以检测一个实例是否是数组,虽然 Kotlin 不允许使用没有指定类型参数的泛型类型,但可以使用星投影*
(这个后边会说到):
fun isArray(a: Any) {
if (a is Array<*>) {
println("is array")
}
}
同样原因,由于类型被擦除,我们也无法安全的将一个实现转换成带有某个类型参数的泛型类型:
fun sumArray(a: Array<*>) {
val intArray = a as? Array<Int> ?: throw IllegalArgumentException("Array的泛型类型必须是Int类型")
println(intArray.sum())
}
因为我们无法判断数组a
的是不是Array<Int>
类型的,所以可能会出现异常的情况。
对于泛型函数,如果在函数内需要使用具体的泛型类型,同样由于运行时泛型信息被擦除的原因,你无法直接使用它(Cannot check for instance of erased type: T
):
fun < T> test(param: Any) {
if (param is T){
println("param type is match")
}
}
但还是有办法的,可以用inline
关键字修饰函数,即内联函数,这样编译器会把每一次函数调用都换成函数实际代码实现,同时用reified
关键字修饰泛型类型,这样就能保留泛型参数的具体类型了:
inline fun <reified T> test(param: Any) {
if (param is T){
println("param type is match")
}
}
四、型变
1、声明处型变
型变是泛型中比较重要的概念,首先我们要知道 Kotlin 中的泛型是不型变的,这点和 Java 类似。那什么是型变呢,看个例子:
open class A
class B : A()
val array1: Array<B> = arrayOf(B(), B(), B())
val array2: Array<A> = array1
你会发现第二个赋值语句会有错误提示,Type mismatch. Required:Array<A> Found:Array<B>
类型不匹配,Array<B>
并不是Array<A>
的子类,就是因为 Kotlin 中的泛型是默认不型变的,无法自动完成类型转换,但B
是A
的子类,这个赋值操作本质上是合理的、安全的,但编译器似乎并不知道,这必然给我们开发过程中带来了麻烦。
为什么Array
无法正常的赋值,而List
、Set
、Map
可以呢?如下代码,编译器不会有错误提示的:
val list1: List<B> = listOf(B(), B(), B())
val list2: List<A> = list1
我们可以对比一下Array
和List
在源码中的定义:
public class Array<T> {}
public interface List<out E> : Collection<E> {}
可以看到List
的泛型类型使用了out
修饰符,这就是关键所在了。这就是 Kotlin 中的声明处型变,用来向编译器解释这种情况。
- 关于
out
修饰符我们可这样理解,当类、接口的泛型类型参数被声明为out
时,则该类型参数是协变
的,泛型类型的子类型是被保留的,它只能出现在函数的输出位置,只能作为返回类型,即生产者
。带来的好处是,A
是B
的父类,那么List<A>
可以是List<B>
的父类。
我们修改下上边List赋值的代码:
val list1: List<A> = listOf(A(), A(), A())
val list2: List<B> = list1
即反过来赋值,由于B
并不是A
的父类,会有Type mismatch. Required:List<B> Found:List<A>
错误提示。为了应对这种情况,Kotlin 还提供了一个in
修饰符。
- 关于
in
修饰符我们可这样理解,当类、接口的泛型类型参数被声明为in
时,则该类型参数是逆变
的,泛型类型的父类型是被保留的,它只能出现在函数的输入位置,作为参数,只能作为消费类型,即消费者
。
其实 Kotlin 中的Comparable
接口使用了in
修饰符:
public interface Comparable<in T> {
public operator fun compareTo(other: T): Int
}
写一个测试函数,编译器并不会报错:
fun test(a: Comparable<A>) {
val b: Comparable<B> = a
}
所以in
修饰符和out
修饰符的作用看起来的相对的,A
是B
的父类,那么Comparable<B>
可以是Comparable<A>
的父类,体会下区别。
2、使用处型变
为了能将Array<B>
赋值给Array<A>
,我们修改下之前的代码:
val array1: Array<B> = arrayOf(B(), B(), B())
val array2: Array<out A> = array1
这就是使用处型变,相比声明处型变,使用处型变就要复杂些,为了完成对应的需求,需要每次使用对应类时都添加型变修饰符。而声明处型变在类、接口声明时就做好了这些工作,因而代码会更加简洁。
再看一个数组拷贝的函数:
fun copy(from: Array<A>, to: Array<A>) {
for (i in from.indices) {
to[i] = from[i]
}
}
我们试着执行如下的拷贝操作:
val array1: Array<B> = arrayOf(B(), B(), B())
val array2: Array<A> = arrayOf(A(), A(), A())
copy(array1, array2)
同样的问题,由于泛型默认不型变的原因,copy(array1, array2)
并不能正常工作。
回想一下,在 Java 中类似的问题可以使用通配符类型参数解决这个问题:
public void copy(ArrayList<? extends A> from, ArrayList<? super A> to) {}
那么在 Kotlin 中我们自然想到的是型变修饰符了:
- Kotlin 中的
out A
类似于 Java 中的? extends A
,即泛型参数类型必须是A
或者A的子类
,用来确定类型的上限
- Kotlin 中的
in A
类似于 Java 中的? super A
,即泛型参数类型必须是B
或者B的父类
,用来确定类型的下限
修改上边的 copy
函数:
fun copy(from: List<out A>, to: List<in A>) {
for (i in from.indices) {
to[i] = from[i]
}
}
这样copy
函数就能正常的工作了。使用处型变其实也是一种类型投影,from
、in
此时都是一个类型受限的投影数组,它们只能返回、接收指定类型的数据。
这些概念很容易把人搞晕,理解其作用才是关键,而不是套概念。
五、类型投影
前边我们已经知道使用处型变也是一种类型投影,除此之外还有一种星投影
。
当我们不知道泛型参数的类型信息时,但仍需要安全的使用它时,可以使用星投影,用星号*
表示,星投影和 Java 中的原始类型很像,但星投影是安全。
官方对星投影语法的解释如下:
- 对于
Foo <out T : TUpper>
,其中T
是一个具有上界TUpper
的协变类型参数,Foo <>
等价于Foo <out TUpper>
。 这意味着当T
未知时,你可以安全地从Foo <>
读取TUpper
的值。 - 对于
Foo < in T>
,其中T
是一个逆变类型参数,Foo <>
等价于Foo <in Nothing>
。 这意味着当T
未知时,没有什么可以以安全的方式写入Foo <>
。 - 对于
Foo <T : TUpper>
,其中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?>
;
我们来看如下的代码:
val array1: Array<B> = arrayOf(B(), B(), B())
val array2: Array<*> = array1
使用星投影,我们可以将array1
赋值给array2
,但由于此时array2
并不知道泛型参数的类型,所以不能对array2
进行数据写入的操作,但可以从中读取数据:
array2[0] =A() //编译器会报错
val a = array2[0] // 正常
可以看出,星投影更适合那些泛型参数的类型不重要的场景