Kotlin总结之协变与逆变

1.为什么List<String>不能赋值给List<Object>?

通过反证法看这个问题,如果List<String>能赋值给List<Object>会出现什么情况?

public static void main(String[] args) {
    List<String> strList = new ArrayList<String>();
    List<Object> objectList = strList;//假设可以,但实际上是通不过编译的。
    objectList.add(new Integer(1));
    String str = strList.get(0);//这里会出错
}

 观察上面代码,如果Java中允许List<String>赋值给List<Object>,就不能保证类型安全了。而Java中明确要求泛型的基本条件是保证类型安全,所以不支持这种行为。

但是在 Kotlin中,可以发现:

fun main() {
    val strList: List<String> = ArrayList<String>()
    val anyList: List<Any> = strList
}

在Kotlin中尽然可以将List<String>赋值给List<Any>,关键其实在于Java 中的List和 Kotlin 中的 List 并不是同一种类型。

Java:

public interface List<E> extends Collection<E> {
    ... ...
}

Kotlin:

public interface List<out E> : Collection<E> {
    ......
}

 两个List都支持泛型,但是Kotlin的List定义的泛型参数前面多了一个out关键字。

其实对于普通方式定义的泛型对于Java和Kotlin都是不变的,就是不管类型A和类型B是什么关系,Generic<A>与Generic<B>(Generic代表泛型类)都没有任何关系。比如在Java中String是Object的子类型,但是List<String>并不是List<Object>的子类型。

一个支持协变的List

Kotlin中的List,其泛型参数前面多了一个out关键字。如果在定义的泛型类和泛型方法的泛型参数前面加上out关键字,说明这个泛型类及泛型方法是协变的,简单来说就是类型A是类型B的子类型,那么Generic<A>也是Generic<B>的子类型。比如在Kotlin中String是Any的子类型,那么List<String>也是List<Any>的子类型,所以List<String>可以赋值给 List<Any>。

如果允许这种行为,将出现类型不安全的问题,那么Kotlin是如何解决这个问题的呢?

因为是一个支持协变的List,所以它无法添加元素,只能从里面读取内容。

下面代码会报错

fun main() {
    val strList: List<String> = ArrayList<String>()
    strList.add("one")//会报错,此时没有add方法
}

 观察下面List的源码,发现确实没有add方法。

public interface List<out E> : Collection<E> {
    // Query Operations
    override val size: Int

    override fun isEmpty(): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>

    // Bulk Operations
    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean

    // Positional Access Operations
    /**
     * Returns the element at the specified index in the list.
     */
    public operator fun get(index: Int): E

    // Search Operations
    /**
     * Returns the index of the first occurrence of the specified element in the list, or -1 if the specified
     * element is not contained in the list.
     */
    public fun indexOf(element: @UnsafeVariance E): Int

    /**
     * Returns the index of the last occurrence of the specified element in the list, or -1 if the specified
     * element is not contained in the list.
     */
    public fun lastIndexOf(element: @UnsafeVariance E): Int

    // List Iterators
    /**
     * Returns a list iterator over the elements in this list (in proper sequence).
     */
    public fun listIterator(): ListIterator<E>

    /**
     * Returns a list iterator over the elements in this list (in proper sequence), starting at the specified [index].
     */
    public fun listIterator(index: Int): ListIterator<E>

    // View
    /**
     * Returns a view of the portion of this list between the specified [fromIndex] (inclusive) and [toIndex] (exclusive).
     * The returned list is backed by this list, so non-structural changes in the returned list are reflected in this list, and vice-versa.
     *
     * Structural changes in the base list make the behavior of the view undefined.
     */
    public fun subList(fromIndex: Int, toIndex: Int): List<E>
}

 可以发现上面的List中本来就没有定义add方法,也没有remove以及replace方法。也就是说这个List一旦创建就不能再被修改。

其实可以通过反证法看这个问题:

假如支持协变的List允许插入新对象,那么它就不再是类型安全的了,也就违背了泛型的初衷。

所有结论是:支持协变的List只可以读取,而不可以添加。从out关键字也可以看出是出的意思,所以List是一个只读列表。

注意一点:

通常情况下,若一个泛型类Generic<out T>支持协变,那么它里面的方法的参数类型不能使用T类型,因为一个方法的参数不允许传入参数父类型的对象,因为这样可能会导致错误。但在Kotlin中,,可以添加@UnsafeVariance注解来解除这个限制,比如上面List中的indexOf等方法。

一个支持逆变的Comparator

考虑一种情况:比如Double是Number的子类型,反过来Generic<Double>却是Generic<Number>的父类型?

假设现在要对一个MutableList<Double>进行排序,利用其sortWith方法,我们需要传入一个比较器,所以可以这样做:

fun main() {
    val doubleComparator = Comparator<Double> { d1, d2 ->
        d1.compareTo(d2)
    }

    val doubleList = mutableListOf<Double>(2.0, 1.0, 3.0)
    doubleList.sortWith(doubleComparator)
    doubleList.forEach { 
        println(it)
    }
}
1.0
2.0
3.0

 在上面代码的基础上需要对MutableList<Int>、MutableList<Long>等进行排序,是不是又需要定义intComparator、longComparator。其实不需要,这些数字类有一个共同的父类Number,使用Number类型的比较器代替它的子类型。

val numberComparator = Comparator<Number> { d1, d2 ->
    d1.toDouble().compareTo(d2.toDouble())
}

val doubleList = mutableListOf<Double>(2.0, 1.0, 3.0)
doubleList.sortWith(numberComparator)
doubleList.forEach {
    println(it)
}
val intList = mutableListOf<Int>(2, 1, 3)
intList.sortWith(numberComparator)
intList.forEach {
    println(it)
}
1.0
2.0
3.0
1
2
3
expect fun <T> MutableList<T>.sortWith(comparator: Comparator<in T>): Unit

 in关键字使泛型拥有了另一个特性,那就是逆变。简单来说,若类型A是类型B的子类型,那么Generic<B>反过来是Generic<A>的子类型。

将泛型参数设置为逆变有什么限制呢?

用in关键字声明的泛型参数类型可以作为方法的参数类型,但是不能作为方法的返回值类型。

例子:

interface WritableList<in T> {

    fun add(t: T): Int //允许

    fun get(index: Int): T //不允许,这句代码的写法是错误的。
}

协变和逆变

in 和out是一个对立面,in 代表泛型参数类型逆变,out 代表泛型参数类型协变。从字面上理解就是,in代表输入,out代表输出。但它们同时与泛型不变相互对立,统称为型变。

interface PayList<out E> : Collection<E> {}

 上述代码是在声明处型变,另外也可以在使用处型变(sortWith方法就是使用处型变)。

下面实现一个copy数组的需求?

实现Double数组的拷贝。

fun payCopy(dest: Array<Double?>, src: Array<Double>) {
    if (dest.size < src.size) {
        throw IndexOutOfBoundsException()
    } else {
        src.forEachIndexed() { index, value ->
            dest[index] = src[index]
        }
    }
}

fun main() {
    val dest = arrayOfNulls<Double>(3)
    val src = arrayOf<Double>(1.0, 2.0, 3.0)
    payCopy(dest, src)
    dest.forEach {
        println(it)
    }
}
1.0
2.0
3.0

进一步抽象,使其可以适应任意类型的拷贝。

fun <T> payCopyCommon(dest: Array<T?>, src: Array<T>) {
    if (dest.size < src.size) {
        throw IndexOutOfBoundsException()
    } else {
        src.forEachIndexed() { index, value ->
            dest[index] = src[index]
        }
    }
}

 但是,上面的方法还是有局限的,就是使用拷贝方法必须是同一种类型,那么如果希望将Array<Double> 拷贝到Array<Number>,该如何实现呢?

in实现版本

fun <T> payCopyIn(dest: Array<in T>, src: Array<T>) {
    if (dest.size < src.size) {
        throw IndexOutOfBoundsException()
    } else {
        src.forEachIndexed() { index, value ->
            dest[index] = src[index]
        }
    }
}

val destNumber = arrayOfNulls<Number>(3)
val srcDouble = arrayOf<Double>(1.0, 2.0, 3.0)
payCopyIn(destNumber, srcDouble)
destNumber.forEach {
    println(it)
}
1.0
2.0
3.0

 out版本

fun <T> payCopyOut(dest: Array<T>, src: Array<out T>) {
    if (dest.size < src.size) {
        throw IndexOutOfBoundsException()
    } else {
        src.forEachIndexed() { index, value ->
            dest[index] = src[index]
        }
    }
}
val destNumber = arrayOfNulls<Number>(3)
val srcDouble = arrayOf<Double>(1.0, 2.0, 3.0)
payCopyOut(destNumber, srcDouble)
destNumber.forEach {
    println(it)
}

 观察in版本与out版本的区别?

in是声明在dest数组上的,out是声明在src数组上的,所以dest可以接收T类型的父类型的Array,out可以接收T类型的子类型的Array。这里的T需要到编译时才会确定。

in版本,T是Double类型,所以dest可以接收Double类型的父类型 Array,比如Array<Number>。

out版本,T是Number类型,所以src可以接收Number类型的子类型Array,比如Array<Double>。

Kotlin 与Java型变的比较

 

协变

逆变

不变

Kotlin

<out T>只能作为消费者,只能读取不能添加。

<in T>只能作为生产者,只能添加,读取受限制

<T>既可以添加,也可以读取

Java

<? extends T>只能作为消费者,只能读取不能添加。

<? super T>只能作为生产者,只能添加,读取受限制

<T>既可以添加,也可以读取

参考 Kotlin核心编程

发布了179 篇原创文章 · 获赞 175 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/zhangying1994/article/details/104403212