Kotlin学习之-5.8 泛型
和Java中一样,Kotlin 也可以使用类型参数:
class Box<T>(t: T) {
var value = t
}
一般情况下,要创建这样的类的实例,我们需要提供类型参数:
val box: Box<Int> = Box<Int>(1)
但是如果参数的类型可以推断出来,例如,从构造器的参数或者其他方法可以推断出类型,这种情况下可以神略类型参数:
val box = Box(1) // 参数‘1’是Int类型,所以编译器可以推断出类型参数是Int类型的
变量
在Java类型系统中比较难理解的是wildcard
类型。详见Java泛型。 然而Kotlin中没有wildcard
。 在Kotlin中,有两个其他东西来替代:declaration-site variance
和type projections
.
首先,我们想一下为什么Java需要这些谜一样的wildcard
。 这个问题在《Effective Java》中解释过,第28条:Use bounded wildcards to increase API flexibility。第一,Java中的泛型类型是invariant的,意味着List<String>
类型不是List<Object>
类型的子类型。为什么这样?如果List不是invariant的,那么它就比Java 的数组Array
好不到哪去,因为如下的代码就可以编译通过,但是导致运行时错误。
// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // **注意** 这里会编译失败,Java不支持
objs.add(1);
String s = strs.get(0); // **ClassCastException**:Cannot cast Integer to String
因此,Java禁止这样的操作是为了保证运行时的安全。 但是这有一些暗示。例如,考虑Collection
接口中的函数addAll()
。 这个方法的签名应该是怎么的呢?直觉上说,我们会这样写:
// Java
interface Collection<E> {
void addAll(Collection<E> items);
}
但是这样的话,我们就无法使用下面简单的例子(这个例子是百分百安全的)
// Java
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from); // **注意**无法和addAll 的声明编译通过,Collection<String> 不是Collection<Object> 的自雷
}
这就是为什么addAll()
真正的签名是这样的:
// Java
interface Collection<E> {
void addAll(Collection<? extends E> items);
}
这里的Wildcard type argument ? extends E
表明这个方法接受一个集合,这个集合的元素的类型都是E
类型的子类,而不是E
本身。这意味着我们可以安全的从元素中读取E
(这个集合的元素是E
的子类的实例),但是不能写入,因为我们不知道什么对象可以和那个不知道类型的E
的子类的类型匹配。 因为这个问题的限制,我们有了想要的行为:Collection<String>
是Collection<? extends Object>
的子类。行话就是,对于继承受限的通配符可以让一个类型协变covariant。
理解为什么这样工作的关键非常简单。当你只能从集合中取元素,然后使用一个String
的集合并且从它里面读取Object
。 相反的, 如果你只能存元素到集合中,去一个Object
的集合的元素然后存一个String
元素到集合中:在Java中我们让List<? super String>
是List<Object>
的父类。
后者叫做超协变 contravariance, 并且你只能调用哪些在List<? super String>
上使用String当做参数方法的函数。 例如, 你可以调用add(String)
或者set(int, String)
。当你想要调用那种从List<T>
中返回T
类型的方法,你无法得到一个String
,而只能得到一个Object
。
Joshua Bloch 说这些对象你只能从生产者中读取, 只能写到消费者中。
“For maximum flexibility, use wildcard types on input parameters that represent producers or consumers”
并且提出了下面的助记方法
PECS stands for Producer-Extends, Consumer-Super.
注意:如果你使用一个生产者对象,假如是List<? extends Foo>
, 你不能调用它的add()
或set()
方法, 但是这并不意味着这个对象是不可变的。例如,没什么可以阻止你调用clear()
方法来移除所有列表中的元素。因为clear()
方法没有任何参数。Wildcard 只是用来保证类型安全的。可变性是另外一个不同的问题。
声明站变量(Declaration-site variance)
假设我们有一个泛型接口Source<T>
,这个接口没有任何方法使用T
作为参数,只是有方法返回T
// java
interface Source<T> {
T nextT();
}
这样就可以非常安全的使用Source<Object>
引用一个Source<String>
对象,因为没有消费者方法可以调用。但是Java并不知道这些,仍然禁止这样使用。
void demo(Source<String> strs) {
Source<Object> objects = strs; // **Java中禁止**
}
为了解决这个问题,我们必须使用Source<? extends Object>
类型来声明对象,但他其实没有意义,因为我们可以用和之前一样的对象调用它所有的方法,所以没有任何意义把这个弄成复杂的类型。但是编译器并不知道这些。
Kotlin中,有一个办法可以给编译器解释这种类型的问题。这就叫做“声明站变量”declaration-site variance
,我们可以给参数类型T
的代码做注解,来保障它只会被Source<T>
的成员返回或者生产,并且永远不会被消费。我们使用out
修饰符来完成这个功能。
abstract class Source<out T> {
abstract fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs
}
基本规则是:当一个类C
的参数类型T
被声明成out
后,它就只能出现在类C
成员的out
位置,这样C<Base>
就可以安全当做C<Derived>
的父类了。
简单的说,在参数T
中类C
是协变的,或者T
是一个协变类型的参数。你可以把C
当做T
的生产者,而不是一个T
的消费者。
修饰符out
被称作变量注解,并且因为它用在类型参数声明站,所以我们称为声明站变量。 这和Java中的使用站变量相反的,在使用站变量中在类型中使用wildcard会让类型编程协变的。
除了out
以外,Kotlin还提供一种辅助的变量注解in
。 它可以让一个类型参数成为协变的:只能被消费不能被生产。一个超协变类的例子是Comparable
:
abstract class Comparable<in T> {
abstract 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!
}
我们相信关键则in
和out
可以很好的自解释(因为他们已经在C#中成功地使用了一段时间了),因此上面的助记实际并不需要,然后可以重写成:
The Existential Transformation: Consumer in, Producer out! :-)
类型映射
使用站变量:类型映射
现在非常方便地可以把一个类型参数T
定义成out
,并且避免在使用站中遇到子类的问题,但是有些类实际上无法被限制成只返回T
类型。下面数组的使用时一个很好的例子:
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<Any>, Array<Any>)
现在考虑一个类似的问题:Array<T>
中的T
是不可变的, 并且Array<Int>
和Array<Any>
都不是对方的子类。为什么?还是同样的,copy
函数中可能会做一些坏事情,例如它可能会去写一个String到from
中,并且如果我们实际上传递一个Int
的数组,将来就很可能会出现ClassCastException
.
然后,唯一我们想要保障的是copy()
函数不做任何坏事。我们想要禁止它给from
写值,我们可以这样:
fun copy(from: Array<out Any>, to: Array<Any>) {
// ...
}
这里发生的事情叫作类型映射,我们认为from
在这里不仅仅是一个数组,而是一个受限(映射)的数组:我们只能这样称那些返回参数类型T
的方法,在这种情况下,它意味着我们只能调用get()
。这就是我们使用用户站变量的方法,对应Java中的Array<? extands Object>
, 但是稍微简单一些。
你也可以用in
映射一个类型:
fun fill(dest: Array<in String>, val: String) {
// ...
}
Array<in String>
对应Java中的Array<? super String>
,例如你可以传一个CharSequence
的数组或者一个Object
的数组给fill()
函数。
星号映射
有时候你想要说你一点都不知道类型参数,但是仍然想要安全地使用它。安全使用它的方法是定义这样一个泛型类型的映射,这样每一个泛型的实体实例就会是这个映射的子类型。
Kotlin提供叫作星号映射的语法:
- 对于
Foo<out T>
当T
是一个协变类型参数并且有一个上界类型TUpper
,Foo<*>
等价于Foo<out TUpper>
. 这意味着当T
是未知的时候,你可以安全的从Foo<*>
中读取TUpper
类型的值 - 对于
Foo<in T>
当T
是一个超协变类型参数,Foo<*>
等价于Foo<in Nothing>
,这意味着如果T
是未知的,你无法安全地写任何内容到Foo<*>
中。 - 对于
Foo<T>
,当T
是一个不可变类型参数,并且有一个上界类型TUpper
,Foo<*>
在读取值得时候等价于Foo<out TUpper>
,在写入值得时候等价于Foo<in Nothing>
如果一个泛型参数有很多参数类型,并且每一个都可以被独立地映射。例如,如果类型被定义成interface Function<in T, out U>
我们可以想象下列星号映射
Funcion<*, String>
意思是Function<in Nothing, String>
Function<Int, *>
意思是Function<Int, out Any?>
Funcion<*, *>
意思是Function<in Nothing, out Any?>
注意,星号映射和Java中的元类型非常像, 但是是类型安全的
泛型函数
不止类可以有类型参数,函数也可以有。类型参数放置在函数名的前面:
fun <T> singletonList(item: T): List<T> {
}
// 扩展函数
fun <T> T.basicToString() : String {
}
调用一个泛型函数,在调用的时候需要在函数名后面,明确类型参数。
泛型约束
所有可能被用来替换指定类型参数的类型集合都约定成泛型约束
类型上界
最常见的类型约束就是类型上界,对应Java中的extends
关键字
fun <T : Comparable<T>> sort(list: List<T>) {
}
在冒号后面指定的类型是类型上界,意味着只有Comparable<T>
的子类型可以被替换成T
。例如:
// 正确。Int 是Comparable<Int> 的子类型
sort(listOf(1, 2, 3))
// 错误。HashMap<Int, String> 不是Comparable<HashMap<Int, String>>的子类型
sort(listOf(HashMap<Int, String>()))
如果没有指定上界的话,那么默认的上界是Any?
.在尖括号中只能指定一个类型上界。如果同一个类型参数需要多于一个类型上界,我们需要一个独立的where
分支语句:
fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T>
where T : Comparable,
T: Cloneable {
return list.filter { it > threshold }.map { it.clone() }
}
---
PS,我会坚持把这个系列写完,有问题可以留言交流,也关注专栏Kotlin for Android Kotlin安卓开发
这篇泛型好长,里面有很多内容发现自己Java部分就理解的不全,甚至不正确。 通过本文,还发现一个关于泛型超级详尽的文档。 收益匪浅。
本文中有很多用词可能不是官方的用语, 毕竟kotlin官方中文版还没有出,我认为可能会不准确的地方都尽量同时写上了原版英文,有问题可以交流沟通。