Kotlin中的泛型
从 Kotlin 的 in 和 out 说起
下面这段 Java 代码在日常开发中应该很常见了:
1 | |
其中 List<TextView> 表示这是一个泛型类型为 TextView 的 List。
那到底什么是泛型呢?我们先来讲讲泛型的由来。
现在的程序开发大都是面向对象的,平时会用到各种类型的对象,一组对象通常需要用集合来存储它们,因而就有了一些集合类,比如 List、Map 等。
这些集合类里面都是装的具体类型的对象,如果每个类型都去实现诸如 TextViewList、ActivityList 这样的具体的类型,显然是不可能的。
因此就诞生了「泛型」,它的意思是把具体的类型泛化,编码的时候用符号来指代类型,在使用的时候,再确定它的类型。
前面那个例子,List<TextView> 就是泛型类型声明。
既然泛型是跟类型相关的,那么是不是也能适用类型的多态呢?
先看一个常见的使用场景:
1 | |
我们知道 Button 是继承自 TextView 的,根据 Java 多态的特性,第一处赋值是正确的。
但是到了 List<TextView> 的时候 IDE 就报错了,这是因为 Java 的泛型本身具有「不可变性 Invariance」,Java 里面认为 List<TextView> 和 List<Button> 类型并不一致,也就是说,子类的泛型(List<Button>)不属于泛型(List<TextView>)的子类。
Java 的泛型类型会在编译时发生类型擦除,为了保证类型安全,不允许这样赋值。至于什么是类型擦除,这里就不展开了。
你可以试一下,在 Java 里用数组做类似的事情,是不会报错的,这是因为数组并没有在编译时擦除类型:
1 | |
但是在实际使用中,我们的确会有这种类似的需求,需要实现上面这种赋值。
Java 提供了「泛型通配符」 ? extends 和 ? super 来解决这个问题。
Java 中的 ? extends
在 Java 里面是这么解决的:
1 | |
这个 ? extends 叫做「上界通配符」,可以使 Java 泛型具有「协变性 Covariance」,协变就是允许上面的赋值是合法的。
在继承关系树中,子类继承自父类,可以认为父类在上,子类在下。
extends限制了泛型类型的父类型,所以叫上界。
它有两层意思:
- 其中
?是个通配符,表示这个List的泛型类型是一个未知类型。 extends限制了这个未知类型的上界,也就是泛型类型必须满足这个extends的限制条件,这里和定义class的extends关键字有点不一样:- 它的范围不仅是所有直接和间接子类,还包括上界定义的父类本身,也就是
TextView。 - 它还有
implements的意思,即这里的上界也可以是interface。
- 它的范围不仅是所有直接和间接子类,还包括上界定义的父类本身,也就是
这里 Button 是 TextView 的子类,满足了泛型类型的限制条件,因而能够成功赋值。
根据刚才的描述,下面几种情况都是可以的:
1 | |
一般集合类都包含了 get 和 add 两种操作,比如 Java 中的 List,它的具体定义如下:
1 | |
上面的代码中,E 就是表示泛型类型的符号(用其他字母甚至单词都可以)。
我们看看在使用了上界通配符之后,List 的使用上有没有什么问题:
1 | |
前面说到 List<? extends TextView> 的泛型类型是个未知类型 ?,编译器也不确定它是啥类型,只是有个限制条件。
由于它满足 ? extends TextView 的限制条件,所以 get 出来的对象,肯定是 TextView 的子类型,根据多态的特性,能够赋值给 TextView,啰嗦一句,赋值给 View 也是没问题的。
到了 add 操作的时候,我们可以这么理解:
-
List<? extends TextView>由于类型未知,它可能是List<Button>,也可能是List<TextView>。 - 对于前者,显然我们要添加 TextView 是不可以的。
- 实际情况是编译器无法确定到底属于哪一种,无法继续执行下去,就报错了。
那我干脆不要 extends TextView ,只用通配符 ? 呢?
这样使用 List<?> 其实是 List<? extends Object> 的缩写。
1 | |
和前面的例子一样,编译器没法确定 ? 的类型,所以这里就只能 get 到 Object 对象。
同时编译器为了保证类型安全,也不能向 List<?> 中添加任何类型的对象,理由同上。
由于 add 的这个限制,使用了 ? extends 泛型通配符的 List,只能够向外提供数据被消费,从这个角度来讲,向外提供数据的一方称为「生产者 Producer」。对应的还有一个概念叫「消费者 Consumer」,对应 Java 里面另一个泛型通配符 ? super。
Java 中的 ? super
先看一下它的写法:
1 | |
这个 ? super 叫做「下界通配符」,可以使 Java 泛型具有「逆变性 Contravariance」。
与上界通配符对应,这里 super 限制了通配符 ? 的子类型,所以称之为下界。
它也有两层意思:
- 通配符
?表示List的泛型类型是一个未知类型。 super限制了这个未知类型的下界,也就是泛型类型必须满足这个super的限制条件。-
super我们在类的方法里面经常用到,这里的范围不仅包括Button的直接和间接父类,也包括下界Button本身。 -
super同样支持interface。
-
上面的例子中,TextView 是 Button 的父类型 ,也就能够满足 super 的限制条件,就可以成功赋值了。
根据刚才的描述,下面几种情况都是可以的:
1 | |
对于使用了下界通配符的 List,我们再看看它的 get 和 add 操作:
1 | |
解释下,首先 ? 表示未知类型,编译器是不确定它的类型的。
虽然不知道它的具体类型,不过在 Java 里任何对象都是 Object 的子类,所以这里能把它赋值给 Object。
Button 对象一定是这个未知类型的子类型,根据多态的特性,这里通过 add 添加 Button 对象是合法的。
使用下界通配符 ? super 的泛型 List,只能读取到 Object 对象,一般没有什么实际的使用场景,通常也只拿它来添加数据,也就是消费已有的 List<? super Button>,往里面添加 Button,因此这种泛型类型声明称之为「消费者 Consumer」。
小结下,Java 的泛型本身是不支持协变和逆变的。
- 可以使用泛型通配符
? extends来使泛型支持协变,但是「只能读取不能修改」,这里的修改仅指对泛型集合添加元素,如果是remove(int index)以及clear当然是可以的。 - 可以使用泛型通配符
? super来使泛型支持逆变,但是「只能修改不能读取」,这里说的不能读取是指不能按照泛型类型读取,你如果按照Object读出来再强转当然也是可以的。
根据前面的说法,这被称为 PECS 法则:「_Producer-Extends, Consumer-Super_」。
理解了 Java 的泛型之后,再理解 Kotlin 中的泛型,有如练完九阳神功再练乾坤大挪移,就比较容易了。
说回 Kotlin 中的 out 和 in
和 Java 泛型一样,Kolin 中的泛型本身也是不可变的。
- 使用关键字
out来支持协变,等同于 Java 中的上界通配符? extends。 - 使用关键字
in来支持逆变,等同于 Java 中的下界通配符? super。
1 | |
换了个写法,但作用是完全一样的。out 表示,我这个变量或者参数只用来输出,不用来输入,你只能读我不能写我;in 就反过来,表示它只用来输入,不用来输出,你只能写我不能读我。
你看,我们 Android 工程师学不会 out 和 in,其实并不是因为这两个关键字多难,而是因为我们应该先学学 Java 的泛型。是吧?
说了这么多 List,其实泛型在非集合类的使用也非常广泛,就以「生产者-消费者」为例子:
1 | |
再来看看消费者:
1 | |
声明处的 out 和 in
在前面的例子中,在声明 Producer 的时候已经确定了泛型 T 只会作为输出来用,但是每次都需要在使用的时候加上 out TextView 来支持协变,写起来很麻烦。
Kotlin 提供了另外一种写法:可以在声明类的时候,给泛型符号加上 out 关键字,表明泛型参数 T 只会用来输出,在使用的时候就不用额外加 out 了。
1 | |
与 out 一样,可以在声明类的时候,给泛型参数加上 in 关键字,来表明这个泛型参数 T 只用来输入。
1 | |
* 号
前面讲到了 Java 中单个 ? 号也能作为泛型通配符使用,相当于 ? extends Object。
它在 Kotlin 中有等效的写法:* 号,相当于 out Any。
1 | |
和 Java 不同的地方是,如果你的类型定义里已经有了 out 或者 in,那这个限制在变量声明时也依然在,不会被 * 号去掉。
比如你的类型定义里是 out T : Number 的,那它加上 <*> 之后的效果就不是 out Any,而是 out Number。
where 关键字
Java 中声明类或接口的时候,可以使用 extends 来设置边界,将泛型类型参数限制为某个类型的子集:
1 | |
注意这个和前面讲的声明变量时的泛型类型声明是不同的东西,这里并没有 ?。
同时这个边界是可以设置多个,用 & 符号连接:
1 | |
Kotlin 只是把 extends 换成了 : 冒号。
1 | |
设置多个边界可以使用 where 关键字:
1 | |
有人在看文档的时候觉得这个 where 是个新东西,其实虽然 Java 里没有 where ,但它并没有带来新功能,只是把一个老功能换了个新写法。
Kotlin 里 where 这样的写法可读性更符合英文里的语法,尤其是如果 Monster 本身还有继承的时候:
1 | |
reified 关键字
由于 Java 中的泛型存在类型擦除的情况,任何在运行时需要知道泛型确切类型信息的操作都没法用了。
比如你不能检查一个对象是否为泛型类型 T 的实例:
1 | |
Kotlin 里同样也不行:
1 | |
这个问题,在 Java 中的解决方案通常是额外传递一个 Class<T> 类型的参数,然后通过 Class#isInstance 方法来检查:
1 | |
Kotlin 中同样可以这么解决,不过还有一个更方便的做法:使用关键字 reified 配合 inline 来解决:
1 | |
两个 Kotlin 泛型与 Java 泛型不一致的地方
- Java 里的数组是支持协变的,而 Kotlin 中的数组
Array不支持协变。
这是因为在 Kotlin 中数组是用Array类来表示的,这个Array类使用泛型就和集合类一样,所以不支持协变。 - Java 中的
List接口不支持协变,而 Kotlin 中的List接口支持协变。
Java 中的List不支持协变,原因在上文已经讲过了,需要使用泛型通配符来解决。
在 Kotlin 中,实际上MutableList接口才相当于 Java 的List。Kotlin 中的List接口实现了只读操作,没有写操作,所以不会有类型安全上的问题,自然可以支持协变。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!