Kotlin泛型中的协变与逆变

70 阅读2分钟

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. 例子

如上所述,inout是一个对立面,其中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>,既能添加也能读取