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); // 👈 这里还是会报错
和前面的例子一样,编译器没法确定 ?
的类型,所以这里就只能 get
到 Object
对象。
同时编译器为了保证类型安全,也不能向 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
。
上面的例子中,TextView
是 Button
的父类型 ,也就能够满足 super
的限制条件,就可以成功赋值了。
根据刚才的描述,下面几种情况都是可以的:
☕️
List<? super Button> buttons = new ArrayList(); // 👈 本身
List<? super Button> buttons = new ArrayList(); // 👈 直接父类
List<? super Button> buttons = new ArrayList(); // 👈 间接父类
对于使用了下界通配符的 List
,我们再看看它的 get
和 add
操作:
☕️
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 中的 out
和 in
和 Java 泛型一样,Kolin 中的泛型本身也是不可变的。
- 使用关键字
out
来支持协变,等同于 Java 中的上界通配符? extends
。 - 使用关键字
in
来支持逆变,等同于 Java 中的下界通配符? super
。
🏝️
var textViews: List
var textViews: List
换了个写法,但作用是完全一样的。out
表示,我这个变量或者参数只用来输出,不用来输入,你只能读我不能写我;in
就反过来,表示它只用来输入,不用来输出,你只能写我不能读我。
你看,我们 Android 工程师学不会 out
和 in
,其实并不是因为这两个关键字多难,而是因为我们应该先学学 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’
声明处的 out
和 in
在前面的例子中,在声明 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 泛型不一致的地方,这里作一下解答。
- Java 里的数组是支持协变的,而 Kotlin 中的数组
Array
不支持协变。
这是因为在 Kotlin 中数组是用 Array
类来表示的,这个 Array
类使用泛型就和集合类一样,所以不支持协变。
- 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学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!