原文章出处:[Kotlin 的泛型](https://kaixue.io/kotlin-generics/)
## 从 Kotlin 的 in 和 out 说起
这期是码上开学 Kotlin 系列的独立技术点部分的第一期,我们来聊一聊泛型。
提到 Kotlin 的泛型,通常离不开`in` 和`out`关键字,但泛型这门武功需要些基本功才能修炼,否则容易走火入魔,待笔者慢慢道来。
下面这段 Java 代码在日常开发中应该很常见了:
~~~java
☕️
List<TextView> textViews = new ArrayList<TextView>();
~~~
其中`List<TextView>`表示这是一个泛型类型为`TextView`的`List`。
那到底什么是泛型呢?我们先来讲讲泛型的由来。
现在的程序开发大都是面向对象的,平时会用到各种类型的对象,一组对象通常需要用集合来存储它们,因而就有了一些集合类,比如`List`、`Map`等。
这些集合类里面都是装的具体类型的对象,如果每个类型都去实现诸如`TextViewList`、`ActivityList`这样的具体的类型,显然是不可能的。
因此就诞生了「泛型」,它的意思是把具体的类型泛化,编码的时候用符号来指代类型,在使用的时候,再确定它的类型。
前面那个例子,`List<TextView>`就是泛型类型声明。
既然泛型是跟类型相关的,那么是不是也能适用类型的多态呢?
先看一个常见的使用场景:
~~~java
☕️
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 里用数组做类似的事情,是不会报错的,这是因为数组并没有在编译时擦除类型:
> ~~~java
> ☕️
> TextView[] textViews = new TextView[10];
>
> ~~~
但是在实际使用中,我们的确会有这种类似的需求,需要实现上面这种赋值。
Java 提供了「泛型通配符」`? extends`和`? super`来解决这个问题。
## Java 中的 ? extends
在 Java 里面是这么解决的:
~~~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`的子类,满足了泛型类型的限制条件,因而能够成功赋值。
根据刚才的描述,下面几种情况都是可以的:
~~~java
☕️
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`,它的具体定义如下:
~~~java
☕️
public interface List<E> extends Collection<E>{
E get(int index);
boolean add(E e);
...
}
~~~
上面的代码中,`E`就是表示泛型类型的符号(用其他字母甚至单词都可以)。
我们看看在使用了上界通配符之后,`List`的使用上有没有什么问题:
~~~java
☕️
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>`。
* 对于前者,显然我们要添加 TextView 是不可以的。
* 实际情况是编译器无法确定到底属于哪一种,无法继续执行下去,就报错了。
那我干脆不要`extends TextView`,只用通配符`?`呢?
这样使用`List<?>`其实是`List<? extends Object>`的缩写。
~~~java
☕️
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`。
## Java 中的 ? super
先看一下它的写法:
~~~java
☕️
👇
List<? super Button> buttons = new ArrayList<TextView>();
~~~
这个`? super`叫做「下界通配符」,可以使 Java 泛型具有「逆变性 Contravariance」。
> 与上界通配符对应,这里 super 限制了通配符 ? 的子类型,所以称之为下界。
它也有两层意思:
* 通配符`?`表示`List`的泛型类型是一个**未知类型**。
* `super`限制了这个未知类型的下界,也就是泛型类型必须满足这个`super`的限制条件。
* `super`我们在类的方法里面经常用到,这里的范围不仅包括`Button`的直接和间接父类,也包括下界`Button`本身。
* `super`同样支持`interface`。
上面的例子中,`TextView`是`Button`的父类型 ,也就能够满足`super`的限制条件,就可以成功赋值了。
根据刚才的描述,下面几种情况都是可以的:
~~~java
☕️
List<? super Button> buttons = new ArrayList<Button>(); // 👈 本身
List<? super Button> buttons = new ArrayList<TextView>(); // 👈 直接父类
List<? super Button> buttons = new ArrayList<Object>(); // 👈 间接父类
~~~
对于使用了下界通配符的`List`,我们再看看它的`get`和`add`操作:
~~~java
☕️
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 中的泛型,有如练完九阳神功再练乾坤大挪移,就比较容易了。
## 说回 Kotlin 中的 out 和 in
和 Java 泛型一样,Kolin 中的泛型本身也是不可变的。
* 使用关键字`out`来支持协变,等同于 Java 中的上界通配符`? extends`。
* 使用关键字`in`来支持逆变,等同于 Java 中的下界通配符`? super`。
~~~kotlin
🏝️
var textViews: List<out TextView>
var textViews: List<in TextView>
~~~
换了个写法,但作用是完全一样的。`out`表示,我这个变量或者参数只用来输出,不用来输入,你只能读我不能写我;`in`就反过来,表示它只用来输入,不用来输出,你只能写我不能读我。
你看,我们 Android 工程师学不会`out`和`in`,其实并不是因为这两个关键字多难,而是因为我们应该先学学 Java 的泛型。是吧?
说了这么多`List`,其实泛型在非集合类的使用也非常广泛,就以「生产者-消费者」为例子:
~~~kotlin
🏝️
class Producer<T> {
fun produce(): T {
...
}
}
val producer: Producer<out TextView> = Producer<Button>()
val textView: TextView = producer.produce() // 👈 相当于 'List' 的 `get`
~~~
再来看看消费者:
~~~kotlin
🏝️
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`了。
~~~kotlin
🏝️ 👇
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`只用来输入。
~~~kotlin
🏝️ 👇
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`**。
~~~kotlin
🏝️ 👇
var list: List<*>
~~~
和 Java 不同的地方是,如果你的类型定义里已经有了`out`或者`in`,那这个限制在变量声明时也依然在,不会被`*`号去掉。
比如你的类型定义里是`out T : Number`的,那它加上`<*>`之后的效果就不是`out Any`,而是`out Number`。
## where 关键字(多重上界)
Java 中声明类或接口的时候,可以使用`extends`来设置边界,将泛型类型参数限制为某个类型的子集:
~~~java
☕️
// 👇 T 的类型必须是 Animal 的子类型
class Monster<T extends Animal>{
}
~~~
>[info]【注意】这个和前面讲的声明变量时的泛型类型声明是不同的东西,这里并没有`?`。
同时这个边界是可以设置多个,用`&`符号连接:
~~~java
☕️
// 👇 T 的类型必须同时是 Animal 和 Food 的子类型
class Monster<T extends Animal & Food>{
}
~~~
Kotlin 只是把`extends`换成了`:`冒号。
~~~kotlin
🏝️ 👇
class Monster<T : Animal>
~~~
设置多个边界可以使用`where`关键字:
~~~kotlin
🏝️ 👇
class Monster<T> where T : Animal, T : Food
~~~
有人在看文档的时候觉得这个`where`是个新东西,其实虽然 Java 里没有`where`,但它并没有带来新功能,只是把一个老功能换了个新写法。
不过笔者觉得 Kotlin 里`where`这样的写法可读性更符合英文里的语法,尤其是如果`Monster`本身还有继承的时候:
~~~kotlin
🏝️
class Monster<T> : MonsterParent<T>
where T : Animal
~~~
## reified 关键字
由于 Java 中的泛型存在类型擦除的情况,任何在运行时需要知道泛型确切类型信息的操作都没法用了。Java泛型里的类型参数,也就是这个T,它不是真正的类型,只是一个代号,所以你不能把它当成一个普通的类型来用,比如你不能在方法里检查一个对象是不是一个T的实例。这个在kotlin和Java都是一样的。
比如你不能检查一个对象是否为泛型类型`T`的实例:
~~~java
☕️
<T> void printIfTypeMatch(Object item) {
if (item instanceof T) { // 👈 IDE 会提示错误,illegal generic type for instanceof
System.out.println(item);
}
}
~~~
Kotlin 里同样也不行:
~~~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`方法来检查:
~~~java
☕️ 👇
<T> void check(Object item, Class<T> type) {
if (type.isInstance(item)) {
👆
System.out.println(item);
}
}
~~~
Kotlin 中同样可以这么解决,不过还有一个更方便的做法:使用关键字`reified`配合`inline`来解决:
~~~kotlin
🏝️ 👇 👇
inline fun <reified T> printIfTypeMatch(item: Any) {
if (item is T) { // 👈 这里就不会在提示错误了
println(item)
}
}
~~~
这具体是怎么回事呢?等到后续章节讲到`inline`的时候会详细说明,这里就不过多延伸了。
还记得第二篇文章中,提到了两个 Kotlin 泛型与 Java 泛型不一致的地方,这里作一下解答。
1. Java 里的数组是支持协变的,而 Kotlin 中的数组`Array`不支持协变。
这是因为在 Kotlin 中数组是用`Array`类来表示的,这个`Array`类使用泛型就和集合类一样,所以不支持协变。
2. Java 中的`List`接口不支持协变,而 Kotlin 中的`List`接口支持协变。
Java 中的`List`不支持协变,原因在上文已经讲过了,需要使用泛型通配符来解决。
在 Kotlin 中,实际上`MutableList`接口才相当于 Java 的`List`。Kotlin 中的`List`接口实现了只读操作,没有写操作,所以不会有类型安全上的问题,自然可以支持协变。
## 练习题
1. 实现一个`fill`函数,传入一个`Array`和一个对象,将对象填充到`Array`中,要求`Array`参数的泛型支持逆变(假设`Array`size 为 1)。
2. 实现一个`copy`函数,传入两个`Array`参数,将一个`Array`中的元素复制到另外个`Array`中,要求`Array`参数的泛型分别支持协变和逆变。(提示:Kotlin 中的`for`循环如果要用索引,需要使用`Array.indices`)
- 前言
- Kotlin简介
- IntelliJ IDEA技巧总结
- idea设置类注释和方法注释模板
- 像Android Studion一样创建工程
- Gradle
- Gradle入门
- Gradle进阶
- 使用Gradle创建一个Kotlin工程
- 环境搭建
- Androidstudio平台搭建
- Eclipse的Kotlin环境配置
- 使用IntelliJ IDEA
- Kotlin学习路线
- Kotlin官方中文版文档教程
- 概述
- kotlin用于服务器端开发
- kotlin用于Android开发
- kotlin用于JavaScript开发
- kotlin用于原生开发
- Kotlin 用于数据科学
- 协程
- 多平台
- 新特性
- 1.1的新特性
- 1.2的新特性
- 1.3的新特性
- 开始
- 基本语法
- 习惯用法
- 编码规范
- 基础
- 基本类型
- 包与导入
- 控制流
- 返回与跳转
- 类与对象
- 类与继承
- 属性与字段
- 接口
- 可见性修饰符
- 扩展
- 数据类
- 密封类
- 泛型
- 嵌套类
- 枚举类
- 对象
- 类型别名
- 内嵌类
- 委托
- 委托属性
- 函数与Lambda表达式
- 函数
- Lambda表达式
- 内联函数
- 集合
- 集合概述
- 构造集合
- 迭代器
- 区间与数列
- 序列
- 操作概述
- 转换
- 过滤
- 加减操作符
- 分组
- 取集合的一部分
- 取单个元素
- 排序
- 聚合操作
- 集合写操作
- List相关操作
- Set相关操作
- Map相关操作
- 多平台程序设计
- 平台相关声明
- 以Gradle创建
- 更多语言结构
- 解构声明
- 类型检测与转换
- This表达式
- 相等性
- 操作符重载
- 空安全
- 异常
- 注解
- 反射
- 作用域函数
- 类型安全的构造器
- Opt-in Requirements
- 核心库
- 标准库
- kotlin.test
- 参考
- 关键字与操作符
- 语法
- 编码风格约定
- Java互操作
- Kotlin中调用Java
- Java中调用Kotlin
- JavaScript
- 动态类型
- kotlin中调用JavaScript
- JavaScript中调用kotlin
- JavaScript模块
- JavaScript反射
- JavaScript DCE
- 原生
- 并发
- 不可变性
- kotlin库
- 平台库
- 与C语言互操作
- 与Object-C及Swift互操作
- CocoaPods集成
- Gradle插件
- 调试
- FAQ
- 协程
- 协程指南
- 基础
- 取消与超时
- 组合挂起函数
- 协程上下文与调度器
- 异步流
- 通道
- 异常处理与监督
- 共享的可变状态与并发
- Select表达式(实验性)
- 工具
- 编写kotlin代码文档
- 使用Kapt
- 使用Gradle
- 使用Maven
- 使用Ant
- Kotlin与OSGI
- 编译器插件
- 编码规范
- 演进
- kotlin语言演进
- 不同组件的稳定性
- kotlin1.3的兼容性指南
- 常见问题
- FAQ
- 与Java比较
- 与Scala比较(官方已删除)
- Google开发者官网简介
- Kotlin and Android
- Get Started with Kotlin on Android
- Kotlin on Android FAQ
- Android KTX
- Resources to Learn Kotlin
- Kotlin样品
- Kotlin零基础到进阶
- 第一阶段兴趣入门
- kotlin简介和学习方法
- 数据类型和类型系统
- 入门
- 分类
- val和var
- 二进制基础
- 基础
- 基本语法
- 包
- 示例
- 编码规范
- 代码注释
- 异常
- 根类型“Any”
- Any? 可空类型
- 可空性的实现原理
- kotlin.Unit类型
- kotlin.Nothing类型
- 基本数据类型
- 数值类型
- 布尔类型
- 字符型
- 位运算符
- 变量和常量
- 语法和运算符
- 关键字
- 硬关键字
- 软关键字
- 修饰符关键字
- 特殊标识符
- 操作符和特殊符号
- 算术运算符
- 赋值运算符
- 比较运算符
- 逻辑运算符
- this关键字
- super关键字
- 操作符重载
- 一元操作符
- 二元操作符
- 字符串
- 字符串介绍和属性
- 字符串常见方法操作
- 字符串模板
- 数组
- 数组介绍创建及遍历
- 数组常见方法和属性
- 数组变化以及下标越界问题
- 原生数组类型
- 区间
- 正向区间
- 逆向区间
- 步长
- 类型检测与类型转换
- is、!is、as、as-运算符
- 空安全
- 可空类型变量
- 安全调用符
- 非空断言
- Elvis操作符
- 可空性深入
- 可空性和Java
- 函数
- 函数式编程概述
- OOP和FOP
- 函数式编程基本特性
- 组合与范畴
- 在Kotlin中使用函数式编程
- 函数入门
- 函数作用域
- 函数加强
- 命名参数
- 默认参数
- 可变参数
- 表达式函数体
- 顶层、嵌套、中缀函数
- 尾递归函数优化
- 函数重载
- 控制流
- if表达式
- when表达式
- for循环
- while循环
- 循环中的 Break 与 continue
- return返回
- 标签处返回
- 集合
- list集合
- list集合介绍和操作
- list常见方法和属性
- list集合变化和下标越界
- set集合
- set集合介绍和常见操作
- set集合常见方法和属性
- set集合变换和下标越界
- map集合
- map集合介绍和常见操作
- map集合常见方法和属性
- map集合变换
- 集合的函数式API
- map函数
- filter函数
- “ all ”“ any ”“ count ”和“ find ”:对集合应用判断式
- 别样的求和方式:sumBy、sum、fold、reduce
- 根据人的性别进行分组:groupBy
- 扁平化——处理嵌套集合:flatMap、flatten
- 惰性集合操作:序列
- 区间、数组、集合之间转换
- 面向对象
- 面向对象-封装
- 类的创建及属性方法访问
- 类属性和字段
- 构造器
- 嵌套类(内部类)
- 枚举类
- 枚举类遍历&枚举常量常用属性
- 数据类
- 密封类
- 印章类(密封类)
- 面向对象-继承
- 类的继承
- 面向对象-多态
- 抽象类
- 接口
- 接口和抽象类的区别
- 面向对象-深入
- 扩展
- 扩展:为别的类添加方法、属性
- Android中的扩展应用
- 优化Snackbar
- 用扩展函数封装Utils
- 解决烦人的findViewById
- 扩展不是万能的
- 调度方式对扩展函数的影响
- 被滥用的扩展函数
- 委托
- 委托类
- 委托属性
- Kotlin5大内置委托
- Kotlin-Object关键字
- 单例模式
- 匿名类对象
- 伴生对象
- 作用域函数
- let函数
- run函数
- with函数
- apply函数
- also函数
- 标准库函数
- takeIf 与 takeUnless
- 第二阶段重点深入
- Lambda编程
- Lambda成员引用高阶函数
- 高阶函数
- 内联函数
- 泛型
- 泛型的分类
- 泛型约束
- 子类和子类型
- 协变与逆变
- 泛型擦除与实化类型
- 泛型类型参数
- 泛型的背后:类型擦除
- Java为什么无法声明一个泛型数组
- 向后兼容的罪
- 类型擦除的矛盾
- 使用内联函数获取泛型
- 打破泛型不变
- 一个支持协变的List
- 一个支持逆变的Comparator
- 协变和逆变
- 第三阶段难点突破
- 注解和反射
- 声明并应用注解
- DSL
- 协程
- 协程简介
- 协程的基本操作
- 协程取消
- 管道
- 慕课霍丙乾协程笔记
- Kotlin与Java互操作
- 在Kotlin中调用Java
- 在Java中调用Kotlin
- Kotlin与Java中的操作对比
- 第四阶段专题练习
- 朱凯Kotlin知识点总结
- Kotlin 基础
- Kotlin 的变量、函数和类型
- Kotlin 里那些「不是那么写的」
- Kotlin 里那些「更方便的」
- Kotlin 进阶
- Kotlin 的泛型
- Kotlin 的高阶函数、匿名函数和 Lambda 表达式
- Kotlin协程
- 初识
- 进阶
- 深入
- Kotlin 扩展
- 会写「18.dp」只是个入门——Kotlin 的扩展函数和扩展属性(Extension Functions / Properties)
- Kotlin实战-开发Android