Kotlin面向对象之泛型(Generics)

和Java类似,Kotlin在定义类的时候可以使用类型参数:

class Box<T>(t: T) {
    var value = t
}

通常,要创建上述类的一个实例,我们需要提供类型参数:

val box: Box<Int> = Box<Int>(1)

但是如果该类型参数可以推断出来,例如通过构造函数参数或其他方式,则可以省略类型参数的实参:

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

差异(Variance)

Java的类型系统中最棘手的部分之一是通配符类型。在Kotlin中则没有这个概念。取而代之的则是declaration-site variance and type projections,后者姑且翻译为类型推断。

首先,我们来想想为什么Java需要这些神秘的通配符。这个问题在Effective Java中被解释到:使用有界通配符来增加API的灵活性。首先,Java中的泛型是不变的,这意味着List <String>不是List <Object>的子类型。 为什么呢? 如果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

所以,Java禁止这样做来保证运行时的安全。但这有一些影响。例如,考虑到Collection接口的addAll()方法。 该方法的签名是什么? 通常,我们会这样想:

// Java
interface Collection<E> ... {
  void addAll(Collection<E> items);
}

但是,我们将无法做如下简单的事情(这是完全安全的):

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

(在Java中,我们艰难地学习了本课程:将集合转换为数组)

这就是为什么addAll()的实际签名如下所示:

// Java
interface Collection<E> ... {
  void addAll(Collection<? extends E> items);
}

通配符类型参数? extends E表示此方法接受E的一些子类型的对象的集合,而不是E本身。这意味着我们可以安全地从集合中读取E(这个集合的元素是E的子类的实例),但不能写入它,因为我们不知道元素到底是E类型的哪个子类。针对该限制,我们渴望实现:Collection <String>Collection <? extends Object>的子类。 用简明的话说,具有继承界限(上限)的通配符使得类型更协变(covariant)。

理解这个窍门为什么起作用的关键是简单的:如果你仅想从一个集合中获取条目,那么使用一个String集合并从中读取Object就可以了。相反,如果您仅想将条目放入集合,则可以使用Object集合并将String放入其中:在Java中,我们有List <? super String>的父类型List <Object>

后者称为逆变(contravariance与协变对应),您只能调用方法将String作为List<? super String>的参数(例如,您可以调用add(String)set(int,String)),而如果调用List <T>的一些方法返回对象为T类型,则不会得到String,而是一个Object

Joshua Bloch谈起过只从生产者读,以及只写向消费者的对象。 他建议:“为了最大限度的灵活性,在代表生产者或消费者的输入参数上使用通配符”,并提出以下助记符:

PECS代表生产者继承,消费者父类( Producer-Extends, Consumer-Super)。

注意:如果你使用一个生产者对象,也就是说属于List<? extends Foo>类型,你不允许调用该对象的add()set()方法,但这并不意味着该对象是不可变的:例如,没有什么可以阻止你调用clear()来从列表中删除所有的项目,因为clear()根本没有任何参数。 通配符(或其他类型的差异)保证的唯一的事情是类型安全。 不变性则是一个完全不同的概念。

分歧声明点(Declaration-site variance)

假设我们有一个通用的接口Source <T>,它没有任何方法可以将T作为参数,只有返回T的方法:

// Java
interface Source<T> {
  T nextT();
}

然后,将Source <String>的实例存储在Source <Object>类型的变量中是绝对安全的 - 没有调用消费者的方法。 但Java不知道这一点,但仍然禁止:

// Java
void demo(Source<String> strs) {
  Source<Object> objects = strs; // !!! Not allowed in Java
  // ...
}

要解决这个问题,我们必须声明Source <? extends Object>类型的对象,这是没有意义的,因为我们可以调用所有相同的方法在这样的变量以前,所以没有增加更复杂的类型的值。 但编译器不知道。

在Kotlin中,有一种方式可以向编译器解释这种事情。 这称为declaration-site variance:我们可以注释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
    // ...
}

一般规则是:当C类的类型参数T被声明为out时,它只能在C的成员的out位置出现,但返回的C <Base>则是安全的C <Derived>的父型。

在“聪明的话”中,他们说C类在参数T中是协变的,或者说是T是一个协变类型的参数。 你可以认为C是T的生产者,而不是T的消费者。

out修饰符被称为分歧注解(variance annotation),并且由于它在类型参数处出现,所以我们称之为分歧声明点(declaration-site variance)。 这与Java的分歧使用点(use-site variance)形成对照,其中类型用法中的通配符使类型协变。

除了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!
}

我们相信这些单词是自我解释的(因为它们已经在C#中成功使用了一段时间),因此上面提到的助记符并不是真正需要的,可以改写为更高的概括:

Consumer in, Producer out!

类型推断(Type projections)

使用分歧点:类型推断(Use-site variance: Type projections)

将类型参数T声明为out非常方便,并避免在使用处进行子类转换的麻烦,但是某些类实际上不能仅限于返回T! 一个很好的例子是Array

class Array<T>(val size: Int) {
    fun get(index: Int): T { /* ... */ }
    fun set(index: Int, value: T) { /* ... */ }
}

该类不能使用类型参数T共同或相反。这种强加了一些不灵活性。考虑以下功能:

fun copy(from: Array<Any>, to: Array<Any>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}

该函数应该将条目从一个数组复制到另一个数组。 我们试着在实践中应用它:

val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3)
copy(ints, any) // Error: expects (Array<Any>, Array<Any>)

这里我们遇到同样的常见问题:Array<T>中的T是不变的,因此Array <Int>Array <Any>都不是另一个的子类型。 为什么? 再次,因为复制可能会做坏事,也就是说可能会尝试写一个String到from参数中,如果我们实际上传递了一个Int的数组,那么ClassCastException将在稍后抛出。

那么,我们唯一要确保的是copy()不会做任何坏事 我们要禁止它向from参数中写,我们可以:

fun copy(from: Array<out Any>, to: Array<Any>) {
 // ...
}

这里发生的事情就是类型推测:我们说,from不仅仅是一个数组,而是一个限制的(可推测)数组:我们只能调用那些返回类型参数T的方法,在这种情况下,这意味着我们只能调用get()。 这是我们使用使用点分歧,对应于Java的Array <? extends Object>,但是稍微简单一点。

你也可以使用in来推测一个类型:

fun fill(dest: Array<in String>, value: String) {
    // ...
}

Array <in String>对应于Java Array <? super String>,即可以将一个CharSequence数组或一个Object数组传递给fill()函数。

星号推测(Star-projections)

有时你想说,你对这个类型参数一无所知,但仍然希望以安全的方式使用它。 这里的安全方法是定义一个通用类型的推测,即该通用类型的每个具体实例都将是该推测的子类型。

Kotlin提供了为此提供了被称为星号推测的语法:

  1. 对于Foo <out T>,其中T是具有上限TUpper的协变类型参数,Foo<*>相当于Foo <out TUpper>。 这意味着当T未知时,您可以从Foo <*>安全地读取TUpper的值。
  2. 对于Foo <in T>,其中T是逆变类型参数,Foo<*>相当于Foo <in Nothing>。 这意味着当T未知时,没有什么可以以安全的方式写入Foo <*>
  3. 对于Foo <T>,其中T是具有上限TUpper的不可变类型参数,Foo <*>等同于用于读取值的Foo <out TUpper>和用于写入值的Foo <in Nothing>

如果泛型具有多个类型参数,则可以独立地推测每个类型参数。 例如,如果类型被声明为interface Function<in T, out U>,我们可以猜想以下星号预测:

  1. Function<*, String> 意味着 Function<in Nothing, String>
  2. Function<Int, *> 意味着 Function<Int, out Any?>
  3. Function<*, *> 意味着 Function<in Nothing, out Any?>

注意:星号预测非常像Java原始类型,但是安全。

泛型函数(Generic functions)

不仅类可以有泛型。函数也可以。泛型放在函数名称之前:

fun <T> singletonList(item: T): List<T> {
    // ...
}

fun <T> T.basicToString() : String {  // extension function
    // ...
}

要调用泛型函数,需要在函数名称之后指定类型参数:

val l = singletonList<Int>(1)

泛型约束

所有可能的类型集合可以被替代,
可以替代给定类型参数的所有可能类型的集合可能受到泛型约束的限制。

上限(Upper bounds)

最常见的约束类型是对应于Javaextends关键字的上限:

fun <T : Comparable<T>> sort(list: List<T>) {
    // ...
}

冒号后面指定的类型是上限:只有Comparable<T>的子类型可以作为T的实参。例如:

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

默认上限(如果没有指定)为Any?。 在尖括号内只能指定一个上限。 如果同一类型的参数需要多个上限,我们需要一个单独的where子句:

fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T>
where T : Comparable,
T : Cloneable {
return list.filter { it > threshold }.map { it.clone() }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • 前言 人生苦多,快来 Kotlin ,快速学习Kotlin! 什么是Kotlin? Kotlin 是种静态类型编程...
    任半生嚣狂阅读 26,142评论 9 118
  • 大家好,我是William李梓峰,欢迎加入我的Kotlin学习之旅。今天是我学习 Kotlin 的第十五天,内容是...
    William李梓峰阅读 1,871评论 -1 1
  • 我曾以为我的人生也就如此糟糕了 你曾告诉我,一切不好的都会结束的 我曾以为你说的是真的 也曾以为我们可以一辈子的 ...
    ZI_Y阅读 176评论 0 1
  • 1最先去看的是哪些地方适合自驾游。我也真是为自己感到醉了。 2有些事情不必太在意。 3自己的情绪要控制好,并且不生...
    海样的心阅读 173评论 0 0