Kotlin 泛型

泛型和委托

泛型的基本用法

Java 早在 1.5 版本中就引入了泛型的机制,因此,Kotlin 自然也就支持了泛型功能。但是 Kotlin 中的泛型与 Java 中的泛型有同有异,这里先看与 Java 中相同的部分。

泛型,是指在一般的编程模式下,我们需要给任何一个变量指定一个具体的类型,而泛型允许我们在不指定具体类型的情况下进行编程,这样编写出来的代码将会拥有更好的扩展性。比如 List 就是使用泛型来实现的。

泛型主要有两种定义方式,一种是定义泛型类,另一种是定义泛型方法,使用的语法结构都是 <T>,这个 T 只是一种约定俗成的泛型写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
fun main(){
// 泛型类的调用方式
val myClass = MyClass<Int>()
val result = myClass.method(123)
println("$result")

// 泛型方法的调用方式
val myClass2 = MyClass2()
// 类型推导机制,所以可省略泛型的指定。
// val result = myClass.method<Int>(123)
val result2 = myClass2.method(123)
println("$result2")
}

/**
* 定义泛型类
*/
class MyClass<T>{

/**
* 方法允许使用 T 类型的参数和返回值
*/
fun method(param: T):T{
return param
}
}

/**
* 定义泛型方法
*/
class MyClass2{

/**
* 泛型方法
*/
fun <T> method(param: T):T{
return param
}

/**
* 指定上界的方式,对泛型的类型进行约束。
* 这里表示,只能将方法的泛型指定为数字类型,比如 Int、Float、Double 等。
*
* 另外,默认情况下,所有泛型都是可以指定成可空类型的,这是因为在不手动指定上界时,泛型的上界默认是 Any?。
* 如果想让泛型的类型不可为空,只需要将泛型的上界手动指定成 Any 就可以了。
*/
fun <T:Number> method2(param: T):T{
return param
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
fun main(){
contentResolver.query(uri,null,null,null,null)?.apply {
while (moveToNext()){
...
}
close()
}

contentResolver.query(uri,null,null,null,null)?.build {
while (moveToNext()){
...
}
close()
}
}

/**
* 前面了解过的,此函数的作用与 apply 函数基本一样,区别在于此函数只能作用在 SB 上。
*/
fun StringBuilder.build(block:StringBuilder.() -> Unit):StringBuilder{
block()
return this
}

/**
* 使用泛型,对上面函数进行扩展,让它实现和 apply 函数完全一样的功能。
*/
fun <T> T.build(block: T.() -> Unit):T {
block()
return this
}

类委托和委托属性

委托是一种设计模式,它的基本理念是:操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。Java 对于委托并没有语言层级的实现,而像 C# 语言就对委托进行了原生的支持。Kotlin 中也是支持委托功能的,并且将之分为两种:类委托和委托属性。

类委托,它的核心思想在于将一个类的具体实现委托给另一个类去完成。Set 这种数据结构,它是一个接口,如果要实现它,需要使用它具体的实现类,比如 HashSet。借助委托模式,我们可以轻松实现一个自己的实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* 构造函数中接收了一个 HashSet 参数,这就相当于一个辅助对象。
* 然后在 Set 接口所有的方法实现中,都没有进行自己的实现,
* 而是调动了辅助对象中相应的方法实现,这其实就是一种委托模式。
*
* 委托模式的意义在于:
* 可以只是让大部分的方法实现调用辅助对象中的方法,少部分的方法实现由自己来重写,甚至加入一些自己独有的方法,
* 这样,MySet 就会成为一个全新的数据结构类。
*
* 这种写法的弊端是,如果接口中待实现的方法太多,那么每个都要去这样调用辅助对象中的相应方法实现。
* 这个问题,在 Java 中没有什么解决方案,但在 Kotlin 中可以通过类委托的功能来解决。
*/
//class MySet<T>(val helperSet: HashSet<T>):Set<T> {
//
// override val size: Int
// get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.
//
// override fun contains(element: T): Boolean {
// TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
// }
//
// override fun containsAll(elements: Collection<T>): Boolean {
// TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
// }
//
// override fun isEmpty(): Boolean {
// TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
// }
//
// override fun iterator(): Iterator<T> {
// TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
// }
//}

/**
* Kotlin 中委托使用的关键字是 by,只需要在接口声明的后面使用 by 关键字,
* 再接上受委托的辅助对象,就可以免去之前所写的一大堆模板式的代码了。
*
* 这两段代码实现的效果是一致的。
*/
//class MySet<T>(val helperSet: HashSet<T>):Set<T> by helperSet{
//
//}

/**
* 如果要对某个方法进行重新实现,只需单独重写。
* 其它 Set 接口中的功能,则和 HashSet 保持一致。
*/
class MySet<T>(val helperSet: HashSet<T>):Set<T> by helperSet{

/**
* 新增方法
*/
fun helloWorld() = println("Hello World")

/**
* 重写方法
*/
override fun isEmpty() = false
}

委托属性的核心思想是将一个属性(字段)的具体实现委托给另一个类去实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class MyClass {

/**
* by 关键字连接了左边的 p 属性和右边的 Delegate() 实例。
* 这种写法就代表,将 p 属性的具体实现委托给了 Delegate 类去完成。
* 当调用 p 属性时,会自动调用 Delegate 类的 getValue(),
* 当给 p 属性赋值时,会自动调用 Delegate 类的 setValue()。
*
* 假设这里使用 val 关键字,那么只需要实现 getValue() 即可,
* 因为 p 属性是无法在初始化后被重新赋值的。
*/
var p by Delegate()
}

/**
* 对 Delegate 类进行具体的实现
*/
class Delegate{

var propValue: Any? = null

/**
* @param myClass 用于声明该类的委托功能可以在什么类中使用,这里表示仅可在 MyClass 类中使用。
* @param property 是 Kotlin 中的一个属性操作类,可用于获取各种属性相关的值。
* 另外 <*> 这种泛型的写法表示你不知道或者不关心泛型的具体类型,只是为了通过语法编译而已,有点类似 Java中 <?> 的写法。
* 返回值可声明成任何类型,根据具体的实现逻辑写,这里只是一种标准的实例写法。
*/
operator fun getValue(myClass: MyClass, property: KProperty<*>): Any? {
return propValue
}

/**
* @param any 表示具体要赋值给委托属性的值,此参数的类型必须和 getValue() 返回值的类型保持一致。
*/
operator fun setValue(myClass: MyClass, property: KProperty<*>, any: Any?) {
propValue = any
}
}

实现一个自己的 lazy 函数(一些诸如同步、空值处理等方面没有实现得很严谨)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 解密 by lazy:
* 懒加载技术,只有当变量首次调用时,代码块中的代码才会执行。
* 这里,by 才是 Kotlin 中的关键字,lazy 在这里只是一个高阶函数。
* 在 lazy 函数中会创建并返回一个 Delegate 对象,当调用 p 属性时,其实调用的是 Delegate 对象的 getValue(),
* 然后 getValue() 中又会调用 lazy 函数传入的 Lambda 表达式,这样表达式中的代码就可以得到执行,
* 并且调用 p 属性后得到的值就是 Lambda 表达式中最后一行代码的返回值。
*/
val p by lazy { }

/**
* 使用自己实现的 lazy 函数
*/
val p by later {
// 验证 later 函数的懒加载功能是否生效。
// 将这段代码放在任何一个 Activity 中,并在按钮的点击事件里调用 p 属性。
// 当 Activity 启动时,later 函数中的那行日志是不会打印的。只有当首次点击按钮调用时才会打印。
// 并且,代码块中的代码只会执行一次,再次点击按钮也不会打印日志。
Log.d("TAG","run codes inside later block")
"test later"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 定义 Later 类,并指定为泛型。
* 构造函数中接收一个函数类型参数,其中不接收任何参数,返回值的类型就是 Later 指定的泛型。
*
* 由于懒加载技术是不会对属性进行赋值的,因此这里就不用实现 setValue() 了。
*/
class Later<T>(val block:() -> T){

/**
* 使用 value 变量对值进行缓存,
* 如果为空就调用构造函数中传入的函数类型参数去获取值,否则就直接返回。
*/
var value: Any? = null

/**
* @param any Any?类型表示希望 Later 的委托功能在所有类中都可以使用。
*/
operator fun getValue(any: Any?,prop:KProperty<*>): T{
if (value == null){
value = block()
}
return value as T
}
}

/**
* 为了让 Later 的用法更加类似于 lazy 函数,这里定义一个顶层函数。
* 并且要定义在 Later 类的外面,因为只有不定义在任何类当中的函数才是顶层函数。
*
* 使用时,将 lazy 替换为 later。
*/
fun <T> later(block: () -> T) = Later(block)

infix 函数

使用 infix 函数构建更可读的语法

对于 A to B 这样的语法结构来构建键值对,包括 Kotlin 自带的 mapOf() 函数,并不是因为 to 是 Kotlin 中的一个关键字,而是 Kotlin 提供了一种高级语法糖特性:infix 函数。

infix 函数实际上是把编程语言函数调用的语法规则调整了一下,比如 A to B 这样的写法,实际上等价于 A.to(B) 的写法。

示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
fun main(){
// 判断一个字符串是否是以某个指定参数开头的
if ("Hello Kotlin".startsWith("Hello")){
// 处理具体逻辑
println("true")
}else {
println("false")
}

// 借助 infix 实现一种更具有可读性的语法来表达上面代码。一种特殊的语法糖格式。
// ------------------------------------------------------------
// infix 函数的语法规则:
// 实际就是调用 "Hello Kotlin" 的 beginsWith(),并传入了一个字符串参数。
// 只不过 infix 函数允许将函数调用时的小数点、括号等计算机相关语法去掉,
// 从而使用一种更接近英语的写法来编写程序,让代码看起来更加具有可读性。
// ------------------------------------------------------------
// infix 函数的语法糖使用限制:
// 一:函数不能定义成顶层函数,必须是某个类的成员函数,可以使用扩展函数的方式将它定义到某个类中。
// 二:必须接收且只能接收一个参数,至于参数类型没有限制。
// 只有同时满足这两点,infix 函数的语法糖才具备使用的条件。
// ------------------------------------------------------------
if ("Hello Kotlin" beginsWith "Hello"){
// 处理具体逻辑
println("true")
}else {
println("false")
}
}

/**
* 这是一个 String 类的扩展函数,加上了 infix 关键字后,此函数变成了一个 infix 函数。
* beginsWith() 也是用于判断一个字符串是否是以某个指定参数开头的,
* 并且它的内部实现就是调用的 String 类的 startsWith()。
*/
infix fun String.beginsWith(prefix: String) = startsWith(prefix)

示例 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fun main(){
val list = listOf("Apple","Banana","Pear")
if (list.contains("Apple")){
println("true")
}else {
println("false")
}

val list1 = listOf("Apple","Banana","Pear")
if (list has "Apple"){
println("true")
}else {
println("false")
}
}

/**
* 给 Collection 接口添加了一个扩展函数,内部调用了 contains(),所以,
* has() 和 contains() 的功能是一致的,只不过多了一个 infix 关键字,从而拥有了其语法糖功能。
*
* Collection 是 Java 以及 Kotlin 所有集合的总接口,所以 has() 可以被所有集合的子类使用了。
*/
infix fun <T> Collection<T>.has(element: T) = contains(element)

示例 3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
fun main(){
val map = mapOf("Apple" to 1,"Banana" to 2,"Pear" to 3)
for (i in map){
println("i is $i, key is ${i.key}, value is ${i.value}")
}

// 模仿 to() 函数的源码来编写自己的键值对构建函数。
val map1 = mapOf("Apple" with 1,"Banana" with 2,"Pear" with 3)
for (i in map1){
println("i is $i, key is ${i.key}, value is ${i.value}")
}
}

/**
* 模仿 to() 函数的源码来编写自己的键值对构建函数
* ---------------------------------------------------------------------------
* to() 函数源码如下:
* public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
* ---------------------------------------------------------------------------
* 首先,使用定义泛型函数的方式将 to() 定义到了 A 类型下,并且接收一个 B 类型的参数。
* 因此 A 和 B 可以是两种不同类型的泛型,也就使得我们可以构建出字符串 to 整型这样的键值对。
* ---------------------------------------------------------------------------
* to() 函数的具体实现:
* 创建并返回了一个 Pair 对象,实际上 A to B 这样的语法结构得到的是一个包含 A、B 数据的 Pair 对象,
* 而 mapOf() 实际上接收的正是一个 Pair 类型的可变参数列表。
*/
infix fun <A,B> A.with(that:B): Pair<A,B> = Pair(this,that)

泛型的高级特性

Kotlin 在泛型方面提供的特有功能

对泛型进行实化

在 JDK 1.5 之前,Java 是没有泛型的,那时诸如 List 之类的数据结构可以存储任意类型的数据,取出数据的时候也需要手动向下转型,这不仅麻烦,而且很危险。比如说在同一个 List 中存储了字符串和整型这两种数据,但是在取出数据时却无法区分具体的数据类型,如果手动将它们强制转成同一种类型,那么就会抛出类型转换异常。

于是在 JDK 1.5 中,Java 引入了泛型功能。这不仅让诸如 List 之类的数据结构变得简单好用,也让代码变得更加安全。但实际上,Java 的泛型功能是通过类型擦除机制来实现的。

泛型对于类型的约束只在编译时期存在,运行时仍然会按照 JDK 1.5 之前的机制来运行,JVM 是识别不出在代码中指定的泛型类型的。比如 List<String>集合,虽然在编译时期只能添加字符串类型的元素,但在运行时期 JVM 并不能知道它本来只打算包含哪种类型的元素,只能识别出它是个 List。

所以基于 JVM 的语言,它们的泛型功能都是通过类型擦除机制来实现的,这种机制使得我们不可能使用 a is T 或者 T::class.java 这样的语法,因为 T 的实际类型在运行时已经被删除了。

然而,Kotlin 提供了一个内联函数的概念,内联函数中的代码会在编译时自动被替换到调用它的地方,这样的话也就不存在什么泛型擦除的问题了,因为代码在编译之后会直接使用实际的类型来替代内联函数中泛型声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 内联函数的代码替换过程
* foo() 函数调用了带有泛型类型的内联函数 bar() 函数,
* 在代码编译之后,bar() 函数中的代码将可以获得泛型的实际类型。
*/
fun foo(){
bar<String>()
}

inline fun <T> bar(){
// do something with T type
}

/**
* 替换完成后的代码
*/
fun foo(){
// do something with T type
}

泛型实化的条件:首先,该函数必须是内联函数。其次,在声明泛型的地方必须加上 reified 关键字来表示该泛型要进行实化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun main(){
val result1 = getGenericType<String>()
val result2= getGenericType<Int>()
println(result1)
println(result2)

// 打印结果:
// class java.lang.String
// class java.lang.Integer
}

/**
* 获取泛型实际类型的功能,这在 Java 中是不可能实现的功能。
* 此函数直接返回了当前指定泛型的实际类型。
* T.clss 这样的语法在 Java 中是不合法的,而在 Kotlin 中借助泛型实化就可以使用 T::class.java 这样的语法了。
*/
inline fun <reified T> getGenericType() = T::class.java

泛型实化的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Intent 的第二个参数本该是一个具体的 Activity 的 Class 类型,
* 但由于 T 已经是一个被实化的泛型了,因此这里可直接传入 T::class.java。
*/
inline fun <reified T> startActivity(context: Context){
val intent = Intent(context,T::class.java)
context.startActivity(intent)
}

/**
* 启动 Activity 时传参
* 增加了一个函数类型参数,并且它的函数类型是定义在 Intent 类当中的
*/
inline fun <reified T> startActivity(context: Context,block:Intent.() ->Unit){
val intent = Intent(context,T::class.java)
intent.block()
context.startActivity(intent)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
companion object{
fun actionStart(context: Context){
// 使用泛型实化来启动 Activity
startActivity<ServiceActivity>(context)

// 带参数启动 Activity
startActivity<Main2Activity>(context){
putExtra("param1",data1)
putExtra("param2",data2)
}

// 其它像 Service 的代码也是基本类似的。
}
}

泛型的协变

泛型的协变和逆变功能不太常用,而且不易理解。但是 Kotlin 的内置 API 中使用了很多协变和逆变的特性。

首先了解一个约定。一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,因此可以称它为 in 位置,而它的返回值是输出数据的地方,因此可以称它为 out 位置。

假设某个方法接收一个 Persion 类型参数,这时传入 Student 的实例是合法的,但如果某个方法接收一个 List<Persion> 类型参数,这时传入一个 List<Student> 的实例,在 Java 中是不允许的,因为 ``List 不能成为 List的子类`,否则将可能存在类型转换的安全隐患。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
fun main(){
val student = Student("Tom",19)
val data = SimpleData<Student>()
data.set(student)
handleSimpleData(data) // 实际上这行代码会报错,这里假设它能编译通过
val studentData = data.get()
}

fun handleSimpleData(data: SimpleData<Person>){
// 传入的是 SimpleData<Student>,但创建的是 Teacher 的实例用来替换 SimpleData<Person> 参数中的原有数据。
// 但在上述 data.get() 获取内部封装的 Student 数据,可现在 SimpleData<Student> 实际包含的却是一个 Teacher 实例,
// 那么,这里必然会产生类型转换异常。所以,Java 是不允许使用这种方式传递参数的。
// 换句话说,即使 Student 是 子类,SimpleData<Student> 并不是 SimpleData<Person> 的子类。
val teacher = Teacher("Jack",35)
data.set(teacher)
}

// 定义 3 个类
open class Person(val name: String,val age:Int)
class Student(name:String,age: Int):Person(name,age)
class Teacher(name:String,age: Int):Person(name,age)

class SimpleData<T>{
private var data:T? = null

fun set(t:T?){
data = t
}
fun get():T?{
return data
}
}

泛型协变的定义:假如定义了一个 MyClass<T>的泛型类,其中 A 是 B 的子类型,同时MyClass<A>又是MyClass<B>的子类型,那么我们就可以称 MyClass 在 T 这个泛型上是协定的。

如何才能让MyClass<A>成为MyClass<B>的子类型:如果一个泛型类在其泛型类型的数据上是只读的,那么它是没有类型转换安全隐患的。而要实现这一点,则需要让MyClass<T>类中的所有方法都不能接收 T 类型的参数。换句话说,T 只能出现在 out 位置上,而不能出现在 in 位置上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
fun main(){
val student = Student("Tom",19)
// 由于 SimpleData 类进行了协变声明,那么 SimpleData<Student> 自然就是 SimpleData<Person> 的子类了。
val data = SimpleData<Student>(student)
handleSimpleData(data)
val studentData = data.get()
}

fun handleSimpleData(data: SimpleData<Person>){
// 获取 SimpleData 封装的数据,虽然泛型声明是 Person 类型,实际获得的会是一个 Student 实例,
// 但由于 Persion 是 Student 的父类,向上转型是完全安全的。
val personData = data.get()
}

open class Person(val name: String,val age:Int)
class Student(name:String,age: Int):Person(name,age)
class Teacher(name:String,age: Int):Person(name,age)

/**
* out 关键字说明 T 只能出现在 out 位置上,同时也意味着 SimpleData 是在泛型 T 上是协变的。
* 由于泛型 T 不能出现在 in 位置上,所以也就不能使用 set() 为 data 参数赋值了,所以这里采用构造函数的方式来赋值。
* 虽然构造函数中的泛型 T 在 in 位置上,但由于使用了 val 关键字,所以 T 仍然是只读的,
* 即使使用 var 关键字,只要给它加上 private 修饰符,保证这个泛型 T 对于外部而言是不可修改的,那么就都是合法的写法。
*/
class SimpleData<out T>(val data:T?){

fun get():T?{
return data
}
}

上述中,如果某个方法接收一个 List<Persion> 类型参数,这时传入一个 List<Student> 的实例,在 Java 中是不允许的,但在 Kotlin 中是合法的,因为已经默认给许多内置的 API 加上了协变声明,其中就包括了各种集合的类与接口。比如 List 本身就是只读的,如果想添加数据,需要使用 MutableList 才行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* List 简化版源码:
* out 关键字说明 List 在泛型 E 上是协变的。
* 原则上声明了协变后,泛型 E 只能出现在 out 位置,但 contaion() 中,仍然出现在 in 位置上,
* 这是因为 contains() 的目的非常明确,它只是为了判断当前集合中是否包含参数中传入的这个元素,而不会修改当前集合中的内容,因此这种操作实际上又是安全的。为了让编译器能够理解这种操作是安全的,使用了 @UnsafeVariance 注解,这样编译器就会允许泛型 E 出现在 in 位置上了。
*/
public interface List<out E> : Collection<E> {

override val size: Int
override fun isEmpty(): Boolean
override fun contains(element: @UnsafeVariance E): Boolean
override fun iterator(): Iterator<E>
public operator fun get(index: Int): E
}

泛型的逆变

逆变的定义:假如定义了一个MyClass<T>的泛型类,其中 A 是 B 的子类型,同时MyClass<B>又是MyClass<A>的子类型,那么就可以称 MyClass 在 T 这个泛型上是逆变的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fun main(){
// 编写了一个 Transformer<Person> 的匿名类实现,
val trans = object :Transformer<Person>{
// 通过 transform() 将传入的 Person 对象转换成了一个字符串
override fun transform(t: Person): String {
return "${t.name} ${t.age}"
}
}
// 因为 Transformer<Person> 并不是 Transformer<Student> 的子类型
handleTransformer(trans) // 这行代码会报错
}

fun handleTransformer(trans:Transformer<Student>){
val student = Student("Tom",19)
val result = trans.transform(student)
}

/**
* 用来执行一些转换操作
*/
interface Transformer<T>{
// 参数 T 经过转换后,将会变成一个字符串,至于具体的转换逻辑则由子类去实现。
fun transform(t:T):String
}


open class Person(val name: String,val age:Int)
class Student(name:String,age: Int):Person(name,age)
class Teacher(name:String,age: Int):Person(name,age)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* 使用逆变
*/
fun main(){
val trans = object :Transformer<Person>{
override fun transform(t: Person): String {
return "${t.name} ${t.age}"
}
}
// 此时,Transformer<Person> 已经是 Transformer<Student> 的子类型了
handleTransformer(trans)

// 如果让泛型 T 出现在 out 位置的隐患
val trans1 = object :Transformer1<Person>{
// 构建了一个 Teacher 对象,并直接返回。
override fun transform(name: String,age: Int): Person {
// transform() 的返回值要求是一个 Person 对象,而 Teacher 是 Person 的子类,这种写法是合法的。
return Teacher(name,age)
}
}
handleTransformer1(trans1)
}

fun handleTransformer(trans:Transformer<Student>){
val student = Student("Tom",19)
val result = trans.transform(student)
println(result)
// 打印结果:
// Tom 19
}

fun handleTransformer1(trans:Transformer1<Student>){
// 期望得到的是一个 Student 对象,但实际上得到的是一个 Teacher 对象,因此造成类型转换异常。
val result = trans.transform("Tom",19)
println(result)
// 打印结果
// Exception in thread "main" java.lang.ClassCastException:
// com.example.myapplication.test.Teacher cannot be cast to com.example.myapplication.test.Student
}

/**
* 用来执行一些转换操作
* in 关键字表示 T 只能出现在 in 位置,Transformer 在泛型 T 上是逆变的
*/
interface Transformer<in T>{
fun transform(t:T):String
}

/**
* 如果让泛型 T 出现在 out 位置的隐患
*/
interface Transformer1<in T>{
fun transform(name:String,age: Int):@UnsafeVariance T
}

open class Person(val name: String,val age:Int)
class Student(name:String,age: Int):Person(name,age)
class Teacher(name:String,age: Int):Person(name,age)

逆变功能在 Kotlin 内置 API 中的应用,比较典型的例子就是 Comparable 的使用,Comparable 是一个用于比较两个对象大小的接口。

1
2
3
4
5
6
7
8
/**
* Comparable 的源码:
* Comparable 接口为逆变的意义在于,如果使用 Comparable<Person> 实现了让两个 Person 对象比较大小的逻辑,那么用这段逻辑去比较两个 Student 对象的大小也一定是成立的,因此让 Comparable<Person> 成为 Comparable<Student> 的子类合情合理,这也是逆变非常典型的应用。
*/
public interface Comparable<in T> {
// 实现具体的比较逻辑
public operator fun compareTo(other: T): Int
}

关于逆变,假设 Student 是 Person 的子类,然后让 Comparable<Person> 成为 Comparable<Student>的子类,这叫逆变,而接收 Student 参数的方法硬要传 Person 参数进去,这是不可能的,因为 Person 永远不会是 Student 的子类。


备注

参考资料

第一行代码(第3版)
官方文档
官方中文翻译站

传送门GitHub