高阶特性入门
# 第 3 章 Kotlin 高阶特性入门
# 目录介绍
- 3.1 扩展函数与属性
- 3.2 Lambda与函数式
- 3.3 作用域函数深潜
- 3.4 空安全实战技巧
- 3.5 DSL构建能力
- 3.6 协程初识
- 3.7 与Java互操作
- 3.8 综合案例练习
- 3.9 训练题三道
- 3.10 新手陷阱Top5
# 3.1 扩展函数与属性
# 3.1.1 扩展函数语法
Kotlin 允许你 不修改类本身,就给一个已存在的类型「加上一个方法」——这类方法叫 扩展函数:
fun String.lastChar(): Char = this[length - 1]
fun main() {
println("hello".lastChar()) // 'o'
}
语法要点:
fun 接收者类型.函数名(...):接收者写在函数名前,用.分隔- 函数体内
this指向接收者实例(可省略) - 调用方看起来跟成员方法 完全一样
看起来像给 String 加了个方法,实际上是 静态函数 + this 参数 的语法糖。
典型使用场景:
// 场景 1:给标准库补充缺失的工具方法
fun <T> List<T>.second(): T = this[1]
// 场景 2:给第三方库/系统 SDK 加语法糖(例如 Android View)
fun View.dp(value: Int) = (value * resources.displayMetrics.density).toInt()
// 场景 3:让接收者对象成为「主语」,读起来更接近自然语言
fun String.isEmail() = matches(Regex("""[\w.]+@[\w.]+"""))
# 3.1.2 静态派发本质
关键区分:扩展函数是 静态派发(按变量声明类型),不是虚方法(按运行时类型):
open class Animal
class Dog : Animal()
fun Animal.name() = "Animal"
fun Dog.name() = "Dog"
fun main() {
val a: Animal = Dog()
println(a.name()) // 输出 "Animal"——按声明类型分发!
val d: Dog = Dog()
println(d.name()) // 输出 "Dog"
}
关键结论:扩展函数 不参与多态——它由调用点变量的静态类型决定调哪一个。
反编译看真相——fun String.lastChar() 编译后的 Java 等价代码:
public final class StringExtKt {
public static final char lastChar(String $this) { // 静态函数
return $this.charAt($this.length() - 1);
}
}
// 调用点:
StringExtKt.lastChar("hello");
从字节码看,扩展函数就是一个「把接收者当第一个静态参数」的普通静态函数。这也解释了为什么它不能被 override——静态函数没有虚表。
同名冲突规则:如果扩展函数和成员函数签名相同,成员函数优先:
class Foo {
fun bar() = "member"
}
fun Foo.bar() = "ext" // 永远不会被调用
fun main() {
println(Foo().bar()) // 输出: member
}
编译器会给出警告 "extension is shadowed by a member"。
# 3.1.3 扩展属性用法
扩展属性长这样:
val String.lastChar: Char
get() = this[length - 1]
fun main() {
println("hello".lastChar) // 'o'
}
限制:扩展属性不能有 backing field,只能有 getter/setter。原因显而易见——你不能在一个已经编译好的类里塞一个字段进去。
只读扩展属性用 val + get(),读写扩展属性用 var + get()/set():
// 读写扩展属性——把 View 的 padding 简化成 View.paddingHor
var View.paddingHor: Int
get() = paddingLeft
set(value) { setPadding(value, paddingTop, value, paddingBottom) }
# 3.1.4 综合案例与思考
Android View 常用扩展——工程实战中每一个 Kotlin Android 项目都会写的一套:
import android.view.View
import android.content.Context
// 尺寸
val Int.dp: Int get() = (this * resources.displayMetrics.density).toInt()
val Int.px: Float get() = this.toFloat() / resources.displayMetrics.density
// 可见性
fun View.visible() { visibility = View.VISIBLE }
fun View.gone() { visibility = View.GONE }
fun View.invisible() { visibility = View.INVISIBLE }
// 条件可见
fun View.showIf(cond: Boolean) { visibility = if (cond) View.VISIBLE else View.GONE }
// 防重复点击
fun View.setSafeClickListener(intervalMs: Long = 500, block: (View) -> Unit) {
var lastClick = 0L
setOnClickListener {
val now = System.currentTimeMillis()
if (now - lastClick >= intervalMs) {
lastClick = now
block(it)
}
}
}
调用侧代码从「一大堆样板」变成「一行链式」:
// 之前
button.setOnClickListener { doSomething() }
button.visibility = View.GONE
// 之后
button.setSafeClickListener { doSomething() }
button.gone()
思考:扩展函数能「覆盖」成员函数吗?如果同名冲突会怎样?
答案:不能覆盖。同名冲突时成员函数永远赢,编译器会警告 "extension is shadowed by a member"。扩展函数本质是静态函数,不参与 Kotlin 的虚方法分发机制。
# 3.2 Lambda与函数式
# 3.2.1 函数类型语法
Kotlin 把「函数」视为一等公民,可以被赋值、传参、作为返回值:
val add: (Int, Int) -> Int = { a, b -> a + b }
val greet: () -> Unit = { println("Hi") }
val withReceiver: String.() -> Int = { length } // 带接收者的 Lambda
fun main() {
println(add(3, 5)) // 8
greet() // Hi
println("hello".withReceiver()) // 5,接收者是 "hello"
}
函数类型语法总览:
| 语法 | 含义 |
|---|---|
() -> Unit | 无参、无返回 |
(Int) -> Int | 一个 Int 参数,返回 Int |
(Int, String) -> Boolean | 两参多返 |
String.() -> Int | 带接收者的 Lambda,内部 this 是 String |
suspend () -> Unit | 挂起函数类型 |
(Int) -> Unit? | 返回可空的 Unit |
核心:(A, B) -> R 是一等类型,本质上是接口 Function2<A, B, R>——Kotlin 标准库定义了 Function0 ~ Function22,最多支持 22 个参数的 Lambda。
# 3.2.2 高阶函数概念
高阶函数 = 接受或返回函数的函数。
// 接受函数作为参数
fun <T, R> Iterable<T>.myMap(transform: (T) -> R): List<R> {
val result = mutableListOf<R>()
for (item in this) result.add(transform(item))
return result
}
// 返回函数
fun multiplier(factor: Int): (Int) -> Int = { x -> x * factor }
fun main() {
val doubled = listOf(1, 2, 3).myMap { it * 2 }
println(doubled) // [2, 4, 6]
val timesTen = multiplier(10)
println(timesTen(7)) // 70
}
标准库大量使用高阶函数:map / filter / fold / reduce / groupBy / sortedBy ... 这套 API 让集合处理彻底摆脱 for 循环。
# 3.2.3 inline函数意义
高阶函数每次调用都会 创建一个 Lambda 对象——如果在热点循环里,可能带来大量分配:
fun <T> List<T>.myFilter(p: (T) -> Boolean): List<T> {
// 每次调用 myFilter 都会 new 一个 Function1 实例传给 p
...
}
inline 让高阶函数在 调用点代码展开,Lambda 不再是对象分配:
inline fun <T> Iterable<T>.myFilter(predicate: (T) -> Boolean): List<T> {
val result = mutableListOf<T>()
for (item in this) if (predicate(item)) result.add(item)
return result
}
用户代码:
val evens = listOf(1, 2, 3, 4).myFilter { it % 2 == 0 }
编译后(大致):
// 相当于把 myFilter 的方法体和 Lambda 都展开到调用点
val result = mutableListOf<Int>()
for (item in listOf(1, 2, 3, 4)) if (item % 2 == 0) result.add(item)
val evens = result
没有 Lambda 对象分配、没有虚方法调用——性能等同于手写循环。
字节码原理见 原理篇 4。
用法准则:
- ✅ 接受 Lambda 参数的高阶函数——建议 inline(性能收益大)
- ❌ 不接受 Lambda 的函数——不要 inline(膨胀无收益)
- ❌ 大函数体的 inline——慎重(大量调用点会导致字节码急剧膨胀)
- ✅ 需要
reified泛型的函数——必须 inline
# 3.2.4 综合案例与思考
通用 retry 函数——inline + Lambda 组合:
inline fun <T> retry(
times: Int = 3,
delayMs: Long = 500,
block: (Int) -> T
): T {
var last: Throwable? = null
repeat(times) { attempt ->
try {
return block(attempt)
} catch (e: Throwable) {
last = e
if (attempt < times - 1) Thread.sleep(delayMs)
}
}
throw last!!
}
fun main() {
val result = retry(times = 3) { attempt ->
println("尝试 #$attempt")
if (attempt < 2) throw RuntimeException("失败")
"成功"
}
println(result)
}
输出:
尝试 #0
尝试 #1
尝试 #2
成功
思考:Kotlin 的 Lambda 与 Java 的 Function<T, R>(java.util.function)如何互相转换?
答案:
- Java → Kotlin:Java 的
Function<T, R>可以直接当 Kotlin 的(T) -> R用(Kotlin 有 SAM 转换支持)- Kotlin → Java:Kotlin Lambda 编译后是
Function1类型,Java 侧调用需要.apply()方法(不是 JavaFunction.apply,而是Function1.invoke)——建议给 Kotlin 函数加@JvmName或改用fun interface
# 3.3 作用域函数深潜
let / run / with / apply / also 是 Kotlin 特有的「作用域函数」,都是把一个对象包在 Lambda 作用域里访问。他们的差别在两个维度:接收者引用方式 和 返回值。
# 3.3.1 五兄弟对比
| 函数 | 接收者引用 | 返回值 | 典型场景 |
|---|---|---|---|
let | it | Lambda 结果 | 空安全链、局部改名 |
run | this | Lambda 结果 | 对象配置 + 计算 |
with | this | Lambda 结果 | 非扩展写法的 run |
apply | this | 接收者本身 | 对象初始化建造 |
also | it | 接收者本身 | 副作用(打印、日志) |
一张图看清区别:
引用方式
this it
┌──────┬──────┐
返回 Lambda │ run │ let │
返回接收者 │apply │ also │
└──────┴──────┘
with = run 的非扩展版本
# 3.3.2 接收者与参数
核心区分:
this接收者:Lambda 内可以直接调用成员(this.可省略),像在类内部写代码it参数:Lambda 内像访问普通变量(可改名为其他),显式
data class Config(var debug: Boolean = false, var timeout: Int = 0)
// this 风格(apply)
val ok = Config().apply {
debug = true // 直接访问成员,等价于 this.debug
timeout = 30
}
// it 风格(let)
val length = "hello".let {
println(it) // 通过 it 访问
it.length
}
// 也可以给 it 改名
val length2 = "hello".let { str ->
println(str)
str.length
}
选择 this 风格的时机:调用大量成员方法/属性时——省略 this. 让代码更简洁。
选择 it 风格的时机:Lambda 内需要多次引用外层作用域,避免 this 混淆;或需要给参数起个业务名。
# 3.3.3 场景决策表
需要返回接收者本身吗?
├─ 是:需要用 this 简洁访问成员?
│ ├─ 是 → apply // 对象建造:Config().apply { debug = true }
│ └─ 否 → also // 副作用:response.also { log(it) }
└─ 否:需要用 this 简洁访问成员?
├─ 是 → run(或 with) // 计算:config.run { timeout * 1000 }
└─ 否 → let // 空安全链:user?.let { fetch(it.id) }
一个真实业务场景多种写法对比:
data class User(var name: String, var age: Int)
// 场景:创建 User,同时打日志,返回 User
// 方案 A:apply(返回接收者,this 引用)
val u1 = User("K", 20).apply {
println("created: $name") // 直接访问 name
}
// 方案 B:also(返回接收者,it 引用)
val u2 = User("K", 20).also {
println("created: ${it.name}") // 通过 it
}
// 方案 C:let(返回 Lambda 结果,it 引用)——不合适!返回值成了 Unit
val u3 = User("K", 20).let {
println("created: ${it.name}") // Unit
}
规则:想「返回对象本身 + 顺便干点事」——apply / also;想「用对象计算个新东西」——let / run。
# 3.3.4 综合案例与思考
五种作用域函数的等价改写练习:
data class HttpConfig(var url: String = "", var timeout: Int = 0)
// 需求:创建一个 HttpConfig,url 设为 "https://x.com",timeout 设 30,然后打印它,最后返回它
// 1. apply(推荐用法)
val a = HttpConfig().apply {
url = "https://x.com"
timeout = 30
}.also { println(it) }
// 2. also 单独用
val b = HttpConfig().also {
it.url = "https://x.com"
it.timeout = 30
println(it)
}
// 3. run(返回值我们不需要,等同于 apply)
val c = HttpConfig().run {
url = "https://x.com"
timeout = 30
this
}.also { println(it) }
// 4. with(非扩展写法)
val d = HttpConfig()
with(d) {
url = "https://x.com"
timeout = 30
println(this)
}
// 5. let(不推荐——需要返回自己)
val e = HttpConfig().let {
it.url = "https://x.com"
it.timeout = 30
println(it)
it
}
思考:apply 与 Java 的 Builder 模式有何异同?
对比:
维度 Java Builder Kotlin apply需要额外定义 Builder 类 是 否 支持链式调用 是 是(返回 this) 编译产物代码量 每个 Builder ~30 行样板 0 编译期字段安全检查 Builder 属性可能忘赋值 若字段有默认值可直接跳过;若无默认值必须构造时给 复用现有类的 setter 需要一起定义 直接复用
# 3.4 空安全实战技巧
# 3.4.1 let链式空安全
user?.address?.city?.let { println("city=$it") }
只有 user、address、city 都非 null 时才执行 Lambda。这是 Kotlin 空安全的经典模式——把「非空后处理」和「访问路径」在同一行表达。
对比 Java 的等价代码:
if (user != null) {
Address addr = user.getAddress();
if (addr != null) {
String city = addr.getCity();
if (city != null) {
System.out.println("city=" + city);
}
}
}
更进阶的用法——链式转换:
val cityUpper: String? = user
?.address // 可空
?.city // 可空
?.let { it.uppercase() } // 只在非空时转大写
?.let { "[$it]" } // 加括号
# 3.4.2 takeIf与takeUnless
takeIf { } 让接收者对象「按条件通过」——满足条件返回自己,否则返回 null:
val positive = x.takeIf { it > 0 } // 满足则返回自身,否则 null
val nonBlank = str.takeUnless { it.isBlank() }
典型模式——「验证 + 使用」一体化:
fun findUser(idInput: String): User? = idInput
.takeIf { it.matches(Regex("\\d+")) } // 必须全数字
?.toLong()
?.let { repo.find(it) }
takeIf vs filter:
takeIf作用于 单个对象——通过返回自己,不通过返回 nullfilter作用于 集合——过滤出满足条件的元素
# 3.4.3 契约函数requireNotNull
fun send(msg: String?) {
val notNull = requireNotNull(msg) { "msg 不能为 null" }
// 从这里开始 notNull 已经被编译器智能转换为 String(非空)
println(notNull.length)
}
关键:requireNotNull / checkNotNull 用 Kotlin contract 机制 让编译器识别为空断言。
三兄弟对比:
| 函数 | 断言失败抛出 | 语义 |
|---|---|---|
requireNotNull(x) | IllegalArgumentException | 校验函数参数 |
checkNotNull(x) | IllegalStateException | 校验对象内部状态 |
x!! | NullPointerException | 无消息,仅用于表达式内 |
推荐姿势:函数入口校验用 requireNotNull(有清晰错误提示),中途表达式用 ?.let 或安全转换,只在「绝无可能为空」时才用 !!。
# 3.4.4 综合案例与思考
处理 JSON 深层嵌套字段——?.let 链 vs 序列化库对比:
// 假设从 JSON 反序列化得到:
// { "user": { "profile": { "avatar": { "url": "..." } } } }
data class Response(val user: User?)
data class User(val profile: Profile?)
data class Profile(val avatar: Avatar?)
data class Avatar(val url: String?)
// ============ 方案 A:手动 ?. 链 ============
fun getAvatar(r: Response): String {
return r.user?.profile?.avatar?.url ?: ""
}
// ============ 方案 B:?. + let 链,加处理 ============
fun getAvatarBig(r: Response): String? =
r.user
?.profile
?.avatar
?.url
?.let { "$it?size=big" } // 拼接尺寸参数
// ============ 方案 C:非空则日志 + 使用 ============
fun logAndLoadAvatar(r: Response) {
r.user?.profile?.avatar?.url
?.also { println("[LOG] load $it") }
?.let { load(it) }
?: println("[LOG] no avatar")
}
思考:?.let 与 if (x != null) 何时选哪个?
答案:
if (x != null)更适合:修改本地变量、大段代码块、x是本地 val(不涉及并发 smart cast 失效)?.let更适合:x是可空 var 或属性(if无法 smart cast)、需要链式表达、想给x起个不同的名字典型陷阱:
var x或类属性做if (x != null) x.length——如果多线程可能失效,编译器要求写x?.length或先val local = x。
# 3.5 DSL构建能力
DSL(Domain Specific Language,领域特定语言)——用 Kotlin 语法描述特定领域的问题,读起来接近自然语言。Compose、Gradle KTS、Ktor、Anko 都是典型 Kotlin DSL。
# 3.5.1 DSL三大支柱
Kotlin DSL 靠 3 个语法支柱:
- 尾随 Lambda:函数最后一个参数是 Lambda 时,可以写在圆括号外,甚至直接省略括号
- 带接收者的 Lambda:
T.() -> Unit类型的 Lambda 内this指向T实例 - 中缀函数:
infix fun a.foo(b: B)→ 调用写成a foo b(省略点和括号)
三支柱结合的例子:
// 支柱 1:尾随 Lambda
fun repeat(n: Int, block: () -> Unit) { for (i in 0 until n) block() }
repeat(3) { println("hi") } // 括号外的 Lambda
// 支柱 2:带接收者的 Lambda
class Builder { var name = "" }
fun build(block: Builder.() -> Unit) = Builder().apply(block)
build { name = "K" } // Lambda 内 this = Builder
// 支柱 3:中缀函数
infix fun Int.pow(n: Int) = Math.pow(this.toDouble(), n.toDouble()).toInt()
println(2 pow 8) // 256
# 3.5.2 mini_html_DSL
一个完整可运行的 mini-HTML 生成器:
class Element(val name: String) {
private val children = mutableListOf<Element>()
private val attrs = mutableMapOf<String, String>()
var text: String? = null
fun attr(key: String, value: String) { attrs[key] = value }
fun child(name: String, block: Element.() -> Unit) {
children += Element(name).apply(block)
}
override fun toString(): String {
val attrStr = attrs.entries.joinToString(" ") { """${it.key}="${it.value}"""" }
val head = if (attrStr.isEmpty()) name else "$name $attrStr"
val body = if (text != null) text else children.joinToString("")
return "<$head>$body</$name>"
}
}
fun html(block: Element.() -> Unit) = Element("html").apply(block)
fun Element.body(block: Element.() -> Unit) { child("body", block) }
fun Element.h1(block: Element.() -> Unit) { child("h1", block) }
fun Element.p(block: Element.() -> Unit) { child("p", block) }
fun main() {
val page = html {
body {
h1 { text = "Kotlin DSL" }
p {
attr("class", "note")
text = "Hello from DSL"
}
}
}
println(page)
}
输出:
<html><body><h1>Kotlin DSL</h1><p class="note">Hello from DSL</p></body></html>
这就是 DSL 的威力——不需要 XML 解析器、不需要模板引擎,用 纯 Kotlin 语法 描述层级化结构。Compose UI、Gradle KTS 都是这个套路。
# 3.5.3 DslMarker防作用域穿透
嵌套 DSL 的作用域穿透问题:
html {
body {
h1 {
text = "标题"
body { } // ❌ 危险:语义是「在 h1 里加个 body」——但 body 是 html 的方法!
}
}
}
Kotlin 允许 Lambda 内部访问外层作用域的成员——正常场景很方便,但 DSL 里这会导致「作用域穿透」的错误。解决方案:@DslMarker 注解。
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class Element(...) // 所有 Element 类的实例受同一个 DslMarker 保护
标注后,内层 Lambda 只能访问最近的接收者——尝试穿透到外层会 编译报错:
h1 {
body { } // ❌ 编译错:'body' 属于外层 receiver,被 @DslMarker 阻断
}
# 3.5.4 综合案例与思考
Retrofit 风格的接口 DSL:
class Api {
private val routes = mutableListOf<Route>()
fun get(path: String, block: Route.() -> Unit) {
routes += Route("GET", path).apply(block)
}
fun post(path: String, block: Route.() -> Unit) {
routes += Route("POST", path).apply(block)
}
class Route(val method: String, val path: String) {
var handler: ((Map<String, String>) -> String)? = null
fun handle(h: (Map<String, String>) -> String) { handler = h }
}
override fun toString() = routes.joinToString("\n") { "${it.method} ${it.path}" }
}
fun api(block: Api.() -> Unit) = Api().apply(block)
fun main() {
val a = api {
get("/users") {
handle { params -> "list all users" }
}
post("/users") {
handle { params -> "create user: ${params["name"]}" }
}
}
println(a)
}
思考:Gradle Kotlin DSL、Compose UI 都是怎么用这套机制的?
答案:
- Gradle KTS:
plugins { ... }/android { ... }/dependencies { ... }全部是「带接收者的 Lambda + 尾随 Lambda」- Compose UI:
@Composable函数就是「带Composer隐式接收者的函数」,Column { ... }里可以调Text(...)因为Text是 Composable 顶级函数
# 3.6 协程初识
协程是 Kotlin 官方推荐的并发模型——在语法上像同步代码,运行时却是异步的。本节只做入门,深入原理见 原理篇 3。
# 3.6.1 suspend关键字
suspend fun fetchUser(id: Long): User {
delay(100) // 挂起 100ms 但不阻塞线程
return User(id, "K")
}
suspend 表示「可挂起」——不占用线程即可等待。挂起点由 delay / withContext / await 等触发。
规则:suspend fun 只能在另一个 suspend fun 或 协程构建器(launch / async / runBlocking)里调用。
为什么需要 suspend——传统回调地狱 vs 协程对比:
// 传统回调
fetchUser(1L) { user ->
fetchOrders(user.id) { orders ->
fetchDetail(orders[0].id) { detail ->
println(detail)
}
}
}
// 协程
suspend fun run() {
val user = fetchUser(1L)
val orders = fetchOrders(user.id)
val detail = fetchDetail(orders[0].id)
println(detail)
}
字节码原理见 原理篇 3。
# 3.6.2 launch与async
| 构建器 | 返回 | 用途 |
|---|---|---|
launch | Job | 启动一个「发射后不管」的协程 |
async | Deferred<T> | 启动一个「要拿返回值」的协程,用 .await() 获取 |
import kotlinx.coroutines.*
fun main() = runBlocking {
// launch:发射后不管,可用 job.join() 等待
val job = launch {
delay(100)
println("launch done")
}
// async:要返回值
val deferred = async {
delay(200)
"async result"
}
job.join()
println(deferred.await())
}
输出:
launch done
async result
# 3.6.3 Dispatchers调度器
| 调度器 | 场景 |
|---|---|
Dispatchers.Main | UI 线程(Android) |
Dispatchers.IO | 网络/文件 IO 密集 |
Dispatchers.Default | CPU 密集计算 |
Dispatchers.Unconfined | 不切线程(很少用) |
suspend fun loadData() {
val data = withContext(Dispatchers.IO) {
httpFetch() // 在 IO 线程池执行
}
// 回到调用者所在的 Dispatcher(比如 Main)
updateUI(data)
}
withContext vs async:withContext 用于 顺序等待,async 用于 并发。想并发拉两个接口:
suspend fun loadAll() = coroutineScope {
val a = async(Dispatchers.IO) { fetchA() }
val b = async(Dispatchers.IO) { fetchB() }
a.await() + b.await() // 两者并发
}
# 3.6.4 结构化并发第一课
suspend fun loadCombined() = coroutineScope {
val a = async { fetchA() }
val b = async { fetchB() }
a.await() + b.await() // 出块时保证所有子协程完成或取消
}
核心原则:每个协程都必须有一个明确的 scope,避免 GlobalScope 泄漏。
coroutineScope { ... }:任一失败 → 全部取消,抛异常给外层supervisorScope { ... }:子失败不影响其他子,只抛给自己
Android 里的典型 Scope:
class MyViewModel : ViewModel() {
fun load() {
viewModelScope.launch { // ViewModel 销毁时自动 cancel
val data = withContext(Dispatchers.IO) { fetch() }
updateState(data)
}
}
}
viewModelScope 由 AndroidX 提供,绑定 ViewModel 生命周期——ViewModel 销毁时所有子协程自动取消,避免内存泄漏。
# 3.6.5 综合案例与思考
并发拉取多接口 + 超时 + 取消:
import kotlinx.coroutines.*
suspend fun fetchUser(id: Long): String {
delay(200); return "user#$id"
}
suspend fun fetchOrders(id: Long): List<String> {
delay(500); return listOf("order-1", "order-2")
}
suspend fun fetchNotifications(id: Long): List<String> {
delay(300); return listOf("notif-1")
}
suspend fun homePage(userId: Long): Triple<String, List<String>, List<String>> =
withTimeout(1_000) { // 整体最多 1s
coroutineScope {
val user = async { fetchUser(userId) }
val orders = async { fetchOrders(userId) }
val notifs = async { fetchNotifications(userId) }
Triple(user.await(), orders.await(), notifs.await())
}
}
fun main() = runBlocking {
val (u, o, n) = homePage(1L)
println("user=$u")
println("orders=$o")
println("notifs=$n")
}
输出(三个接口并发,总耗时约 500ms 而非 1000ms):
user=user#1
orders=[order-1, order-2]
notifs=[notif-1]
思考:为什么 runBlocking 只应出现在 main 或测试里,不能在业务代码里?
答案:
runBlocking会 阻塞当前线程 直到协程完成——这违背了协程「不阻塞线程」的初衷。在业务代码(尤其 UI 线程)里调runBlocking= ANR 死锁风险。只有在 main 入口(没有协程 scope 的地方)或者测试代码里,需要「桥接同步和协程」时才用。
# 3.7 与Java互操作
Kotlin 编译到 JVM 字节码,可以无缝调用 Java——但在「Kotlin 类被 Java 调用」的方向上有一些陷阱和优化开关。
# 3.7.1 JvmStatic与JvmField
class Api {
companion object {
@JvmStatic
fun of() = Api()
@JvmField
val TAG = "Api"
const val VERSION = "1.0" // const 隐式相当于 @JvmField + 编译期常量
}
}
Java 调用效果:
| Kotlin 注解 | Java 调用 |
|---|---|
| 无标注 | Api.Companion.of()、Api.Companion.getTAG() |
@JvmStatic 方法 | Api.of() |
@JvmField 字段 | Api.TAG(直接访问字段) |
const val | Api.VERSION(编译期常量,可用作 case 标签) |
建议:SDK 类的 companion 成员统统标注,让 Java 用户能用「像 Java static 一样」的语法访问。
# 3.7.2 JvmOverloads
Kotlin 的 默认参数 在 Java 侧看不到——Java 调用只能全传:
class HttpClient(val timeout: Int = 30, val retry: Int = 3)
Java 侧:new HttpClient(30, 3)——必须全传。加上 @JvmOverloads:
class HttpClient @JvmOverloads constructor(
val timeout: Int = 30,
val retry: Int = 3
)
编译器 生成多个重载构造:
HttpClient(int timeout, int retry) // 全参
HttpClient(int timeout) // 用默认 retry
HttpClient() // 用默认 timeout 和 retry
Java 侧就能享受「省参数」的语法。同样规则适用于 fun:
@JvmOverloads
fun greet(msg: String = "Hi", tag: String = "APP") { }
# 3.7.3 平台类型陷阱
Java 侧的 String 传给 Kotlin,类型被标为 String!——既可能非空也可能可空,Kotlin 不再帮你做静态检查:
// Java 侧
public class JavaApi {
public String getName() { return null; } // 可能返回 null
}
// Kotlin 侧
val name = JavaApi().getName() // 类型是 String!(平台类型)
println(name.length) // 编译通过,运行时 NPE
解决方案:
- Java 侧加
@Nullable/@NotNull注解(JetBrains、Android、JSR-305 都可以):
public class JavaApi {
@Nullable
public String getName() { return null; }
}
Kotlin 就会正确识别为 String?,编译期就能拦截 NPE。
- Kotlin 侧显式转成可空:
val name: String? = JavaApi().getName()
println(name?.length)
- 在类边界(数据反序列化入口)主动做非空断言:
val safeName = requireNotNull(JavaApi().getName()) { "name 必须存在" }
# 3.7.4 SAM转换
Java 单方法接口(如 Runnable、Comparator)可用 Lambda 直接构造:
Thread { println("run") }.start() // 传 Lambda 就是 Runnable
val cmp = Comparator<Int> { a, b -> a - b }
注意:Kotlin 接口自身默认不支持 SAM:
interface OnClick {
fun onClick()
}
// ❌ Kotlin 1.3 不允许
button.setOnClick { }
Kotlin 1.4+ 提供 fun interface 关键字,声明后就支持 Lambda:
fun interface OnClick {
fun onClick()
}
button.setOnClick { } // ✅
# 3.7.5 综合案例与思考
Kotlin SDK 对 Java 使用方友好的清单:
// ✅ 全部 companion 成员加 @JvmStatic / @JvmField
class NetSdk private constructor(config: Config) {
companion object {
@JvmStatic
fun create(config: Config) = NetSdk(config)
@JvmField
val DEFAULT_TIMEOUT = 30
const val VERSION = "1.0"
}
// ✅ 有默认参数的构造/方法加 @JvmOverloads
@JvmOverloads
fun request(url: String, timeout: Int = DEFAULT_TIMEOUT, retry: Int = 3) { }
// ✅ 想让 Java 直接看到 property 而非 getter/setter,用 @JvmField
@JvmField
var debug = false
// ✅ 单方法接口用 fun interface
fun interface Callback {
fun onResponse(json: String)
}
fun requestAsync(url: String, cb: Callback) { }
// ✅ 抛出 checked 异常要加 @Throws(Kotlin 无 checked exception)
@Throws(java.io.IOException::class)
fun readFile(path: String): ByteArray = TODO()
// ✅ 给顶层函数/类文件加 @file:JvmName 让 Java 调起来自然
}
思考:为什么 Kotlin 类的属性在 Java 侧调用时会看到 getXxx() 而不是 xxx?
答案:Kotlin 属性编译后是 一个私有字段 + getter/setter——遵循 JavaBean 规范。Java 侧只能通过 getter/setter 访问;想让 Java 直接看到字段,就加
@JvmField(此时 Kotlin 不再生成 getter/setter,属性变成公有字段)。
# 3.8 综合案例练习
# 3.8.1 网络请求实战
需求:并发拉取用户信息 + 用户订单,任一失败整体失败;最长超时 3 秒;结果通过扩展函数封装成 Result。
import kotlinx.coroutines.*
// ============ 数据模型 ============
data class User(val id: Long, val name: String)
data class Order(val id: Long, val amount: Long)
sealed class ApiResult<out T> {
data class Ok<T>(val data: T) : ApiResult<T>()
data class Err(val err: Throwable) : ApiResult<Nothing>()
}
// ============ 扩展函数:把 suspend 块封装成 ApiResult ============
suspend inline fun <T> runCatchingApi(block: suspend () -> T): ApiResult<T> = try {
ApiResult.Ok(block())
} catch (e: Throwable) {
ApiResult.Err(e)
}
// ============ 模拟接口 ============
suspend fun fetchUser(id: Long): User {
delay(500)
return User(id, "K")
}
suspend fun fetchOrders(userId: Long): List<Order> {
delay(800)
return listOf(Order(101, 8999), Order(102, 199))
}
// ============ 业务函数 ============
suspend fun loadHomePage(userId: Long): ApiResult<Pair<User, List<Order>>> =
runCatchingApi {
withTimeout(3_000) {
coroutineScope {
val user = async { fetchUser(userId) }
val orders = async { fetchOrders(userId) }
user.await() to orders.await()
}
}
}
// ============ 使用 ============
fun main() = runBlocking {
when (val r = loadHomePage(1L)) {
is ApiResult.Ok -> println("OK: user=${r.data.first}, orders=${r.data.second}")
is ApiResult.Err -> println("ERR: ${r.err.message}")
}
}
输出:
OK: user=User(id=1, name=K), orders=[Order(id=101, amount=8999), Order(id=102, amount=199)]
综合运用:
- 协程结构化并发(
coroutineScope { async; async; await }) - 超时保护(
withTimeout) - 扩展函数封装(
runCatchingApi) - sealed class 建模结果(
ApiResult) - when 穷尽分支
# 3.8.2 UI状态DSL
Compose 风格的 UI 状态 DSL:
class Screen {
private val components = mutableListOf<String>()
fun toolbar(block: Toolbar.() -> Unit) {
components += Toolbar().apply(block).render()
}
fun content(block: Content.() -> Unit) {
components += Content().apply(block).render()
}
fun render() = components.joinToString("\n")
class Toolbar {
var title: String = ""
var back: Boolean = false
fun render() = "[Toolbar] title=$title back=$back"
}
class Content {
private val items = mutableListOf<String>()
fun text(msg: String) { items += " Text: $msg" }
fun button(label: String, action: () -> Unit = {}) { items += " Button: $label" }
fun render() = "[Content]\n" + items.joinToString("\n")
}
}
fun screen(block: Screen.() -> Unit) = Screen().apply(block).render()
fun main() {
val ui = screen {
toolbar {
title = "首页"
back = false
}
content {
text("欢迎来到 Kotlin")
button("点我") { println("clicked") }
button("退出")
}
}
println(ui)
}
输出:
[Toolbar] title=首页 back=false
[Content]
Text: 欢迎来到 Kotlin
Button: 点我
Button: 退出
# 3.9 训练题三道
训练题 1(DSL 设计):为 SQL 查询设计一个 Kotlin DSL —— select { from("user").where { age gt 18 } }。
参考答案
class SelectBuilder {
private var from: String = ""
private var where: String = ""
fun from(table: String): SelectBuilder { this.from = table; return this }
fun where(block: WhereBuilder.() -> Unit): SelectBuilder {
where = WhereBuilder().apply(block).build()
return this
}
fun build() = "SELECT * FROM $from" + if (where.isNotEmpty()) " WHERE $where" else ""
}
class WhereBuilder {
private val conditions = mutableListOf<String>()
infix fun String.gt(value: Int) { conditions += "$this > $value" }
infix fun String.eq(value: Any) { conditions += "$this = '$value'" }
fun build() = conditions.joinToString(" AND ")
}
fun select(block: SelectBuilder.() -> String) = SelectBuilder().apply { block() }.build()
fun main() {
val sql = select {
from("user").where {
"age" gt 18
"status" eq "active"
}.build()
}
println(sql)
}
输出:SELECT * FROM user WHERE age > 18 AND status = 'active'
陷阱点:中缀函数必须是 单参数扩展/成员函数,infix 只能修饰恰好一个参数的函数。
训练题 2(扩展函数封装):为 LiveData<T> 写一个 observe { } 扩展,简化 Observer<T> 匿名类写法。
参考答案
// 假设有 LiveData / Observer / LifecycleOwner 接口(简化模拟)
interface Observer<T> { fun onChanged(t: T) }
class LiveData<T> {
fun observe(owner: LifecycleOwner, obs: Observer<T>) { /* ... */ }
}
interface LifecycleOwner
// 扩展:把 Observer<T> 匿名类改成 Lambda
inline fun <T> LiveData<T>.observe(owner: LifecycleOwner, crossinline block: (T) -> Unit) {
observe(owner, object : Observer<T> {
override fun onChanged(t: T) = block(t)
})
}
// 使用侧
class ActivityMain : LifecycleOwner {
val vm: LiveData<Int> = LiveData()
fun onCreate() {
vm.observe(this) { value -> println("changed: $value") } // 一行搞定
}
}
陷阱点:inline 函数里用 Lambda 构造匿名 Observer——Lambda 需要 crossinline,否则内部匿名类不能捕获 Lambda。
训练题 3(协程封装):写一个 withTimeoutOrDefault(timeMs: Long, default: T, block: suspend () -> T),超时返回默认值。
参考答案
import kotlinx.coroutines.*
suspend fun <T> withTimeoutOrDefault(
timeMs: Long,
default: T,
block: suspend () -> T
): T = try {
withTimeout(timeMs) { block() }
} catch (e: TimeoutCancellationException) {
default
}
fun main() = runBlocking {
val fast = withTimeoutOrDefault(1000, "default") {
delay(200); "quick"
}
val slow = withTimeoutOrDefault(300, "default") {
delay(1000); "slow"
}
println("fast=$fast, slow=$slow")
}
输出:fast=quick, slow=default
陷阱点:withTimeout 超时抛的是 TimeoutCancellationException(而非普通 Exception),需要精确捕获——用 catch (e: Exception) 会误吞其他协程异常。
# 3.10 新手陷阱Top5
| # | 陷阱 | 症状 | 正确姿势 |
|---|---|---|---|
| 1 | inline 过度或不当使用 | JAR 膨胀 / public inline 破坏 ABI | 只在接受 Lambda 的高阶函数上用 |
| 2 | suspend 不能在裸 Lambda 里用 | 编译报错 | Lambda 声明为 suspend () -> Unit |
| 3 | launch 未捕获异常 | 静默崩溃 | 用 CoroutineExceptionHandler 或 try-catch |
| 4 | 扩展函数被当多态 | 覆盖失败 | 记住「扩展是静态派发」 |
| 5 | companion object 未加 @JvmStatic 让 Java 调用蛋疼 | Java 侧多一层 .Companion. | SDK 场景务必标 @JvmStatic |
扩展陷阱 4 项:
- 陷阱 6:
GlobalScope.launch泄漏——GlobalScope 的协程不受任何生命周期约束,Activity 销毁后仍在跑。永远用viewModelScope/lifecycleScope/coroutineScope。 - 陷阱 7:
runBlocking里的Dispatchers.Main死锁——在主线程调runBlocking { withContext(Main) { } },主线程被 runBlocking 阻塞、withContext(Main)又等主线程,永远等不到。 - 陷阱 8:
apply里返回值被吞——val x = Config().apply { debug = true; loadCache() }——loadCache()的返回值被 apply 吞了,只返回 Config。想拿到值应用run或let。 - 陷阱 9:
?.let { }的 return 语义——return在 Lambda 里默认是 非局部返回(返回 外层函数),不是「返回 Lambda」。想只返回 Lambda 用return@let。
下一章预告:入门篇到此结束。接下来进入 核心原理专栏,我们将沉到字节码级别,揭开 Kotlin 每一个语法糖背后的真相。