注册

扫盲:Kotlin 的泛型(2)

Kotlin 的 out 和 in


和 Java 泛型一样,Kolin 中的泛型本身也是不可变的。


不过换了一种表现形式:



  • 使用关键字 out 来支持协变,等同于 Java 中的上界通配符 ? extends
  • 使用关键字 in 来支持逆变,等同于 Java 中的下界通配符 ? super

val appleShop: Shop<out Fruit>
val fruitShop: Shop<in Apple>
复制代码

它们完全等价于:


Shop<? extends Fruit> appleShop;
Shop<? super Apple> fruitShop;
复制代码

换了个写法,但作用是完全一样的。out 表示,我这个变量或者参数只用来输出,不用来输入,你只能读我不能写我;in 就反过来,表示它只用来输入,不用来输出,你只能写我不能读我。


泛型的上下界约束


上面讲的都是在使用的时候再对泛型进行限制,我们称之为「上界通配符」和「下界通配符」。那我们可以在函数设计的时候,就设置这个限制么?


可以的可以的。


比如:


open class Animal
class PetShop<T : Animal?>(val t: T)
复制代码

等同于 Java 的:


class PetShop<T extends Animal> {
private T t;

PetShop(T t) {
this.t = t;
}
}
复制代码

这样,我们在设计宠物店类 PetShop 就给支持的泛型设置了上界约束,支持的泛型类型必须是 Animal 的子类。所以我们使用的话:


class Cat : Animal()

val catShop = PetShop(Cat())
val appleShop = PetShop(Apple())
// 👆 报错:Type mismatch. Required: Animal? Found: Apple
复制代码

很明显,Apple 并不是 Animal 的子类,当然不满足 PetShop 泛型类型的上界约束。


那....可以设置多个上界约束么?


当然可以,在 Java 中,给一个泛型参数声明多个约束的方式是,使用 &


class PetShop<T extends Animal & Serializable> {
// 👆 通过 & 实现了两个上界,必须是 Animal 和 Serializable 的子类或实现类
private T t;

PetShop(T t) {
this.t = t;
}
}
复制代码

而在 Kotlin 中舍弃了 & 这种方式,而是增加了 where 关键字:


open class Animal
class PetShop<T>(val t: T) where T : Animal?, T : Serializable
复制代码

通过上面的方式,就实现了多个上界的约束。


Kotlin 的通配符 *


前面我们说的泛型类型都是在我们需要知道参数类型是什么类型的,那如果我们对泛型参数的类型不感兴趣,有没有一种方式处理这个情况呢?


有的有的。


在 Kotlin 中,可以用通配符 * 来替代泛型参数。比如:


val list: MutableList<*> = mutableListOf(1, "nanchen2251")
list.add("nanchen2251")
// 👆 报错:Type mismatch. Required: Nothing Found: String
复制代码

这个报错确实让人匪夷所思,上面用通配符代表了 MutableList 的泛型参数类型。初始化里面也加入了 String 类型,但在新 add 字符串的时候,却发生了编译错误。


而如果是这样的代码:


val list: MutableList<Any> = mutableListOf(1, "nanchen2251")
list.add("nanchen2251")
// 👆 不再报错
复制代码

看来,所谓的通配符作为泛型参数并不等价于 Any 作为泛型参数。MutableList<*>MutableList<Any> 并不是同一种列表,后者的类型是确定的,而前者的类型并不确定,编译器并不能知道这是一种什么类型。所以它不被允许添加元素,因为会导致类型不安全。


不过细心的同学肯定发现了,这个和前面泛型的协变非常类似。其实通配符 * 不过是一种语法糖,背后也是用协变来实现的。所以:MutableList<*> 等价于 MutableList<out Any?>,使用通配符与协变有着一样的特性。


在 Java 中,也有一样意义的通配符,不过使用的是 ? 作为通配。


List<?> list = new ArrayList<Apple>(); 
复制代码

Java 中的通配符 ? 也等价于 ? extends Object


多个泛型参数声明


那可以声明多个泛型么?


可以的可以的。


HashMap 不就是一个典型的例子么?


class HashMap<K,V>
复制代码

多个泛型,可以通过 , 进行分割,多个声明,上面是两个,实际上多个都是可以的。


class HashMap<K: Animal, V, T, M, Z : Serializable>
复制代码

泛型方法


上面讲的都是都是在类上声明泛型类型,那可以声明在方法上么?


可以的可以的。


如果你是一名 Android 开发,ViewfindViewById 不就是最好的例子么?


public final <T extends View> T findViewById(@IdRes int id) {
if (id == NO_ID) {
return null;
}
return findViewTraversal(id);
}
复制代码

很明显,View 是没有泛型参数类型的,但其 findViewById 就是典型的泛型方法,泛型声明就在方法上。


上述写法改写成 Kotlin 也非常简单:


fun <T : View?> findViewById(@IdRes id: Int): T? {
return if (id == View.NO_ID) {
null
} else findViewTraversal(id)
}
复制代码

Kotlin 的 reified


前面有说到,由于 Java 中的泛型存在类型擦除的情况,任何在运行时需要知道泛型确切类型信息的操作都没法用了。比如你不能检查一个对象是否为泛型类型 T 的实例:


<T> void printIfTypeMatch(Object item) {
if (item instanceof T) { // 👈 IDE 会提示错误,illegal generic type for instanceof

}
}
复制代码

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 方法来检查:


                               👇
<T> void check(Object item, Class<T> type) {
if (type.isInstance(item)) {
👆
}
}
复制代码

Kotlin 中同样可以这么解决,不过还有一个更方便的做法:使用关键字 reified 配合 inline 来解决:


  👇          👇
inline fun <reified T> printIfTypeMatch(item: Any) {
if (item is T) { // 👈 这里就不会在提示错误了

}
}
复制代码

上面的 Gson 解析的时候用的非常广泛,比如咱们项目里就有这样的扩展方法:


inline fun <reified T> String?.toObject(type: Type? = null): T? {
return if (type != null) {
GsonFactory.GSON.fromJson(this, type)
} else {
GsonFactory.GSON.fromJson(this, T::class.java)
}
}
复制代码

总结


本文花了非常大的篇幅来讲 Kotlin 的泛型和 Java 的泛型,现在再回过头去回答文首的几个问题,同学你有谱了吗?如果还是感觉一知半解,不妨多看几遍。


作者:nanchen2251
链接:https://juejin.cn/post/6911659839282216973
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册