Kotlin泛型学习篇
泛型: in、out、where
Kotlin 中的类可以有类型参数,与 Java 类似:
class Box<T>(t: T) {
var value = t
}
创建这样类的实例只需提供类型参数即可:
val box: Box<Int> = Box<Int>(1)
但是如果类型参数可以推断出来,例如从构造函数的参数或者从其他途径, 就可以省略类型参数:
val box = Box(1) // 1 具有类型 Int,所以编译器推算出它是 Box<Int>
1 型变
Java 类型系统中最棘手的部分之一是通配符类型(参见 Java Generics FAQ)。 而 Kotlin 中没有。 相反,Kotlin 有声明处型变(declaration-site variance)与类型投影(type projections)。
我们来思考下为什么 Java 需要这些神秘的通配符。 首先,Java 中的泛型是不型变的, 这意味着 List<String>
并不是 List<Object>
的子类型。 如果 List
不是不型变的,它就没比 Java 的数组好到哪去,因为如下代码会通过编译但是导致运行时异常:
// Java
List<String> strs = new ArrayList<String>();
// Java reports a type mismatch here at compile-time.
List<Object> objs = strs;
// What if it didn't?
// We would be able to put an Integer into a list of Strings.
objs.add(1);
// And then at runtime, Java would throw
// a ClassCastException: Integer cannot be cast to String
String s = strs.get(0);
Java 禁止这样的事情以保证运行时的安全。但这样会有一些影响。例如, 考虑Collection
接口中的addAll()
方法。该方法的签名应该是什么?直觉上, 需要这样写:
// Java
interface Collection<E> …… {
void addAll(Collection<E> items);
}
但随后,就无法做到以下这样(完全安全的)的事:
// Java
// The following would not compile with the naive declaration of addAll:
// Collection<String> is not a subtype of Collection<Object>
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from);
}
这就是为什么addAll()
的实际声明是以下这样:
// Java
interface Collection<E> …… {
void addAll(Collection<? extends E> items);
}
通配符类型参数? extends E
表示此方法接受E
或者*E*
的一个子类型对象的集合,而不只是E
自身。 这意味着我们可以安全地从其中 (该集合中的元素是 E 的子类的实例)读取E
,但不能写入, 因为我们不知道什么对象符合那个未知的E
的子类型。 反过来,该限制可以得到想要的行为:Collection<String>
表示为Collection<? extends Object>
的子类型。 简而言之,带extends限定(上界)的通配符类型使得类型是协变的(covariant)。
理解为什么这能够工作的关键相当简单:如果只能从集合中获取元素, 那么使用 String
的集合, 并且从其中读取 Object
也没问题 。反过来,如果只能向集合中 放入 元素 , 就可以用 Object
集合并向其中放入 String
:in Java there is List<? super String>
, which accepts String
s or any of its supertypes.
后者称为逆变性(contravariance),并且对于 List <? super String>
你只能调用接受 String
作为参数的方法 (例如,你可以调用 add(String)
或者 set(int, String)
),如果调用函数返回 List<T>
中的 T
, 你得到的并非一个 String
而是一个 Object
。
Joshua Bloch 在其著作《Effective Java》第三版 中很好地解释了该问题 (第 31 条:“利用有限制通配符来提升 API 的灵活性”)。 他称那些你只能从中读取的对象为生产者, 并称那些只能向其写入的对象为消费者。他建议:
“为了灵活性最大化,在表示生产者或消费者的输入参数上使用通配符类型。”
他还提出了以下助记符:PECS 代表生产者-Extends、消费者-Super(Producer-Extends, Consumer-Super)。
如果你使用一个生产者对象,如
List<? extends Foo>
,在该对象上不允许调用add()
或set()
, 但这并不意味着它是不可变的:例如,没有什么阻止你调用clear()
从列表中删除所有元素,因为clear()
根本无需任何参数。通配符(或其他类型的型变)保证的唯一的事情是类型安全。不可变性完全是另一回事。
2 声明处型变
假设有一个泛型接口 Source<T>
,该接口中不存在任何以 T
作为参数的方法,只是方法返回 T
类型值:
// Java
interface Source<T> {
T nextT();
}
那么,在 Source <Object>
类型的变量中存储 Source <String>
实例的引用是极为安全的—— 没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作:
// Java
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!!在 Java 中不允许
// ……
}
为了修正这一点,我们必须声明对象的类型为 Source<? extends Object>
。这么做毫无意义, 因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。 但编译器并不知道。
interface Source<out T> {
fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // 这个没问题,因为 T 是一个 out-参数
// ……
}
一般原则是:当一个类 C
的类型参数 T
被声明为 out
时,它就只能出现在 C
的成员的输出-位置, 但回报是 C<Base>
可以安全地作为 C<Derived>
的超类。
简而言之,可以说类 C
是在参数 T
上是协变的,或者说 T
是一个协变的类型参数。 可以认为 C
是 T
的生产者,而不是 T
的消费者。
out
修饰符称为型变注解,并且由于它在类型参数声明处提供, 所以它提供了声明处型变。 这与 Java 的使用处型变相反,其类型用途通配符使得类型协变。
另外除了 out
,Kotlin 又补充了一个型变注解: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!
}
in和out两词看起来是自解释的(因为它们已经在 C# 中成功使用很长时间了), 因此上面提到的助记符不是真正需要的。可以将其改写为更高级的抽象:
存在性(The Existential)变换:消费者 in, 生产者 out!😃
3 类型投影
使用处型变:类型投影
将类型参数 T
声明为 out
非常简单,并且能避免使用处子类型化的麻烦, 但是有些类实际上不能限制为只返回 T
! 一个很好的例子是 Array
:
class Array<T>(val size: Int) {
operator fun get(index: Int): T { …… }
operator 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
可能有非预期行为,例如它可能尝试写一个 String
到 from
, 并且如果我们实际上传递一个 Int
的数组,以后会抛 ClassCastException
异常。
要禁止复制功能写入from,可以执行以下操作:
fun copy(from: Array<out Any>, to: Array<Any>) { …… }
这就是类型投影:意味着 from
不仅仅是一个数组,而是一个受限制的(投影的)数组。 只可以调用返回类型为类型参数 T
的方法,如上,这意味着只能调用 get()
。 这就是使用处型变的用法,并且是对应于 Java 的 Array<? extends Object>
、 但更简单。
你也可以使用in
投影一个类型:
fun fill(dest: Array<in String>, value: String) { …… }
Array<in String>
对应于 Java 的 Array<? super String>
,也就是说,你可以传递一个 CharSequence
数组或一个 Object
数组给 fill()
函数。
星投影
有时你想说,你对类型参数一无所知,但仍然希望以安全的方式使用它。 这里的安全方式是定义泛型类型的这种投影,该泛型类型的每个具体实例化都会是该投影的子类型。
Kotlin 为此提供了所谓的星投影语法:
- 对于
Foo <out T : TUpper>
,其中T
是一个具有上界TUpper
的协变类型参数,Foo <*>
等价于Foo <out TUpper>
。 意味着当T
未知时,你可以安全地从Foo <*>
读取TUpper
的值。 - 对于
Foo <in T>
,其中T
是一个逆变类型参数,Foo <*>
等价于Foo <in Nothing>
。 意味着当T
未知时, 没有什么可以以安全的方式写入Foo <*>
。 - 对于
Foo <T : TUpper>
,其中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?>
。
4 泛型函数
不仅类可以有类型参数。函数也可以有。类型参数要放在函数名称之前:
fun <T> singletonList(item: T): List<T> {
// ……
}
fun <T> T.basicToString(): String { // 扩展函数
// ……
}
要调用泛型函数,在调用处函数名之后指定类型参数即可:
val l = singletonList<Int>(1)
可以省略能够从上下文中推断出来的类型参数,所以以下示例同样适用:
val l = singletonList(1)
5 泛型约束
能够替换给定类型参数的所有可能类型的集合可以由泛型约束限制。
上界
最常见的约束类型是上界,与 Java 的extends
关键字对应:
fun <T : Comparable<T>> sort(list: List<T>) { …… }
冒号之后指定的类型是上界,表明只有Comparable<T>
的子类型可以替代T
。 例如:
sort(listOf(1, 2, 3)) // OK。Int 是 Comparable<Int> 的子类型
sort(listOf(HashMap<Int, String>())) // 错误:HashMap<Int, String> 不是 Comparable<HashMap<Int, String>> 的子类型
默认的上界(如果没有声明)是Any?
。在尖括号中只能指定一个上界。 如果同一类型参数需要多个上界,需要一个单独的where-子句:
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
where T : CharSequence,
T : Comparable<T> {
return list.filter { it > threshold }.map { it.toString() }
}
所传递的类型必须同时满足where
子句的所有条件。在上述示例中,类型T
必须既实现了CharSequence
也实现了Comparable
。
声明non-null 的类型
为了更轻松地与泛型 Java 类和接口进行互操作,Kotlin 支持将泛型类型参数声明为绝对不可为 null。
要将泛型类型 T 声明为绝对不可为 null,请使用 & Any 声明该类型。例如:T & Any。
绝对不可为空的类型必须具有可为空的上限。
声明绝对不可空类型的最常见用例是当您想要重写包含 @NotNull 作为参数的 Java 方法时。例如,考虑 load() 方法:
import org.jetbrains.annotations.*;
public interface Game<T> {
public T save(T x) {}
@NotNull
public T load(@NotNull T x) {}
}
要成功重写 Kotlin 中的 load() 方法,您需要将 T1 声明为绝对不可为空:
interface ArcadeGame<T1> : Game<T1> {
override fun save(x: T1): T1
// T1 is definitely non-nullable
override fun load(x: T1 & Any): T1 & Any
}
仅使用 Kotlin 时,您不太可能需要显式声明绝对不可为 null 的类型,因为 Kotlin 的类型推断会为您处理此问题。
6 类型擦除
Kotlin 为泛型声明用法执行的类型安全检测在编译期进行。 运行时泛型类型的实例不保留关于其类型实参的任何信息。 其类型信息称为被擦除。例如,Foo<Bar>
与 Foo<Baz?>
的实例都会被擦除为 Foo<*>
。
7 泛型类型检测与类型转换
由于类型擦除,并没有通用的方法在运行时检测一个泛型类型的实例是否通过指定类型参数所创建 ,并且编译器禁止这种is
检测,例如ints is List<Int>
orlist is T
(type parameter). 当然,你可以对一个实例检测星投影的类型:
if (something is List<*>) {
something.forEach { println(it) } // 每一项的类型都是 `Any?`
}
类似地,当已经让一个实例的类型参数(在编译期)静态检测, 就可以对涉及非泛型部分做is
检测或者类型转换。请注意, 在这种情况下,会省略尖括号:
fun handleStrings(list: MutableList<String>) {
if (list is ArrayList) {
// `list` 智能转换为 `ArrayList<String>`
}
}