8.4 Kotlin泛型

泛型,即 "参数化类型",将类型参数化,可以用在类、接口、方法上。
与Java语言中的非常相似,但是Kotlin语言的创建者试图通过引入特殊的关键字(如out和in)来使它们更加直观和易于理解。
以下是使用泛型的主要优点:

  • 类型安全:通用允许仅保留单一类型的对象。泛型不允许存储其他对象。
  • 不需要类型转换:不需要对对象进行类型转换。
  • 编译时间检查:在编译时检查泛型代码,以便在运行时避免任何问题

泛型类

像 java 一样,Kotlin 中的类可以拥有类型参数:

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

通常来说,创建一个这样类的实例,我们需要提供类型参数:

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

但如果类型有可能是推断的,比如来自构造函数的参数或者通过其它的一些方式,一个可以忽略类型的参数:

val box = Box(1)    //1是 Int 型,因此编译器会推导出我们调用的是 Box<Int>

泛型接口

声明泛型接口的格式与声明泛型类相似,定义如下泛型接口。

interface GenericsInterface<T> {
    public T generate();
}

在实现泛型接口的类中,指定泛型实参。

class GenericsClass implements GenericsInterface<String> {
    public String generate() {
        return "hello";
    }
}

泛型函数

范型函数
函数也可以像类一样有类型参数。类型参数在函数名之前:

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

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

调用范型函数需要在函数名后面制定类型参数:

val l = singletonList<Int>(1)

范型约束

指定类型参数代替的类型集合可以用通过范型约束进行限制。
上界(upper bound):最常用的类型约束是上界,在 Java 中对应 extends关键字:

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() }
}

声明处变型*

java声明处变型

假如有个范型接口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声明处变型

在 Kotlin 中,有种可以将这些东西解释给编译器的办法,叫做声明处变型:通过注解类型参数 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 的成员的输出-位置,结果是 C<Base> 可以安全地作为 C<Derived>的超类。

更聪明的说法就是,当类 C 在类型参数 T 之下是协变的,或者 T 是一个协变类型。可以把 C 想象成 T 的生产者,而不是 T 的消费者。

out 修饰符本来被称之为变型注解,但由于同处与类型参数声明处,我们称之为声明处变型。这与 Java 中的使用处变型相反。

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

使用处变型:类型投影*

声明类型参数 T 为 out 很方便,而且可以避免在使用出子类型的麻烦,但有些类 不能 限制它只返回 T ,Array 就是一个例子:

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

这个类既不能是协变的也不能是逆变的,这会在一定程度上降低灵活性。考虑下面的函数:

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

该函数作用是复制 array ,让我们来实际应用一下:

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> 互不为对方的子类,导致复制失败。为什么呢?应为复制可能会有不合适的操作,比如尝试写入,当我们尝试将 Int 写入 String 类型的 array 时候将会导致 ClassCastException 异常。

我们想做的就是确保 copy() 不会做类似的不合适的操作,为阻止向from写入,我们可以这样:

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

这就是类型投影:这里的from不是一个简单的 array, 而是一个投影,我们只能调用那些返回类型参数 T 的方法,在这里意味着我们只能调用get()。这是我们处理调用处变型的方法,类似 Java 中Array<? extends Object>,但更简单。

当然也可以用in做投影:

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

Array<in String> 对应 Java 中的 Array<? super String>,fill()函数可以接受任何CharSequence 类型或 Object类型的 array 。

参考文章:
kotlin-in-chinese

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