End

Java 和 Kotlin 中的泛型

本文地址


目录

Java 和 Kotlin 中的泛型

参考

Java 泛型的不可变性

先看一个常见的使用场景:

TextView textView = new Button(context);
// 👆 这是多态

ArrayList<TextView> textViews = new ArrayList<Button>();
// 👆 这里会报错:Required type: ArrayList<TextView>, Provided: ArrayList<Button>

我们知道 Button 是继承自 TextView 的,根据 Java 多态的特性,第一处赋值是正确的。

但是到了 ArrayList<TextView> 的时候 IDE 就报错了,这是因为 Java 的泛型本身具有「不可变性 Invariance」,Java 里面认为 ArrayList<TextView>ArrayList<Button> 类型并不一致,也就是说,子类的泛型(ArrayList<Button>)不属于泛型(ArrayList<TextView>)的子类。

Java 的泛型类型会在编译时发生类型擦除,为了保证类型安全,不允许这样赋值。

在 Java 里用数组做类似的事情,是不会报错的,这是因为数组并没有在编译时擦除类型:

TextView[] textViews = new Button[10];

但是在实际使用中,我们的确会有这种类似的需求,需要实现上面这种赋值。

Java 提供了「泛型通配符」 ? extends? super 来解决这个问题。

Java 中的 ? extends

上面遇到的问题,在 Java 里面是这么解决的:

ArrayList<? extends TextView> textViews = new ArrayList<Button>();

这个 ? extends 叫做「上界通配符」,可以使 Java 泛型具有「协变性 Covariance」。

在继承关系树中,子类继承自父类,可以认为父类在上,子类在下。extends 限制了泛型类型的父类型,所以叫上界。

它有两层意思:

  • 其中 ? 是个通配符,表示这个 List 的泛型类型是一个未知类型
  • extends 限制了这个未知类型的上界,它的范围包括:所有直接和间接子类,以及这个上界类型本身。

这里 ButtonTextView 的子类,满足了泛型类型的限制条件,因而能够成功赋值。

因此,下面几种情况都是可以的:

List<? extends TextView> tvs = new ArrayList<TextView>();    // 👈 本身
List<? extends TextView> tvs = new ArrayList<Button>();      // 👈 直接子类
List<? extends TextView> tvs = new ArrayList<RadioButton>(); // 👈 间接子类

集合

在使用了上界通配符之后,List 的使用上会有些问题:

List<? extends TextView> textViews = new ArrayList<Button>();
TextView textView = textViews.get(0); // get 可以
textViews.add(textView);              // add 会报错

前面说到 List<? extends TextView> 的泛型类型是个未知类型,编译器也不确定它是啥类型,只是有个限制条件。

根据限制条件,get 出来的对象,肯定是 TextView 的子类型,因此能够赋值给 TextView

到了 add 操作的时候,我们可以这么理解:

  • List<? extends TextView> 由于类型未知,它可能是 List<Button>,也可能是 List<TextView>
  • 如果是 List<Button>,显然我们要添加 TextView 是不可以的。
  • 实际情况是,编译器无法确定到底属于哪一种,所以就报错了。

不要 extends

那我干脆不要 extends TextView ,只用通配符 ? 呢?

这样使用 List<?> 其实是 List<? extends Object> 的缩写。

List<?> list = new ArrayList<Button>();
Object obj = list.get(0); // 只能 get 到 Object 对象
list.add(obj);            // 这里还是会报错

和前面的例子一样,编译器没法确定 ? 的类型,所以这里就只能 getObject 对象。

同时编译器为了保证类型安全,也不能向 List<?> 中添加任何类型的对象,理由同上。

总结

由于 add 的这个限制,使用了 ? extends 泛型通配符的 List,只能够向外提供数据被消费,从这个角度来讲,向外提供数据的一方称为「生产者 Producer」。对应的还有一个概念叫「消费者 Consumer」,对应 Java 里面另一个泛型通配符 ? super

Java 中的 ? super

这个 ? super 叫做「下界通配符」,可以使 Java 泛型具有「逆变性 Contravariance」。

与上界通配符对应,这里 super 限制了通配符 ? 的子类型,所以称之为下界。

List<? super Button> buttons = new ArrayList<TextView>();

它也有两层意思:

  • 通配符 ? 表示 List 的泛型类型是一个未知类型
  • super 限制了这个未知类型的下界,它的范围包括:所有直接和间接父类,以及这个下界类型本身。

上面的例子中,TextViewButton 的父类型 ,也就能够满足 super 的限制条件,就可以成功赋值了。

因此,下面几种情况都是可以的:

List<? super Button> buttons = new ArrayList<Button>();   // 👈 本身
List<? super Button> buttons = new ArrayList<TextView>(); // 👈 直接父类
List<? super Button> buttons = new ArrayList<Object>();   // 👈 间接父类

集合

对于使用了下界通配符的 List,我们再看看它的 getadd 操作:

List<? super Button> buttons = new ArrayList<TextView>();
Object object = buttons.get(0);     // 👈 get 出来的是 Object 类型
buttons.add(new Button(context));   // 👈 add 操作是可以的
buttons.add(new TextView(context)); // 👈 add 操作是可以的

解释下,首先 ? 表示未知类型,编译器是不确定它的类型的。

虽然不知道它的具体类型,不过在 Java 里任何对象都是 Object 的子类,所以这里能把它赋值给 Object

Button 对象一定是这个未知类型的子类型,所以这里通过 add 添加 Button 对象是合法的。

总结

使用下界通配符 ? super 的泛型 List,只能读取到 Object 对象,通常也只拿它来添加数据,也就是消费已有的 List<? super Button>,往里面添加 Button,因此这种泛型类型声明称之为「消费者 Consumer」。

Java 泛型总结

Java 的泛型本身是不支持协变和逆变的。

  • 可以使用泛型通配符 ? extends 来使泛型支持协变,但是「只能读取不能修改」,这里的修改仅指对泛型集合添加元素,如果是 remove(int index) 以及 clear 当然是可以的。
  • 可以使用泛型通配符 ? super 来使泛型支持逆变,但是「只能修改不能读取」,这里说的不能读取是指不能按照泛型类型读取,你如果按照 Object 读出来再强转当然也是可以的。

根据前面的说法,这被称为 PECS 法则:Producer-Extends, Consumer-Super

Kotlin 中的 out 和 in

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

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

换了个写法,但作用是完全一样的。

  • out 表示,我这个变量或者参数只用来输出,不用来输入,你只能读我不能写我;
  • in 表示它只用来输入,不用来输出,你只能写我不能读我。

说了这么多 List,其实泛型在非集合类的使用也非常广泛,就以「生产者-消费者」为例子:

class Producer<T> {
    fun produce(): T = ...
}

val producer: Producer<out TextView> = Producer<Button>()
val textView: TextView = producer.produce() // 👈 相当于 List 的 get
class Consumer<T> {
    fun consume(t: T) = ...
}

val consumer: Consumer<in Button> = Consumer<TextView>()
consumer.consume(Button(context)) // 👈 相当于 List 的 add

声明处的 out 和 in

在前面的例子中,在声明 Producer 的时候已经确定了泛型 T 只会作为输出来用,但是每次都需要在使用的时候加上 out TextView 来支持协变,写起来很麻烦。

Kotlin 提供了另外一种写法:可以在声明类的时候,给泛型符号加上 out 关键字,表明泛型参数 T 只会用来输出,在使用的时候就不用额外加 out 了。

out 一样,可以在声明类的时候,给泛型参数加上 in 关键字,来表明这个泛型参数 T 只用来输入。

Kotlin 中的 * 号

前面讲到了 Java 中单个 ? 号也能作为泛型通配符使用,相当于 ? extends Object

在 Kotlin 中有等效的写法:* 号,相当于 out Any

var list: List<*>

和 Java 不同的地方是,如果你的类型定义里已经有了 out 或者 in,那这个限制在变量声明时也依然在,不会被 * 号去掉。

比如你的类型定义里是 out T : Number 的,那它加上 <*> 之后的效果就不是 out Any,而是 out Number

where 关键字

Java 中声明类或接口的时候,可以使用 extends 来设置边界,将泛型类型参数限制为某个类型的子集:

class Monster<T extends Animal> {}

注意这个和前面讲的声明变量时的泛型类型声明是不同的东西,这里并没有 ?

同时这个边界是可以设置多个,用 & 符号连接:

// T 的类型必须同时是 Animal 和 Food 的子类型
class Monster<T extends Animal & Food> {}

Kotlin 只是把 extends 换成了 : 冒号,设置多个边界可以使用 where 关键字:

class Monster<T : Animal>
class Monster<T> where T : Animal, T : Food
class Monster<T> : MonsterParent<T> where T : Animal

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) print() // IDE 会提示错误,Cannot check for instance of erased type: T
}

这个问题,在 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) println() // 👈 这里就不会在提示错误了
}

数组和集合的区别

1、Java 里的数组是支持协变的,而 Kotlin 中的数组 Array 不支持协变。

这是因为在 Kotlin 中数组是用 Array 类来表示的,这个 Array 类使用泛型就和集合类一样,所以不支持协变。

2、Java 中的 List 接口不支持协变,而 Kotlin 中的 List 接口支持协变。

Java 中的 List 不支持协变,原因在上文已经讲过了,需要使用泛型通配符来解决。

在 Kotlin 中,实际上 MutableList 接口才相当于 Java 的 List。Kotlin 中的 List 接口实现了只读操作,没有写操作,所以不会有类型安全上的问题,自然可以支持协变。

2017-09-17

posted @   白乾涛  阅读(2174)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示