大家好,我是William李梓峰,欢迎加入我的Kotlin学习之旅。
今天是我学习 Kotlin 的第十五天,内容是 Generics - 泛型。
本篇内容众多,翻译不当之处,请多多包含,看中文理解不了就看代码,代码理解不了就看英文,看英文理解不了就自己动手打开 IDEA 练练手。就譬如什么是查克拉原理我不懂,但我会耍螺旋丸。
官方文档:
Generics - 泛型
As in Java, classes in Kotlin may have type parameters:
Java抄袭了 C++ 的模板类并改了个名字叫泛型,同样,在 Kotlin 里边也有泛型:
class Box<T>(t: T) {
var value = t
}
In general, to create an instance of such a class, we need to provide the type arguments:
通常来说,要创建这样的泛型类,我们是要事先提供好类型的:
val box: Box<Int> = Box<Int>(1) // java6 可以靠猜 new Box<>(1);
But if the parameters may be inferred, e.g. from the constructor arguments or by some other means, one is allowed to omit the type arguments:
但是如果泛型参数可以推断的话,例如从构造器形参或者其他地方去猜,那么这样就可以不用显式地写泛型了:
val box = Box(1) // 1 has type Int, so the compiler figures out that we are talking about Box<Int>
Variance - 可变性(绝对不是方差)
One of the most tricky parts of Java's type system is wildcard types (see Java Generics FAQ).
And Kotlin doesn't have any. Instead, it has two other things: declaration-site variance and type projections.
Java 最屌的机制之一就有泛型的通配符类型。例如 List<? extends Integer> 之类的。但是 Kotlin 却不支持这种特性。相反,Kotlin 有两个东西补充这种机制:声明层面的方差以及类型预测。
First, let's think about why Java needs those mysterious wildcards. The problem is explained in Effective Java, Item 28: Use bounded wildcards to increase API flexibility.
首先,我们要好好想想为啥 Java 需要神秘的通配符机制。通配符的问题在 Effective Java 里面有提到:使用有界通配符会增加 API 的复杂性。
First, generic types in Java are invariant, meaning that List<String>
is not a subtype of List<Object>
.
再想想,泛型在 Java 里面是 不可变的,意思是 List<String> 并非是 List<Object> 的子类。(确实是这样,泛型不能强转,开发过的都知道当中的滋味)
Why so? If List was not invariant, it would have been no better than Java's arrays, since the following code would have compiled and caused an exception at runtime:
所以为啥要这样子搞?如果 List 是可变的,那么它就是跟 Java 的数组一样了,从编译开始到运行的时候抛出个异常:
// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!! The cause of the upcoming problem sits here. Java prohibits this!
objs.add(1); // Here we put an Integer into a list of Strings
String s = strs.get(0); // !!! ClassCastException: Cannot cast Integer to String
(插播个人评论:上面的代码是工作一两年的职场新手都会犯的错误,我也试过,虽然不一定是 String 转 Object,但很多时候都是败在了自己写的父类和子类身上,甚至是一些接口强转。在这种情况下,我通常都是用序列化+反序列化完成类型转换,例如用 Jackson来做这些操作,有些人可能会跟我一样想到先序列化成 json 再反序列化某个DTO回来,但其实编程语言本身的缺陷不应该让开发者自己去承受这种痛苦。所以 Kotlin 就有如下对策。)
So, Java prohibits such things in order to guarantee run-time safety. But this has some implications. For example, consider the addAll()
method from Collection
interface. What's the signature of this method? Intuitively, we'd put it this way:
所以,Java 禁止这些东西是为了保证运行时是安全的。(Java 以安全性稳定性著称。)但是这蕴含了一些启示。例如,Collection 的 addAll() 方法。这种方法到底要怎么写?直觉告诉我们要这样子写:
// Java
interface Collection<E> ... {
void addAll(Collection<E> items);
}
But then, we would not be able to do the following simple thing (which is perfectly safe):
然后呢,我们不会干这种简单的事情:
// Java
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from); // !!! Would not compile with the naive declaration of addAll:
// Collection<String> is not a subtype of Collection<Object>
}
(插播个人评论:这种代码见太多了,一般是先序列化再反序列化,不能直接调用方法 addAll() ,因为根本编译不通过,直接在 IDE 报错。)
(In Java, we learned this lesson the hard way, see Effective Java, Item 25: Prefer lists to arrays)
(上面是插播 Java 官方教程,甲骨文出品)
That's why the actual signature of addAll()
is the following:
这就是为啥 addAll() 要写成这样子:
// Java
interface Collection<E> ... {
void addAll(Collection<? extends E> items); // String 继承 Object 哦
}
The wildcard type argument ? extends E
indicates that this method accepts a collection of objects of some subtype of E
, not E
itself.
那个通配符类型参数啊 ‘? extends E’ 表明了这个方法接受一个对象集合,它们都是 E 类的某个子类,而不是 E 自己本身。(学过 Java 都懂的,还有反过来理解的 ? super E 呢,指代任何E类的父类,哈哈)
This means that we can safely read E
's from items (elements of this collection are instances of a subclass of E), but cannot write to it since we do not know what objects comply to that unknown subtype of E
.
上面说的意思就是我们可以安全地读取 E 类型的列表元素(面向对象的多态性),但不可以修改它们,因为我们一直都不知道 E 类型的子类是什么。
In return for this limitation, we have the desired behaviour: Collection<String>
is a subtype of Collection<? extends Object>
.
在这种局限性下,我们希望:‘Collection<String>’ 是 Collection<? extends Object> 的子类。
In "clever words", the wildcard with an extends-bound (upper bound) makes the type covariant.
言简意赅的来说,有 extends 绑定的通配符类型能够让其类型进行协变(不理解也罢,照旧先看懂代码)。
The key to understanding why this trick works is rather simple: if you can only take items from a collection, then using a collection of String
s and reading Object
s from it is fine. Conversely, if you can only put items into the collection, it's OK to take a collection of Object
s and put String
s into it: in Java we have List<? super String>
a supertype of List<Object>
.
理解这些最关键的就是:如果你可以只是仅仅取出集合元素,然后用 String 类型的方式以及 Object 类型的方式去处理。相反,如果你可以只是插入集合元素,只要是 Object 类型的集合插入 String 类型的元素:在 Java 里面啊,我们有 List<? super String> 来指代任何 String 类的父类,如 List<Object>。
The latter is called contravariance, and you can only call methods that take String as an argument on List<? super String>
(e.g., you can call add(String)
or set(int, String)
), while if you call something that returns T
in List<T>
, you don't get a String
, but an Object
.
那个后者叫逆变,你可以直接调取方法,用 在List<? super String> 下面用 String 的实参(例如你可以 add(String) 或 set(int, String)),当你调取那些东西返回 List<T> 的 T,你就拿不到 String 而是 Object。
Joshua Bloch calls those objects you only read from Producers, and those you only write to Consumers. He recommends: "For maximum flexibility, use wildcard types on input parameters that represent producers or consumers", and proposes the following mnemonic:
Joshua Bloch(Java 老大)称这些对象,你只能从生产者读以及写入到消费者。他推荐:“为了最大的复杂度,使用通配符类型到入参,用于生产者或消费者”。建议用这样助记符:
PECS stands for Producer-Extends, Consumer-Super.
PECS 之于 生产者子类,消费者的超类
NOTE: if you use a producer-object, say, List<? extends Foo>
, you are not allowed to call add()
or set()
on this object, but this does not mean that this object is immutable: for example, nothing prevents you from calling clear()
to remove all items from the list, since clear()
does not take any parameters at all. The only thing guaranteed by wildcards (or other types of variance) is type safety. Immutability is a completely different story.
注意:如果你是用一个生产者的对象,也就是说,List<? extends Foo> 你就不被允许调用那个对象的 add() 或 set() ,但是也不能说这个对象是不可变的:比如说,没有人能够阻止你调用 clear() 去移除那些列表,因为clear() 不接受任何参数。唯一可以保证的是类型是安全的(有个卵用)。不可变性是完全另外一个话题了。
Declaration-site variance - 声明方的可变性
Suppose we have a generic interface Source<T>
that does not have any methods that take T
as a parameter, only methods that return T
:
假设我们有个泛型接口 Source<T> 不包含任何方法,以 T 作为类型参数,只有个方法只是返回 T :
// Java
interface Source<T> {
T nextT();
}
Then, it would be perfectly safe to store a reference to an instance of Source<String>
in a variable of type Source<Object>
-- there are no consumer-methods to call. But Java does not know this, and still prohibits it:
然后,它可能就是百分百安全地存储一个引用,这个引用是 Source<String> 在一个可变类型 Source<Object> -- 存在没有消费者方法的调用。但是 Java 就不行,而且禁止这么玩:
// Java
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!! Not allowed in Java
// ...
}
To fix this, we have to declare objects of type Source<? extends Object>
, which is sort of meaningless, because we can call all the same methods on such a variable as before, so there's no value added by the more complex type. But the compiler does not know that.
为了修复这种缺陷,我们必须声明 Source<? extends Object> 对象,它们是没有任何意义的,因为我们可以通过一个变量来调用所有的同样的方法,所以没有任何值被更加复杂的类型添加过。但是编译器并不知道这事儿。
In Kotlin, there is a way to explain this sort of thing to the compiler. This is called declaration-site variance: we can annotate the type parameter T
of Source to make sure that it is only returned (produced) from members of Source<T>
, and never consumed. To do this we provide the out modifier:
在 Kotlin 那里,存在一种方式来解释编译器的一些事。这事儿叫做声明方可变性:我们可以注解 Source 的 类型参数 ‘T’ 来确保方法只返回(生产) Source<T> 的成员,并且永不被消费(修改)。因此我们提供out 修饰符:
abstract class Source<out T> {
abstract fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // This is OK, since T is an out-parameter
// ...
}
The general rule is: when a type parameter T
of a class C
is declared out, it may occur only in out-position in the members of C
, but in return C<Base>
can safely be a supertype of C<Derived>
.
一般来说,当一个类 C 的类型参数 T 声明了 out,它也许只有出现在 out 位置的类 C 的成员的前面,但返回的 C<Base> 可以安全地成为 C<Derived> 的超类型。
In "clever words" they say that the class C
is covariant in the parameter T
, or that T
is a covariant type parameter. You can think of C
as being a producer of T
's, and NOT a consumer of T
's.
明确地说,类 C 在参数 T 中是协变的,或者说 T 是一个协变类型参数。你可以想想 C 是个 T 的生产者,而不是一个 T 的消费者。
The out modifier is called a variance annotation, and since it is provided at the type parameter declaration site, we talk about declaration-site variance. This is in contrast with Java's use-site variance where wildcards in the type usages make the types covariant.
out 修饰符被称为 可变性注解,而且自从在类型参数声明方上提及它以来,我们都在讨论声明方可变性(List<Object> objs = new ArrayList<String>(); 就这种叫声明方可变性,指的是List<Object> 是可变的,就如 Object obj = new String() 一样,泛型也能够实现多态性)。这与 Java 的调用方可变相反,让通配符在类型参数的使用上协变(List<? extends Object> or List<? super String>)。
In addition to out, Kotlin provides a complementary variance annotation: in. It makes a type parameter contravariant: it can only be consumed and never produced. A good example of a contravariant class is Comparable
:
对于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 has type Double, which is a subtype of Number
// Thus, we can assign x to a variable of type Comparable<Double>
val y: Comparable<Double> = x // OK!
}
We believe that the words in and out are self-explaining (as they were successfully used in C# for quite some time already), thus the mnemonic mentioned above is not really needed, and one can rephrase it for a higher purpose:
我们相信 in 和 out 都是自带告白的(因为在 C# 里面就干过这么一件事了),再加上助记符在上面提及过,不是真的需要它,但用于表达更高层面的目标:
The Existential Transformation: Consumer in, Producer out! :-)
Type projections - 类型推断
Use-site variance: Type projections
调用方可变性:类型推断
// 不翻译了自己看吧,我觉得这章节的东西说多了都是懵逼,还不如直接看代码是怎么一会儿事儿,老外讲的很啰嗦,我也没那个时间和水平去做深度翻译。但我基本看完代码后就知道是怎么一回事儿了,毕竟都是从大二开始自学 Java 到现在为止已经四年了。没啥难懂的,只要 Java 基础好,Kotlin 学起来蛮简单的。
It is very convenient to declare a type parameter T as out and avoid trouble with subtyping on the use site, but some classes can't actually be restricted to only return T
's!
A good example of this is Array:
class Array<T>(val size: Int) {
fun get(index: Int): T { /* ... */ }
fun set(index: Int, value: T) { /* ... */ }
}
This class cannot be either co- or contravariant in T
. And this imposes certain inflexibilities. Consider the following function:
fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}
This function is supposed to copy items from one array to another. Let's try to apply it in practice:
val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" }
copy(ints, any) // Error: expects (Array<Any>, Array<Any>)
Here we run into the same familiar problem: Array<T>
is invariant in T
, thus neither of Array<Int>
and Array<Any>
is a subtype of the other. Why? Again, because copy might be doing bad things, i.e. it might attempt to write, say, a String to from
, and if we actually passed an array of Int
there, a ClassCastException
would have been thrown sometime later.
Then, the only thing we want to ensure is that copy()
does not do any bad things. We want to prohibit it from writing to from
, and we can:
fun copy(from: Array<out Any>, to: Array<Any>) {
// ...
}
What has happened here is called type projection: we said that from
is not simply an array, but a restricted (projected) one: we can only call those methods that return the type parameter T
, in this case it means that we can only call get()
. This is our approach to use-site variance, and corresponds to Java's Array<? extends Object>
, but in a slightly simpler way.
You can project a type with in as well:
fun fill(dest: Array<in String>, value: String) {
// ...
}
Array<in String>
corresponds to Java's Array<? super String>
, i.e. you can pass an array of CharSequence
or an array of Object
to the fill()
function.
Star-projections
Sometimes you want to say that you know nothing about the type argument, but still want to use it in a safe way. The safe way here is to define such a projection of the generic type, that every concrete instantiation of that generic type would be a subtype of that projection.
Kotlin provides so called star-projection syntax for this:
- For
Foo<out T>
, whereT
is a covariant type parameter with the upper boundTUpper
,Foo<*>
is equivalent toFoo<out TUpper>
. It means that when theT
is unknown you can safely read values ofTUpper
fromFoo<*>
. - For
Foo<in T>
, whereT
is a contravariant type parameter,Foo<*>
is equivalent toFoo<in Nothing>
. It means there is nothing you can write toFoo<*>
in a safe way whenT
is unknown. - For
Foo<T>
, whereT
is an invariant type parameter with the upper boundTUpper
,Foo<*>
is equivalent toFoo<out TUpper>
for reading values and toFoo<in Nothing>
for writing values.
If a generic type has several type parameters each of them can be projected independently.
For example, if the type is declared as interface Function<in T, out U>
we can imagine the following star-projections:
-
Function<*, String>
meansFunction<in Nothing, String>
; -
Function<Int, *>
meansFunction<Int, out Any?>
; -
Function<*, *>
meansFunction<in Nothing, out Any?>
.
Note: star-projections are very much like Java's raw types, but safe.
Generic functions
Not only classes can have type parameters. Functions can, too. Type parameters are placed before the name of the function:
fun <T> singletonList(item: T): List<T> {
// ...
}
fun <T> T.basicToString() : String { // extension function
// ...
}
To call a generic function, specify the type arguments at the call site after the name of the function:
val l = singletonList<Int>(1)
Generic constraints
The set of all possible types that can be substituted for a given type parameter may be restricted by generic constraints.
Upper bounds
The most common type of constraint is an upper bound that corresponds to Java's extends keyword:
fun <T : Comparable<T>> sort(list: List<T>) {
// ...
}
The type specified after a colon is the upper bound: only a subtype of Comparable<T>
may be substituted for T
. For example
sort(listOf(1, 2, 3)) // OK. Int is a subtype of Comparable<Int>
sort(listOf(HashMap<Int, String>())) // Error: HashMap<Int, String> is not a subtype of Comparable<HashMap<Int, String>>
The default upper bound (if none specified) is Any?
. Only one upper bound can be specified inside the angle brackets.
If the same type parameter needs more than one upper bound, we need a separate where-clause:
fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T>
where T : Comparable,
T : Cloneable {
return list.filter { it > threshold }.map { it.clone() }
}