【码上开学】Kotlin 的泛型

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 接口实现了只读操作,没有写操作,所以不会有类型安全上的问题,自然可以支持协变。

练习题

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
转存中…(img-45AAuhue-1715802811858)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值