往期文章
《00. 写给 Java 开发者的 Kotlin 入坑指南》
前言
这是一篇文科生都能读泛型入门教程。(亲测,我女朋友都能看懂。)
本文以故事
的形式介绍 Kotlin 泛型及其不变性
,声明处型变
,使用处型变
,最后再搭配一个实战环节,将泛型应用到我们的 Demo 当中来。
前期准备
- 将 Android Studio 版本升级到最新
- 将我们的 Demo 工程 clone 到本地,用 Android Studio 打开:
github.com/chaxiu/Kotl… - 切换到分支:
chapter_05_generics
- 强烈建议各位小伙伴小伙伴跟着本文一起实战,实战才是本文的精髓
正文
1. 遥控器的故事:泛型
女朋友:好想要一个万能遥控器啊。
我:要不我教你用 Kotlin 的泛型实现一个吧!
女朋友:切,又想忽悠我学 Kotlin。[白眼]
我:真的很简单,保证你一看就会。
1-1 泛型类
我:这是一个万能遥控器,它带有一个泛型参数
1 | kotlin复制代码// 类的泛型参数(形参) |
我:它用起来也简单,想控制什么,把对应的泛型传进去就行,就跟选模式一样:
1 | kotlin复制代码// 电视机作为泛型实参 |
借助 Kotlin 的顶层函数,Controller 类甚至都可以省掉,直接用泛型函数:
1-2 泛型函数
1 | kotlin复制代码// 函数的泛型参数 |
泛型函数用起来也简单:
1 | kotlin复制代码// 控制电视 |
女朋友:我知道怎么用啦!是不是这样?
1 | kotlin复制代码val boyFriend = BoyFriend() |
我:……
2. 招聘的故事:泛型的不变性(Invariant)
女朋友:我想招几个大学生做兼职,你推荐几个大学吧。
我:好嘞,不过我要通过 Kotlin 泛型来给你推荐。
女朋友:呃……刚才你讲的泛型还挺简单,这次有什么新花样吗?
我:你看下去就知道了。
我:先来点准备工作:
1 | kotlin复制代码// 学生 |
我:你的招聘需求可以用这样的代码描述:
1 | kotlin复制代码// 注意这里 |
女朋友:原来 Kotlin 也没那么难……
女朋友:能赋值一个”女子大学”吗?
我:不行,会报错。
1 | kotlin复制代码// 注意这里 |
女朋友:什么鬼。。。
我:虽然 Student 和 FemaleStudent 之间是父子关系,但是 University
和 University 之间没有任何关系。这叫泛型的不变性。 女朋友:这不合理!女子大学招聘出来的学生,难道就不是学生?
我:招聘当然符合逻辑,但别忘了 University 还有一个 put 方法。
我:你怎么防止别人把一个男学生放到女子大学里去?
我:让我们看看如果可以将“女子大学”当作“普通大学”用,会出现什么问题:
1 | kotlin复制代码// 声明的类型是:普通大学,然而,实际类型是:女子大学。 |
女朋友:明白了,原来这就是泛型不变性的原因,确实能避免不少麻烦。
1 | kotlin复制代码// 默认情况下,编译器只允许这么做 |
3. 搞定招聘:泛型的协变(Covariant)
女朋友:如果我把 University 类里面的 put 方法删掉,是不是就可以用“女子大学”赋值了?这样就不用担心
把男学生放到女子大学
的问题了。我:这还不够,还需要加一个关键字
out
告诉编译器:我们只会从 University 类往外取,不会往里面放。这时候,University就可以当作 University 的子类。 我:这叫做泛型的
协变
。
1 | kotlin复制代码open class Student() |
女朋友:我试试,果然好了!
1 | kotlin复制代码// 不再报错 |
我:你不来写代码真浪费了。
4. 填志愿的故事:泛型的逆变(Contravariant)
女朋友:我妹妹刚高考完,马上要填志愿了,你给推荐个大学吧。
我:咱刚看过泛型协变,要不你试试自己解决这个填志愿的问题?正好 University 里有个 put 方法,你就把 put 当作填志愿就行了。
女朋友:那我依葫芦画瓢试试…… 给我妹妹报一个女子大学。
1 | kotlin复制代码open class Student() |
女朋友:完美!
我:厉害。
女朋友:能不能再报一个普通综合大学?
我:不行,你忘记泛型不变性了吗?
1 | kotlin复制代码val sister: FemaleStudent = FemaleStudent() |
女朋友:我妹能报女子大学,居然不能报普通的综合大学?这不合理吧!
我:你别忘了 University 还有一个 get 方法吗?普通综合大学 get 出来的可不一定是女学生。
女朋友:哦。那我把 get 方法删了,再加个关键字?
我:对。删掉 get 方法,再加一个关键字:
in
就行了。它的作用是告诉编译器:我们只会往 University 类里放,不会往外取。这时候,University就可以当作 University 的子类。 我:这其实就叫做泛型的
逆变
,它们的继承关系反过来了。
1 | kotlin复制代码// 看这里 |
女朋友:泛型还挺有意思。
我:上面提到的
协变
和逆变
。它们都是通过修改 University 类的泛型声明
实现的,所以它们统称为:声明处型变
,这是 Kotlin 才有的概念,Java 中没有。
5. 使用处型变(Use-site Variance)
女朋友:万一 University 是第三方提供的,我们无法修改,怎么办?能不能在不修改 University 类的前提下实现同样的目的?
我:可以,这就要用到
使用处型变
了。他们也分为:使用处协变
,使用处逆变
。
1 | kotlin复制代码open class Student() |
5-1 使用处协变
我:在泛型的
实参
前面增加一个out
关键字,代表我们只会从 University 往外取,不会往里放。这么做就实现了使用处协变
。
1 | kotlin复制代码// 看这里 |
女朋友:这也挺容易理解的。那使用处逆变呢?加个
in
?
5-2 使用处逆变
我:对。在泛型的
实参
前面增加一个in
关键字,代表我们只会从 University 往里放,不会往外取。这么做就实现了使用处逆变
。
1 | kotlin复制代码// 看这里 |
女朋友:思想是一样的。
女朋友:如果是从 University 招聘学生,就是往外取,这种情况下就是
协变
,可以用 University替代 University ,因为 女子大学
取出来的女学生,和普通大学
取出来的学生,都是学生。女朋友:如果是 University 要招生,就是往里放,这种情况下,就只能用 University
替代 University ,因为 普通大学
的招生范围更广,女子大学
能接收的学生,普通大学
也接收。我:你总结的真好。顺便提一句:Kotlin 的
使用处型变
,还有个名字叫:类型投影(Type Projections)
,这名字真烂。
以上代码的具体细节可以看我这个 GitHub Commit。
5-3 Kotlin 和 Java 对比
我:既然你 Kotlin 泛型理解起来毫无压力,那我再给你给加个餐,对比一下 Java 的
使用处型变
。女朋友:呃…… Java 是啥玩意?
我:没事,你就当看个乐呵。
使用处协变 | 使用处逆变 | |
---|---|---|
Kotlin | University |
University |
Java | University<? extends Student> | University<? super FemaleStudent> |
我:是不是简单明了?
女朋友:还是 Kotlin 的容易理解:
out
代表只能往外取(get),in
代表只能往里放(put)。我:没错。
女朋友:对比起来,Java 的表达方式真是无力吐槽。(-_-)
1 | java复制代码// Java 这辣鸡协变语法 |
以上代码的具体细节可以看我这个 GitHub Commit。
6. Kotlin 泛型实战
我:这里有一个 Kotlin 的 Demo,要不你来看看有哪些地方能用泛型优化的?
女朋友:过分了啊!你让我学 Kotlin 就算了,还想让我帮你写代码?
女朋友:你来写,我来看。
我:呃……听领导的。
6-1 泛型版本的 apply 函数
我:这是上一个章节里的代码,这个 apply 函数其实可以用泛型来简化,让所有的类都能使用。
1 | kotlin复制代码 |
我:使用泛型替代以后的 apply 函数就是这样:
1 | kotlin复制代码 |
女朋友:Kotlin 官方的 apply 函数也是这么实现的吗?
我:几乎一样,它只是多了个
contract
,你暂时还不懂。女朋友:呃……还有其他例子吗?
6-2 泛型版本的 HTML 构建器
我:在上一个章节里,我实现了一个简单的
类型安全的 HTML 构建器
,其中有不少重复的代码。女朋友:咱们可以利用泛型
消灭重复代码
,对吧?我:没错。
1 | kotlin复制代码class Body : BaseElement("body") { |
我:让我们用泛型来优化:
1 | kotlin复制代码 |
我:还有一个地方有重复代码:
1 | kotlin复制代码class HTML : BaseElement("html") { |
我:优化后:
1 | kotlin复制代码open class BaseElement(var name: String, var content: String = "") : Element { |
女朋友:嗯,顺眼了很多!
以上代码的具体细节可以看我这个 GitHub Commit。
7. 总结
- 受限于篇幅,Kotlin 泛型剩余知识点留到以后再讲,本文作为入门暂时够用了。泛型要讲透彻得写一本书,这不是本文的目的。
- 泛型的思想是一样的,理解了 Kotlin 型变,迁移到 Java 也是一样的。
- Kotlin 的泛型,由于借鉴了别的语言(C#),所以理解起来其实要比 Java 简单很多。
- 文章看完了,快去敲代码吧:github.com/chaxiu/Kotl…
- 找这个分支:
chapter_05_generics
8. 思考题:
Kotlin 的声明处型变
和使用处型变
它们分别有哪些优势和劣势?
都看到这了,给点个赞呗!
回目录–>【Kotlin Jetpack 实战】
本文转载自: 掘金