Kotlin总结之协变与逆变

1.为什么List不能赋值给List

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

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

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

但是在 Kotlin中,可以发现:

fun main() {
    val strList: List = ArrayList()
    val anyList: List = strList
}

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

Java:

public interface List extends Collection {
    ... ...
}

Kotlin:

public interface List : Collection {
    ......
}

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

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

 

一个支持协变的List

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

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

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

下面代码会报错

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

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

public interface List : Collection {
    // Query Operations
    override val size: Int

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

    // 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

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

    // 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
}

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

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

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

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

注意一点:

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

 

一个支持逆变的Comparator

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

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

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

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

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

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

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

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

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

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

例子:

interface WritableList {

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

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

 

协变和逆变

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

interface PayList : Collection {}

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

 

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

实现Double数组的拷贝。

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

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

 

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

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

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

 

in实现版本

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

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

 out版本

fun  payCopyOut(dest: Array, src: Array) {
    if (dest.size < src.size) {
        throw IndexOutOfBoundsException()
    } else {
        src.forEachIndexed() { index, value ->
            dest[index] = src[index]
        }
    }
}
val destNumber = arrayOfNulls(3)
val srcDouble = arrayOf(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

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

 

Kotlin 与Java型变的比较

 

协变

逆变

不变

Kotlin

只能作为消费者,只能读取不能添加。

只能作为生产者,只能添加,读取受限制

既可以添加,也可以读取

Java

只能作为消费者,只能读取不能添加。

只能作为生产者,只能添加,读取受限制

既可以添加,也可以读取

 

 

参考 Kotlin核心编程

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(Kotlin,泛型协变out,泛型逆变,in)