出炉:一份进阶自检表,和什么时候仍该写 Java
二十五章之前,我用十道题把一条河指给你看:「会用」和「说得清」之间的那条河。现在你应该已经在对岸了。这一章做三件事:把那十道题的答案系统地收口(每一道现在都该有一个「因为…所以…」)、交给你一份能贴进 PR 模板的审查清单、然后给一个不讨喜但诚实的答案 —— 有些时候,你确实还该写 Java。
回到那十道题
现在再看一遍。如果你能对每一道说出「因为…所以…」,这本书的任务就完成了:
| 题 | 一句话答案 | 底下的原理 |
|---|---|---|
| Q1 data class 的 equals | 只认主构造里的属性 | 它是编译期生成的代码(02) |
| Q2 var 属性不智能转换 | 编译器无法证明两次读取值相同 | 智能转换只对「能证明是死存储」的东西生效(04) |
| Q3 扩展函数不听多态 | 它是静态方法,按静态类型分派 | 接收者只是第一个参数(10) |
| Q4 companion 不是 static | 它是嵌套类的单例实例 | 同上:糖有价签(02) |
| Q5 value class 何时装箱 | 可空、泛型、集合、当 Any 用时 | 零开销的前提是「类型能被抹掉」(06) |
| Q6 List 能协变,MutableList 不能 | 只生产 → out;又读又写 → 不变 | 危险来自写入,不来自读取(05) |
| Q7 try/catch 抓不到 async | 异常存进 Deferred 等 await;同时沿 Job 树炸掉父作用域 | 异常走树,不走栈(17) |
| Q8 conflate vs collectLatest | 一个「不启动」,一个「启动了再杀掉」 | 背压来自 emit 是 suspend 的(18) |
| Q9 那个 5000 | 在等屏幕旋转(配置变更的重订阅宽限期) | 热流的启动策略(19) |
| Q10 runCatching 的危险 | 吞掉 CancellationException,协程取消不掉 | 取消就是靠这个异常传播的(15) |
注意这张表右边那一列 —— 十个问题,只有五六条原理。这就是「说得清」的本质:不是记住一百条规则,而是掌握少数几条能推导出规则的原理。
- 糖有价签。Kotlin 的每个便利特性都编译成了具体的东西,而那个东西决定了它的行为边界(02)。
- 让编译器替你干活。能在编译期发现的错误,就不要留到运行时 —— sealed 的穷尽检查、value class 的类型、context parameters 的能力约束,全是这一条(06、07、12)。
- 把状态空间压到最小。非法状态不该「被防御」,应该无法表示(07)。
- 协程的异常和取消走 Job 树,不走调用栈。你的 try/catch 直觉在这里失效(15、17)。
- 不可变让系统敢于缓存。Compose 跳过重组、StateFlow 去重、状态回放,全靠它(13、19)。
- 失败是值,不是控制流。从 sealed 到 Rich Errors,Kotlin 十年演进的主线之一(20、21)。
一份可以贴进 PR 模板的审查清单
这是本书最实用的产出。直接抄进你团队的 PR 模板或者 review checklist:
类型与建模
- 状态类是 sealed 吗?还是一堆 boolean + 可空字段?(07)
when里有else吗?如果主语是 sealed/enum,删掉它(07)- 签名里有相邻的同类型参数(两个 String、两个 Long)吗?上 value class(06)
- data class 的字段有
MutableList吗?(13) - 从 Java / JSON / Intent 来的值,类型钉死了吗?(03)
空安全
- 每个
!!旁边有解释「为什么不可能是 null」的注释吗?(03) - 每个
lateinit:会不会有东西把它抽走(视图销毁)?(03)
协程(重灾区)
runCatching/catch (e: Exception)包住 suspend 调用了吗?→ 正在吞取消信号(15、20)async在coroutineScope里吗?被await了吗?(17)- 有
GlobalScope吗?(15) - CPU 密集循环里有
ensureActive()吗?(15) withContext(Dispatchers.IO)包的是 Retrofit/Room 吗?→ 多余(16)- CPU 密集的活扔进 IO 池了吗?→ 该用 Default(16)
finally里的 suspend 清理有NonCancellable吗?(15)
Android / Flow
collectAsState()→ 换成collectAsStateWithLifecycle()(19)stateIn用的是WhileSubscribed(5000)吗?(19)_state.value =→ 换成_state.update { }(19)- UiState 里有事件性字段(toastMsg、navigateTo)吗?(19)
- 高频 Flow 有
conflate()吗?(18) - 自定义 View 的构造函数有
@JvmOverloads吗?(02)
什么时候仍该写 Java
说好的诚实答案。这四种情况,Java 依然是更好的选择:
| 场景 | 为什么 |
|---|---|
| 一个纯 Java 团队的存量模块,改动很少 | 混编的成本(工具链、心智、review)大于收益。「全面 Kotlin 化」不是目标,交付才是。让它安静地活着 |
| 要给 Java 使用者用的公开 API / SDK | 第 2、6 章讲过:默认参数无效、companion 难调、value class 的方法名被混淆、suspend 函数难用。你的 Kotlin 糖会变成使用者的苦。(也可以用 Kotlin 写但保持「脱糖后依然好看」—— 那需要功力) |
| 大量依赖 Java 注解处理器的老框架 | 某些老框架对 Kotlin 的 final 默认、null 检查、property 语义有敌意。为它们打补丁的时间,可能比重写还多 |
| 团队里没有一个人真的懂 Kotlin | 这是最诚实的一条。用一门没人懂的语言写出来的代码,比用一门大家都懂的语言写出来的笨代码更危险 —— 因为没人能 review 它,没人敢改它 |
还有一条元规则,比上面四条都重要:别把语言当宗教。本书花了二十五章讲 Kotlin 的优势,但它们全都是工程手段,不是信仰。能让你的 App 更稳、更快交付、更好维护的东西才是对的 —— 有时候那个东西是 sealed + Raise,有时候它就是一个朴素的 if (x == null) return。
继续深造:一条务实的路线
- 先把这本书的「动手锻打」做完。说真的 —— 读一百章不如把一个 UiState 重构成 sealed 并数出「32 → 4」这个数字。进阶不发生在阅读时。
- 读标准库的源码。从
Iterables.kt(集合操作怎么实现的)和Standard.kt(那五个作用域函数,加起来不到五十行)开始。你会发现你以为的「语言特性」大多只是普通函数 —— 这个认知比任何教程都解放人。 - 读 kotlinx.coroutines 的源码。从
Builders.common.kt(launch/async)到JobSupport.kt(那棵树)。硬,但值 —— 第 14—17 章的一切都在里面。 - 关注 KEEP 仓库(Kotlin Evolution and Enhancement Process)。语言的下一步在那里讨论,而且讨论过程本身就是最好的语言设计教材。
- 去上游看看。Kotlin 的很多好东西 —— sealed(和类型)、Optional、模式匹配、不可变、Either —— 都不是它发明的。想知道它们的原产地长什么样,本站有一本姊妹作 《上游》,它的第 18 章正好是从那边走回 Kotlin/Java 的落地清单,和这本书能接上。
最后一句
这本书从头到尾只在做一件事:把「熟练」换成「说得清」。
这两者的差距,在代码能跑的时候看不出来。它出现在你 review 别人代码时能不能一眼看出那个 runCatching 会吞掉取消信号;出现在新人问「为什么这里不能智能转换」时你是回答「Kotlin 就这样」还是「因为编译器无法证明」;出现在 AI 给你一段能跑但有隐患的代码时,你能不能看出来。
写代码的成本正在归零,判断的成本没有。—— 这就是你花时间读完这二十六章的全部理由。
出炉了。回到你的项目里去,找那个 isLoading + hasError 的 UiState。