无论是 Java 还是 Kotlin, 泛型都是一个非常重要的概念, 简单的泛型应用很容易理解, 不过也有理解起来麻烦的时候.
泛型基础
在了解 Kotlin 的泛型之前, 先来看看 Java 中的泛型:
举个栗子: 在 JDK 中, 有一类列表对象, 这些对象对应的类都实现了 List 接口. List 中可以保存任何对象:
- List list=new ArrayList();
- list.add(55);
- list.add("hello");
上面的代码中, List 中保存了 Integer 和 String 两种类型值. 尽管这样做是可以保存任意类型的对象, 但每个列表元素就失去了原来对象的特性, 因为在 Java 中任何类都是 Object 的子类, 这样做的弊端就是原有对象类型的属性和方法都不能再使用了.
但在定义 List 时, 可以指定元素的数据类型, 那么这个 List 就不再是通用的了, 只能存储一种类型的数据. JDK1.5 之后引入了一个新的概念: 泛型.
所谓泛型, 就是指在定义数据结构时, 只指定类型的占位符, 待到使用该数据结构时再指定具体的数据类型:
- public class Box<T> {
- private T t;
- public Box(T t) {
- this.t = t;
- }
- }
- Box<Integer> box=new Box(2);
在 Kotlin 中同样也支持泛型, 下面是 Kotlin 实现上面同样的功能:
- class Box<T>(t: T) {
- var value = t
- }
- var box: Box<String> = Box("haha")
类型变异
Java 中
Java 泛型中有类型通配符这一机制, 不过在 Kotlin 泛型中, 没有通配符.
先看一个 Java 的栗子:
- List<String> list1 = new ArrayList<>();
- List<Object> list2 = list1; // 编译错误
以上代码编译错误. 这里有两个 List 对象, 很明显 String 是 Object 的子类, 但遗憾的是, Java 编译器并不认为 List <String > 和 List <Object> 有任何关系, 直接将 list1 赋值给 list2 是会编译报错的, 这是由于 List 的父接口是 Collection:
public interface Collection<E> extends Iterable<E> {..}
为了解决这个问题, Java 泛型提供了问号 (?) 通配符来解决这个问题. 例如 Collection 接口中的 addAll 方法定义如下:
boolean addAll(Collection<? extends E> var1);
? extends E 表示什么呢, 表示任何父类是 E(或者 E 的任何子类和自己)都满足条件, 这样就解决了 List <String> 给 List <Object> 赋值的问题.
出了 extend 还有 super, 这里不再过多介绍.
Kotlin 中
Kotlin 泛型并没有提供通配符, 取而代之的是 out 和 in 关键字. 用 out 声明的泛型占位符只能在获取泛型类型值得地方, 如函数的返回值. 用 in 声明的泛型占位符只能在设置泛型类型值的地方, 如函数的参数.
我们习惯将只能读取的对象称为生产者, 将只能设置的对象称为消费者. 如果你使用一个生产者对象, 将无法对这个对象调用 add 或 set 等方法, 但这并不代表这个对象的值是不变的. 例如, 你完全可以调用 clear 方法来删除 List 中的所有元素, 因为 clear 方法不需要任何参数.
通配符类型(或者其他任何的类型变异), 唯一能够确保的仅仅是类型安全.
- abstract class Source<out T> {
- abstract fun func(): T
- }
- abstract class Comparable<in T> {
- abstract fun func(t: T)
- }
类型投射
如果将泛型类型 T 声明为 out, 就可以将其子类化(List <String> 是 List <Object> 的子类型), 这是非常方便的. 如果我们的类能够仅仅只返回 T 类型的值, 那么的确可以将其子类化. 但如果在声明泛型时未使用 out 声明 T 呢?
现在有一个 Array 类如下:
- class Array<T>(val size: Int) {
- fun get(index: Int): T {
- }
- fun set(index: Int, t: T) {
- }
- }
此类中的 T 既是 get 方法的返回值, 又是 set 方法的参数, 也就是说 Array 类既是 T 的生产者, 也是 T 的消费者, 这样的类就无法进行子类化.
- fun copy(from: Array<Any>, to: Array<Any>) {
- assert(from.size == to.size)
- for(i in from.indices){
- to[i]=from[i]
- }
- }
这个 copy 方法, 就是将一个 Array 复制到另一个 Array 中, 现在尝试使用一下:
- val ints: Array<Int> = arrayOf(1, 2, 3)
- val any: Array<Any> = Array(3)
- copy(ints, any) // 编译错误, 因为 Array<Int> 不是 Array<Any > 的子类型
Array<T> 对于类型参数 T 是不可变的, 因此 Array<Int> 和 Array<Any > 他们没有任何关系, 为什么呢? 因为 copy 可能会进行一些不安全的操作, 也就是说, 这个函数可能会试图向 from 中写入数据, 这样可能会抛类型转换异常.
可以这样:
- fun copy(from: Array<out Any>, to: Array<Any>) {
- ...
- }
将 from 的泛型使用 out 修饰.
这种声明在 Kotlin 中称为类型投射: from 不是一个单纯的数组, 而是一个被限制 (投射) 的数组, 我们只能对这个数组调用那些返回值为类型参数 T 的函数, 在这个例子中, 我们只能调用 get 方法, 这就是我们事先使用处的类型变异的方案.
in 关键字也是同理.
泛型函数
不仅类可以有泛型参数, 函数一样可以有泛型参数. 泛型参数放在函数名称之前.
- fun <T> getList(item: T): List<T> {
- ...
- }
调用泛型函数时, 应该在函数名称之后指定调用端类型参数.
val value = getList<Int>(1)
泛型约束
对于一个给定的泛型参数, 所允许使用的类型, 可以通过泛型约束来限制, 最常见的约束是上界, 与 Java 中的 extends 类似.
- fun <T : Any> sort(list: List<T>) {
- }
冒号之后指定的类型就是泛型参数的上界: 对于泛型参数 T, 允许使用 Any 的子类型. 如果没有指定, 则默认使用的上界类型是 "Any?", 在定义泛型参数的尖括号内, 值允许定义唯一一个上界.
小结
Kotlin 泛型是在 Java 泛型的基础上进行了改进, 变得更好用, 更安全, 尽管上述的泛型技术不一定都用得上, 但对于全面了解 Kotlin 泛型会起到很大作用.
来源: https://juejin.im/post/5afd68cc518825428630b76e