# 泛型
[TOC]
>[info]注意:本篇文章,初次阅读很难理解,建议可参考这篇文章,[泛型](https://c.lioil.win/2017/06/23/Kotlin-generics.html)
与 Java 类似,Kotlin 中的类也可以有类型参数:
```kotlin
class Box<T>(t: T) {
var value = t
}
```
一般来说,要创建这样类的实例,我们需要提供类型参数:
```kotlin
val box: Box<Int> = Box<Int>(1)
```
但是**如果类型参数可以推断出来**,例如从构造函数的参数或者从其他途径,**允许省略类型参数**:
```kotlin
val box = Box(1) // 1 具有类型 Int,所以编译器知道我们说的是 Box<Int>。
```
## 型变
Java 类型系统中最棘手的部分之一是通配符类型(参见 [Java Generics FAQ](http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html))。而 Kotlin 中没有。 相反,它有两个其他的东西:声明处型变(declaration-site variance)与类型投影(type projections)。
首先,让我们思考为什么 Java 需要那些神秘的通配符。在 [《Effective Java》第三版](http://www.oracle.com/technetwork/java/effectivejava-136174.html) 解释了该问题——第 31 条:*利用有限制通配符来提升 API 的灵活性*。
首先,Java 中的泛型是**不型变的**,这意味着 `List<String>` 并**不是** `List<Object>` 的子类型。
为什么这样? 如果 List 不是**不型变的**,它就没比 Java 的数组好到哪去,因为如下代码会通过编译然后导致运行时异常:
``` java
// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!!即将来临的问题的原因就在这里。Java 禁止这样!
objs.add(1); // 这里我们把一个整数放入一个字符串列表
String s = strs.get(0); // !!! ClassCastException:无法将整数转换为字符串
```
因此,Java 禁止这样的事情以保证运行时的安全。但这样会有一些影响。例如,考虑 `Collection` 接口中的 `addAll()`
方法。该方法的签名应该是什么?直觉上,我们会这样:
``` java
// Java
interface Collection<E> …… {
void addAll(Collection<E> items);
}
```
但随后,我们将无法做到以下简单的事情(这是完全安全):
``` java
// Java
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from);
// !!!对于这种简单声明的 addAll 将不能编译:
// Collection<String> 不是 Collection<Object> 的子类型
}
```
(在 Java 中,我们艰难地学到了这个教训,参见[《Effective Java》第三版](http://www.oracle.com/technetwork/java/effectivejava-136174.html),第 28 条:*列表优先于数组*)
这就是为什么 `addAll()` 的实际签名是以下这样:
``` java
// Java
interface Collection<E> …… {
void addAll(Collection<? extends E> items);
}
```
**通配符类型参数** `? extends E` 表示此方法接受 `E` *或者 `E` 的 一些子类型*对象的集合,而不只是 `E` 自身。这意味着我们可以安全地从其中(该集合中的元素是 E 的子类的实例)**读取** `E`,但**不能写入**,因为我们不知道什么对象符合那个未知的 `E` 的子类型。反过来,该限制可以让`Collection<String>`表示为`Collection<? extends Object>`的子类型。简而言之,带 **extends** 限定(**上界**)的通配符类型使得类型是**协变的(covariant)**。
理解为什么这个技巧能够工作的关键相当简单:如果只能从集合中获取项目,那么使用 `String` 的集合,
并且从其中读取 `Object` 也没问题 。反过来,如果只能向集合中 _放入_ 项目,就可以用`Object` 集合并向其中放入 `String`:在 Java 中有 `List<? super String>` 是 `List<Object>` 的一个**超类**。
后者称为**逆变性(contravariance)**,并且对于 `List <? super String>` 你只能调用接受 String 作为参数的方法(例如,你可以调用 `add(String)` 或者 `set(int, String)`),当然如果调用函数返回 `List<T>` 中的 `T`,你得到的并非一个 `String` 而是一个 `Object`。
Joshua Bloch 称那些你只能从中**读取**的对象为**生产者**,并称那些你只能**写入**的对象为**消费者**。他建议:“*为了灵活性最大化,在表示生产者或消费者的输入参数上使用通配符类型*”,并提出了以下助记符:
*****
*PECS 代表生产者-Extens,消费者-Super(Producer-Extends, Consumer-Super)。*
*****
>[info]*注意*:如果你使用一个生产者对象,如 `List<? extends Foo>`,在该对象上不允许调用 `add()` 或 `set()`。但这并不意味着该对象是**不可变的**:例如,没有什么阻止你调用 `clear()`从列表中删除所有项目,因为 `clear()`根本无需任何参数。通配符(或其他类型的型变)保证的唯一的事情是**类型安全**。不可变性完全是另一回事。
### 声明处型变
假设有一个泛型接口 `Source<T>`,该接口中不存在任何以 `T` 作为参数的方法,只是方法返回 `T` 类型值:
``` java
// Java
interface Source<T> {
T nextT();
}
```
那么,在 `Source <Object>` 类型的变量中存储 `Source <String>` 实例的引用是极为安全的——没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作:
``` java
// Java
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!!在 Java 中不允许
// ……
}
```
为了修正这一点,我们必须声明对象的类型为 `Source<? extends Object>`,这是毫无意义的,因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。但编译器并不知道。
在 Kotlin 中,有一种方法向编译器解释这种情况。这称为**声明处型变**:我们可以标注 `Source` 的**类型参数** `T` 来确保它仅从 `Source<T>` 成员中**返回**(生产),并从不被消费。为此,我们提供 **out** 修饰符:
```kotlin
interface Source<out T> {
fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // 这个没问题,因为 T 是一个 out-参数
// ……
}
```
一般原则是:当一个类 `C` 的类型参数 `T` 被声明为 **out** 时,它就只能出现在 `C` 的成员的**输出**\-位置,但回报是 `C<Base>` 可以安全地作为`C<Derived>`的超类。
简而言之,他们说类 `C` 是在参数 `T` 上是**协变的**,或者说 `T` 是一个**协变的**类型参数。你可以认为 `C` 是 `T` 的**生产者**,而不是 `T` 的**消费者**。
**out**修饰符称为**型变注解**,并且由于它在类型参数声明处提供,所以我们称之为**声明处型变**。这与 Java 的**使用处型变**相反,其类型用途通配符使得类型协变。
另外除了 **out**,Kotlin 又补充了一个型变注释:**in**。它使得一个类型参数**逆变**:只可以被消费而不可以被生产。逆变类型的一个很好的例子是 `Comparable`:
```kotlin
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
// 因此,我们可以将 x 赋给类型为 Comparable <Double> 的变量
val y: Comparable<Double> = x // OK!
}
```
我们相信 **in** 和 **out** 两词是自解释的(因为它们已经在 C# 中成功使用很长时间了),因此上面提到的助记符不是真正需要的,并且可以将其改写为更高的目标:
**[存在性(The Existential)](https://zh.wikipedia.org/wiki/%E5%AD%98%E5%9C%A8%E4%B8%BB%E4%B9%89) 转换:消费者 in, 生产者 out\!** :-)
## 类型投影
### 使用处型变:类型投影
将类型参数 T 声明为 *out* 非常方便,并且能避免使用处子类型化的麻烦,但是有些类实际上**不能**限制为只返回 `T`!
一个很好的例子是 Array:
```kotlin
class Array<T>(val size: Int) {
fun get(index: Int): T { …… }
fun set(index: Int, value: T) { …… }
}
```
该类在 `T` 上既不能是协变的也不能是逆变的。这造成了一些不灵活性。考虑下述函数:
```kotlin
fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}
```
这个函数应该将项目从一个数组复制到另一个数组。让我们尝试在实践中应用它:
```kotlin
val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" }
copy(ints, any)
// ^ 其类型为 Array<Int> 但此处期望 Array<Any>
```
这里我们遇到同样熟悉的问题:`Array <T>` 在 `T` 上是**不型变的**,因此 `Array <Int>` 和 `Array <Any>` 都不是另一个的子类型。为什么? 再次重复,因为 copy **可能**做坏事,也就是说,例如它可能尝试**写**一个 String 到 `from`,并且如果我们实际上传递一个 `Int` 的数组,一段时间后将会抛出一个 `ClassCastException` 异常。
那么,我们唯一要确保的是 `copy()` 不会做任何坏事。我们想阻止它**写**到 `from`,我们可以:
```kotlin
fun copy(from: Array<out Any>, to: Array<Any>) { …… }
```
这里发生的事情称为**类型投影**:我们说`from`不仅仅是一个数组,而是一个受限制的(**投影的**)数组:我们只可以调用返回类型为类型参数`T` 的方法,如上,这意味着我们只能调用 `get()`。这就是我们的**使用处型变**的用法,并且是对应于 Java 的 `Array<? extends Object>`、但使用更简单些的方式。
你也可以使用 **in** 投影一个类型:
```kotlin
fun fill(dest: Array<in String>, value: String) { …… }
```
`Array<in String>` 对应于 Java 的 `Array<? super String>`,也就是说,你可以传递一个 `CharSequence` 数组或一个 `Object` 数组给 `fill()` 函数。
### 星投影
有时你想说,你对类型参数一无所知,但仍然希望以安全的方式使用它。
这里的安全方式是定义泛型类型的这种投影,该泛型类型的每个具体实例化将是该投影的子类型。
Kotlin 为此提供了所谓的**星投影**语法:
- 对于 `Foo <out T : TUpper>`,其中 `T` 是一个具有上界 `TUpper` 的协变类型参数,`Foo <*>` 等价于 `Foo <out TUpper>`。 这意味着当 `T` 未知时,你可以安全地从 `Foo <*>` *读取* `TUpper` 的值。
- 对于 `Foo <in T>`,其中 `T` 是一个逆变类型参数,`Foo <*>` 等价于 `Foo <in Nothing>`。 这意味着当 `T` 未知时,没有什么可以以安全的方式*写入* `Foo <*>`。
- 对于 `Foo <T : TUpper>`,其中 `T` 是一个具有上界 `TUpper` 的不型变类型参数,`Foo<*>` 对于读取值时等价于 `Foo<out TUpper>` 而对于写值时等价于 `Foo<in Nothing>`。
如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影。例如,如果类型被声明为 `interface Function <in T, out U>`,我们可以想象以下星投影:
- `Function<*, String>` 表示 `Function<in Nothing, String>`;
- `Function<Int, *>` 表示 `Function<Int, out Any?>`;
- `Function<*, *>` 表示 `Function<in Nothing, out Any?>`。
*注意*:星投影非常像 Java 的原始类型,但是安全。
## 泛型函数
不仅类可以有类型参数。函数也可以有。类型参数要放在函数名称**之前**:
```kotlin
fun <T> singletonList(item: T): List<T> {
// ……
}
fun <T> T.basicToString(): String { // 扩展函数
// ……
}
```
要调用泛型函数,在调用处函数名**之后**指定类型参数即可:
```kotlin
val l = singletonList<Int>(1)
```
可以省略能够从上下文中推断出来的类型参数,所以以下示例同样适用:
```kotlin
val l = singletonList(1)
```
## 泛型约束
能够替换给定类型参数的所有可能类型的集合可以由**泛型约束**限制。
### 上界
最常见的约束类型是与 Java 的 *extends* 关键字对应的 **上界**:
```kotlin
fun <T : Comparable<T>> sort(list: List<T>) { …… }
```
冒号之后指定的类型是**上界**:只有 `Comparable<T>` 的子类型可以替代 `T`。 例如:
```kotlin
sort(listOf(1, 2, 3)) // OK。Int 是 Comparable<Int> 的子类型
sort(listOf(HashMap<Int, String>())) // 错误:HashMap<Int, String> 不是 Comparable<HashMap<Int, String>> 的子类型
```
默认的上界(如果没有声明)是 `Any?`。在尖括号中只能指定一个上界。如果同一类型参数需要多个上界,我们需要一个单独的 **where**-子句:
```kotlin
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
where T : CharSequence,
T : Comparable<T> {
return list.filter { it > threshold }.map { it.toString() }
}
```
所传递的类型必须同时满足 `where` 子句的所有条件。在上述示例中,类型 `T` 必须*既*实现了 `CharSequence` *也*实现了 `Comparable`。
## 类型擦除
Kotlin 为泛型声明用法执行的类型安全检测仅在编译期进行。运行时泛型类型的实例不保留关于其类型实参的任何信息。其类型信息称为被*擦除*。例如,`Foo<Bar>` 与 `Foo<Baz?>` 的实例都会被擦除为`Foo<*>`。
因此,并没有通用的方法在运行时检测一个泛型类型的实例是否通过指定类型参数所创建,并且编译器[禁止这种 *is*检测](http://www.kotlincn.net/docs/reference/typecasts.html#%E7%B1%BB%E5%9E%8B%E6%93%A6%E9%99%A4%E4%B8%8E%E6%B3%9B%E5%9E%8B%E6%A3%80%E6%B5%8B)。
类型转换为带有具体类型参数的泛型类型,如 `foo as List<String>` 无法在运行时检测。当高级程序逻辑隐含了类型转换的类型安全而无法直接通过编译器推断时,可以使用这种[非受检类型转换](http://www.kotlincn.net/docs/reference/typecasts.html#%E9%9D%9E%E5%8F%97%E6%A3%80%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2)。编译器会对非受检类型转换发出警告,并且在运行时只对非泛型部分检测(相当于 `foo as List<*>`)。
泛型函数调用的类型参数也同样只在编译期检测。在函数体内部,类型参数不能用于类型检测,并且类型转换为类型参数(`foo as T`)也是非受检的。然而,内联函数的[具体化的类型参数](http://www.kotlincn.net/docs/reference/inline-functions.html#%E5%85%B7%E4%BD%93%E5%8C%96%E7%9A%84%E7%B1%BB%E5%9E%8B%E5%8F%82%E6%95%B0)会由调用处内联函数体中的类型实参所代入,因此可以用于类型检测与转换,与上述泛型类型的实例具有相同限制。
- 前言
- 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