【Kotlin學習日記】Day15:泛型

大家好,我是William李梓峰株搔,歡迎加入我的Kotlin學習之旅纵隔。
今天是我學習 Kotlin 的第十五天捌刮,內(nèi)容是 Generics - 泛型糊啡。

本篇內(nèi)容眾多,翻譯不當之處梭依,請多多包含役拴,看中文理解不了就看代碼河闰,代碼理解不了就看英文姜性,看英文理解不了就自己動手打開 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:
通常來說沮趣,要創(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 - 可變性(絕對不是方差)

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> 的子類疲吸。(確實是這樣,泛型不能強轉(zhuǎn)蹂喻,開發(fā)過的都知道當中的滋味)

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ù)組一樣了,從編譯開始到運行的時候拋出個異常:

// 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 轉(zhuǎn) Object腌闯,但很多時候都是敗在了自己寫的父類和子類身上,甚至是一些接口強轉(zhuǎn)分瘦。在這種情況下悦施,我通常都是用序列化+反序列化完成類型轉(zhuǎn)換去团,例如用 Jackson來做這些操作抡诞,有些人可能會跟我一樣想到先序列化成 json 再反序列化某個DTO回來,但其實編程語言本身的缺陷不應該讓開發(fā)者自己去承受這種痛苦渗勘。所以 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 以安全性穩(wěn)定性著稱乔遮。)但是這蘊含了一些啟示。例如取刃,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>
}

(插播個人評論:這種代碼見太多了,一般是先序列化再反序列化,不能直接調(diào)用方法 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.
那個通配符類型參數(shù)啊 ‘嚼沿? 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 類型的列表元素(面向?qū)ο蟮亩鄳B(tài)性),但不可以修改它們,因為我們一直都不知道 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 綁定的通配符類型能夠讓其類型進行協(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>.
理解這些最關鍵的就是:如果你可以只是僅僅出集合元素匆骗,然后用 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.
那個后者叫逆變筋量,你可以直接調(diào)取方法烹吵,用 在List<? super String> 下面用 String 的實參(例如你可以 add(String) 或 set(int, String)),當你調(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 老大)稱這些對象,你只能從生產(chǎn)者讀以及寫入到消費者呀酸。他推薦:“為了最大的復雜度凉蜂,使用通配符類型到入?yún)ⅲ糜谏a(chǎn)者或消費者”性誉。建議用這樣助記符:

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

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

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 作為類型參數(shù)曾棕,只有個方法只是返回 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> -- 存在沒有消費者方法的調(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.
為了修復這種缺陷,我們必須聲明 Source<? extends Object> 對象衙耕,它們是沒有任何意義的昧穿,因為我們可以通過一個變量來調(diào)用所有的同樣的方法,所以沒有任何值被更加復雜的類型添加過橙喘。但是編譯器并不知道這事兒时鸵。

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 的 類型參數(shù) ‘T’ 來確保方法只返回(生產(chǎn)) 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 的類型參數(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.
明確地說薯酝,類 C 在參數(shù) T 中是協(xié)變的南誊,或者說 T 是一個協(xié)變類型參數(shù)。你可以想想 C 是個 T 的生產(chǎn)者蜜托,而不是一個 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 修飾符被稱為 可變性注解,而且自從在類型參數(shù)聲明方上提及它以來橄务,我們都在討論聲明方可變性(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:
對于out來說谬哀,Kotlin 還提供了一個補充可變性注解:in。它把一個類型參數(shù)給逆變了:它可以只被消費而不被生產(chǎ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 都是自帶告白的(因為在 C# 里面就干過這么一件事了)史煎,再加上助記符在上面提及過,不是真的需要它驳糯,但用于表達更高層面的目標:

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

Type projections - 類型推斷

Use-site variance: Type projections

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

// 不翻譯了自己看吧篇梭,我覺得這章節(jié)的東西說多了都是懵逼,還不如直接看代碼是怎么一會兒事兒酝枢,老外講的很啰嗦恬偷,我也沒那個時間和水平去做深度翻譯。但我基本看完代碼后就知道是怎么一回事兒了帘睦,畢竟都是從大二開始自學 Java 到現(xiàn)在為止已經(jīng)四年了袍患。沒啥難懂的,只要 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() }
}
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市卑笨,隨后出現(xiàn)的幾起案子孕暇,更是在濱河造成了極大的恐慌,老刑警劉巖赤兴,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件妖滔,死亡現(xiàn)場離奇詭異,居然都是意外死亡桶良,警方通過查閱死者的電腦和手機座舍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來陨帆,“玉大人曲秉,你說我怎么就攤上這事采蚀。” “怎么了承二?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵榆鼠,是天一觀的道長。 經(jīng)常有香客問我亥鸠,道長妆够,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任负蚊,我火速辦了婚禮神妹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘家妆。我一直安慰自己鸵荠,他們只是感情好,可當我...
    茶點故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布伤极。 她就那樣靜靜地躺著蛹找,像睡著了一般。 火紅的嫁衣襯著肌膚如雪塑荒。 梳的紋絲不亂的頭發(fā)上熄赡,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天,我揣著相機與錄音齿税,去河邊找鬼。 笑死炊豪,一個胖子當著我的面吹牛凌箕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播词渤,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼牵舱,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了缺虐?” 一聲冷哼從身側(cè)響起芜壁,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎高氮,沒想到半個月后慧妄,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡剪芍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年塞淹,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片罪裹。...
    茶點故事閱讀 40,865評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡饱普,死狀恐怖运挫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情套耕,我是刑警寧澤谁帕,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站冯袍,受9級特大地震影響匈挖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜颠猴,卻給世界環(huán)境...
    茶點故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一关划、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧翘瓮,春花似錦贮折、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至呵扛,卻和暖如春每庆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背今穿。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工缤灵, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蓝晒。 一個月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓腮出,卻偏偏與公主長得像,于是被迫代替她去往敵國和親芝薇。 傳聞我的和親對象是個殘疾皇子胚嘲,可洞房花燭夜當晚...
    茶點故事閱讀 45,870評論 2 361

推薦閱讀更多精彩內(nèi)容