本期做者:java
视频:扔物线(朱凯)git
文章:Bruce(郑啸天)github
你们好,我是扔物线朱凯。你在看的是码上开学项目的 Kotlin 高级部分的第 1 篇:Kotlin 的泛型。首当其冲的固然仍是香香的视频香香的我啦:数组
由于我一直没有学会怎么在掘金贴视频,因此请点击 这里 去哔哩哔哩看,或者点击 这里 去 YouTube 看。安全
如下内容来自文章做者Bruce。ide
这期是码上开学 Kotlin 系列的独立技术点部分的第一期,咱们来聊一聊泛型。函数
提到 Kotlin 的泛型,一般离不开 in
和 out
关键字,但泛型这门武功须要些基本功才能修炼,不然容易走火入魔,待笔者慢慢道来。post
下面这段 Java 代码在平常开发中应该很常见了:网站
☕️
List<TextView> textViews = new ArrayList<TextView>();
复制代码
其中 List<TextView>
表示这是一个泛型类型为 TextView
的 List
。ui
那到底什么是泛型呢?咱们先来说讲泛型的由来。
如今的程序开发大都是面向对象的,平时会用到各类类型的对象,一组对象一般须要用集合来存储它们,于是就有了一些集合类,好比 List
、Map
等。
这些集合类里面都是装的具体类型的对象,若是每一个类型都去实现诸如 TextViewList
、ActivityList
这样的具体的类型,显然是不可能的。
所以就诞生了「泛型」,它的意思是把具体的类型泛化,编码的时候用符号来指代类型,在使用的时候,再肯定它的类型。
前面那个例子,List<TextView>
就是泛型类型声明。
既然泛型是跟类型相关的,那么是否是也能适用类型的多态呢?
先看一个常见的使用场景:
☕️
TextView textView = new Button(context);
// 👆 这是多态
List<Button> buttons = new ArrayList<Button>();
List<TextView> textViews = buttons;
// 👆 多态用在这里会报错 incompatible types: List<Button> cannot be converted to List<TextView>
复制代码
咱们知道 Button
是继承自 TextView
的,根据 Java 多态的特性,第一处赋值是正确的。
可是到了 List<TextView>
的时候 IDE 就报错了,这是由于 Java 的泛型自己具备「不可变性 Invariance」,Java 里面认为 List<TextView>
和 List<Button>
类型并不一致,也就是说,子类的泛型(List<Button>
)不属于泛型(List<TextView>
)的子类。
Java 的泛型类型会在编译时发生类型擦除,为了保证类型安全,不容许这样赋值。至于什么是类型擦除,这里就不展开了。
你能够试一下,在 Java 里用数组作相似的事情,是不会报错的,这是由于数组并无在编译时擦除类型:
☕️ TextView[] textViews = new TextView[10]; 复制代码
可是在实际使用中,咱们的确会有这种相似的需求,须要实现上面这种赋值。
Java 提供了「泛型通配符」 ? extends
和 ? super
来解决这个问题。
? extends
在 Java 里面是这么解决的:
☕️
List<Button> buttons = new ArrayList<Button>();
👇
List<? extends TextView> textViews = buttons;
复制代码
这个 ? extends
叫作「上界通配符」,可使 Java 泛型具备「协变性 Covariance」,协变就是容许上面的赋值是合法的。
在继承关系树中,子类继承自父类,能够认为父类在上,子类在下。
extends
限制了泛型类型的父类型,因此叫上界。
它有两层意思:
?
是个通配符,表示这个 List
的泛型类型是一个未知类型。extends
限制了这个未知类型的上界,也就是泛型类型必须知足这个 extends
的限制条件,这里和定义 class
的 extends
关键字有点不同:
TextView
。implements
的意思,即这里的上界也能够是 interface
。这里 Button
是 TextView
的子类,知足了泛型类型的限制条件,于是可以成功赋值。
根据刚才的描述,下面几种状况都是能够的:
☕️
List<? extends TextView> textViews = new ArrayList<TextView>(); // 👈 自己
List<? extends TextView> textViews = new ArrayList<Button>(); // 👈 直接子类
List<? extends TextView> textViews = new ArrayList<RadioButton>(); // 👈 间接子类
复制代码
通常集合类都包含了 get
和 add
两种操做,好比 Java 中的 List
,它的具体定义以下:
☕️
public interface List<E> extends Collection<E>{
E get(int index);
boolean add(E e);
...
}
复制代码
上面的代码中,E
就是表示泛型类型的符号(用其余字母甚至单词均可以)。
咱们看看在使用了上界通配符以后,List
的使用上有没有什么问题:
☕️
List<? extends TextView> textViews = new ArrayList<Button>();
TextView textView = textViews.get(0); // 👈 get 能够
textViews.add(textView);
// 👆 add 会报错,no suitable method found for add(TextView)
复制代码
前面说到 List<? extends TextView>
的泛型类型是个未知类型 ?
,编译器也不肯定它是啥类型,只是有个限制条件。
因为它知足 ? extends TextView
的限制条件,因此 get
出来的对象,确定是 TextView
的子类型,根据多态的特性,可以赋值给 TextView
,啰嗦一句,赋值给 View
也是没问题的。
到了 add
操做的时候,咱们能够这么理解:
List<? extends TextView>
因为类型未知,它多是 List<Button>
,也多是 List<TextView>
。那我干脆不要 extends TextView
,只用通配符 ?
呢?
这样使用 List<?>
实际上是 List<? extends Object>
的缩写。
☕️
List<Button> buttons = new ArrayList<>();
List<?> list = buttons;
Object obj = list.get(0);
list.add(obj); // 👈 这里仍是会报错
复制代码
和前面的例子同样,编译器无法肯定 ?
的类型,因此这里就只能 get
到 Object
对象。
同时编译器为了保证类型安全,也不能向 List<?>
中添加任何类型的对象,理由同上。
因为 add
的这个限制,使用了 ? extends
泛型通配符的 List
,只可以向外提供数据被消费,从这个角度来说,向外提供数据的一方称为「生产者 Producer」。对应的还有一个概念叫「消费者 Consumer」,对应 Java 里面另外一个泛型通配符 ? super
。
? super
先看一下它的写法:
☕️
👇
List<? super Button> buttons = new ArrayList<TextView>();
复制代码
这个 ? super
叫作「下界通配符」,可使 Java 泛型具备「逆变性 Contravariance」。
与上界通配符对应,这里 super 限制了通配符 ? 的子类型,因此称之为下界。
它也有两层意思:
?
表示 List
的泛型类型是一个未知类型。super
限制了这个未知类型的下界,也就是泛型类型必须知足这个 super
的限制条件。
super
咱们在类的方法里面常常用到,这里的范围不只包括 Button
的直接和间接父类,也包括下界 Button
自己。super
一样支持 interface
。上面的例子中,TextView
是 Button
的父类型 ,也就可以知足 super
的限制条件,就能够成功赋值了。
根据刚才的描述,下面几种状况都是能够的:
☕️
List<? super Button> buttons = new ArrayList<Button>(); // 👈 自己
List<? super Button> buttons = new ArrayList<TextView>(); // 👈 直接父类
List<? super Button> buttons = new ArrayList<Object>(); // 👈 间接父类
复制代码
对于使用了下界通配符的 List
,咱们再看看它的 get
和 add
操做:
☕️
List<? super Button> buttons = new ArrayList<TextView>();
Object object = buttons.get(0); // 👈 get 出来的是 Object 类型
Button button = ...
buttons.add(button); // 👈 add 操做是能够的
复制代码
解释下,首先 ?
表示未知类型,编译器是不肯定它的类型的。
虽然不知道它的具体类型,不过在 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 中的泛型,有如练完九阳神功再练乾坤大挪移,就比较容易了。
out
和 in
和 Java 泛型同样,Kolin 中的泛型自己也是不可变的。
out
来支持协变,等同于 Java 中的上界通配符 ? extends
。in
来支持逆变,等同于 Java 中的下界通配符 ? super
。🏝️
var textViews: List<out TextView>
var textViews: List<in TextView>
复制代码
换了个写法,但做用是彻底同样的。out
表示,我这个变量或者参数只用来输出,不用来输入,你只能读我不能写我;in
就反过来,表示它只用来输入,不用来输出,你只能写我不能读我。
你看,咱们 Android 工程师学不会 out
和 in
,其实并非由于这两个关键字多难,而是由于咱们应该先学学 Java 的泛型。是吧?
说了这么多 List
,其实泛型在非集合类的使用也很是普遍,就以「生产者-消费者」为例子:
🏝️
class Producer<T> {
fun produce(): T {
...
}
}
val producer: Producer<out TextView> = Producer<Button>()
val textView: TextView = producer.produce() // 👈 至关于 'List' 的 `get`
复制代码
再来看看消费者:
🏝️
class Consumer<T> {
fun consume(t: T) {
...
}
}
val consumer: Consumer<in Button> = Consumer<TextView>()
consumer.consume(Button(context)) // 👈 至关于 'List' 的 'add'
复制代码
out
和 in
在前面的例子中,在声明 Producer
的时候已经肯定了泛型 T
只会做为输出来用,可是每次都须要在使用的时候加上 out TextView
来支持协变,写起来很麻烦。
Kotlin 提供了另一种写法:能够在声明类的时候,给泛型符号加上 out
关键字,代表泛型参数 T
只会用来输出,在使用的时候就不用额外加 out
了。
🏝️ 👇
class Producer<out T> {
fun produce(): T {
...
}
}
val producer: Producer<TextView> = Producer<Button>() // 👈 这里不写 out 也不会报错
val producer: Producer<out TextView> = Producer<Button>() // 👈 out 能够但不必
复制代码
与 out
同样,能够在声明类的时候,给泛型参数加上 in
关键字,来代表这个泛型参数 T
只用来输入。
🏝️ 👇
class Consumer<in T> {
fun consume(t: T) {
...
}
}
val consumer: Consumer<Button> = Consumer<TextView>() // 👈 这里不写 in 也不会报错
val consumer: Consumer<in Button> = Consumer<TextView>() // 👈 in 能够但不必
复制代码
*
号前面讲到了 Java 中单个 ?
号也能做为泛型通配符使用,至关于 ? extends Object
。 它在 Kotlin 中有等效的写法:*
号,至关于 out Any
。
🏝️ 👇
var list: List<*>
复制代码
和 Java 不一样的地方是,若是你的类型定义里已经有了 out
或者 in
,那这个限制在变量声明时也依然在,不会被 *
号去掉。
好比你的类型定义里是 out T : Number
的,那它加上 <*>
以后的效果就不是 out Any
,而是 out Number
。
where
关键字Java 中声明类或接口的时候,可使用 extends
来设置边界,将泛型类型参数限制为某个类型的子集:
☕️
// 👇 T 的类型必须是 Animal 的子类型
class Monster<T extends Animal>{
}
复制代码
注意这个和前面讲的声明变量时的泛型类型声明是不一样的东西,这里并无 ?
。
同时这个边界是能够设置多个,用 &
符号链接:
☕️
// 👇 T 的类型必须同时是 Animal 和 Food 的子类型
class Monster<T extends Animal & Food>{
}
复制代码
Kotlin 只是把 extends
换成了 :
冒号。
🏝️ 👇
class Monster<T : Animal>
复制代码
设置多个边界可使用 where
关键字:
🏝️ 👇
class Monster<T> where T : Animal, T : Food
复制代码
有人在看文档的时候以为这个 where
是个新东西,其实虽然 Java 里没有 where
,但它并无带来新功能,只是把一个老功能换了个新写法。
不过笔者以为 Kotlin 里 where
这样的写法可读性更符合英文里的语法,尤为是若是 Monster
自己还有继承的时候:
🏝️
class Monster<T> : MonsterParent<T>
where T : Animal
复制代码
reified
关键字因为 Java 中的泛型存在类型擦除的状况,任何在运行时须要知道泛型确切类型信息的操做都无法用了。
好比你不能检查一个对象是否为泛型类型 T
的实例:
☕️
<T> void printIfTypeMatch(Object item) {
if (item instanceof T) { // 👈 IDE 会提示错误,illegal generic type for instanceof
System.out.println(item);
}
}
复制代码
Kotlin 里一样也不行:
🏝️
fun <T> printIfTypeMatch(item: Any) {
if (item is T) { // 👈 IDE 会提示错误,Cannot check for instance of erased type: T
println(item)
}
}
复制代码
这个问题,在 Java 中的解决方案一般是额外传递一个 Class<T>
类型的参数,而后经过 Class#isInstance
方法来检查:
☕️ 👇
<T> void check(Object item, Class<T> type) {
if (type.isInstance(item)) {
👆
System.out.println(item);
}
}
复制代码
Kotlin 中一样能够这么解决,不过还有一个更方便的作法:使用关键字 reified
配合 inline
来解决:
🏝️ 👇 👇
inline fun <reified T> printIfTypeMatch(item: Any) {
if (item is T) { // 👈 这里就不会在提示错误了
println(item)
}
}
复制代码
这具体是怎么回事呢?等到后续章节讲到 inline
的时候会详细说明,这里就不过多延伸了。
还记得第二篇文章中,提到了两个 Kotlin 泛型与 Java 泛型不一致的地方,这里做一下解答。
Java 里的数组是支持协变的,而 Kotlin 中的数组 Array
不支持协变。
这是由于在 Kotlin 中数组是用 Array
类来表示的,这个 Array
类使用泛型就和集合类同样,因此不支持协变。
Java 中的 List
接口不支持协变,而 Kotlin 中的 List
接口支持协变。
Java 中的 List
不支持协变,缘由在上文已经讲过了,须要使用泛型通配符来解决。
在 Kotlin 中,实际上 MutableList
接口才至关于 Java 的 List
。Kotlin 中的 List
接口实现了只读操做,没有写操做,因此不会有类型安全上的问题,天然能够支持协变。
fill
函数,传入一个 Array
和一个对象,将对象填充到 Array
中,要求 Array
参数的泛型支持逆变(假设 Array
size 为 1)。copy
函数,传入两个 Array
参数,将一个 Array
中的元素复制到另外个 Array
中,要求 Array
参数的泛型分别支持协变和逆变。(提示:Kotlin 中的 for
循环若是要用索引,须要使用 Array.indices
)Bruce(郑啸天) ,即刻 Android 工程师。2018 年加入即刻,参与了即刻多个版本的迭代。多年 Android 开发经验,如今负责即刻客户端中台基础建设。