前言
Kotlin 也拥有泛型的概念,和 Java 的有些相似,但是又不尽相同,本文主要记录 Java 和 kotlin 泛型的相同及差异点。
什么是泛型?
泛型就是参数化类型,也就是说所操作的数据类型被指定为一个参数(type parameter)这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
为什么需要泛型?
在没有泛型之前,从集合中读取到的每一个对象都必须进行显示的强转,由于集合中可以存储所有对象,所有可能会出现强转错误,对于强转错误的情况,编译器不会给予提示,但是运行时会抛出异常。
泛型拥有以下优点:
- 类型安全
- 消除显示强转
举个例子:
List<String> urls = new ArrayList<>(); |
其中 List 表示这是一个泛型类型为 String 的 List。urls 中只能添加 String 类型的对象(也可以添加子类,但是由于 String 类是 final 类,所以不存在子类),对于其它类型编译器会直接报错,避免运行时出错,并且从 urls 里面取出对象时直接就是 String 类型不需要显示的强转。
Java中的泛型
泛型类
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。泛型类内部非静态成员可以直接使用泛型类型。
// T 可以为任意标识符比如 K(key)、V(value)、E(element) |
泛型接口
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中。
public interface Generator<T> { |
泛型方法
非静态方法可以使用类声明的泛型类型。
public <T> T genericMethod(Class<T> tClass){} |
泛型的协变和逆变
先来看数组和泛型列表的区别。
public class Test { |
上例中可以看出数组是协变的,但是其不安全,fruitArray 里可以放入任何 Fruit 及其子类对象编译器不会报错,但是运行时可能会出错,而泛型是不变的,把 ArrayList
赋值给 List
会直接编译出错。
不过泛型借助于通配符也是可以实现协变和逆变。
协变
先来看看协变。
List<? extends Fruit> fruitList = new ArrayList<Apple>(); |
现在 fruitList 的类型是表示了该泛型上界是 Fruit,可以把任何泛型类型为 Fruit 及其子类的 ArrayList
赋值给它,在 fruitList 里面存的可能是 Fruit 或者其子类,由于编译器无法确定具体是哪个子类,因此拒绝往 fruitsList 里面放入任何对象。
相反由于知道了 fruitList 里面存的一定是 Fruit 或其子类所以调用 get 方法获取到的一定是 Fruit 实例。
// List.java |
看看上述两个方法声明有什么不同点,为什么List要使用第一种方式,其有什么优势?假设 E 为 Number 类型。
- 优势一
由于方法一内部无法往 val1
中添加任何对象,一定程度杜绝了方法影响参数内容。
- 优势二
方法二只能接受参数 Collection<Number>,而不能接受 Collection<Integer> 等泛型类型是 Number 子类的 Collection。
逆变
接着来看看逆变
// List<? super Fruit> fruitList = new ArrayList<Apple>(); compile error |
现在 fruitList 的类型是 <? super Fruit>,表示了该泛型下界是 Fruit,可以把任何 ArrayList<Fruit及其父类> 赋值给它,在 fruitList 里面存的可能是 Fruit 或者其父类,所以可以把 Fruit 及其子类添加到 fruitList 中(多态)。
相反由于编译器无法知道 fruitList 确切存的是 Fruit 的哪个父类,因此 get 方法获取到的只能是 Object 对象。
// Collections.java |
看看上述两个方法声明有什么不同点,为什么 Collections 要使用第一种方式,其有什么优势?假设 E 为 Number类型。
优势为方法二只能传递 Collection<Number>,而不能传递 Collection<Object> 等泛型类型是 Number 父类的 Collection。
协变与逆变总结
什么时候用 extends?,什么时候用 super?,<<Effecitive Java>> 第5章总结为 PECS(producer-extends, consumer-super)
了解了 java 中的泛型后现在回到正题上探究下 kotlin 的泛型。
Kotlin中的泛型
Array
上文已经讲到了 Java 的数组支持协变,然而 Kotlin 中的数组是不支持协变的,因为 Kotlin 中数组使用 Array 类,而该类泛型定义和集合类没什么区别:
fun main() { |
in 和 out 关键字
与 Java 中的泛型一样,Kotlin 中的泛型也是不可变的。
- Kotlin使用关键字
out
来支持协变,等价于Java中的上界通配符? extends
。 - Kotlin使用关键字
in
来支持逆变,等价于Java中的下界通配符? super
。
fun <T> addAll(var1: Collection<out T>) |
上述两组各自完全等价,唯一区别是 Kotlin 中 Collection 被设计成只读的,所以逆变需要使用 MutableCollection。
换了个写法,但作用是完全一样的。out 表示,我这个变量或者参数只用来输出,不用来输入,你只能读我不能写我;in 就反过来,表示它只用来输入,不用来输出,你只能写我不能读我。
class Producer<T> { |
此外 out 和 in 可以在泛型类声明类时使用,这点 Java 也是不支持的。
public class JavaOut<? extends T> { } // compile error |
那么为什么 Kotlin 要新增这种语法呢?有什么优势?来看看下面的代码。
fun main() { |
这段代码可以正确的编译通过,说明了如果在声明泛型类时使用了 out 那么该类就自带协变,使用了 in 那么该类就自带逆变。但是以下代码是错误的。
fun main() { |
如果在声明泛型类时使用了 out 那么该类泛型就无法再被声明成 in,反之亦然。
通配符
Java使用 ?
做为通配符,Kotlin中使用 *
做为通配符。
where 关键字
Java 中声明类、接口、方法的时候,可以使用 extends 来设置边界,将泛型类型参数限制为某个类型的子集:
class Java<T extends Number> { } |
上述代码表示 T 的类型必须是 Number 或者其子类,注意:这里并没有用到?,要与前面说的分开。边界也可以设置多个。
class Java<T extends OnClickListener & OnItemClickListener> { } |
Kotlin中只是把设置单个边界从 extends
换成了 :
,设置多个边界使用 where
关键字。
class Kotlin<T> where T : OnClickListener, T : OnItemClickListener {} |
reified 关键字
先看看一个需求,比如我想从 Model 通过 Handler 发来的数据中取出 String 对象,但是由于发送过来的对象(Result#obj) 是一个 Objec t对象,所以就不得不进行强转,而强转有可能会失败,导致抛出异常,进而导致 App崩溃,因此就想写一个安全的强转方法,如下:
public static <T> T safelyCast(Object object) { |
但是由于 Java 中的泛型存在类型擦除的情况,任何在运行时需要知道泛型确切类型信息的操作都没法用了,因此 object instanceof T
不能通过编译。kotlin 中同样也不行:
fun <T> Any.safelyCast(): T? { |
难道就没有办法可以做到吗?Java、Kotlin都是可以做到的,Java中可以通过传递一个Class对象进来如下所示:
public static <T> T safelyCast(Object object, Class<T> clazz) { |
在 Kotlin 中也可以这么解决,不过还有一种更优雅的实现,借助 reified 和 inline。
inline fun <reified T> Any.safelyCast(): T? { |
下面是实际代码使用保证了绝无异常抛出:
override fun handleMessage(msg: Message): Boolean { |
再举一个 startActivity
的例子
inline fun <reified T: Activity> Context.startActivity() { |
有了这个方法后启动Activity就可以简单的写为:
fun startMainActivity(ctx: Context) { |