Kotlin学习之泛型
Kotlin的泛型与Java一样,都是一种语法糖,只在源代码里出现,编译时会进行简单的字符替换。
泛型其实就是类型参数,它给强类型编程语言加入了更强的灵活性。
在Java中,只要是有类型的元素,都可以泛型化。泛型类、泛型接口、泛型方法和泛型属性。泛型类和泛型接口统称为泛型类型。最重要的是泛型类型和泛型方法。
在Kotlin,类也可以有类型参数:
class Box<T>(t: T) {
var value = t
}
一般来说,要创建如上这样类的实例,我们需要提供类型参数:
val box: Box<Int> = Box<Int>(1)
如果类型参数是可以推断出来的,也可以将其省略掉:
val box = Box(1)
- 定义泛型类型,是在类型名之后、主构造函数之前用尖括号(
<>
)括起的大写字母类型参数指定。 - 定义泛型类型变量,可以完整的写明类型参数;如果编译器可以自动推定类型参数,也可以省略类型参数。
在泛型方法的类型参数里可以用冒号:
指定上界。
fun <T : Comparable<T>> sort(list: List<T>) {/*...*/}
对于多个上界约束条件,可以用where子句:
fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T>
where T : Comparable, Cloneable {
return list.filter(it > threshold).map(it.clone())
}
一、型变
在Kotlin中,没有通配符类型。但是它有:声明处型变和类型投影。
在Java中的泛型是不型变的。通配符类型参数 ? extends E表示此方法接受E或者E的一些子类型对象的集合,而不只是E自身。也就是说可以安全地从中读取E,但不能写入,因为我们不知道什么对象符合未知的E的子类型。带extends限定的通配符类型使得类型是协变的。
只能从中读取的对象为生产者,只能写入的对象为消费者。通配符保证的唯一的事情就是类型安全。
1.1声明处型变
如果有一个泛型接口Source<T>
,这个接口中不存在任何以T为参数的方法,只是方法返回T类性值:
// Java
interface Source<T> {
T nextT();
}
void demo(Source<String> str) {
// Java 中这种写法是不允许的
Source<Object> obj = str;
/*...*/
}
因为Java中泛型是不型变的,Source<String>
不是Source<Object>
的子类型,所以不能把Source<String>
类型变量赋值给Source<Object>
类型变量。
在Kotlin中,有一种方法向编译器解释这种情况。称为声明处型变:可以标注Source的类型参数T来确保它仅从Source<T>
成员中返回,并从不被消费。Kotlin提供了out修饰符:
abstract class Source<out T> {
abstract fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // 这个没问题,因为 T 是一个 out-参数
// ……
}
- 当一个类C的类型参数T被声明为out时,就只能出现在C的成员的输出位置,但是C<Base>可以安全地作为C<Derived>的超类。
out修饰符称为型变注解,并且由于它在类型参数声明处提供,所以叫声明处型变。
除了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!
}
1.2类型投影
使用处型变:类型投影
将类型参数T声明为out很方便,而且可以避免使用处子类型化的麻烦,但是有些类实际上不能限制为只返回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]
}
要确保的是copy()不会做任何坏事。所以如下:
fun copy(from: Array<out Any>, to: Array<Any>) {
// ……
}
这就发生了类型投影。这就是使用处型变的用法,并且是对应Java中的Array<? extends Object>
,但是使用更简单。
也可以使用in投影一个类型:
fun fill(dest: Array<in String>, value: String) {
// ……
}
1.2.1星投影
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>,我们可以想象以下星投影:
- Function<*, String> 表示 Function<in Nothing, String>;
- Function<Int, *> 表示 Function<Int, out Any?>;
- Function<*, *> 表示 Function<in Nothing, out Any?>。
网友评论