文章:Bruce(郑啸天)
大家好,我是扔物线朱凯。你在看的是码上开学项目的 Kotlin 高级部分的第 1 篇:Kotlin 的泛型。首当其冲的当然还是香香的视频香香的我啦:
因为我一直没有学会怎么在掘金贴视频,所以请点击 这里 去哔哩哔哩看,或者点击 这里 去 YouTube 看。
以下内容来自文章作者Bruce。
这期是码上开学 Kotlin 系列的独立技术点部分的第一期,我们来聊一聊泛型。
提到 Kotlin 的泛型,通常离不开 in
和 out
关键字,但泛型这门武功需要些基本功才能修炼,否则容易走火入魔,待笔者慢慢道来。
下面这段 Java 代码在日常开发中应该很常见了:
☕️
List textViews = new ArrayList();
其中 List<TextView>
表示这是一个泛型类型为 TextView
的 List
。
那到底什么是泛型呢?我们先来讲讲泛型的由来。
现在的程序开发大都是面向对象的,平时会用到各种类型的对象,一组对象通常需要用集合来存储它们,因而就有了一些集合类,比如 List
、Map
等。
这些集合类里面都是装的具体类型的对象,如果每个类型都去实现诸如 TextViewList
、ActivityList
这样的具体的类型,显然是不可能的。
因此就诞生了「泛型」,它的意思是把具体的类型泛化,编码的时候用符号来指代类型,在使用的时候,再确定它的类型。
前面那个例子,List<TextView>
就是泛型类型声明。
既然泛型是跟类型相关的,那么是不是也能适用类型的多态呢?
先看一个常见的使用场景:
☕️
TextView textView = new Button(context);
// 👆 这是多态
List buttons = new ArrayList();
List textViews = buttons;
// 👆 多态用在这里会报错 incompatible types: List cannot be converted to List
我们知道 Button
是继承自 TextView
的,根据 Java 多态的特性,第一处赋值是正确的。
但是到了 List<TextView>
的时候 IDE 就报错了,这是因为 Java 的泛型本身具有「不可变性 Invariance」,Java 里面认为 List<TextView>
和 List<Button>
类型并不一致,也就是说,子类的泛型(List<Button>
)不属于泛型(List<TextView>
)的子类。
Java 的泛型类型会在编译时发生类型擦除,为了保证类型安全,不允许这样赋值。至于什么是类型擦除,这里就不展开了。
你可以试一下,在 Java 里用数组做类似的事情,是不会报错的,这是因为数组并没有在编译时擦除类型:
☕️
TextView[] textViews = new TextView[10];
但是在实际使用中,我们的确会有这种类似的需求,需要实现上面这种赋值。
Java 提供了「泛型通配符」 ? extends
和 ? super
来解决这个问题。
Java 中的 ? extends
在 Java 里面是这么解决的:
☕️
List buttons = new ArrayList();
👇
List<? extends TextView> textViews = buttons;
这个 ? extends
叫做「上界通配符」,可以使 Java 泛型具有「协变性 Covariance」,协变就是允许上面的赋值是合法的。
在继承关系树中,子类继承自父类,可以认为父类在上,子类在下。
extends
限制了泛型类型的父类型,所以叫上界。
它有两层意思:
- 其中
?
是个通配符,表示这个List
的泛型类型是一个未知类型。 extends
限制了这个未知类型的上界,也就是泛型类型必须满足这个extends
的限制条件,这里和定义class
的extends
关键字有点不一样:- 它的范围不仅是所有直接和间接子类,还包括上界定义的父类本身,也就是
TextView
。 - 它还有
implements
的意思,即这里的上界也可以是interface
。
这里 Button
是 TextView
的子类,满足了泛型类型的限制条件,因而能够成功赋值。
根据刚才的描述,下面几种情况都是可以的:
☕️
List<? extends TextView> textViews = new ArrayList(); // 👈 本身
List<? extends TextView> textViews = new ArrayList(); // 👈 直接子类
List<? extends TextView> textViews = new ArrayList(); // 👈 间接子类
一般集合类都包含了 get
和 add
两种操作,比如 Java 中的 List
,它的具体定义如下:
☕️
public interface List extends Collection{
E get(int index);
boolean add(E e);
…
}
上面的代码中,E
就是表示泛型类型的符号(用其他字母甚至单词都可以)。
我们看看在使用了上界通配符之后,List
的使用上有没有什么问题:
☕️
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
。
🏝️
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
最后
我见过很多技术leader在面试的时候,遇到处于迷茫期的大龄程序员,比面试官年龄都大。这些人有一些共同特征:可能工作了5、6年,还是每天重复给业务部门写代码,工作内容的重复性比较高,没有什么技术含量的工作。问到这些人的职业规划时,他们也没有太多想法。
其实30岁到40岁是一个人职业发展的黄金阶段,一定要在业务范围内的扩张,技术广度和深度提升上有自己的计划,才有助于在职业发展上有持续的发展路径,而不至于停滞不前。
不断奔跑,你就知道学习的意义所在!
《Android高级架构师面试指导+2021大厂面试真题》免费领取
务范围内的扩张,技术广度和深度提升上有自己的计划,才有助于在职业发展上有持续的发展路径,而不至于停滞不前。
不断奔跑,你就知道学习的意义所在!
[外链图片转存中…(img-03HecxKs-1710678131297)]
《Android高级架构师面试指导+2021大厂面试真题》免费领取
[外链图片转存中…(img-GxPH850Z-1710678131298)]