Kotlin 的泛型
Java 中的泛型
关于 Java 中泛型的使用可以参考:Java-泛型
Effective Java 中的泛型要点
术语介绍
术语 | 示例 |
---|---|
类型参数 | E |
泛型 | List |
参数化类型 | List |
原生态类型 | List |
无限制通配符类型 | List<?> |
有限制通配符类型 | List<? extentds Object> |
有限制通配符类型 | List<? super Object> |
有限制类型参数 | List |
不要使用原生态类型
声明中具有一个或者多个类型参数的类或者接口就是泛型类或者泛型接口,泛型类和泛型接口统称为泛型。
每个泛型都对应一个原生态类型,即不带任何实际类型参数的泛型名称,例如 List<E>
对应的原生态类型是 List
。原生态类型 List
和 Java 平台没有泛型之前的接口类型 List
完全一样。
没有泛型之前,你可以向原生态类型中添加任何类型的数据,并且可以成功编译运行,直到获取其中的数据并将其强制转化为指定的类型时才会出错。
使用泛型相比之前,有以下好处:
- 错误前置,可以将运行时错误提前到编译期间,确保了编译期的类型安全,只能使用正确类型的对象。
- 避免强制类型转换,获取数据直接是想要的类型数据
因此,新代码中不要使用原生态类型,它们逃避了泛型检查,没有类型安全性。
无限制通配符类型
在不确定或者不在意集合中元素类型的情况下,你也许会使用原生态类型。例如,假设想要编写一个方法,它有两个集合参数,需要从中返回它们共有的元素数量。一种实现如下:
1 | public int numElementsInCommon(Set a, Set b) { |
这种实现是可以的,但它使用了原生态类型,这是危险的。如果要使用泛型,但是不确定或者不关心实际的参数类型,就可以使用一个 ?
代替。上面的代码安全实现方式如下:
1 | public int numElementsInCommon(Set<?> a, Set<?> b) { |
Set<?>
相比 Set
肯定是安全的,Set
允许放入任何类型的元素,很容易破坏该集合的类型约束条件。但是你不能将任何元素(除了 null
)放入 Set<?>
中,如果你做了,编译器会拒绝编译。
两个例外
先给出这两个例外情况:
- 在类文字中,必须使用原生态类型。也就是说 List.class、String[].class、int.class 是合法的,但是 List
.class 是不合法的。 instanceof
不可以用于参数化类型,但是可以用于无限制通配符类型和原生态类型,在这种情况下 <> 和 ? 就多余了,所以直接使用原生态类型。
注意:
- 上面这两个例外都是由于泛型信息在运行时被擦除导致的,泛型擦除的内容可以看后面的内容。
- 目前还允许使用原生态类型的原因是为了提供兼容性,为了兼容大量没有使用泛型的 Java 代码。
列表优先于数组
数组相比泛型有两个重要的不同点:
- 数组是协变的(convariant),泛型是不可变的(invariant)
- 数组是具体的(reified),泛型是可以被擦除的
首先说说数组是协变的,它的意思就是说如果 Sub
是 Sup
的子类型,那么数组类型 Sub[]
就是 Sup[]
的子类型。但是针对 List<String>
和 List<Object>
,虽然 String
是 Object
的子类,但是 List<String>
和 List<Object>
并没有任何关系,但是我们可以说 List<String>
是原生态类型 List
的一个子类。
看下面的代码:
1 | Object oArray = new Long[0]; |
上面两种方法都不能将 “test” 放入 Long 容器中,但是数组需要到运行时才能发现错误,而集合可以做到编译的时候就发现错误。
其次说说数组是具体化的,因此数组会在运行时才知道并检查它们的元素类型约束。如上所示,如果企图将 String 实例保存到 Long 数组中就会得到一个 ArrayStoreException。相比之下,泛型是通过擦除实现。因此泛型只在编译时强化它们的类型信息,并在运行时丢弃类型信息。擦除就是使泛型可以与没有使用泛型的代码随意进行互用。
由于上述原因,数组和泛型并不能很好的混用,所以当混用后出现错误或者警告,第一反应是用列表代替数组。
有限制通配符的使用
上面说到泛型是不可变的,也就是说 List<String>
和 List<Object>
是任何没有关系的,List<String>
不是 List<Object>
的子类型。可是,有时候我们的灵活性要比不可变类型所能提供的更多。先看下面一个例子:
1 | public class Stack<E> { |
先说说 putAll
方法,下面的使用从逻辑上来说应该也是可以的,但实际上会编译错误。
1 | Stack<Number> stack = new Stack<>(); |
幸运的是,可以通过有限制的通配符类型来处理类似的情况。比如上面的问题,我们只需要对 putAll
方法进行简单的修改即可:
1 | public void putAll(List<? extends E> lists) { |
注意:前面提到的有限制通配符类型 <?> 其实是 <? extends Object> 的缩写
再说说 popAll
方法,下面的使用从逻辑上来说应该也是可以的,但实际上也会编译错误。
1 | Stack<Number> stack = new Stack<>(); |
针对上面的情况我们同样可以使用有限制的通配符类型来解决,不过稍有不同:
1 | public void popAll(List<? super E> lists) { |
至于选择 extends
还是 super
可以参照 PECS
准则:producter-extends, consumer-super。
<? extends Object>
和<E extends Object>
的区别:
<E extends Object>
是有限制类型参数,它要求实际的类型参数必须是Object
的子类,主要是为了E
可以使用Object
上的方法。<? extends Object>
是有限制的通配符类型,和<E extends Object>
没有直接关系。
泛型擦除
参考文章:Java 泛型擦除以及擦除带来的问题
Java 泛型是伪泛型,它是通过在编译期间将所有的泛型信息擦掉来实现的。Java 泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含类型信息的。
比如代码中使用的 List<String>
或 List<Integer>
,编译器是看不到添加的 String
或 Integer
信息的,它们在编译后都会变成 List
。可以通过反射证明这个想法:
1 | public static void main(String[] args) { |
上面的例子说明了 List<Integer>
类型信息在编译的时候被擦除了,只保留了原始类型。那么这个原始类型到底是什么呢?原始类型就是擦除了泛型信息,最后在字节码中的类型变量的真正类型。无论何时定义一个泛型,其原始类型都会被自动提供。
类型擦除后,类型变量默认使用 Object
类型。如果有限定类型信息,类型变量就使用限定类型。 例如下面这个泛型以及对应的原始类型:
1 | public class Pair<T> { |
因为在 Pair<T>
中,T
是一个无限定的类型变量,所以用 Object
替换,其结果就是一个普通的类,如同泛型加入 Java 之前已经实现的样子。
如果 T
有限定,那就用限定的类型来替换,例如下面就会用 Comparable
来代替类型变量的类型。
1 | public class Pair<T extends Comparable> { |
面试中的问题
Kotlin 中的泛型
Kotlin 的泛型和 Java 的泛型一致,它们都是伪泛型,原理上十分相似,这里简单只说说 Kotlin 中不一样的地方。
out 和 in
先看下面的例子,这是刚才说 Java 有限制通配符类型时候提到的例子,改写成 Kotlin 如下:
1 | class Stack<E> { |
out
这里的 putAll
和 Java 中一样,也会由于不协变的原因,导致下面的使用会出错:
1 | val stacks = Stack<Number>() |
在 Java 中使用 ? extends
解决这个问题,在 Kotlin 中使用 out
来解决:
1 | fun putAll(lists: MutableList<out E>) { |
in
针对下面的使用,也会由于不协变的原因导致不可编译。
1 | val stacks = Stack<Number>() |
在 Java 中使用 ? super
解决这个问题,在 Kotlin 中使用 in
来解决:
1 | fun popAll(lists: MutableList<in E>) { |
注意:
- Java 中的
List
不支持协变的,但是 Kotlin 中的List
是支持协变的,不支持协变的是MutableList
。- Java 里面的数组是支持协变的,但是 Kotlin 里面的数组
Array
是不支持协变的。
*
前面有说到 Java 中有无限制通配符类型 ?
,它相当于 ? extends Object
。Kotlin 中与 ?
对应的符号是 *
,它相当于 out Any
。
1 | val list: List<*> |
和 Java 不同的地方是,如果你的类型定义里已经有了 out
或者 in
,那这个限制在变量声明时也依然在,不会被 *
号去掉。
比如你的类型定义里是 out T : Number
的,那它加上 <*>
之后的效果就不是 out Any
,而是 out Number
。
1 | interface Counter<out T : Number> { |
Kotlin 和 Java 不同,它不再保留原生态类型了。
where
前面 Java 说到有限制类型参数的时候,可以使用 extends
来设置上界:
1 | // T 的类型必须是 Animal 的子类型 |
如果有多个上界,需要使用 &
连接:
1 | class Monster<T extends Animal & Food>{ |
上面两个在 Kotlin 中对应的写法如下:
1 | class Monster<T : Animal> |
reified
由于 Java 中的泛型存在类型擦除的情况,任何在运行时需要知道泛型确切类型信息的操作都没法用了。
比如你不能检查一个对象是否为泛型类型 T
的实例:
1 | <T> void printIfTypeMatch(Object item) { |
正常情况下,在 Kotlin 中也不可以:
1 | fun <T> printIfTypeMatch(item: Any) { |
面对这种情况,Java 的解决方案使传入一个显示的 Class
1 | <T> void printIfTypeMatch(Object item, Class<T> type) { |
但是 Kotlin 中可以使用关键字 reified
配合 inline
来便捷解决:
1 | inline fun <reified T> printIfTypeMatch(item: Any) { |
为啥需要在 inline
中可以参考这两篇文章:
可以阅读下面的例子,以及实际编译的结果体会 reified
关键字:
1 | // 正常代码 |