1. out和in
如果在定义的泛型类和泛型方法的泛型参数前面加上out关键字,说明这个泛型类和泛型方法是协变,简单来说就是A是B的子类,那么Generic<A>也是Generic<B>的子类;协变只能读数据,不能写数据;
注意:若一个泛型类Generic<out T>支持协变,那么它里面方法的参数类型不能使用T
在Kotlin中用in关键字来声明逆变泛型,如果类型A是类型B的子类型,反过来Generic<B>是Generic<A>的子类型,逆变只能写数据,不能读数据
注意:若一个泛型类Generic<in T>支持协变,那么它里面方法的返回值类型不能是T
2. 例子
如上所述,in和out是一个对立面,其中in代表泛型参数逆变,out代表泛型参数协变,从字面上也可以理解,in代表输入,out代表输出,但同时它们又与泛型不变相对立,统称为型变。
假设现在有个场景,需要将数据从一个Double数据拷贝到另一个Double数组,该怎么实现?
/**
* 拷贝数组,泛型实现,支持任意类型的List拷贝
*
* @param target 目标数组
* @param src 源数组
*/
fun <T> copy(target: Array<T?>, src: Array<T>) {
if (target.size >= src.size) {
src.forEachIndexed { index, _ -> target[index] = src[index] }
} else {
throw IndexOutOfBoundsException()
}
}
这样写的局限在于copy方法必须是同一类型,使用如下:
// Double类型数组
val targetDouble = arrayOfNulls<Double>(3)
val srcDouble = arrayOf<Double>(2.0, 1.0, 3.0)
copy(targetDouble, srcDouble)
// Int类型数组
val targetInt = arrayOfNulls<Number>(3)
val srcInt = arrayOf<Number>(2, 1, 3)
copy(targetInt, srcInt)
//...
如果想把Array<Double>拷贝到Array<Number>中,这样是不允许的,这时候就可以利用型变了
/**
* 拷贝数组,in版本
*
* @param target 目标数组
* @param src 源数组
*/
fun <T> copyIn(target: Array<in T>, src: Array<T>) {
if (target.size >= src.size) {
src.forEachIndexed { index, _ -> target[index] = src[index] }
} else {
throw IndexOutOfBoundsException()
}
}
/**
* 拷贝数组,out版本
*
* @param target 目标数组
* @param src 源数组
*/
fun <T> copyOut(target: Array<T>, src: Array<out T>) {
if (target.size >= src.size) {
src.forEachIndexed { index, _ -> target[index] = src[index] }
} else {
throw IndexOutOfBoundsException()
}
}
仔细看,in是声明在target数组上,而out是声明在src数组上,所以target能接收T的父类型Array,src能接收T的子类型Array,当然,这个T类型需要在编译时才能确定。
使用如下:
val targetDouble = arrayOfNulls<Number>(3)
val srcDouble = arrayOf<Number>(2.0, 1.0, 3.0)
// 允许
copyIn(targetDouble, srcDouble)
// 允许
copyOut(targetDouble, srcDouble)
3.总结
| 协变 | 逆变 | 不变 | |
|---|---|---|---|
| Kotlin | 实现方式:<out T>,只能作为消费者,只能读取不能添加 | 实现方式:<in T>,只能作为生产者,只能添加不能读取 | 实现方式: <T>,既能添加也能读取 |
| Java | 实现方式:<? extends T>,只能作为消费者,只能读取不能添加 | 实现方式:<? super T>,只能作为生产者,只能添加不能读取 | 实现方式: <T>,既能添加也能读取 |