【Kotlin学习日记】Day15:泛型

大家好,我是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 Strings and reading Objects from it is fine. Conversely, if you can only put items into the collection, it's OK to take a collection of Objects and put Strings 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:
我们相信 inout 都是自带告白的(因为在 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>, where T is a covariant type parameter with the upper bound TUpper, Foo<*> is equivalent to Foo<out TUpper>. It means that when the T is unknown you can safely read values of TUpper from Foo<*>.
  • For Foo<in T>, where T is a contravariant type parameter, Foo<*> is equivalent to Foo<in Nothing>. It means there is nothing you can write to Foo<*> in a safe way when T is unknown.
  • For Foo<T>, where T is an invariant type parameter with the upper bound TUpper, Foo<*> is equivalent to Foo<out TUpper> for reading values and to Foo<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> means Function<in Nothing, String>;
  • Function<Int, *> means Function<Int, out Any?>;
  • Function<*, *> means Function<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() }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容