【码上开学】Kotlin 的泛型

本文解释了Java和Kotlin中的泛型通配符,特别是?extends和?super的作用,以及它们如何影响数据的读写操作。同时介绍了Producer-Consumer模式在泛型中的应用,并讨论了Kotlin中的out和in关键字以及reified关键字的使用。
摘要由CSDN通过智能技术生成

List<? extends TextView> textViews = new ArrayList();
TextView textView = textViews.get(0); // 👈 get 可以
textViews.add(textView);
// 👆 add 会报错,no suitable method found for add(TextView)

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

由于它满足 ? extends TextView 的限制条件,所以 get 出来的对象,肯定是 TextView 的子类型,根据多态的特性,能够赋值给 TextView,啰嗦一句,赋值给 View 也是没问题的。

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

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

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

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

☕️
List buttons = new ArrayList<>();

List<?> list = buttons;
Object obj = list.get(0);

list.add(obj); // 👈 这里还是会报错

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

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

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

Java 中的 ? super

先看一下它的写法:

☕️
👇
List<? super Button> buttons = new ArrayList();

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

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

它也有两层意思:

  • 通配符 ? 表示 List 的泛型类型是一个未知类型
  • super 限制了这个未知类型的下界,也就是泛型类型必须满足这个 super 的限制条件。
  • super 我们在类的方法里面经常用到,这里的范围不仅包括 Button 的直接和间接父类,也包括下界 Button 本身。
  • super 同样支持 interface

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

根据刚才的描述,下面几种情况都是可以的:

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

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

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

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

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

Button 对象一定是这个未知类型的子类型,根据多态的特性,这里通过 add 添加 Button 对象是合法的。

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


小结下,Java 的泛型本身是不支持协变和逆变的。

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

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

理解了 Java 的泛型之后,再理解 Kotlin 中的泛型,有如练完九阳神功再练乾坤大挪移,就比较容易了。

Kotlin 中的 outin

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

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

🏝️
var textViews: List
var textViews: List

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

你看,我们 Android 工程师学不会 outin,其实并不是因为这两个关键字多难,而是因为我们应该先学学 Java 的泛型。是吧?

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

🏝️
class Producer {
fun produce(): T {

}
}

val producer: Producer = Producer()
val textView: TextView = producer.produce() // 👈 相当于 ‘List’ 的 get

再来看看消费者:

🏝️
class Consumer {
fun consume(t: T) {

}
}

val consumer: Consumer = Consumer()
consumer.consume(Button(context)) // 👈 相当于 ‘List’ 的 ‘add’

声明处的 outin

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

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

🏝️ 👇
class Producer {
fun produce(): T {

}
}

val producer: Producer = Producer() // 👈 这里不写 out 也不会报错
val producer: Producer = Producer() // 👈 out 可以但没必要

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

🏝️ 👇
class Consumer {
fun consume(t: T) {

}
}

val consumer: Consumer = Consumer() // 👈 这里不写 in 也不会报错
val consumer: Consumer = Consumer() // 👈 in 可以但没必要

*

前面讲到了 Java 中单个 ? 号也能作为泛型通配符使用,相当于 ? extends Object。 它在 Kotlin 中有等效的写法:* 号,相当于 out Any

🏝️ 👇
var list: List<*>

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

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

where 关键字

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

☕️
// 👇 T 的类型必须是 Animal 的子类型
class Monster{
}

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

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

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

Kotlin 只是把 extends 换成了 : 冒号。

🏝️ 👇
class Monster

设置多个边界可以使用 where 关键字:

🏝️ 👇
class Monster where T : Animal, T : Food

有人在看文档的时候觉得这个 where 是个新东西,其实虽然 Java 里没有 where ,但它并没有带来新功能,只是把一个老功能换了个新写法。

不过笔者觉得 Kotlin 里 where 这样的写法可读性更符合英文里的语法,尤其是如果 Monster 本身还有继承的时候:

🏝️
class Monster : MonsterParent
where T : Animal

reified 关键字

由于 Java 中的泛型存在类型擦除的情况,任何在运行时需要知道泛型确切类型信息的操作都没法用了。

比如你不能检查一个对象是否为泛型类型 T 的实例:

☕️
void printIfTypeMatch(Object item) {
if (item instanceof T) { // 👈 IDE 会提示错误,illegal generic type for instanceof
System.out.println(item);
}
}

Kotlin 里同样也不行:

🏝️
fun printIfTypeMatch(item: Any) {
if (item is T) { // 👈 IDE 会提示错误,Cannot check for instance of erased type: T
println(item)
}
}

这个问题,在 Java 中的解决方案通常是额外传递一个 Class<T> 类型的参数,然后通过 Class#isInstance 方法来检查:

☕️ 👇
void check(Object item, Class type) {
if (type.isInstance(item)) {
👆
System.out.println(item);
}
}

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

🏝️ 👇 👇
inline fun printIfTypeMatch(item: Any) {
if (item is T) { // 👈 这里就不会在提示错误了
println(item)
}
}

这具体是怎么回事呢?等到后续章节讲到 inline 的时候会详细说明,这里就不过多延伸了。

还记得第二篇文章中,提到了两个 Kotlin 泛型与 Java 泛型不一致的地方,这里作一下解答。

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

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

  1. Java 中的 List 接口不支持协变,而 Kotlin 中的 List 接口支持协变。

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

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

练习题

学习福利

【Android 详细知识点思维脑图(技能树)】

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

[外链图片转存中…(img-RXRrgdDl-1714945947215)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 19
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值