Skip to content

Kotlin 与协程 — 面试题篇

更新: 5/15/2026 字数: 0 字 时长: 0 分钟


Q1: Kotlin 协程的挂起原理是什么?

考察点:协程底层实现

完整回答

Kotlin 协程的挂起本质是 CPS(Continuation Passing Style)变换 + 状态机。

编译器将 suspend 函数转换为带 Continuation 参数的普通函数,函数体被改写为一个 when(label) 状态机。每个挂起点对应一个 label 状态。

执行到挂起点时:

  1. 保存当前局部变量到 Continuation 对象
  2. 设置下一个 label
  3. 调用挂起函数,如果返回 COROUTINE_SUSPENDED 则函数返回(挂起)
  4. 异步操作完成后,调用 continuation.resumeWith(result) 恢复执行
  5. 恢复时从上次的 label 继续执行,从 Continuation 中恢复局部变量

关键点:协程挂起不会阻塞线程,线程可以去执行其他协程。恢复时由调度器决定在哪个线程继续执行。

追问:suspend 关键字的作用?

suspend 只是一个标记,告诉编译器这个函数可能挂起。编译器会为它添加 Continuation 参数并生成状态机代码。suspend 函数只能在协程或其他 suspend 函数中调用。

追问:协程比线程轻量在哪?

  • 协程是用户态调度,不需要内核态切换(线程切换需要系统调用,开销约 1-10μs)
  • 协程挂起时只保存少量状态(Continuation 对象),线程需要保存完整的栈帧(默认 1MB 栈空间)
  • 一个线程上可以运行成千上万个协程

Q2: launch 和 async 的区别?异常处理有什么不同?

考察点:协程构建器与异常传播

完整回答

launch 返回 Job,用于不需要返回值的场景。async 返回 Deferred(继承 Job),可以通过 await() 获取返回值。

异常处理的关键区别:

  • launch:异常立即向上传播到父协程,如果没有 CoroutineExceptionHandler,会导致整个协程作用域取消
  • async:异常被封装在 Deferred 中,调用 await() 时才抛出
kotlin
// launch 异常立即传播
val scope = CoroutineScope(Job())
scope.launch {
    throw RuntimeException("boom") // 立即传播,scope 被取消
}

// async 异常延迟到 await
val deferred = scope.async {
    throw RuntimeException("boom") // 暂时不传播
}
deferred.await() // 这里才抛出异常

追问:CoroutineExceptionHandler 在哪里生效?

只在根协程(顶层 launch)中生效,子协程的异常会向上传播到父协程,不会被子协程的 handler 捕获。

kotlin
val handler = CoroutineExceptionHandler { _, e -> println("Caught: $e") }

// ✅ 生效:handler 在根协程
CoroutineScope(Job() + handler).launch {
    throw RuntimeException()
}

// ❌ 不生效:handler 在子协程
CoroutineScope(Job()).launch {
    launch(handler) { // handler 在子协程,不生效
        throw RuntimeException()
    }
}

追问:supervisorScope 和 coroutineScope 的区别?

  • coroutineScope:任何子协程失败,所有子协程都被取消
  • supervisorScope:子协程失败不影响兄弟协程

SupervisorJob 的原理是重写了 childCancelled() 返回 false,不将子协程的失败传播给父协程。


Q3: Flow 和 LiveData 的区别?什么时候用哪个?

考察点:响应式数据流

完整回答

维度FlowLiveData
所属Kotlin 协程库Android Jetpack
生命周期感知需要配合 lifecycleScope.collect自动感知,只在 STARTED 以上分发
操作符丰富(map/filter/flatMap/combine/zip 等)只有 map/switchMap
背压支持(buffer/conflate/collectLatest)不支持
线程切换flowOn 指定上游线程只在主线程观察
冷/热Flow 冷流,SharedFlow/StateFlow 热流类似热流

选择建议:

  • 纯 UI 层观察简单状态:StateFlow 或 LiveData 都可以
  • 需要复杂数据变换、组合多个数据源:Flow
  • Repository/数据层:Flow(不依赖 Android 框架)
  • 新项目推荐全面使用 Flow + StateFlow 替代 LiveData

追问:StateFlow 和 SharedFlow 的区别?

  • StateFlow:必须有初始值,replay = 1,自动 distinctUntilChanged(相同值不重复发射)
  • SharedFlow:无需初始值,可配置 replay 和 buffer,不去重

StateFlow 适合表示状态(UI State),SharedFlow 适合表示事件(一次性事件如 Toast、导航)。

追问:LiveData 的粘性事件问题?

LiveData 内部有版本号机制,新观察者注册时如果版本号落后于最新值,会立即收到最后一次数据。这在事件场景下会导致重复消费(如 Toast 重复弹出)。解决方案:用 SharedFlow(replay=0) 替代,或用 Event 包装类。


Q4: Kotlin 的内联函数原理?什么时候用?

考察点:inline 机制

完整回答

inline 函数在编译时将函数体和 lambda 参数直接内联到调用处,不会创建 Function 对象和额外的方法调用。

主要用途:

  1. 消除 lambda 开销:普通高阶函数每次调用会创建一个 Function 对象(匿名内部类),inline 消除这个开销
  2. reified 泛型:只有 inline 函数才能用 reified,在运行时获取泛型类型
  3. 非局部返回:inline lambda 中可以 return 直接从外层函数返回
kotlin
inline fun <reified T> isType(value: Any): Boolean = value is T

// 调用处编译后直接展开为:value is String
isType<String>("hello")

noinline:标记不需要内联的 lambda(需要将 lambda 存储或传递给非内联函数时)。 crossinline:禁止 lambda 中的非局部返回(lambda 会在其他执行上下文中调用时)。

追问:什么时候不该用 inline?

  • 函数体很大时:内联会导致调用处代码膨胀
  • 没有 lambda 参数时:没有 Function 对象开销需要消除,inline 没有意义
  • 递归函数:无法内联

Q5: Kotlin data class 需要注意什么?

考察点:Kotlin 语法细节

完整回答

data class 编译器自动生成 equals/hashCode/toString/copy/componentN。

注意事项:

  1. 只基于主构造函数参数:body 中声明的属性不参与 equals/hashCode
kotlin
data class User(val name: String) {
    var age: Int = 0 // 不参与 equals/hashCode!
}
User("张三").apply { age = 20 } == User("张三").apply { age = 30 } // true
  1. copy 是浅拷贝:引用类型属性共享同一对象
  2. 必须有至少一个主构造函数参数
  3. 不能是 abstract/open/sealed/inner
  4. 解构声明按顺序val (name, age) = user 依赖 component1/component2 的顺序,如果属性顺序变了会出 bug

追问:data class 和普通 class 在 HashMap 中的表现?

data class 自动生成基于属性值的 equals/hashCode,所以两个属性相同的 data class 实例在 HashMap 中被视为同一个 key。普通 class 默认用 Object 的 equals(引用比较),两个属性相同但不同实例的对象是不同的 key。


Q6: Kotlin 的扩展函数是怎么实现的?有什么限制?

考察点:扩展函数原理

完整回答

扩展函数编译后是一个静态方法,接收者对象作为第一个参数:

kotlin
fun String.addStar() = "*$this*"
// 编译为:
public static String addStar(String $this) { return "*" + $this + "*"; }

限制:

  1. 静态分发:根据声明类型而非运行时类型调用,不支持多态
  2. 成员函数优先:如果类有同名同参数的成员函数,成员函数优先
  3. 不能访问 private/protected 成员:扩展函数本质是外部静态方法
  4. 可以被遮蔽:子类和父类定义同名扩展函数时,调用哪个取决于变量的声明类型

加分点:扩展函数非常适合给第三方库的类添加工具方法,比如给 Context 添加 toast 扩展、给 View 添加 visible/gone 扩展,代码更简洁且不需要继承。


Q7: 协程的结构化并发是什么?为什么重要?

考察点:协程生命周期管理

完整回答

结构化并发是指协程的生命周期被限定在一个作用域(CoroutineScope)内,形成父子层级关系:

  1. 父协程取消时,所有子协程自动取消
  2. 父协程会等待所有子协程完成后才完成
  3. 子协程的异常会传播到父协程
kotlin
// viewModelScope 绑定 ViewModel 生命周期
// ViewModel 销毁时,所有协程自动取消,不会泄漏
class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            val user = fetchUser()    // ViewModel 销毁时自动取消
            val posts = fetchPosts()  // 不会泄漏
        }
    }
}

为什么重要:

  • 防止协程泄漏(类似内存泄漏)
  • 不需要手动管理每个协程的取消
  • 异常传播有明确的层级关系

追问:GlobalScope 为什么不推荐?

GlobalScope 的生命周期是整个应用进程,启动的协程不会自动取消,容易泄漏。而且 GlobalScope 没有父 Job,异常不会传播,难以管理。应该使用与组件生命周期绑定的 scope(viewModelScope、lifecycleScope)。

加分点:提到 coroutineScope {}withContext() 都是结构化并发的体现——它们创建子作用域,等待内部所有协程完成后才返回。


Q8: Kotlin 的 object 关键字有哪些用法?

考察点:Kotlin 语法

完整回答

三种用法:

  1. 对象声明(单例)
kotlin
object AppConfig {
    val baseUrl = "https://api.example.com"
}
// 编译为:static final INSTANCE + static 初始化块(线程安全的饿汉式单例)
  1. 伴生对象(companion object)
kotlin
class User {
    companion object {
        fun create(): User = User() // 类似 Java 的静态方法
    }
}
User.create()
  1. 匿名对象(对象表达式)
kotlin
val listener = object : View.OnClickListener {
    override fun onClick(v: View) { /* ... */ }
}
// 类似 Java 的匿名内部类,但可以实现多个接口

追问:companion object 和 Java static 的区别?

companion object 本质是一个单例对象,不是真正的 static。Java 调用时需要 User.Companion.create()。加 @JvmStatic 注解才会生成真正的静态方法。


Q9: Kotlin 的协程取消是怎么工作的?

考察点:协程取消机制

完整回答

协程取消是协作式的,不是强制中断。调用 job.cancel() 后:

  1. Job 的状态变为 Cancelling
  2. 在下一个挂起点(suspend 函数调用处)检查取消状态
  3. 如果已取消,抛出 CancellationException
  4. CancellationException 不会被当作异常传播(正常取消流程)

关键点:如果协程中没有挂起点(纯 CPU 计算),取消不会生效:

kotlin
val job = launch {
    var i = 0
    while (i < 1000000) { // 没有挂起点,cancel 无效!
        i++
    }
}
job.cancel() // 不会立即取消

// ✅ 正确:检查 isActive
val job = launch {
    var i = 0
    while (isActive && i < 1000000) { // 检查取消状态
        i++
    }
}

// ✅ 或使用 ensureActive()
while (i < 1000000) {
    ensureActive() // 已取消则抛 CancellationException
    i++
}

// ✅ 或使用 yield()
while (i < 1000000) {
    yield() // 让出执行权,同时检查取消
    i++
}

追问:取消后怎么做清理工作?

kotlin
val job = launch {
    try {
        doWork()
    } finally {
        // 取消后执行清理
        withContext(NonCancellable) {
            // NonCancellable 确保即使已取消也能执行挂起函数
            closeResource()
        }
    }
}

Q10: Kotlin 的 val 和 var 的区别?val 是不是线程安全的?

考察点:Kotlin 基础

完整回答

  • val:只读引用(类似 Java final),赋值后不能重新赋值
  • var:可变引用,可以重新赋值

val 不等于不可变:

kotlin
val list = mutableListOf(1, 2, 3)
list.add(4) // ✅ 可以修改内容!val 只是引用不可变
// list = mutableListOf() // ❌ 不能重新赋值

val 不是线程安全的:

  • 自定义 getter 每次可能返回不同值
  • 引用的对象内部状态可能被其他线程修改
  • 只有 val + 不可变对象(如 String、data class)才是线程安全的

Q11: repeatOnLifecycle 和 flowWithLifecycle 是什么?为什么需要它们?

考察点:Flow 与生命周期

完整回答

直接在 lifecycleScope.launch 中 collect Flow,即使 Activity 进入后台(onStop),collect 仍在继续,浪费资源甚至导致崩溃(如更新已销毁的 View)。

kotlin
// ❌ 不安全:后台仍在收集
lifecycleScope.launch {
    viewModel.uiState.collect { state -> updateUI(state) }
}

// ✅ 安全:只在 STARTED 以上状态收集
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { state -> updateUI(state) }
    }
}

// ✅ 简写(单个 Flow)
viewModel.uiState
    .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
    .onEach { state -> updateUI(state) }
    .launchIn(lifecycleScope)

repeatOnLifecycle 在生命周期进入目标状态时启动协程,离开时取消,再次进入时重新启动。


实习面试补充:Kotlin 入门高频题

实习 Android 岗常把 Kotlin 当作基础能力考察,重点是空安全、常用语法和协程的正确使用方式。

Q12: Kotlin 的 ??.?:!! 分别是什么意思?

考察点:空安全

完整回答

  • String?:表示变量可以为 null。
  • ?.:安全调用,左边为 null 时整体返回 null,不继续调用。
  • ?::Elvis 操作符,左边为 null 时返回右边的默认值。
  • !!:非空断言,强制认为不为 null;如果实际为 null,会抛 NullPointerException
kotlin
val name: String? = null
val length = name?.length ?: 0

追问:开发中为什么要少用 !!

!! 会绕开 Kotlin 的空安全检查,一旦数据为空就会崩溃。更推荐使用安全调用、默认值、提前 return 或明确的异常处理。


Q13: valvar 的区别?

考察点:变量声明、不可变思想

完整回答

  • val 声明只读引用,初始化后不能重新赋值。
  • var 声明可变变量,可以重新赋值。
kotlin
val list = mutableListOf(1, 2)
list.add(3)      // 可以,list 指向的对象内容可变
// list = mutableListOf(4) // 不可以,val 不能重新赋值

加分点:优先使用 val 可以减少状态变化,降低代码理解和并发问题的风险。


Q14: data class 有什么作用?

考察点:Kotlin 常用类

完整回答

data class 适合表示数据模型,编译器会自动生成:

  • equals() / hashCode()
  • toString()
  • copy()
  • componentN() 解构方法
kotlin
data class User(val id: Long, val name: String)

val user = User(1, "Tom")
val newUser = user.copy(name = "Jerry")

追问:data class 适合做什么?

适合接口响应、列表 item、页面 UI 状态等数据承载对象。不适合承载复杂业务行为或需要继承体系的对象。