Skip to content

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

特性StateFlowLiveData
初始值必须有可以没有
空安全泛型约束可能为 null
生命周期感知需要 repeatOnLifecycle自动感知
相同值自动去重每次都通知
背压支持不支持
平台依赖纯 KotlinAndroid 专属
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. 常见坑总结

  1. 协程取消不是立即生效的:需要在耗时操作中检查 isActive 或使用可取消的挂起函数
  2. GlobalScope 不要用:没有结构化并发,泄漏风险
  3. Flow 在 collect 时才执行:不要期望 flow{} 块在创建时就运行
  4. StateFlow 需要 repeatOnLifecycle:直接在 lifecycleScope.launch 中 collect 会在后台继续收集
  5. by lazy 默认加锁:如果确定单线程访问,用 lazy(LazyThreadSafetyMode.NONE) 提升性能
  6. 扩展函数是静态分发:不要期望多态行为
  7. 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 本身

速记表

函数引用方式返回值典型用途
letitlambda 结果非空判断、转换
runthislambda 结果对象配置 + 计算
withthislambda 结果多次操作同一对象
applythis对象本身对象初始化配置
alsoit对象本身附加操作(日志、验证)

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