1. Java 的型变
如果 Parent 是 Child 的父类,那么 List<Parent>
和List<Child>
的关系是什么呢?对于 Java 来说,没有关系。
也就是说下面的代码是无法编译的:
|
|
不过 numbers 中可以添加 Number 类型的对象,所以我添加个 Integer 可以不呢?可以的:
那么我要想添加一堆 Integer 呢?用 addAll 是吧?注意看下 addAll 的签名:
这个泛型参数又是什么鬼?如果我把这个签名写成下面这样:
我想要在 numbers 当中addAll
一个 ArrayList<Integer>
,那就不可能了,因为我们说过,ArrayList<Number>
和 ArrayList<Integer>
是两个不同的类型,毛关系都没有。
? extends E
其实就是使用点协变,允许传入的参数可以是泛型参数类型为Number
子类的任意类型。
当然,也有 ? super E
的用法,这表示元素类型为E
及其父类,这个通常也叫作逆变。
2. Kotlin 的型变
型变包括协变、逆变、不变三种
下面我们看看 Kotlin 是怎么支持这个特性的。Kotlin 支持声明点型变,我们直接看 Collection 接口的定义:
out E
就是型变的定义,表明 Collection 的元素类型是协变的,即 Collection<Number>
也是 Collection<Int>
的父类。
而对于 MutableList
来说,它的元素类型就是不变的:
换言之,MutableCollection<Number>
与MutableCollection<Int>
没有什么关系。
那么请注意看 addAll 的声明,参数是 Collection<E>
,而 Collection 是协变的,所以传入的参数可以是任意 E 或者其子类的集合。
逆变的写法也简单一些: Collection<in E>
。
那么 Kotlin 是否支持使用点型变呢?当然支持。
我们刚才说 MutableCollection
是不变的,那么如果下面的参数改成这样:
|
|
结果就是,当 E 为 Number 时,addAll 无法接类受似ArrayList<Int>
的参数。而为了接受这样的参数,我们可以修改一下签名:
|
|
这其实就与 Java 的型变完全一致了。
3. @UnsafeVariance
型变是一个让人费解的话题,很多人接触这东西的时候一开始都会比较晕,我们来看看下面的例子:
为什么会报错呢?因为 T 是协变的,所以外部传入的参数类型如果是 T 的话,会出问题,不信你看:
上面的代码毫无疑问可以编译,但运行时就会比较尴尬,因为 MyCollection<Int>
希望接受的是 Int
,没想到来了一个 Double
。。
对于协变的类型,通常我们是不允许将泛型类型作为传入参数的类型的,或者说,对于协变类型,我们通常是不允许其涉及泛型参数的部分被改变的。这也很容易解释为什么 MutableCollection 是不变的,而 Collection 是协变的,因为在 Kotlin 当中,前者是可被修改的,后者是不可被修改的。
逆变的情形正好相反,即不可以将泛型参数作为方法的返回值。
但实际上有些情况下,我们不得已需要在协变的情况下使用泛型参数类型作为方法参数的类型:
|
|
比如这种情形,为了让编译器放过一马,我们就可以用 @UnsafeVariance 来告诉编译器:“我知道我在干啥,保证不会出错,你不用担心”。
最后再给大家提一个点,现在你们知道为什么 in 表示逆变,out 表示协变了吗?