前言
这是一篇文科生都能读泛型入门教程。(亲测,我女朋友都能看懂。)
本文以故事
的形式介绍 Kotlin 泛型及其不变性
,声明处型变
,使用处型变
,最后再搭配一个实战环节,将泛型应用到我们的 Demo 当中来。
注:墙裂建议!时间比较充裕的小伙伴在文末观看案例实战的视频讲解
前期准备
- 将 Android Studio 版本升级到最新
- 将我们的 Demo 工程 clone 到本地,用 Android Studio 打开: github.com/chaxiu/Kotl…
- 切换到分支:
chapter_05_generics
- 强烈建议各位小伙伴小伙伴跟着本文一起实战,实战才是本文的精髓
正文
1. 遥控器的故事:泛型
女朋友:好想要一个万能遥控器啊。
我:要不我教你用 Kotlin 的泛型实现一个吧!
女朋友:切,又想忽悠我学 Kotlin。[白眼]
我:真的很简单,保证你一看就会。
1-1 泛型类
我:这是一个万能遥控器,它带有一个泛型参数
// 类的泛型参数(形参)
// ↓
class Controller<T>() {
fun turnOn(obj: T){ ... }
fun turnOff(obj: T){ ... }
}
我:它用起来也简单,想控制什么,把对应的泛型传进去就行,就跟选模式一样:
// 电视机作为泛型实参
// ↓
val tvController: Controller<TV> = Controller<TV>()
val tv = TV()
// 控制电视机
tvController.turnOn(tv)
tvController.turnOff(tv)
// 电风扇作为泛型实参
// ↓
val fanController: Controller<Fan> = Controller<Fan>()
val fan = Fan()
// 控制电风扇
fanController.turnOn(fan)
fanController.turnOff(fan)
借助 Kotlin 的顶层函数,Controller 类甚至都可以省掉,直接用泛型函数:
1-2 泛型函数
// 函数的泛型参数
// ↓ ↓
fun <T> turnOn(obj: T){ ... }
fun <T> turnOff(obj: T){ ... }
泛型函数用起来也简单:
// 控制电视
val tv = TV()
turnOn<TV>(tv)
turnOff<TV>(tv)
// 控制风扇
val fan = Fan()
turnOn<Fan>(fan)
turnOff<Fan>(fan)
女朋友:我知道怎么用啦!是不是这样?
val boyFriend = BoyFriend()
turnOff<BoyFriend>(boyFriend)
我:……
2. 招聘的故事:泛型的不变性(Invariant)
女朋友:我想招几个大学生做兼职,你推荐几个大学吧。
我:好嘞,不过我要通过 Kotlin 泛型来给你推荐。
女朋友:呃……刚才你讲的泛型还挺简单,这次有什么新花样吗?
我:你看下去就知道了。
我:先来点准备工作:
// 学生
open class Student()
// 女学生
class FemaleStudent: Student()
// 大学
class University<T>(val name: String) {
// 往外取,代表招聘
fun get(): T { ... }
fun put(student: T){ ... }
}
我:你的招聘需求可以用这样的代码描述:
// 注意这里
// 女朋友需要一个大学(变量声明) ↓
lateinit var university: University<Student>
// 注意这里
// 我随便推荐一个大学 ↓
university = University<Student>("某大学")
val student: Student = university.get()// 招聘
女朋友:原来 Kotlin 也没那么难……
女朋友:能赋值一个"女子大学"吗?
我:不行,会报错。
// 注意这里
// ↓
lateinit var university: University<Student>
// 这是报错的原因
// ↓
university = University<FemaleStudent>("女子大学")
val student: Student = university.get()
// 编译器报错!!
/*
Type mismatch.
Required: University<Student>
Found: University<FemaleStudent>
*/
女朋友:什么鬼。。。
我:虽然 Student 和 FemaleStudent 之间是父子关系,但是 University<Student> 和 University<FemaleStudent> 之间没有任何关系。这叫泛型的不变性。
女朋友:这不合理!女子大学招聘出来的学生,难道就不是学生?
我:招聘当然符合逻辑,但别忘了 University 还有一个 put 方法。
我:你怎么防止别人把一个男学生放到女子大学里去?
我:让我们看看如果可以将“女子大学”当作“普通大学”用,会出现什么问题:
// 声明的类型是:普通大学,然而,实际类型是:女子大学。
// ↓ ↓
var university: University<Student> = University<FemaleStudent>("女子大学")
val maleStudent: Student = Student()
// 男学生被放进女子大学!不合理。
university.put(maleStudent)
女朋友:明白了,原来这就是泛型不变性的原因,确实能避免不少麻烦。
// 默认情况下,编译器只允许这么做
// 声明的泛型参数与实际的要一致
↓ ↓
var normalUniversity: University<Student> = University<Student>
↓ ↓
var wUniversity: University<FemaleStudent> = University<FemaleStudent>
3. 搞定招聘:泛型的协变(Covariant)
女朋友:如果我把 University 类里面的 put 方法删掉,是不是就可以用“女子大学”赋值了?这样就不用担心
把男学生放到女子大学
的问题了。我:这还不够,还需要加一个关键字
out
告诉编译器:我们只会从 University 类往外取,不会往里面放。这时候,University<FemaleStudent> 就可以当作 University<Student> 的子类。我:这叫做泛型的
协变
。
open class Student()
class FemaleStudent: Student()
// 看这里
// ↓
class University<out T>(val name: String) {
fun get(): T { ... }
}
女朋友:我试试,果然好了!
// 不再报错
var university: University<Student> = University<FemaleStudent>("女子大学")
val student: Student = university.get()
我:你不来写代码真浪费了。
4. 填志愿的故事:泛型的逆变(Contravariant)
女朋友:我妹妹刚高考完,马上要填志愿了,你给推荐个大学吧。
我:咱刚看过泛型协变,要不你试试自己解决这个填志愿的问题?正好 University 里有个 put 方法,你就把 put 当作填志愿就行了。
女朋友:那我依葫芦画瓢试试…… 给我妹妹报一个女子大学。
open class Student()
class FemaleStudent: Student()
class University<T>(val name: String) {
fun get(): T { ... }
// 往里放,代表填志愿
fun put(student: T){ ... }
}
val sister: FemaleStudent = FemaleStudent()
val university: University<FemaleStudent> = University<FemaleStudent>("女子大学")
university.put(sister)//填报女子大学
女朋友:完美!
我:厉害。
女朋友:能不能再报一个普通综合大学?
我:不行,你忘记泛型不变性了吗?
val sister: FemaleStudent = FemaleStudent()
// 报错原因:声明类型是:女子大学 赋值的类型是:普通大学
// ↓ ↓
val university: University<FemaleStudent> = University<Student>("普通大学")
university.put(sister)
// 报错
/*
Type mismatch.
Required: University<FemaleStudent>
Found: University<Student>
*/
女朋友:我妹能报女子大学,居然不能报普通的综合大学?这不合理吧!
我:你别忘了 University 还有一个 get 方法吗?普通综合大学 get 出来的可不一定是女学生。
女朋友:哦。那我把 get 方法删了,再加个关键字?
我:对。删掉 get 方法,再加一个关键字:
in
就行了。它的作用是告诉编译器:我们只会往 University 类里放,不会往外取。这时候,University<Student> 就可以当作 University<FemaleStudent> 的子类。我:这其实就叫做泛型的
逆变
,它们的继承关系反过来了。
// 看这里
// ↓
class University<in T>(val name: String) {
fun put(student: T){ ... }
}
val sister: FemaleStudent = FemaleStudent()
// 编译通过
val university: University<FemaleStudent> = University<Student>("普通大学")
university.put(sister)
女朋友:泛型还挺有意思。
我:上面提到的
协变
和逆变
。它们都是通过修改 University 类的泛型声明
实现的,所以它们统称为:声明处型变
,这是 Kotlin 才有的概念,Java 中没有。
5. 使用处型变(Use-site Variance)
女朋友:万一 University 是第三方提供的,我们无法修改,怎么办?能不能在不修改 University 类的前提下实现同样的目的?
我:可以,这就要用到
使用处型变
了。他们也分为:使用处协变
,使用处逆变
。
open class Student()
class FemaleStudent: Student()
// 假设 University 无法修改
class University<T>(val name: String) {
fun get(): T { ... }
fun put(student: T){ ... }
}
5-1 使用处协变
我:在泛型的
实参
前面增加一个out
关键字,代表我们只会从 University 往外取,不会往里放。这么做就实现了使用处协变
。
// 看这里
// ↓
fun useSiteCovariant(university: University<out Student>) {
val femaleStudent: Student? = university.get()
// 报错: Require Nothing? found Student?
// university.put(femaleStudent)
}
女朋友:这也挺容易理解的。那使用处逆变呢?加个
in
?
5-2 使用处逆变
我:对。在泛型的
实参
前面增加一个in
关键字,代表我们只会从 University 往里放,不会往外取。这么做就实现了使用处逆变
。
// 看这里
// ↓
fun useSiteContravariant(universityIn: University<in FemaleStudent>) {
universityIn.put(FemaleStudent())
// 报错: Require FemaleStudent? found Any?
// val femaleStudent: FemaleStudent? = universityIn.get()
}
女朋友:思想是一样的。
女朋友:如果是从 University 招聘学生,就是往外取,这种情况下就是
协变
,可以用 University<FemaleStudent> 替代 University<Student>,因为女子大学
取出来的女学生,和普通大学
取出来的学生,都是学生。女朋友:如果是 University 要招生,就是往里放,这种情况下,就只能用 University<Student> 替代 University<FemaleStudent>,因为
普通大学
的招生范围更广,女子大学
能接收的学生,普通大学
也接收。我:你总结的真好。顺便提一句:Kotlin 的
使用处型变
,还有个名字叫:类型投影(Type Projections)
,这名字真烂。
以上代码的具体细节可以看我这个 GitHub Commit。
5-3 Kotlin 和 Java 对比
我:既然你 Kotlin 泛型理解起来毫无压力,那我再给你给加个餐,对比一下 Java 的
使用处型变
。女朋友:呃…… Java 是啥玩意?
我:没事,你就当看个乐呵。
使用处协变 | 使用处逆变 | |
---|---|---|
Kotlin | University<out Student> | University<in FemaleStudent> |
Java | University<? extends Student> | University<? super FemaleStudent> |
我:是不是简单明了?
女朋友:还是 Kotlin 的容易理解:
out
代表只能往外取(get),in
代表只能往里放(put)。我:没错。
女朋友:对比起来,Java 的表达方式真是无力吐槽。(-_-)
// Java 这辣鸡协变语法
// ↓
University<? extends Student> covariant = new University<FemaleStudent>("女子大学");
Student student = covariant.get();
// 报错
covariant.put(student);
// Java 这辣鸡逆变语法
// ↓
University<? super FemaleStudent> contravariant = new University<Student>("普通大学");
contravariant.put(new FemaleStudent())
// 报错
Student s = contravariant.get();
以上代码的具体细节可以看我这个 GitHub Commit。
6. Kotlin 泛型实战
我:这里有一个 Kotlin 的 Demo,要不你来看看有哪些地方能用泛型优化的?
女朋友:过分了啊!你让我学 Kotlin 就算了,还想让我帮你写代码?
女朋友:你来写,我来看。
我:呃……听领导的。
6-1 泛型版本的 apply 函数
我:这是上一个章节里的代码,这个 apply 函数其实可以用泛型来简化,让所有的类都能使用。
// 替代 替代 替代
// ↓ ↓ ↓
fun User.apply(block: User.() -> Unit): User{
block()
return this
}
user?.apply { this: User ->
...
username.text = this.name
website.text = this.blog
image.setOnClickListener { gotoImagePreviewActivity(this) }
}
我:使用泛型替代以后的 apply 函数就是这样:
// 泛型 泛型 泛型
// ↓ ↓ ↓ ↓
fun <T> T.apply(block: T.() -> Unit): T{
block()
return this
}
女朋友:Kotlin 官方的 apply 函数也是这么实现的吗?
我:几乎一样,它只是多了个
contract
,你暂时还不懂。女朋友:呃……还有其他例子吗?
6-2 泛型版本的 HTML 构建器
我:在上一个章节里,我实现了一个简单的
类型安全的 HTML 构建器
,其中有不少重复的代码。女朋友:咱们可以利用泛型
消灭重复代码
,对吧?我:没错。
class Body : BaseElement("body") {
fun h1(block: () -> String): H1 {
val content = block()
val h1 = H1(content)
this.children += h1
return h1
}
// ↑
// 看看这重复的模板代码
// ↓
fun p(block: () -> String): P {
val content = block()
val p = P(content)
this.children += p
return p
}
}
// ↑
// 看看这重复的模板代码
// ↓
class Head : BaseElement("head") {
fun title(block: () -> String): Title {
val content = block()
val title = Title(content)
this.children += title
return title
}
}
我:让我们用泛型来优化:
open class BaseElement(var name: String, var content: String = "") : Element {
// 在父类增加一个共有的泛型方法
protected fun <T : BaseElement> initString(element: T, init: T.() -> String): T {
val content = element.init()
element.content = content
children.add(element)
return element
}
}
class Body : BaseElement("body") {
fun h1(block: H1.() -> String) = initString(H1("h1"), block)
fun p(block: P.() -> String) = initString(P("p"), block)
}
class Head : BaseElement("head") {
fun title(block: Title.() -> String) = initString(Title(), block)
}
我:还有一个地方有重复代码:
class HTML : BaseElement("html") {
fun head(block: Head.() -> Unit): Head {
val head = Head()
head.block()
this.children += head
return head
}
// ↑
// 看看这重复的模板代码
// ↓
fun body(block: Body.() -> Unit): Body {
val body = Body()
body.block()
this.children += body
return body
}
}
我:优化后:
open class BaseElement(var name: String, var content: String = "") : Element {
// 在父类增加一个共有的泛型方法
protected fun <T : Element> init(element: T, init: T.() -> Unit): T {
element.init()
children.add(element)
return element
}
}
class HTML : BaseElement("html") {
fun head(block: Head.() -> Unit) = init(Head(), block)
fun body(block: Body.() -> Unit) = init(Body(), block)
}
女朋友:嗯,顺眼了很多!
以上代码的具体细节可以看我这个 GitHub Commit。
文末
都看到这了,点个赞呗!
作者:朱涛的自习室
链接:https://juejin.im/post/6854573222457769991
B站视频解析:一节课搞定!Kotlin核心技术(LamBda、高级函数、泛型应用)
感谢大家关注我,分享Android干货,交流Android技术。
对文章有何见解,或者有何技术问题,都可以在评论区一起留言讨论,我会虔诚为你解答。
也欢迎大家来我的B站找我玩,有各类Android架构师进阶技术难点的视频讲解,希望助你早日升职加薪。
B站直通车:https://space.bilibili.com/544650554
网友评论