【Kotlin學(xué)習(xí)日記】Day15:泛型

大家好,我是William李梓峰,歡迎加入我的Kotlin學(xué)習(xí)之旅。
今天是我學(xué)習(xí) Kotlin 的第十五天,內(nèi)容是 Generics - 泛型。

本篇內(nèi)容眾多,翻譯不當(dāng)之處,請(qǐng)多多包含,看中文理解不了就看代碼,代碼理解不了就看英文,看英文理解不了就自己動(dòng)手打開(kāi) IDEA 練練手。就譬如什么是查克拉原理我不懂,但我會(huì)耍螺旋丸。

官方文檔:

Generics - 泛型

As in Java, classes in Kotlin may have type parameters:
Java抄襲了 C++ 的模板類并改了個(gè)名字叫泛型,同樣,在 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:
通常來(lái)說(shuō),要?jiǎng)?chuàng)建這樣的泛型類,我們是要事先提供好類型的:

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:
但是如果泛型參數(shù)可以推斷的話,例如從構(gòu)造器形參或者其他地方去猜,那么這樣就可以不用顯式地寫泛型了:

val box = Box(1) // 1 has type Int, so the compiler figures out that we are talking about Box<Int>

Variance - 可變性(絕對(duì)不是方差)

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 最屌的機(jī)制之一就有泛型的通配符類型。例如 List<? extends Integer> 之類的。但是 Kotlin 卻不支持這種特性。相反,Kotlin 有兩個(gè)東西補(bǔ)充這種機(jī)制:聲明層面的方差以及類型預(yù)測(cè)。

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 需要神秘的通配符機(jī)制。通配符的問(wèn)題在 Effective Java 里面有提到:使用有界通配符會(huì)增加 API 的復(fù)雜性。

First, generic types in Java are invariant, meaning that List<String> is not a subtype of List<Object>.
再想想,泛型在 Java 里面是 不可變的,意思是 List<String> 并非是 List<Object> 的子類。(確實(shí)是這樣,泛型不能強(qiáng)轉(zhuǎn),開(kāi)發(fā)過(guò)的都知道當(dāng)中的滋味)

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 的數(shù)組一樣了,從編譯開(kāi)始到運(yùn)行的時(shí)候拋出個(gè)異常:

// 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

(插播個(gè)人評(píng)論:上面的代碼是工作一兩年的職場(chǎng)新手都會(huì)犯的錯(cuò)誤,我也試過(guò),雖然不一定是 String 轉(zhuǎn) Object,但很多時(shí)候都是敗在了自己寫的父類和子類身上,甚至是一些接口強(qiáng)轉(zhuǎn)。在這種情況下,我通常都是用序列化+反序列化完成類型轉(zhuǎn)換,例如用 Jackson來(lái)做這些操作,有些人可能會(huì)跟我一樣想到先序列化成 json 再反序列化某個(gè)DTO回來(lái),但其實(shí)編程語(yǔ)言本身的缺陷不應(yīng)該讓開(kāi)發(fā)者自己去承受這種痛苦。所以 Kotlin 就有如下對(duì)策。)

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 禁止這些東西是為了保證運(yùn)行時(shí)是安全的。(Java 以安全性穩(wěn)定性著稱。)但是這蘊(yùn)含了一些啟示。例如,Collection 的 addAll() 方法。這種方法到底要怎么寫?直覺(jué)告訴我們要這樣子寫:

// 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):
然后呢,我們不會(huì)干這種簡(jiǎn)單的事情:

// 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>
}

(插播個(gè)人評(píng)論:這種代碼見(jiàn)太多了,一般是先序列化再反序列化,不能直接調(diào)用方法 addAll() ,因?yàn)楦揪幾g不通過(guò),直接在 IDE 報(bào)錯(cuò)。)

(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.
那個(gè)通配符類型參數(shù)啊 ‘? extends E’ 表明了這個(gè)方法接受一個(gè)對(duì)象集合,它們都是 E 類的某個(gè)子類,而不是 E 自己本身。(學(xué)過(guò) Java 都懂的,還有反過(guò)來(lái)理解的 ? 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.
上面說(shuō)的意思就是我們可以安全地讀取 E 類型的列表元素(面向?qū)ο蟮亩鄳B(tài)性),但不可以修改它們,因?yàn)槲覀円恢倍疾恢?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.
言簡(jiǎn)意賅的來(lái)說(shuō),有 extends 綁定的通配符類型能夠讓其類型進(jìn)行協(xié)變(不理解也罷,照舊先看懂代碼)。

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>.
理解這些最關(guān)鍵的就是:如果你可以只是僅僅出集合元素,然后用 String 類型的方式以及 Object 類型的方式去處理。相反,如果你可以只是插入集合元素,只要是 Object 類型的集合插入 String 類型的元素:在 Java 里面啊,我們有 List<? super String> 來(lái)指代任何 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.
那個(gè)后者叫逆變,你可以直接調(diào)取方法,用 在List<? super String> 下面用 String 的實(shí)參(例如你可以 add(String) 或 set(int, String)),當(dāng)你調(diào)取那些東西返回 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 老大)稱這些對(duì)象,你只能從生產(chǎn)者讀以及寫入到消費(fèi)者。他推薦:“為了最大的復(fù)雜度,使用通配符類型到入?yún)?,用于生產(chǎn)者或消費(fèi)者”。建議用這樣助記符:

PECS stands for Producer-Extends, Consumer-Super.
PECS 之于 生產(chǎn)者子類,消費(fèi)者的超類

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.
注意:如果你是用一個(gè)生產(chǎn)者的對(duì)象,也就是說(shuō),List<? extends Foo> 你就不被允許調(diào)用那個(gè)對(duì)象的 add() 或 set() ,但是也不能說(shuō)這個(gè)對(duì)象是不可變的:比如說(shuō),沒(méi)有人能夠阻止你調(diào)用 clear() 去移除那些列表,因?yàn)閏lear() 不接受任何參數(shù)。唯一可以保證的是類型是安全的(有個(gè)卵用)。不可變性是完全另外一個(gè)話題了。

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:
假設(shè)我們有個(gè)泛型接口 Source<T> 不包含任何方法,以 T 作為類型參數(shù),只有個(gè)方法只是返回 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:
然后,它可能就是百分百安全地存儲(chǔ)一個(gè)引用,這個(gè)引用是 Source<String> 在一個(gè)可變類型 Source<Object> -- 存在沒(méi)有消費(fèi)者方法的調(diào)用。但是 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.
為了修復(fù)這種缺陷,我們必須聲明 Source<? extends Object> 對(duì)象,它們是沒(méi)有任何意義的,因?yàn)槲覀兛梢酝ㄟ^(guò)一個(gè)變量來(lái)調(diào)用所有的同樣的方法,所以沒(méi)有任何值被更加復(fù)雜的類型添加過(guò)。但是編譯器并不知道這事兒。

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 那里,存在一種方式來(lái)解釋編譯器的一些事。這事兒叫做聲明方可變性:我們可以注解 Source 的 類型參數(shù) ‘T’ 來(lái)確保方法只返回(生產(chǎn)) Source<T> 的成員,并且永不被消費(fèi)(修改)。因此我們提供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>.
一般來(lái)說(shuō),當(dāng)一個(gè)類 C 的類型參數(shù) T 聲明了 out,它也許只有出現(xiàn)在 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.
明確地說(shuō),類 C 在參數(shù) T 中是協(xié)變的,或者說(shuō) T 是一個(gè)協(xié)變類型參數(shù)。你可以想想 C 是個(gè) T 的生產(chǎn)者,而不是一個(gè) T 的消費(fèi)者

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 修飾符被稱為 可變性注解,而且自從在類型參數(shù)聲明方上提及它以來(lái),我們都在討論聲明方可變性(List<Object> objs = new ArrayList<String>(); 就這種叫聲明方可變性,指的是List<Object> 是可變的,就如 Object obj = new String() 一樣,泛型也能夠?qū)崿F(xiàn)多態(tài)性)。這與 Java 的調(diào)用方可變相反,讓通配符在類型參數(shù)的使用上協(xié)變(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:
對(duì)于out來(lái)說(shuō),Kotlin 還提供了一個(gè)補(bǔ)充可變性注解:in。它把一個(gè)類型參數(shù)給逆變了:它可以只被消費(fèi)而不被生產(chǎn)。一個(gè)關(guān)于逆變的很好的例子就是 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 都是自帶告白的(因?yàn)樵?C# 里面就干過(guò)這么一件事了),再加上助記符在上面提及過(guò),不是真的需要它,但用于表達(dá)更高層面的目標(biāo):

The Existential Transformation: Consumer in, Producer out! :-)

Type projections - 類型推斷

Use-site variance: Type projections

調(diào)用方可變性:類型推斷

// 不翻譯了自己看吧,我覺(jué)得這章節(jié)的東西說(shuō)多了都是懵逼,還不如直接看代碼是怎么一會(huì)兒事兒,老外講的很啰嗦,我也沒(méi)那個(gè)時(shí)間和水平去做深度翻譯。但我基本看完代碼后就知道是怎么一回事兒了,畢竟都是從大二開(kāi)始自學(xué) Java 到現(xiàn)在為止已經(jīng)四年了。沒(méi)啥難懂的,只要 Java 基礎(chǔ)好,Kotlin 學(xué)起來(lái)蠻簡(jiǎn)單的。

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() }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容