大家好,我是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 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
.
那個后者叫逆變筋量,你可以直接調(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:
我們相信 in 和 out 都是自帶告白的(因為在 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>
, 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() }
}