扫盲: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 开发,View
的 findViewById
不就是最好的例子么?
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
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。