Java中所有的类都是不变型的
在Java中,List<String>并不是List<Object>的子类型。
如果List<String>并是List<Object>的子类型, 那么就是意味着将一个List<Object>类型的对象指向List<String>不会报错,那么我们就可以对这个List<String>对象添加一些Object对象,比如:
// Java List<String> strs = new ArrayList<String>(); List<Object> objs = strs; // !!!即将来临的问题的原因就在这里。Java 禁止这样! objs.add(1); // 这里我们把一个整数放入一个字符串列表 String s = strs.get(0); // !!! ClassCastException:无法将整数转换为字符串 复制代码
这就是在Java中禁止List<String>是List<Object>子类型的原因
然而我们将无法做到一下简单的事情(这是完全安全):
// Java void copyAll(Collection<Object> to, Collection<String> from) { to.addAll(from); // !!!对于这种简单声明的 addAll 将不能编译: // Collection<String> 不是 Collection<Object> 的子类型 } 复制代码
如果不引入通配符类型参数 ?
,那么必须手动的为每一个类型写一个addAll方法
比如:addAll(Collection<A>),addAll(Collection<B>),addAll(Collection<C>),等等
// Java interface Collection<E> …… { void addAll(Collection<String> items); } 复制代码
然而Collection的 addAll()
的实际签名是以下这样:
// Java interface Collection<E> …… { void addAll(Collection<? extends E> items); } 复制代码
通配符类型参数 ? extends E
表示此方法接受 E
或者 E 的 一些子类型
对象的集合,而不只是 E
自身。 这意味着我们可以安全地从其中(该集合中的元素是 E 的子类的实例) 读取
E
,但 不能写入
。
将一个集合作为参数传递给一个有着 ? extends E
参数的方法,就说明在该方法中,该集合只会被读取,不会被写入
相反的,将一个集合作为参数传递给一个有着 ? super E
参数的方法,就说明在该方法中,该集合只会被写入,不会被读取
与Java不同,Kotlin并没有直接使用通配符,而是使用了 声明处型变
Joshua Bloch 称那些你只能从中 读取 的对象为 生产者 ,并称那些你只能 写入 的对象为 消费者 。他建议:“ 为了灵活性最大化,在表示生产者或消费者的输入参数上使用通配符类型 ”,并提出了以下助记符:
PECS 代表生产者-Extens,消费者-Super(Producer-Extends, Consumer-Super)。
当一个类Person的类型参数 T
被声明为 out
时,它就只能出现在Person的成员的 输出
位置,通常也就是返回位置,可以说Person在参数 T
上是 协变的
,Person是 T
的生产者,而不是 T
的消费者
这和Java中 ? extends E
是一致的,凡是用Out修饰符修饰的T只能被读取,无法被消费
当一个类的类型参数被声明为 In 时,类型参数 逆变 , 它就只能写入,无法被读取,充当一个消费者的角色
比如 Comparable
interface Comparable<in T> { operator fun compareTo(other: T): Int } fun demo(x: Comparable<Number>) { x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型 // 因此,我们可以将 x 赋给类型为 Comparable <Double> 的变量 val y: Comparable<Double> = x // OK! } 复制代码
将类型参数声明为 out
非常方便,而且能够避免使用出子类型化的麻烦,但有些类实际上不能限制只返回 T
,有些时候在一个类中 T
既有充当生产者的方法,又有充当消费者的方法。
一个很好的例子是Array:
class Array<T>(val size: Int) { fun get(index: Int): T { …… } fun set(index: Int, value: T) { …… } } 复制代码
该类在 T
上既不能是协变的也不能是逆变的。
那么下面的函数:
fun copy(from: Array<Any>, to: Array<Any>) { assert(from.size == to.size) for (i in from.indices) to[i] = from[i] } 复制代码
该函数将一个数组复制到另一个数组。
val ints: Array<Int> = arrayOf(1, 2, 3) val any = Array<Any>(3) { "" } copy(ints, any) // ^ 其类型为 Array<Int> 但此处期望 Array<Any> 复制代码
由于Array<T>在 T
上是不型变的,因此 Array<Int>
和 Array<Any>
都不是另一个的子类型。因为copy()方法可能会做一些不正确的事情,比如会向 from
写入一些不被允许的类。因此,只要确保 copy()
的 from
参数只会被读取,而 to
参数只会被写入
我们可以:
fun copy(from: Array<out Any>, to: Array<Any>) { …… } 复制代码
这里发生的事情称为 类型投影
, from
是一个受限制的数组,我们只能调用返回类型为类型参数 T
的方法。这就意味着我们只能读取,这就是我们的 使用处型变
的用法,并且是对应于 Java 的 Array<? extends Object>
、 但使用更简单些的方式。
如果有什么写的不好的地方欢迎批评和指正。
"( ̄_, ̄ )"