Kotlin 核心语法与协程 — 知识详解
更新: 5/15/2026 字数: 0 字 时长: 0 分钟
1. Kotlin 核心语法
1.1 data class
data class 编译器自动生成以下方法:
kotlin
data class User(val name: String, val age: Int)
// 编译后等价于:
public final class User {
private final String name;
private final int age;
// 主构造函数
public User(String name, int age) { ... }
// getter
public final String getName() { return name; }
public final int getAge() { return age; }
// equals:比较所有主构造函数中声明的属性
public boolean equals(Object other) {
if (this == other) return true;
if (!(other instanceof User)) return false;
User user = (User) other;
return name.equals(user.name) && age == user.age;
}
// hashCode:基于所有属性计算
public int hashCode() {
return name.hashCode() * 31 + age;
}
// toString
public String toString() {
return "User(name=" + name + ", age=" + age + ")";
}
// copy:浅拷贝,可修改部分属性
public User copy(String name, int age) {
return new User(name, age);
}
// componentN:解构声明
public String component1() { return name; }
public int component2() { return age; }
}注意事项:
equals/hashCode只基于主构造函数中的属性,body 中声明的属性不参与copy是浅拷贝,引用类型属性共享同一对象- data class 必须有至少一个主构造函数参数
1.2 sealed class / sealed interface
kotlin
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Throwable) : Result<Nothing>()
object Loading : Result<Nothing>()
}
// when 表达式可以穷举所有子类,不需要 else
fun handleResult(result: Result<String>) = when (result) {
is Result.Success -> println(result.data)
is Result.Error -> println(result.exception.message)
Result.Loading -> println("Loading...")
// 编译器保证穷举,新增子类时会编译报错
}sealed class vs enum:
- enum:每个类型只有一个实例
- sealed class:每个子类可以有多个实例,可以携带不同数据
1.3 委托属性
kotlin
// by lazy:延迟初始化,默认线程安全(SYNCHRONIZED 模式)
val heavyObject: HeavyObject by lazy {
println("初始化")
HeavyObject()
}by lazy 三种模式:
LazyThreadSafetyMode.SYNCHRONIZED(默认):双重检查锁,线程安全LazyThreadSafetyMode.PUBLICATION:多线程可能同时初始化,但只有第一个完成的值被使用LazyThreadSafetyMode.NONE:不加锁,单线程使用
kotlin
// by observable:属性变化时回调
var name: String by Delegates.observable("初始值") { prop, old, new ->
println("$old → $new")
}
// by map:从 Map 中读取属性值(常用于解析 JSON/Bundle)
class User(map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
val user = User(mapOf("name" to "张三", "age" to 25))委托原理:编译器生成一个 $$delegate 字段,属性的 get/set 转发给委托对象的 getValue/setValue。
1.4 扩展函数
kotlin
fun String.addExclamation(): String = "$this!"
// 编译后(Java 字节码):
public static String addExclamation(String $this$addExclamation) {
return $this$addExclamation + "!";
}关键点:
- 扩展函数是静态分发的,不是虚函数,不支持多态
- 如果类有同名成员函数,成员函数优先
- 扩展函数可以访问 public 成员,不能访问 private/protected
kotlin
open class Animal
class Dog : Animal()
fun Animal.speak() = "Animal"
fun Dog.speak() = "Dog"
val animal: Animal = Dog()
println(animal.speak()) // "Animal"!不是 "Dog",因为静态分发看声明类型1.5 内联函数
kotlin
inline fun <T> measureTime(block: () -> T): T {
val start = System.currentTimeMillis()
val result = block()
println("耗时: ${System.currentTimeMillis() - start}ms")
return result
}
// 调用处编译后,lambda 代码直接内联展开,不会创建 Function 对象
val result = measureTime {
// 这段代码直接嵌入调用处
heavyComputation()
}noinline:标记不需要内联的 lambda 参数(需要将 lambda 作为对象传递时)
kotlin
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
inlined() // 内联展开
bar(notInlined) // 作为对象传递给其他函数
}crossinline:禁止 lambda 中的非局部返回
kotlin
inline fun foo(crossinline block: () -> Unit) {
Runnable {
block() // 如果 block 中有 return,会从 foo 返回,但这里在 Runnable 中不允许
}.run()
}1.6 空安全
kotlin
var name: String? = null
// 安全调用
val length = name?.length // null
// Elvis 操作符
val length = name?.length ?: 0 // 0
// 非空断言(慎用!)
val length = name!!.length // 抛 NullPointerException
// let 配合安全调用
name?.let { nonNullName ->
println(nonNullName.length) // 只在非空时执行
}
// 平台类型:Java 返回的类型在 Kotlin 中标记为 String!
// 既可以当 String 也可以当 String? 使用,但如果实际为 null 会崩溃2. Kotlin 协程
2.1 挂起与恢复原理
CPS 变换
编译器将 suspend 函数转换为带 Continuation 参数的普通函数:
kotlin
// 源码
suspend fun fetchUser(): User {
val token = getToken() // 挂起点 1
val user = getUser(token) // 挂起点 2
return user
}
// 编译后(伪代码)
fun fetchUser(continuation: Continuation<User>): Any? {
val sm = continuation as? FetchUserSM ?: FetchUserSM(continuation)
when (sm.label) {
0 -> {
sm.label = 1
val result = getToken(sm)
if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
// 没有挂起,继续执行
}
1 -> {
val token = sm.result as String
sm.label = 2
val result = getUser(token, sm)
if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
}
2 -> {
return sm.result as User
}
}
}核心概念:
- 每个
suspend函数编译为一个状态机 - 每个挂起点对应一个
label状态 Continuation保存了当前状态和局部变量- 返回
COROUTINE_SUSPENDED表示真正挂起了 - 恢复时调用
continuation.resumeWith(result)
Continuation 接口
kotlin
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}2.2 CoroutineContext
CoroutineContext 是一个类似 Map 的结构,由多个 Element 组合:
kotlin
val context = Job() + Dispatchers.IO + CoroutineName("myCoroutine")
// 等价于
val context = CoroutineContext(
Job(),
Dispatchers.IO,
CoroutineName("myCoroutine")
)常用 Element:
Job:控制协程生命周期(取消、等待完成)CoroutineDispatcher:决定协程在哪个线程执行CoroutineName:调试用的名称CoroutineExceptionHandler:未捕获异常处理器
2.3 调度器
kotlin
Dispatchers.Main // Android 主线程(通过 Handler(Looper.getMainLooper()) 实现)
Dispatchers.IO // IO 密集型,共享线程池,最大 max(64, CPU核心数) 个线程
Dispatchers.Default // CPU 密集型,线程数 = CPU 核心数
Dispatchers.Unconfined // 不切换线程,在当前线程执行到第一个挂起点IO 和 Default 共享线程池:它们底层使用同一个线程池,但通过 LimitingDispatcher 限制并发数。IO 允许更多并发(因为 IO 操作大部分时间在等待),Default 限制为 CPU 核心数。
kotlin
// 切换调度器
withContext(Dispatchers.IO) {
// 在 IO 线程执行
val data = api.fetchData()
}
// 自动切回原来的调度器2.4 结构化并发
kotlin
// CoroutineScope 管理协程生命周期
class MyViewModel : ViewModel() {
// viewModelScope 在 ViewModel.onCleared() 时自动取消
fun loadData() {
viewModelScope.launch {
val user = fetchUser() // 子协程
val posts = fetchPosts() // 子协程
}
// ViewModel 销毁时,所有子协程自动取消
}
}Job 层级关系:
父 Job
├── 子 Job A
│ ├── 孙 Job A1
│ └── 孙 Job A2
└── 子 Job B- 父 Job 取消 → 所有子 Job 递归取消
- 子 Job 失败 → 默认取消父 Job → 取消所有兄弟 Job
SupervisorJob:子 Job 失败不影响父和兄弟
kotlin
// 普通 Job:一个子协程失败,全部取消
coroutineScope {
launch { throw Exception("失败") } // 导致整个 scope 取消
launch { delay(1000) } // 也会被取消
}
// SupervisorJob:子协程失败不影响兄弟
supervisorScope {
launch { throw Exception("失败") } // 只有这个失败
launch { delay(1000) } // 正常执行
}2.5 异常传播
launch vs async:
kotlin
// launch:异常立即向上传播
val job = scope.launch {
throw RuntimeException("boom") // 异常传播到父协程
}
// async:异常在 await() 时抛出
val deferred = scope.async {
throw RuntimeException("boom") // 暂时不传播
}
try {
deferred.await() // 这里才抛出异常
} catch (e: Exception) {
// 处理异常
}CoroutineExceptionHandler:
kotlin
val handler = CoroutineExceptionHandler { _, exception ->
println("捕获异常: ${exception.message}")
}
// 只在根协程(scope.launch)上设置才有效
val scope = CoroutineScope(SupervisorJob() + handler)
scope.launch {
throw RuntimeException("boom") // 被 handler 捕获
}
// ❌ 在子协程上设置无效
scope.launch {
launch(handler) { // handler 不会生效!
throw RuntimeException("boom")
}
}2.6 Flow
冷流 vs 热流
kotlin
// 冷流:每次 collect 都重新执行
val coldFlow = flow {
println("开始发射")
emit(1)
emit(2)
emit(3)
}
coldFlow.collect { println(it) } // 打印:开始发射 1 2 3
coldFlow.collect { println(it) } // 再次打印:开始发射 1 2 3
// SharedFlow(热流):多个收集者共享
val sharedFlow = MutableSharedFlow<Int>(
replay = 1, // 新收集者能收到最近 1 个值
extraBufferCapacity = 64,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
// StateFlow(特殊的 SharedFlow):
// replay = 1,必须有初始值,自动 distinctUntilChanged
val stateFlow = MutableStateFlow(0)
stateFlow.value = 1 // 更新值
stateFlow.value = 1 // 相同值不会通知收集者StateFlow vs LiveData
| 特性 | StateFlow | LiveData |
|---|---|---|
| 初始值 | 必须有 | 可以没有 |
| 空安全 | 泛型约束 | 可能为 null |
| 生命周期感知 | 需要 repeatOnLifecycle | 自动感知 |
| 相同值 | 自动去重 | 每次都通知 |
| 背压 | 支持 | 不支持 |
| 平台依赖 | 纯 Kotlin | Android 专属 |
kotlin
// 在 Activity/Fragment 中安全收集 StateFlow
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
updateUI(state)
}
}
}背压处理
kotlin
flow {
for (i in 1..100) {
emit(i)
delay(10) // 生产快
}
}
.buffer(64) // 缓冲区,生产者不等消费者
// .conflate() // 只保留最新值,丢弃中间值
// .collectLatest {} // 新值来时取消上一次处理
.collect { value ->
delay(100) // 消费慢
println(value)
}常用操作符
kotlin
flowOf(1, 2, 3, 4, 5)
.filter { it % 2 == 0 } // 过滤:2, 4
.map { it * 10 } // 变换:20, 40
.onEach { println("处理: $it") } // 副作用
.catch { e -> emit(-1) } // 异常处理
.flowOn(Dispatchers.IO) // 上游在 IO 线程执行
.collect { println(it) } // 收集
// combine:组合多个 Flow,任一发射都触发
combine(flow1, flow2) { a, b -> "$a-$b" }
// zip:配对组合,等待两个都发射
flow1.zip(flow2) { a, b -> "$a-$b" }
// flatMapConcat:顺序展开
// flatMapMerge:并发展开
// flatMapLatest:只处理最新的2.7 Channel
kotlin
// Channel:协程间通信(CSP 模型)
val channel = Channel<Int>(capacity = Channel.BUFFERED)
// 生产者
launch {
for (i in 1..5) {
channel.send(i) // 缓冲区满时挂起
}
channel.close()
}
// 消费者
launch {
for (value in channel) { // 自动在 close 后结束
println(value)
}
}缓冲策略:
Channel.RENDEZVOUS(0):无缓冲,send 和 receive 必须同时就绪Channel.BUFFERED(64):默认缓冲大小Channel.CONFLATED:只保留最新值Channel.UNLIMITED:无限缓冲(注意内存)
Channel vs Flow:
- Channel 是热的,创建即开始
- Flow 是冷的,collect 才开始
- Channel 适合一对一通信
- Flow 适合一对多的数据流
3. 常见坑总结
- 协程取消不是立即生效的:需要在耗时操作中检查
isActive或使用可取消的挂起函数 - GlobalScope 不要用:没有结构化并发,泄漏风险
- Flow 在 collect 时才执行:不要期望 flow{} 块在创建时就运行
- StateFlow 需要 repeatOnLifecycle:直接在 lifecycleScope.launch 中 collect 会在后台继续收集
- by lazy 默认加锁:如果确定单线程访问,用
lazy(LazyThreadSafetyMode.NONE)提升性能 - 扩展函数是静态分发:不要期望多态行为
- data class 的 equals 只看主构造函数参数:body 中的属性不参与比较
4. Kotlin 作用域函数
面试常考的五个作用域函数对比:
kotlin
// let:非空判断 + 转换,it 引用
val length = str?.let { it.length } // 返回 lambda 结果
// run:对象配置 + 计算,this 引用
val result = service.run {
port = 8080
query() // 返回 lambda 结果
}
// with:对同一对象多次操作,this 引用(非扩展函数)
with(config) {
host = "localhost"
port = 8080
}
// apply:对象配置,this 引用,返回对象本身
val intent = Intent().apply {
action = "com.example.ACTION"
putExtra("key", "value")
} // 返回 intent 本身
// also:附加操作,it 引用,返回对象本身
val list = mutableListOf(1, 2, 3).also {
println("创建了列表: $it")
} // 返回 list 本身速记表:
| 函数 | 引用方式 | 返回值 | 典型用途 |
|---|---|---|---|
| let | it | lambda 结果 | 非空判断、转换 |
| run | this | lambda 结果 | 对象配置 + 计算 |
| with | this | lambda 结果 | 多次操作同一对象 |
| apply | this | 对象本身 | 对象初始化配置 |
| also | it | 对象本身 | 附加操作(日志、验证) |
5. Kotlin 与 Java 互操作
5.1 常见注解
kotlin
@JvmStatic // 生成真正的静态方法(companion object 中使用)
@JvmField // 暴露为 public 字段而非 getter/setter
@JvmOverloads // 为有默认参数的函数生成重载方法
@JvmName // 指定生成的 Java 方法名
@Throws // 声明受检异常(Kotlin 没有受检异常)5.2 平台类型
Java 返回的类型在 Kotlin 中标记为 String!(平台类型),既可以当 String 也可以当 String?。如果实际为 null 会在运行时抛 NullPointerException。
kotlin
// Java 方法返回 String(无 @Nullable/@NonNull 注解)
val name: String = javaObj.getName() // 如果返回 null,这里崩溃
val name: String? = javaObj.getName() // 安全,需要空判断最佳实践:Java 代码加 @Nullable/@NonNull 注解,Kotlin 侧就能正确推断。
5.3 协程与 Java 互操作
kotlin
// Kotlin 协程暴露给 Java 调用
fun fetchDataForJava(): CompletableFuture<Data> {
return GlobalScope.future { fetchData() } // kotlinx-coroutines-jdk8
}
// Java 回调转协程
suspend fun <T> Call<T>.await(): T = suspendCancellableCoroutine { cont ->
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
cont.resume(response.body()!!)
}
override fun onFailure(call: Call<T>, t: Throwable) {
cont.resumeWithException(t)
}
})
cont.invokeOnCancellation { cancel() }
}6. withContext vs launch vs async
kotlin
// launch:启动新协程,不返回结果(返回 Job)
// 用于"发射后不管"的场景
viewModelScope.launch {
repository.saveData(data) // 不需要返回值
}
// async:启动新协程,返回 Deferred(可 await 获取结果)
// 用于并行执行多个任务
val user = async { fetchUser() }
val posts = async { fetchPosts() }
showData(user.await(), posts.await()) // 并行执行,等待两个结果
// withContext:切换上下文执行,挂起当前协程等待结果
// 用于切换线程(如主线程切到 IO 线程)
val data = withContext(Dispatchers.IO) {
repository.loadData() // 在 IO 线程执行
} // 返回结果,回到原来的线程
updateUI(data)关键区别:
withContext是顺序执行(挂起等待),async是并行执行withContext不创建新协程,只切换上下文- 单个任务切线程用
withContext,多个任务并行用async
