Android 高级岗 Kotlin 面试题:这些答不上来,基本告别大厂了

2026 Android 高级岗 Kotlin 面试题:这些答不上来,基本告别大厂了

一、基础语法与核心特性

1.1 val 和 var 到底有什么区别?

核心回答 :val 声明的是只读引用 ,相当于 Java 的 final;var 声明的是可变变量。面试官真正想问的是:你知道 val 就一定线程安全吗?

kotlin

scss 复制代码
// val 只是引用不可变,但引用指向的对象内部可能可变
val list = mutableListOf(1, 2, 3)
list.add(4)  // ✅ 编译通过,list 引用没变,但内容变了

// 想要真正的不可变,用 immutable 集合
val list2 = listOf(1, 2, 3)  // List 接口没有 add 方法
// list2.add(4)  // ❌ 编译报错

Android 实战场景

kotlin 复制代码
// ViewModel 中推荐用 val 声明状态
class MainViewModel : ViewModel() {
    // ✅ 状态用 val,整个 ViewModel 生命周期内引用不变
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()
    
    // ✅ 配置数据用 val
    private val config: AppConfig = ConfigProvider.getConfig()
}

// mutable 变量只暴露给内部,不暴露给外部
class UserRepository {
    private var cachedUser: User? = null  // 只读接口暴露出去
    
    suspend fun getUser(id: String): User {
        return cachedUser ?: fetchFromNetwork(id).also { 
            cachedUser = it 
        }
    }
}

面试加分点

  • val 底层实现是 ACC_FINAL 标志位,JVM 会优化,可能直接内联常量
  • 但 val 懒加载时(如 val a by lazy { }),在多线程环境下可能多次执行 lazy 块,除非用 SynchronizedBlockingScheduler
  • 进阶:Kotlin 1.9 之后有 @JvmStatic、@JvmField 等注解影响编译结果

1.2 Kotlin 的空安全机制,null 到底是怎么被管理的?

核心回答 :Kotlin 在类型系统层面区分可空和非空类型,编译器在编译期强制检查,NPE 只会在特定场景出现(显式抛、unsafe 操作、Java 互调)。

java 复制代码
var name: String = null     // ❌ 编译错误
var name: String? = null   // ✅ 可空类型

// 安全调用操作符 ?. 底层会生成 if-null-check
val length = name?.length  
// 编译后约等于: if (name != null) name.length else null

// Elvis 操作符 ?: 提供默认值
val len = name?.length ?: 0

// 非空断言 !! ------ 面试官会问你什么时候用它
val len2 = name!!.length   // ⚠️ 抛出 KNullPointerException

Android 实战场景

kotlin 复制代码
// Retrofit 回调中的空安全处理
interface ApiService {
    @GET("user/{id}")
    suspend fun getUser(@Path("id") id: String): Response<UserDto>
}

class UserRepository(private val api: ApiService) {
    suspend fun getUserName(id: String): String {
        return try {
            // Response 可能为空,网络请求可能失败
            api.getUser(id).body()?.name ?: "匿名用户"
        } catch (e: Exception) {
            "获取失败"
        }
    }
}

// Bundle 数据传递 ------ Android 的 null 重灾区
class SecondActivity : AppCompatActivity() {
    companion object {
        fun newIntent(context: Context, userId: String): Intent {
            return Intent(context, SecondActivity::class.java).apply {
                putExtra(EXTRA_USER_ID, userId)  // 永远不会传 null
            }
        }
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Intent.getStringExtra 返回 String?,必须处理
        val userId: String? = intent.getStringExtra(EXTRA_USER_ID)
        if (userId == null) {
            finish()  // 没有必要参数直接关闭
            return
        }
        // 非空后可以直接用,Kotlin 智能类型转换
        loadUserData(userId)
    }
}

面试加分点

  • JVM 底层 :可空类型编译后会加上 @Nullable 注解(配合 Java 使用),非空类型加 @NotNull
  • 平台类型 Platform Type :Java 返回的值在 Kotlin 中显示为 String!,既不是 String 也不是 String?,需要手动处理
  • @Nullable vs @NonNull:Java 代码需要加这些注解,Kotlin 才能正确识别

1.3 data class 除了自动生成 equals/hashCode,还能干点啥?

核心回答 :data class 会自动生成 equalshashCodetoStringcopycomponentN 函数。copy 是精髓,不可变数据模型的最佳实践。

kotlin 复制代码
data class User(
    val id: String,
    val name: String,
    val age: Int
)

// 自动生成的东西
val user1 = User("1", "张三", 25)
val user2 = user1.copy(name = "李四")  // 只改 name,其他属性保持不变

// 解构
val (id, name, age) = user1  // 自动生成 component1/2/3

Android 实战场景

kotlin 复制代码
// ViewModel 状态用 data class + copy 实现不可变更新
data class MainUiState(
    val isLoading: Boolean = false,
    val user: User? = null,
    val error: String? = null,
    val items: List<Item> = emptyList()
)

class MainViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(MainUiState())
    val uiState: StateFlow<MainUiState> = _uiState.asStateFlow()
    
    fun loadUser() {
        _uiState.update { it.copy(isLoading = true, error = null) }
        
        viewModelScope.launch {
            try {
                val user = repository.getUser()
                // copy 只更新 user 字段,其他状态保持不变
                _uiState.update { it.copy(isLoading = false, user = user) }
            } catch (e: Exception) {
                _uiState.update { it.copy(isLoading = false, error = e.message) }
            }
        }
    }
}

// Room 数据库 Entity 自动实现 toString,方便日志
@Entity(tableName = "users")
data class UserEntity(
    @PrimaryKey val id: String,
    val name: String,
    val email: String
)

// Retrofit DTO 转 Entity
fun UserDto.toEntity() = UserEntity(id, name, email)

面试加分点

  • copy 的原理:生成一个新对象,浅拷贝所有属性
  • 限制 :不能继承 data class(componentN 函数签名冲突)
  • copy vs mutableCopy :data class + copy = 真正的不可变;JavaBean + mutable = 到处 setXxx()
  • equals 生成规则:只比较主构造函数中的属性,secondary constructor 里的属性不参与

1.4 == 和 === 到底比的是什么?

核心回答

  • == 调用 equals(),比较
  • === 比较引用地址(Java 的 ==)
ini 复制代码
val a = "hello"
val b = "hello"
val c = String(charArrayOf('h','e','l','l','o'))

println(a == b)   // ✅ true,值相等
println(a === b)  // ✅ true,字符串字面量在 String Pool 中
println(a === c)  // ❌ false,new 出来的不在池中

Android 实战场景

kotlin 复制代码
// RecyclerView DiffUtil 中的比较
class UserDiffCallback : DiffUtil.ItemCallback<User>() {
    override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
        return oldItem.id === newItem.id  // 引用比较,ID 相同就是同一个 item
    }
    
    override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
        return oldItem == newItem  // 内容比较,data class 自动生成 equals
    }
}

// 避免在 Kotlin 中误用 ===
class ArticleRepository {
    fun isSameArticle(a: Article?, b: Article?): Boolean {
        // 很多新人会写成 ===,这是错的
        return a == b  // 正确做法
    }
}

面试加分点

  • 字符串 intern :JVM 会缓存字符串字面量,所以 "abc" === "abc" 是 true
  • 对象池 :Integer、Long 等包装类型也有缓存,-128 到 127 范围内的值 ===== 结果相同
  • Kotlin 的 Any:默认 equals 需要自己实现,但 data class 会自动生成

1.5 const val vs val 的底层差异(字节码层面)

核心回答 :const val 是编译期常量 ,直接内联到字节码;val 是运行时求值。这个区别在做 AOP 或 Gradle Plugin 时特别重要。

kotlin

kotlin 复制代码
const val API_BASE = "https://api.example.com"  // 编译期常量
val API_TIMEOUT = 3000L                          // 运行时求值

字节码对比

java

java 复制代码
// const val 编译后
public static final String API_BASE = "https://api.example.com";
// 使用处直接内联:ldc "https://api.example.com"

// val 编译后
private static final Long API_TIMEOUT = 3000L;
// 使用处:getstatic ApiConfig.API_TIMEOUT

Android 实战场景

kotlin 复制代码
object ApiConfig {
    // ✅ 用于注解参数,注解必须是编译期常量
    const val BASE_URL = "https://api.example.com"
    
    // ❌ Retrofit @Url 不能用 val,会报 "must be a compile-time constant"
    // const val ENDPOINT = "https://api.example.com/user"
    
    // 用于条件判断
    val isDebug: Boolean = BuildConfig.DEBUG  // 运行时才确定
    
    // ✅ if 条件必须是 const
    const val MAX_RETRY = 3
}

// Intent 传递常量必须用 const
class SecondActivity : AppCompatActivity() {
    companion object {
        fun newIntent(context: Context, userId: String): Intent {
            return Intent(context, SecondActivity::class.java).apply {
                putExtra(EXTRA_USER_ID, userId)  // key 必须是编译期常量
            }
        }
    }
}

面试加分点

  • const 限制:只能修饰基本类型和 String、必须全局作用域或 companion object
  • @JvmField:可以让 val 生成 Java 静态字段(而不是 getter 方法)
  • 实际影响:Gradle 配置缓存、BuildConfig 字段、注解处理器等场景必须用 const

1.6 lateinit vs by lazy:我到底该用哪个?

核心回答 :lateinit 是可空变量的延迟赋值 ,用于 var;by lazy 是真正的懒加载,用于 val。lateinit 可能会出现 UninitializedPropertyAccessException。

kotlin 复制代码
// lateinit:先声明,后赋值
lateinit var adapter: UserAdapter  // var,不能用 val

fun initRecyclerView() {
    adapter = UserAdapter()  // 在使用前必须赋值
}

// by lazy:第一次访问时执行初始化
val adapter: UserAdapter by lazy {
    UserAdapter()  // 只执行一次,线程安全
}

Android 实战场景

kotlin 复制代码
class MainActivity : AppCompatActivity() {
    
    // ✅ lateinit:View 需要在 onCreate 后才能初始化
    private lateinit var binding: ActivityMainBinding
    private lateinit var adapter: UserAdapter
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        adapter = UserAdapter()  // 在这里初始化
        
        binding.recyclerView.adapter = adapter
    }
    
    // ✅ by lazy:依赖注入的 Service,懒加载避免不必要的初始化
    private val repository: UserRepository by lazy {
        UserRepository(RetrofitClient.api)
    }
    
    // ❌ by lazy 不能用于 var,否则会编译错误
    // private lazy var someVar: String = "test"
    
    // ✅ 想要可修改的懒加载?自己实现或用 notThreadSafeLazy
    private var heavyConfig: Config? by lazy(mode = LazyThreadSafetyMode.PUBLICATION) {
        loadHeavyConfig()
    }
}

面试加分点

  • lateinit 本质:生成一个 boolean flag,访问时检查是否已初始化

  • by lazy 原理:默认线程安全,用 synchronized 或 CAS 实现

  • 错误场景

    go 复制代码
    lateinit var name: String
    println(name)  // 抛出 UninitializedPropertyAccessException
  • 现代方案:依赖注入框架(Hilt)可以 @Inject lateinit var,减少自己管理

1.7 Kotlin when vs Java switch:看似差不多,其实差很多

核心回答 :when 比 switch 强大太多:可以是表达式返回值、支持任意对象匹配、可以有条件判断、配合 sealed class 简直是状态机神器。

rust 复制代码
// switch 的局限性
when (day) {
    1 -> println("周一")
    2 -> println("周二")
    // 只能匹配常量,不能匹配范围、类型、对象
}

// when 的强大
when {
    x in 1..10 -> println("1到10之间")
    x is String -> println("是字符串,长度${x.length}")
    name == "张三" || name == "李四" -> println("中国人")
    else -> println("其他")
}

// when 作为表达式
val result = when (status) {
    Status.SUCCESS -> "成功"
    Status.ERROR -> "失败"
    else -> "未知"
}

Android 实战场景

kotlin 复制代码
// sealed class + when 实现状态机
sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String) : UiState<Nothing>()
}

@Composable
fun <T> StateContent(state: UiState<T>, onSuccess: @Composable (T) -> Unit) {
    when (state) {
        is UiState.Loading -> CircularProgressIndicator()
        is UiState.Success -> onSuccess(state.data)
        is UiState.Error -> Text(state.message)
        // 编译器会检查是否穷举,不需要 default
    }
}

// Intent Action 匹配
fun handleIntent(intent: Intent) {
    when (intent.action) {
        Intent.ACTION_VIEW -> openBrowser(intent.data)
        Intent.ACTION_SEND -> handleShare(intent)
        else -> super.onNewIntent(intent)
    }
}

面试加分点

  • 穷尽性检查:when 对 sealed class 会做编译期检查,加新子类不改 when 会报错

  • 带参数的 when

    kotlin

    rust 复制代码
    when (val result = calculate()) {
        in 0..60 -> "不及格"
        in 60..80 -> "及格"
        else -> "优秀"
    }

二、函数与 Lambda

2.1 扩展函数到底是什么原理?能重写吗?

核心回答 :扩展函数是静态分发 的语法糖,编译后变成一个静态方法,第一个参数是被扩展的类型。不能重写,因为不是真正的继承。

kotlin 复制代码
fun String.addExclamation() = this + "!"

// 编译后等价于
public static final String addExclamation(String $receiver) {
    return $receiver + "!";
}

Android 实战场景

kotlin 复制代码
// Context 扩展函数 ------ Android 开发必备
fun Context.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(this, message, duration).show()
}

// String 扩展 ------ 避免空指针
fun String?.orEmpty(): String = this ?: ""

fun String.isValidEmail(): Boolean {
    return this.isNotEmpty() && Patterns.EMAIL_ADDRESS.matcher(this).matches()
}

// View 扩展 ------ 简化 View 操作
fun View.visible() {
    visibility = View.VISIBLE
}

fun View.gone() {
    visibility = View.GONE
}

// Activity 扩展
inline fun <reified T : Activity> Activity.startActivity(
    vararg params: Pair<String, String>
) {
    val intent = Intent(this, T::class.java).apply {
        params.forEach { (key, value) -> putExtra(key, value) }
    }
    startActivity(intent)
}

// 使用
class MainActivity : AppCompatActivity() {
    fun onClick() {
        toast("Hello")  // Context 扩展
        etUsername.text.isValidEmail()
        binding.btnSubmit.visible()
        startActivity<SecondActivity>("userId" to "123")
    }
}

面试加分点

  • 扩展函数不能重写 :因为是静态分发,看的是变量的静态类型而非运行时类型

    kotlin 复制代码
    open class A
    class B : A()
    
    fun A.foo() = "A"
    fun B.foo() = "B"
    
    val a: A = B()
    a.foo()  // 调用的是 A 的 foo,不是 B 的!
  • 扩展属性:只能添加 getter/setter,不能有 backing field

  • Kotlin 如何决定调用哪个扩展函数 :Java 8 之前的分发机制,基于接收者类型

2.2 inline、reified、noinline、crossinline:这几个到底怎么配合?

核心回答 :inline 是告诉编译器把函数调用处直接替换成函数体,避免 lambda 带来的额外开销;reified 让泛型在运行时保留;noinline/crossinline 是inline的补充控制。

kotlin 复制代码
// 普通高阶函数:有 lambda 额外调用开销
inline fun measureTime(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    println("cost ${System.currentTimeMillis() - start}ms")
}

// reified:泛型参数在运行时可用
inline fun <reified T> Gson.fromJson(json: String): T {
    return this.fromJson(json, T::class.java)  // T::class.java 这里能用!
}

// noinline:不需要内联的 lambda 参数
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
    inlined()
    notInlined()  // 必须是 noinline,否则 inline 函数内的 lambda 必须内联
}

// crossinline:lambda 不能直接 return,但可以有非局部 return
inline fun startWork(crossinline onComplete: () -> Unit) {
    thread {
        // 不能 return,但可以 throw 或用标签 return
        onComplete()  // crossinline 强制 lambda 必须执行
    }
}

Android 实战场景

kotlin 复制代码
// 经典 useCase 封装
inline fun <T> useCase(
    crossinline block: suspend () -> T
): suspend () -> Result<T> = {
    try {
        Result.success(block())
    } catch (e: Exception) {
        Result.failure(e)
    }
}

// ViewModelScope 中的协程 useCase
class GetUserUseCase(
    private val repository: UserRepository
) {
    suspend operator fun invoke(userId: String): Result<User> {
        return try {
            Result.success(repository.getUser(userId))
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

// reified 在 KTX 中的应用
inline fun <reified T : ViewModel> ViewModelStoreOwner.viewModel(): T {
    return ViewModelProvider(this)[T::class.java]
}

// 使用
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModel()
}

面试加分点

  • 内联的代价:代码膨胀,大函数内联会很糟糕
  • reified 限制:必须是 inline 函数才能用 reified
  • noinline 用途:当 lambda 需要作为变量传递、或需要引用它的泛型类型时
  • 内联与协程suspend 函数本身就是 inline 的,不用额外加 inline

2.3 Lambda 表达式和匿名内部类有什么区别?

核心回答 :Lambda 底层会生成单例匿名类 (只有一个方法时)或新 class (多个方法时),而匿名内部类每次调用都生成新 class。Lambda 不能访问外部非 final 变量(但 Kotlin 做了捕获)。

kotlin 复制代码
// Kotlin Lambda
val lambda = { x: Int -> x * 2 }
// 编译后生成:Lambda$1.class

// Java 匿名内部类
val runnable = object : Runnable {
    override fun run() { }
}
// 编译后生成:MainActivity$1.class(外部类$序号)

Android 实战场景

kotlin 复制代码
class MainActivity : AppCompatActivity() {
    
    // Lambda 捕获变量
    fun demo() {
        var count = 0
        btnClick.setOnClickListener {
            count++  // 捕获了 count,编译器会生成 Wrapper 类
        }
    }
    
    // 协程中的变量捕获 ------ 特别注意!
    fun loadData() {
        var data: String? = null
        
        lifecycleScope.launch {
            data = fetchData()  // 协程中赋值
        }
        
        // ⚠️ 这里 data 可能还是 null!
        // 因为协程是异步的
    }
    
    // 正确做法:StateFlow 替代
    private val _data = MutableStateFlow<String?>(null)
    val data: StateFlow<String?> = _data.asStateFlow()
    
    fun loadDataCorrect() {
        lifecycleScope.launch {
            val result = fetchData()
            _data.value = result  // 赋值到 StateFlow
        }
    }
}

面试加分点

  • Lambda 捕获的本质 :生成 VariableCapturingClass,内部类持有外部变量的引用
  • 内存泄漏风险:非静态内部类持有外部类引用,如果 lambda 逃逸到生命周期之外,可能导致内存泄漏
  • JVM 层面的优化:Lambda 会在第一次调用时才生成类( invokedynamic),而不是编译时

2.4 Lambda 捕获机制:会造成内存泄漏吗?

核心回答 :会。Lambda 捕获外部变量时,会生成持有外部类引用的内部类,如果这个 lambda 逃逸到比外部类生命周期更长的地方,就会内存泄漏。

kotlin 复制代码
class leaky {
    private val data = heavyObject()  // 大对象
    
    fun createLeak() {
        // 这个 listener 持有 leaky 的引用
        // 如果 leaky 先销毁,但 listener 还被 static 持有
        staticMap["key"] = OnClickListener { 
            data.doSomething()  // 引用了 data
        }
    }
    
    // 正确做法:弱引用或内部使用完及时清理
    fun createSafe() {
        val listener = OnClickListener { 
            it.doSomething()  // 不捕获外部类
        }
    }
}

Android 实战场景

kotlin 复制代码
class DemoActivity : AppCompatActivity() {
    
    // ❌ 经典泄漏:协程逃逸
    private val scope = CoroutineScope(Dispatchers.Main)
    
    override fun onDestroy() {
        super.onDestroy()
        // ⚠️ 如果没有 cancel,scope 会继续运行,持有 Activity 引用
        scope.cancel()
    }
    
    // ✅ 正确做法:使用 viewModelScope/lifecycleScope
    fun loadData() {
        lifecycleScope.launch {
            val data = repository.getData()
            updateUI(data)  // 自动绑定生命周期
        }
    }
    
    // ❌ 泄漏:Handler 持有 Activity
    private val handler = Handler(Looper.getMainLooper())
    private val callback = Handler.Callback {
        // this 指向 Activity
        true
    }
    
    // ✅ 正确:使用弱引用或避免持有 Activity 引用
    private val handlerRunnable = Runnable {
        // 访问 View 时需要判空
        if (isFinishing || isDestroyed) return@Runnable
        binding.textView.text = "更新"
    }
}

面试加分点

  • 逃逸场景:存储到 static 集合、传递到其他线程、作为回调注册给生命周期更长的对象
  • ViewModelScope:内部使用的是 Job + SupervisorJob,自动取消,不需要手动管理
  • LeakCanary 原理:监控 Activity.onDestroy 后是否还有引用

2.5 Kotlin 函数参数为什么是 val 不能修改?

核心回答 :函数参数默认是 val,因为参数不是你的 ,是调用方传进来的。如果允许修改参数,可能会让调用者感到困惑,而且 Kotlin 的设计哲学是不可变优先

kotlin 复制代码
fun foo(x: Int) {
    x = 10  // ❌ 编译错误
}

fun bar(list: List<Int>) {
    // list = mutableListOf()  // ❌ 编译错误
    list.forEach { it * 2 }  // ✅ 可以读取
}

Android 实战场景

kotlin 复制代码
// 参数不可变是好事,代码更可预测
fun processUser(user: User) {
    // user.name = "新名字"  // ❌ 不允许
    // 如果需要修改,copy 一个新的
    val updatedUser = user.copy(name = "新名字")
    // 或者用 builder 模式
}

// Retrofit Service 定义
interface ApiService {
    @POST("user/update")
    suspend fun updateUser(@Body user: User): Response<User>
    // user 参数不可变,保证调用方知道数据没被篡改
}

// UseCase 规范
class GetUserUseCase {
    operator fun invoke(userId: String): Flow<User> {
        // userId 是 val,保证输入参数不被修改
        return repository.getUserById(userId)
    }
}

三、协程

3.1 协程到底是什么?和线程有什么区别?

核心回答 :协程是用户态的轻量级线程,由 Kotlin 运行时管理,不需要 OS 切换。一个线程可以跑多个协程,切换成本比线程低几个数量级。

css 复制代码
线程模型:
Thread 1 → [任务A] → [任务B](阻塞等待)
Thread 2 → [任务C]

协程模型:
Thread 1 → [协程A] ↔ [协程B] ↔ [协程C](主动让出)

Android 实战场景

kotlin 复制代码
class MainViewModel : ViewModel() {
    
    // ✅ 协程 vs 线程的直观对比
    fun loadDataOldWay() {
        // 线程方式:阻塞、回调地狱
        Thread {
            val data = fetchDataSync()  // 同步获取
            runOnUiThread {
                updateUI(data)  // 回到主线程
            }
        }.start()
    }
    
    fun loadDataCoroutine() {
        // 协程方式:顺序写法,异步执行
        viewModelScope.launch {
            val data = fetchData()  // 自动挂起,不会阻塞线程
            updateUI(data)          // 继续在主线程执行
        }
    }
    
    // ✅ 挂起函数的本质
    private suspend fun fetchData(): String {
        return withContext(Dispatchers.IO) {
            api.getData()  // IO 操作挂起,让出线程
        }
    }
}

面试加分点

  • 挂起原理 :编译后生成 Continuation 状态机,函数会被拆分成多个"片段"
  • StateMachine:每个 suspend 函数编译后会变成 switch-case 状态机
  • Continuation:保存挂起点和局部变量,从挂起点恢复执行

3.2 CoroutineScope、launch、async 到底怎么选?

核心回答

  • CoroutineScope:管理协程生命周期,协程在其上下文中运行
  • launch:启动一个不需要返回值的协程,返回 Job
  • async/await:启动一个需要返回值的协程,返回 Deferred
scss 复制代码
// launch:fire and forget
viewModelScope.launch {
    val data = fetchData()
    _state.value = data
}

// async:需要返回结果
viewModelScope.launch {
    val deferred = async { fetchUser() }
    val user = deferred.await()  // 等待结果
}

// 并行执行多个任务
viewModelScope.launch {
    val user = async { api.getUser() }
    val posts = async { api.getPosts() }
    
    // 两个任务并行执行,都完成后才继续
    val combined = UserWithPosts(user.await(), posts.await())
}

Android 实战场景

kotlin 复制代码
class UserDetailViewModel(
    private val getUser: GetUserUseCase,
    private val getPosts: GetPostsUseCase
) : ViewModel() {
    
    private val _uiState = MutableStateFlow<UserDetailUiState>(UserDetailUiState.Loading)
    val uiState: StateFlow<UserDetailUiState> = _uiState.asStateFlow()
    
    fun loadUserDetail(userId: String) {
        viewModelScope.launch {
            try {
                // 串行执行
                // val user = getUser(userId)
                // val posts = getPosts(userId)
                
                // ✅ 并行执行,效率更高
                val userDeferred = async { getUser(userId) }
                val postsDeferred = async { getPosts(userId) }
                
                val user = userDeferred.await()
                val posts = postsDeferred.await()
                
                _uiState.value = UserDetailUiState.Success(
                    user = user,
                    posts = posts
                )
            } catch (e: Exception) {
                _uiState.value = UserDetailUiState.Error(e.message ?: "未知错误")
            }
        }
    }
}

面试加分点

  • 协程是轻量的:创建协程几乎没有开销,可以成千上万个同时运行
  • Structured Concurrency:协程有父子关系,父协程取消会取消所有子协程
  • Dispatchers.Main :Android 主线程调度器,viewModelScope 默认使用

3.3 suspend 函数到底是怎么实现的?

核心回答suspend 修饰的函数编译后会变成状态机 ,每次遇到 await 或其他挂起点就会切出去,等条件满足再切回来继续执行。

kotlin 复制代码
// 源码
suspend fun fetchData(): String {
    val result = api.getData()  // 挂起点1
    return process(result)      // 挂起点2
}

// 编译后等价于状态机
class FetchDataContinuation : Continuation<String> {
    var label = 0
    var result: String? = null
    
    override fun resumeWith(data: Result<String>) {
        when (label) {
            0 -> {
                // 执行 api.getData(),完成后调用 resume
                label = 1
                api.getDataAsync(continuation = this)
            }
            1 -> {
                // 从挂起点恢复,继续执行 process
                result = data.getOrThrow()
                process(result!!)
            }
        }
    }
}

Android 实战场景

kotlin 复制代码
// ViewModel 中的 suspend 函数
class MainViewModel : ViewModel() {
    
    // ✅ 标准写法:suspend 函数配合 viewModelScope
    fun loadData() {
        viewModelScope.launch {
            try {
                val data = fetchData()  // suspend,挂起在这里
                _state.value = data
            } catch (e: Exception) {
                _state.value = Error(e.message)
            }
        }
    }
    
    // ⚠️ 错误写法:在非协程环境调用 suspend 函数
    // val data = fetchData()  // ❌ 编译错误
    
    // ✅ 如果确实需要在非协程环境调用,用 runBlocking(仅测试用)
    fun blockingDemo() {
        runBlocking {
            val data = fetchData()  // OK,但会阻塞线程
        }
    }
}

面试加分点

  • Continuation Passing Style (CPS) :所有 suspend 函数都会被编译器转换
  • 栈帧保存:局部变量在挂起时保存到 Continuation 对象
  • 恢复执行:条件满足时从 Continuation 取出状态,继续执行

3.4 withContext vs async/await:什么时候用哪个?

核心回答 :withContext 是强制在指定线程执行 ,结束后回到原线程;async/await 是并行执行多个任务,最后汇总结果。

kotlin 复制代码
// withContext:切换线程
suspend fun fetchData(): String {
    return withContext(Dispatchers.IO) {
        api.getData()  // 在 IO 线程执行
    }  // 自动切回原线程
}

// async/await:并行执行
suspend fun fetchMultiple(): Pair<String, Int> {
    val name = async { getName() }    // 启动协程1
    val age = async { getAge() }      // 启动协程2,并行!
    return Pair(name.await(), age.await())
}

Android 实战场景

kotlin 复制代码
class ProfileViewModel : ViewModel() {
    
    // ✅ withContext 用于线程切换
    suspend fun loadUserProfile(userId: String): Profile {
        return withContext(Dispatchers.IO) {
            // 数据库操作
            val local = database.getUser(userId)
            // 网络请求
            val remote = api.getUser(userId)
            
            // 数据合并(计算密集)
            withContext(Dispatchers.Default) {
                mergeProfile(local, remote)
            }
        }
    }
    
    // ✅ async/await 用于并行获取不相关数据
    fun loadDashboard() {
        viewModelScope.launch {
            try {
                _state.value = _state.value.copy(isLoading = true)
                
                // 三个请求并行执行
                val bannerJob = async { api.getBanners() }
                val categoryJob = async { api.getCategories() }
                val productJob = async { api.getHotProducts() }
                
                val dashboard = Dashboard(
                    banners = bannerJob.await(),
                    categories = categoryJob.await(),
                    products = productJob.await()
                )
                
                _state.value = _state.value.copy(
                    isLoading = false,
                    dashboard = dashboard
                )
            } catch (e: Exception) {
                _state.value = _state.value.copy(
                    isLoading = false,
                    error = e.message
                )
            }
        }
    }
}

面试加分点

  • 性能对比:并行 async 相比串行执行,时间约为最慢任务的耗时
  • 异常处理:async 内部异常不会立即抛出,而是在 await 时抛出
  • 取消行为:withContext 内部抛 CancellationException 表示正常取消

3.5 协程异常处理:supervisorScope vs coroutineScope

核心回答

  • coroutineScope:任一子协程异常会取消其他所有子协程
  • supervisorScope:子协程异常不影响其他子协程,各自独立
kotlin 复制代码
// coroutineScope:一个失败,全部取消
suspend fun demo1() = coroutineScope {
    launch { throw Exception("任务1失败") }  // 会导致...
    launch { /* 任务2也被取消 */ }
}

// supervisorScope:失败不影响其他
suspend fun demo2() = supervisorScope {
    launch { throw Exception("任务1失败") }  // 不会影响...
    launch { /* 任务2继续执行 */ }
}

Android 实战场景

ini 复制代码
class OrderViewModel : ViewModel() {
    
    // ✅ 典型场景:多个独立请求,一个失败不影响其他
    fun loadOrderDetail(orderId: String) {
        viewModelScope.launch {
            try {
                supervisorScope {
                    // 订单信息:必须成功
                    val orderJob = launch {
                        val order = orderRepository.getOrder(orderId)
                        _order.value = order
                    }
                    
                    // 物流信息:可选,失败不影响
                    launch {
                        try {
                            val tracking = logisticsRepository.getTracking(orderId)
                            _tracking.value = tracking
                        } catch (e: Exception) {
                            // 吞掉异常,不影响其他协程
                            _tracking.value = null
                        }
                    }
                    
                    // 商品详情:可选
                    launch {
                        val details = productRepository.getDetails(orderId)
                        _details.value = details
                    }
                }
                
                _state.value = _state.value.copy(isLoading = false)
            } catch (e: Exception) {
                _state.value = _state.value.copy(
                    isLoading = false,
                    error = e.message
                )
            }
        }
    }
}

面试加分点

  • ViewModelScope 使用 SupervisorJobviewModelScope.launch 失败不会影响其他 launch
  • CoroutineExceptionHandler:全局异常处理器,用于记录日志、上报
  • CancellationException:协程取消时会抛出这个异常,通常不需要捕获

3.6 Flow vs LiveData:该怎么选?

核心回答 :LiveData 是Android 专属 ,只能在主线程观察;Flow 是Kotlin 协程原生,支持背压、任意线程、更多操作符。

表格

特性 LiveData Flow
线程安全 主线程观察 可配置
背压处理 有(buffer、collectLatest等)
操作符 有限 丰富(map、filter、flatMap...)
Android 生命周期 自动绑定 需要 repeatOnLifecycle
null 支持 可观察 null nullflowOf(null)

kotlin

scss 复制代码
// LiveData 写法
val user = MutableLiveData<User>()
user.observe(this) { u -> updateUI(u) }

// Flow 写法
val user: Flow<User> = repository.getUser()
lifecycleScope.launch {
    user.collect { u -> updateUI(u) }
}

// ✅ Flow 的优势:背压处理
fun loadMessages() {
    repository.getMessages()
        .buffer(capacity = 50)  // 缓冲区处理背压
        .collectLatest { messages ->
            adapter.submitList(messages)  // 只处理最新
        }
}

Android 实战场景

kotlin 复制代码
class UserListViewModel : ViewModel() {
    private val _users = MutableLiveData<List<User>>()  // LiveData
    val users: LiveData<List<User>> = _users
    
    // ✅ 改用 Flow:更多操作、线程安全
    private val _usersFlow = MutableStateFlow<List<User>>(emptyList())
    val usersFlow: StateFlow<List<User>> = _usersFlow.asStateFlow()
    
    // Repository 返回 Flow
    private val repository = UserRepository()
    val usersFromRepo: Flow<List<User>> = repository.getUsers()
    
    // ✅ Flow 配合 repeatOnLifecycle(生命周期安全)
    fun observeUsersOnUI(scope: LifecycleCoroutineScope) {
        scope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                usersFlow.collect { users ->
                    adapter.submitList(users)
                }
            }
        }
    }
    
    // ✅ Flow 链式操作
    fun searchUsers(query: String) {
        viewModelScope.launch {
            repository.getUsers()
                .map { users -> users.filter { it.name.contains(query) } }
                .flowOn(Dispatchers.IO)
                .collect { filtered ->
                    _usersFlow.value = filtered
                }
        }
    }
}

面试加分点

  • StateFlow vs SharedFlow:StateFlow 有初始值,始终有值;SharedFlow 无初始值
  • conflate vs buffer:conflate 跳过中间值,buffer 保留所有值
  • Lifecycle.repeatOnLifecycle:Compose 和 XML 都能用,自动绑定生命周期

3.7 Flow 的 shareIn vs stateIn 区别

核心回答

  • stateIn:有初始值,始终有值,多个 collector 共享同一值
  • shareIn:无初始值,可以配置重放策略
kotlin 复制代码
// stateIn:需要初始值,始终有值
private val _data = MutableStateFlow<List<Item>>(emptyList())  // 初始值
val data: StateFlow<List<Item>> = _data.asStateFlow()

// shareIn:不需要初始值
private val events = MutableSharedFlow<Event>()  // 无初始值
val events: SharedFlow<Event> = events.asSharedFlow()

// shareIn 配置重放
private val dataFromNetwork = flow {
    emit(api.getData())
}.shareIn(viewModelScope, SharingStarted.Lazily, replay = 1)  // 重放最近1条

Android 实战场景

kotlin 复制代码
class NewsViewModel(
    private val repository: NewsRepository
) : ViewModel() {
    
    // ✅ stateIn:新闻列表,有初始值
    val news: StateFlow<List<News>> = repository.getNews()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),  // 订阅策略
            initialValue = emptyList()
        )
    
    // ✅ shareIn:一次性事件,如 Toast、导航
    private val _events = MutableSharedFlow<UiEvent>()
    val events: SharedFlow<UiEvent> = _events.asSharedFlow()
    
    fun onNewsClicked(news: News) {
        viewModelScope.launch {
            _events.emit(UiEvent.NavigateToDetail(news.id))
        }
    }
    
    // ✅ shareIn with replay:配置重放策略
    val latestNews = repository.getNews()
        .shareIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            replay = 1  // 新订阅者收到最近1条
        )
}

class NewsActivity : AppCompatActivity() {
    fun observeEvents(viewModel: NewsViewModel) {
        lifecycleScope.launch {
            viewModel.events.collect { event ->
                when (event) {
                    is UiEvent.NavigateToDetail -> navigate(event.newsId)
                    is UiEvent.ShowToast -> toast(event.message)
                }
            }
        }
    }
}

面试加分点

  • SharingStarted:Lazily(首次订阅才启动)、Eagerly(立即启动)、WhileSubscribed(订阅时启动,结束时取消)
  • WhileSubscribed(5000) :5秒内重新订阅不重启上游
  • 背压场景 :SharedFlow 用 tryEmit 尝试发送,失败则 drop 或 buffer

3.8 协程取消的坑:cancellable / ensureActive

核心回答 :协程取消是协作式 的,如果协程内没有检查取消状态,可能无法取消。isActive/ensureActive()/yield() 是常用的取消检查点。

scss 复制代码
// ❌ 坑:循环内没有检查取消状态,无法取消
launch {
    while (true) {
        doHeavyWork()  // 不会响应取消
    }
}

// ✅ 正确:在循环内检查 isActive
launch {
    while (isActive) {  // 检查取消状态
        doHeavyWork()
    }
}

// ✅ 或者用 ensureActive
launch {
    while (true) {
        ensureActive()  // 抛出 CancellationException
        doHeavyWork()
    }
}

// ✅ 或者用 yield 主动让出
launch {
    while (true) {
        yield()  // 检查取消并让出线程
        doHeavyWork()
    }
}

Android 实战场景

kotlin 复制代码
class SearchViewModel(
    private val repository: SearchRepository
) : ViewModel() {
    
    private var searchJob: Job? = null
    
    fun search(query: String) {
        searchJob?.cancel()  // 取消上一次搜索
        
        searchJob = viewModelScope.launch {
            // ✅ 使用 debounce + cancellable
            delay(300)  // debounce
            
            ensureActive()  // 确保未被取消
            
            val results = repository.search(query)
            
            ensureActive()  // 网络请求后再次检查
            
            _state.value = _state.value.copy(results = results)
        }
    }
    
    // ✅ 长时间循环任务:定期检查取消
    fun processItems(items: List<Item>) {
        viewModelScope.launch {
            items.forEachIndexed { index, item ->
                ensureActive()  // 每个 item 处理前检查
                processItem(item)
            }
        }
    }
}

面试加分点

  • suspend 函数内部withContextdelayyield 等 suspending 函数会自动检查取消
  • Blocking 操作 :如 Thread.sleep()while (true) 不会检查取消,需要手动处理
  • CancellationException:协程取消时抛出,finally 块会执行,但不能抛异常

四、集合与序列

4.1 List、Set、Map 的区别和使用场景

核心回答

  • List:有序、可重复,按索引访问
  • Set:无序(LinkedHashSet 保持插入顺序)、不重复
  • Map:键值对,键不重复
kotlin 复制代码
val list = listOf(1, 2, 2, 3)      // [1, 2, 2, 3],有重复
val set = setOf(1, 2, 2, 3)        // [1, 2, 3],去重
val map = mapOf("a" to 1, "b" to 2)

// 按场景选择
val userNames = listOf("Tom", "Jerry", "Tom")  // 按顺序遍历,允许重复
val userIds = setOf(1, 2, 3)                     // 存储 ID,不允许重复
val userMap = mapOf(1 to user1, 2 to user2)     // ID -> User 映射

Android 实战场景

kotlin 复制代码
// RecyclerView adapter
class UserAdapter : ListAdapter<User, UserViewHolder>(UserDiffCallback) {
    
    // List:按顺序展示,允许重名用户
    override fun submitList(list: List<User>?) {
        super.submitList(list)
    }
}

// Set:去重场景
class TagSelectionManager {
    private val _selectedTags = MutableStateFlow<Set<String>>(emptySet())
    val selectedTags: StateFlow<Set<String>> = _selectedTags.asStateFlow()
    
    fun toggleTag(tag: String) {
        _selectedTags.update { tags ->
            if (tag in tags) tags - tag else tags + tag
        }
    }
    
    // Set 自动去重
    fun addTags(newTags: List<String>) {
        _selectedTags.update { it + newTags.toSet() }
    }
}

// Map:缓存、配置、分组
class CacheManager {
    private val memoryCache = mutableMapOf<String, Bitmap>()
    
    fun put(key: String, bitmap: Bitmap) {
        memoryCache[key] = bitmap
    }
    
    fun get(key: String): Bitmap? = memoryCache[key]
    
    // 按前缀分组
    fun getByPrefix(prefix: String): Map<String, Bitmap> {
        return memoryCache.filter { it.key.startsWith(prefix) }
    }
}

4.2 Sequence vs List:什么时候用 Sequence?

核心回答 :Sequence 是惰性求值 ,链式操作不会立即执行,只在被消费时才计算;List 是立即求值,每一步都生成新集合。

scss 复制代码
// List:每一步都创建新集合
listOf(1, 2, 3)
    .map { it * 2 }    // [2, 4, 6] 创建新集合
    .filter { it > 4 } // [6] 再创建新集合

// Sequence:惰性,只遍历一次
sequenceOf(1, 2, 3)
    .map { it * 2 }    // 不执行,只是标记
    .filter { it > 4 } // 不执行,只是标记
    .toList()          // 现在才开始执行:1->2->[2] 跳过,2->4->[4] 跳过,3->6->[6]

Android 实战场景

kotlin 复制代码
// ✅ 大数据量处理用 Sequence
class DataProcessor {
    
    // 处理百万级数据
    fun processLargeList(items: List<HeavyItem>): List<Result> {
        // 如果用 List:每个操作都创建新集合,内存爆炸
        // 如果用 Sequence:只遍历一次
        
        return items.asSequence()
            .filter { it.isValid }           // 过滤无效数据
            .map { it.toResult() }            // 转换
            .take(100)                        // 只取前100条
            .toList()                         // 触发执行
    }
    
    // 嵌套循环优化
    fun findDuplicateIds(list1: List<Item>, list2: List<Item>): Set<String> {
        return list1.asSequence()
            .flatMap { list2.asSequence() }  // 懒展开
            .map { it.id }
            .filter { id -> list1.any { it.id == id } }
            .toSet()
    }
    
    // 配合 Flow 使用
    fun observeItems(): Flow<List<Item>> {
        return database.getItemsFlow()
            .map { items -> 
                items.asSequence()
                    .filter { !it.isDeleted }
                    .sortedBy { it.createdAt }
                    .toList()
            }
    }
}

面试加分点

  • 何时用 List:数据量小、需要多次遍历、并发安全
  • 何时用 Sequence:大数据量、链式操作复杂、避免中间集合创建
  • Sequence 本质Iterator<T> 接口,每次 next() 计算一步

4.3 集合函数性能:map/filter/fold/reduce 哪个最耗时?

核心回答 :链式操作中,每个函数都可能创建新集合(List)或产生新元素(Sequence)。性能敏感场景用 Sequence + 融合操作。

scss 复制代码
// List 版本:每次创建新集合
val result = listOf(1, 2, 3, 4, 5)
    .map { it * 2 }    // 创建 1 个新 List
    .filter { it > 5 } // 创建 1 个新 List
    .sum()             // 遍历求和

// 等价于
val mapped = listOf(2, 4, 6, 8, 10)
val filtered = listOf(6, 8, 10)
val sum = 6 + 8 + 10

// reduce vs fold:fold 有初始值,reduce 无
val sum1 = listOf(1, 2, 3).fold(0) { acc, i -> acc + i }  // 0 + 1 + 2 + 3 = 6
val sum2 = listOf(1, 2, 3).reduce { acc, i -> acc + i }    // 1 + 2 + 3 = 6

// ✅ 性能优化:合并 map + flatten
val nested = listOf(listOf(1, 2), listOf(3, 4))
val flatMapResult = nested.flatMap { it }  // [1,2,3,4]

// 替代 filter + map 的双遍历
val result = items
    .mapNotNull { item -> item.tryTransform() }  // 一步完成过滤+转换

Android 实战场景

kotlin 复制代码
class StatisticsCalculator {
    
    // ✅ 避免多次遍历
    fun calculate(items: List<SaleItem>): SaleStats {
        // ❌ 多次遍历
        // val total = items.sumOf { it.amount }
        // val count = items.count { it.amount > 100 }
        // val max = items.maxOf { it.amount }
        
        // ✅ 单次遍历:fold
        return items.fold(SaleStats()) { acc, item ->
            acc.copy(
                total = acc.total + item.amount,
                count = acc.count + if (item.amount > 100) 1 else 0,
                max = maxOf(acc.max, item.amount)
            )
        }
    }
    
    // ✅ groupBy 替代多次 filter
    fun groupUsers(users: List<User>): Map<String, List<User>> {
        return users.groupBy { it.department }
    }
    
    // ✅ associate 替代 toMap
    fun buildUserMap(users: List<User>): Map<String, User> {
        return users.associate { it.id to it }  // 比 toMap 更高效
    }
}

data class SaleStats(
    val total: Double = 0.0,
    val count: Int = 0,
    val max: Double = 0.0
)

五、泛型与注解

5.1 泛型型变:out/in 到底怎么理解?

核心回答

  • out(协变) :只能生产 (返回)T,不能消费(参数)T,类似"生产者"
  • in(逆变) :只能消费 T,不能生产T,类似"消费者"
kotlin 复制代码
// out:生产
class Producer<out T> {
    fun produce(): T = ...  // 返回 T
    // fun consume(item: T)  ❌ 不能接受 T
}

// in:消费
class Consumer<in T> {
    fun consume(item: T)  // 接受 T
    // fun produce(): T    ❌ 不能返回 T
}

// 实际例子
interface List<out E> {  // List<E> 可以被 List<Number> 赋值
    fun get(index: Int): E  // 生产 E ✅
    // add(element: E)      不能有 ❌
}

Android 实战场景

kotlin 复制代码
// ✅ Repository 返回协变类型
class UserRepository {
    // 返回 List<String> 可以赋值给 List<Any>
    fun getAllUsers(): List<User> = database.getAll()
}

val users: List<User> = UserRepository().getAllUsers()
val names: List<String> = users.map { it.name }  // List<String> -> List<Any> 可以

// ✅ Hilt 依赖注入的协变
interface Repository<out T> {
    fun getAll(): List<T>
}

class UserRepo : Repository<User> {
    override fun getAll(): List<User> = emptyList()
}

// ✅ 回调接口的逆变
interface Consumer<in T> {
    fun accept(t: T)
}

// T 可以接收 String,Number 也可以
val numberConsumer: Consumer<Number> = object : Consumer<Number> {
    override fun accept(t: Number) { println(t.toFloat()) }
}
val anyConsumer: Consumer<Any> = numberConsumer  // 逆变

面试加分点

  • Use-site variance :在参数位置用 in/out

    kotlin 复制代码
    fun copy(from: Array<out Any>, to: Array<Any>) { }
  • Declaration-site variance :在类声明处用 out,如 List<out T>

  • PECS 原则:Producer-Extends, Consumer-Super(来自 Java)

5.2 @Parcelize 原理:Parcelable 为何能自动生成?

核心回答@Parcelize 是 Kotlin 1.1 引入的注解,配合 Parceler 接口,自动生成 writeToParcel/describeContents。底层是通过 注解处理器(KAPT/KSP) 生成代码。

kotlin

kotlin 复制代码
@Parcelize
data class User(
    val id: String,
    val name: String,
    val age: Int
) : Parcelable

// 编译后生成
class User : Parcelable {
    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeString(id)
        parcel.writeString(name)
        parcel.writeInt(age)
    }
    
    override fun describeContents() = 0
    
    companion object CREATOR : Parcelable.Creator<User> {
        override fun createFromParcel(parcel: Parcel): User {
            return User(
                parcel.readString(),
                parcel.readString(),
                parcel.readInt()
            )
        }
        
        override fun newArray(size: Int): Array<User?> = arrayOfNulls(size)
    }
}

Android 实战场景

kotlin 复制代码
@Parcelize
data class Article(
    val id: String,
    val title: String,
    val author: User,           // ✅ 嵌套 Parcelable 自动处理
    val tags: List<String>,      // ✅ Parcelize 支持 List
    val extras: Map<String, String>  // ✅ 也支持 Map
) : Parcelable

// Navigation 组件传参
class ArticleDetailFragment : Fragment() {
    
    // ✅ 用 navArgs 生成类型安全的参数访问
    private val args: ArticleDetailFragmentArgs by navArgs()
    
    fun showArticle() {
        val article = args.article  // 类型安全,不会传错
    }
}

// Intent 传递
class SecondActivity : AppCompatActivity() {
    companion object {
        fun newIntent(context: Context, article: Article): Intent {
            return Intent(context, SecondActivity::class.java).apply {
                putExtra(EXTRA_ARTICLE, article)  // Parcelize 自动序列化
            }
        }
    }
}

面试加分点

  • 自定义 Parceler:对于不支持的类型

    kotlin 复制代码
    @Parcelize
    data class Config(
        val url: URL  // URL 不直接支持
    ) : Parcelable {
        companion object {
            val CREATOR = object : Parcelable.Creator<Config> {
                // 自定义序列化
            }
        }
    }
  • KSK vs KAPT:Kotlin Symbol Processing 比 Annotation Processing 更快

5.3 reified 的原理和使用场景

核心回答reified 让泛型参数在运行时可用 (正常泛型擦除后不可见),本质是因为 inline 函数会在编译时内联,泛型在调用处是具体的。

kotlin 复制代码
// 普通泛型:运行时无法获取 T::class
fun <T> getClassName(): String {
    return T::class.java.name  // ❌ 编译错误,泛型擦除
}

// reified:运行时可以获取 T::class
inline fun <reified T> getClassName(): String {
    return T::class.java.name  // ✅ 编译通过
}

Android 实战场景

kotlin 复制代码
// ✅ KTX 经典用法:fragmentViewModels
inline fun <reified VM : ViewModel> Fragment.fragmentViewModels(): ViewModelDelegate<VM> {
    return ViewModelProvider(this)[VM::class.java]  // 这里用到 T::class
}

// ✅ Intent 解析
inline fun <reified T : Parcelable> Intent.getParcelable(key: String): T? {
    return getParcelableExtra(key, T::class.java)
}

// ✅ ViewModelFactory 简化
class GenericViewModelFactory<VM : ViewModel>(
    private val creator: () -> VM
) : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(VM::class.java)) {
            return creator() as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

// ✅ Flow 数据过滤
inline fun <reified T> Flow<Any>.filterIsInstance(): Flow<T> {
    return filter { it is T }.map { it as T }
}

// ✅ Navigation Args 简化
inline fun <reified T : NavArgs> Fragment.navArgs(): T {
    return NavArgsBundleCompat(this, T::class.java).getArgs()
}

面试加分点

  • reified 必须配合 inline:因为非 inline 函数没有具体的调用点,泛型会被擦除
  • 泛型擦除 :JVM 运行时不保留泛型信息,List<String>List<Int> 运行时都是 List
  • 类型Token :Java 中用 Class<T> 解决,Kotlin 用 reified 更优雅

5.4 泛型擦除在 Kotlin 中的表现

核心回答 :Kotlin 和 Java 一样,泛型信息在运行时会被擦除为上界(通常是 Object)。可以用内联 + reified桥接方法来应对。

kotlin 复制代码
// 泛型擦除演示
fun <T> printType(list: List<T>) {
    println(list.javaClass.genericSuperclass)  // 只知道是 List,不知道 T
}

// 擦除后的签名
// List<T> -> List<Object> (或 List<?>)

Android 实战场景

kotlin 复制代码
// ✅ 利用 @UnsafeVariance 绕过类型限制
class Container<T> {
    // comparator 要求 Consumer<T>,但 T 是 in 位置
    private val comparators = mutableMapOf<String, Comparator<in T>>()
    
    fun addComparator(key: String, comparator: Comparator<in T>) {
        comparators[key] = comparator
    }
}

// ✅ 泛型擦除导致的问题:inline + reified 解决
// ❌ 不能这样用
fun <T> parseJson(json: String): T {
    return Gson().fromJson(json)  // 不知道 T,无法创建
}

// ✅ 用 reified
inline fun <reified T> parseJson(json: String): T {
    return Gson().fromJson(json, T::class.java)
}

// ✅ Room 中泛型 DAO 的处理
abstract class BaseDao<T> {
    @Insert
    abstract suspend fun insert(item: T)
    
    @Query("SELECT * FROM $TABLE_NAME WHERE id = :id")
    abstract suspend fun getById(id: String): T?
}

面试加分点

  • 桥接方法:Java 泛型擦除后,编译器会生成桥接方法保持多态

    csharp 复制代码
    class Node<T> {
        T data;
        void setData(T data) { }
    }
    // 擦除后生成桥接方法:void setData(Object data)
  • 类型Token解决方案 :用 TypeTokenreified 获取运行时类型

5.5 where 子句的多约束泛型

核心回答where 关键字让泛型参数满足多个约束条件 ,类似 Java 的 T extends A & B

kotlin 复制代码
// Kotlin where 语法
fun <T> serializeWithMeta(
    item: T
): String where T : Serializable, T : JsonConvertible {
    return item.toJson()  // 两个接口的方法都能用
}

// Java 等价
// <T extends Serializable & JsonConvertible>
// void serializeWithMeta(T item) { ... }

Android 实战场景

kotlin 复制代码
// ✅ 同时实现 Parcelable 和 Comparable
@Parcelize
data class SortableItem(
    val id: String,
    val priority: Int
) : Parcelable, Comparable<SortableItem> {
    override fun compareTo(other: SortableItem): Int {
        return this.priority.compareTo(other.priority)
    }
}

// ✅ 多约束的排序函数
fun <T> sortWithMeta(
    items: List<T>
): List<T> where T : Comparable<T>, T : Serializable {
    return items.sorted().also { items.forEach { it.serialize() } }
}

// ✅ 依赖注入工厂
interface ViewModelFactory<T : ViewModel> {
    fun create(dependencies: Dependencies): T
}

// 多约束的场景
class CombinedFactory<T>(
    private val local: LocalStorage<T>,
    private val remote: RemoteApi<T>
) where T : Entity, T : Serializable {
    
    suspend fun sync(): List<T> {
        val localData = local.getAll()
        val remoteData = remote.fetchAll()
        return (localData + remoteData).distinctBy { it.id }
    }
}

面试加分点

  • 性能影响:多约束编译后,所有约束都会成为桥接方法
  • 协变/逆变组合T : Comparable<in T> 允许协变接口和逆变泛型组合

六、面向对象与设计模式

6.1 密封类 vs 枚举:什么时候用哪个?

核心回答 :枚举适合固定数量的常量 ;密封类适合有限子类型(可以有状态、数据、继承层次)。

kotlin 复制代码
// 枚举:固定实例
enum class Color {
    RED, GREEN, BLUE  // 只有这三个实例
}

// 密封类:有限但复杂的类型
sealed class Result<out T> {
    class Success<T>(val data: T) : Result<T>()
    class Error(val message: String) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

Android 实战场景

kotlin 复制代码
// ✅ 状态机用 sealed class
sealed class UiState<out T> {
    object Idle : UiState<Nothing>()
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String, val code: Int) : UiState<Nothing>()
}

@Composable
fun <T> StateContent(state: UiState<T>) {
    when (state) {
        is UiState.Idle -> Unit
        is UiState.Loading -> CircularProgressIndicator()
        is UiState.Success -> ShowData(state.data)
        is UiState.Error -> ErrorScreen(state.message)
        // 编译器检查是否穷举
    }
}

// ✅ 导航结果用 sealed class
sealed class NavResult {
    object Success : NavResult()
    object Cancelled : NavResult()
    data class Error(val e: Throwable) : NavResult()
}

// ✅ 事件用 sealed class
sealed class UserEvent {
    data class Login(val username: String, val password: String) : UserEvent()
    data class Logout(val reason: String?) : UserEvent()
    object Refresh : UserEvent()
}

面试加分点

  • 穷尽性检查:sealed class + when 是绝配,加新子类不改 when 会编译错误
  • 子类型可以是 data class:可以有状态、多个实例
  • 枚举的局限:不能有状态,实例是单例

6.2 单例模式:Kotlin 比 Java 优雅多少?

核心回答 :Kotlin 用 object 声明就是单例,JVM 保证线程安全、延迟加载。Java 需要 double-check、volatile 等繁琐写法。

kotlin 复制代码
// Kotlin 单例:一行搞定
object AppConfig {
    val baseUrl = "https://api.example.com"
    fun init() { /* 初始化 */ }
}

// 使用
val config = AppConfig  // 不用 new,直接用

字节码分析

arduino 复制代码
// 编译后等价于
public final class AppConfig {
    private static final AppConfig INSTANCE;
    
    private AppConfig() {}
    
    public static final AppConfig getInstance() {  // 编译器生成
        return INSTANCE;
    }
    
    static {
        INSTANCE = new AppConfig();
    }
}

Android 实战场景

kotlin 复制代码
// ✅ App 级配置
object AppManager {
    lateinit var application: Application
        private set
    
    fun init(app: Application) {
        application = app
    }
    
    val prefs: SharedPreferences
        get() = application.getSharedPreferences("app", Context.MODE_PRIVATE)
}

// ✅ Retrofit 单例
object RetrofitClient {
    private val retrofit = Retrofit.Builder()
        .baseUrl("https://api.example.com/")
        .client(OkHttpClient.Builder().build())
        .build()
    
    val api: ApiService = retrofit.create(ApiService::class.java)
}

// ✅ ServiceLocator 模式
object ServiceLocator {
    private val services = mutableMapOf<Class<*>, Any>()
    
    fun <T> register(clazz: Class<T>, instance: T) {
        services[clazz] = instance
    }
    
    inline fun <reified T> get(): T = services[T::class.java] as T
}

面试加分点

  • 懒加载单例 :用 by lazy 实现懒加载

    kotlin 复制代码
    object HeavyResource {
        val data by lazy { loadData() }
    }
  • 线程安全 :JVM 保证 <clinit> 线程安全

  • Java 互调@JvmStatic@JvmField 影响生成的 Java 代码

6.3 委托 by:属性委托和类委托有什么区别?

核心回答

  • 属性委托by lazyby viewModels()、自定义 by X
  • 类委托:接口实现委托给另一个对象
kotlin 复制代码
// 属性委托
val name: String by lazy { "init" }

// 类委托:让 A 把 I 接口的实现委托给 B
interface Base { fun print() }
class BaseImpl(val x: Int) : Base { override fun print() = print(x) }
class Derived(b: Base) : Base by b  // 不需要自己实现 print

Android 实战场景

kotlin 复制代码
// ✅ ViewModel 委托(KTX 扩展)
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()  // 属性委托
    private val binding: ActivityMainBinding by viewBinding()  // viewBinding 委托
}

// ✅ 自定义委托:SharedPreferences
class Preference<T>(
    private val key: String,
    private val default: T
) : ReadWriteProperty<SharedPreferences, T> {
    
    override fun getValue(thisRef: SharedPreferences, property: KProperty<*>): T {
        return when (default) {
            is String -> thisRef.getString(key, default) as T
            is Int -> thisRef.getInt(key, default) as T
            is Boolean -> thisRef.getBoolean(key, default) as T
            else -> throw UnsupportedOperationException()
        }
    }
    
    override fun setValue(thisRef: SharedPreferences, property: KProperty<*>, value: T) {
        thisRef.edit().apply {
            when (value) {
                is String -> putString(key, value)
                is Int -> putInt(key, value)
                is Boolean -> putBoolean(key, value)
            }
        }.apply()
    }
}

// 使用
class UserPrefs(private val prefs: SharedPreferences) {
    var nickname: String by Preference("nickname", "")
    var age: Int by Preference("age", 0)
}

// ✅ 类委托:复用接口实现
class LoggingList<T>(private val inner: MutableList<T>) : MutableList<T> by inner {
    override fun add(element: T): Boolean {
        Log.d("List", "Adding $element")
        return inner.add(element)
    }
}

面试加分点

  • by lazy 线程安全模式LazyThreadSafetyMode.SYNCHRONIZED(默认)、PUBLICATIONNONE

  • ReadOnlyProperty vs ReadWriteProperty:区分只读和可写属性

  • map 委托:直接用 Map 存储属性

    kotlin 复制代码
    class User(props: Map<String, Any>) {
        val name: String by props
        val age: Int by props
    }

6.4 companion object vs Java static:真的完全等价吗?

核心回答 :companion object 是 Kotlin 的类内部单例对象,比 Java static 更强大:可以实现接口、继承、有自己的属性和方法、还可以用 by 委托。

kotlin 复制代码
class MyClass {
    companion object {
        const val TAG = "MyClass"  // 编译期常量
        @JvmStatic lateinit var instance: MyClass  // Java 互调
        
        fun create(): MyClass = MyClass()
    }
}

// companion object 可以实现接口!
interface Factory<T> {
    fun create(): T
}

class User private constructor(val name: String) {
    companion object : Factory<User> {
        override fun create() = User("Default")
    }
}

Android 实战场景

kotlin 复制代码
// ✅ ViewModelFactory
class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {
    
    companion object {
        val CREATOR: Parcelable.Creator<UserViewModel> = object : Parcelable.Creator<UserViewModel> {
            override fun createFromParcel(parcel: Parcel): UserViewModel {
                // 需要手动实现,companion object 不能继承 Parcelable.Creator
            }
            override fun newArray(size: Int): Array<UserViewModel?> = arrayOfNulls(size)
        }
    }
}

// ✅ Intent 创建工厂
class DetailActivity : AppCompatActivity() {
    companion object {
        private const val EXTRA_ID = "extra_id"
        private const val EXTRA_NAME = "extra_name"
        
        fun newIntent(context: Context, id: String, name: String): Intent {
            return Intent(context, DetailActivity::class.java).apply {
                putExtra(EXTRA_ID, id)
                putExtra(EXTRA_NAME, name)
            }
        }
    }
}

// ✅ KTX 扩展 companion 方法
fun MyClass.Companion.fromJson(json: String): MyClass {
    return Gson().fromJson(json, MyClass::class.java)
}

面试加分点

  • @JvmStatic vs @JvmField@JvmStatic 生成静态方法,@JvmField 生成静态字段
  • 伴生对象扩展函数 :可以用 MyClass.Companion.xxx 扩展
  • 伴生对象与 object 的区别:companion 只有一个,object 可以有多个

6.5 object 表达式 vs object 声明:这两个到底怎么用?

核心回答

  • object 声明 :定义一个单例(object MySingleton
  • object 表达式 :创建一个匿名对象(object : MyInterface {}
kotlin 复制代码
// object 声明:全局单例
object Config {
    val url = "https://api.example.com"
}

// object 表达式:一次性匿名对象
val listener = object : OnClickListener {
    override fun onClick(v: View) { }
}

Android 实战场景

kotlin 复制代码
// ✅ object 表达式:快速实现接口
binding.btnSubmit.setOnClickListener(object : View.OnClickListener {
    override fun onClick(v: View?) {
        // 实现逻辑
    }
})

// 简写后
binding.btnSubmit.setOnClickListener { /* 直接用 lambda */ }

// ✅ object 声明:ViewPager Adapter
class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
    // 匿名对象作为 DiffUtil Callback
    private val diffCallback = object : DiffUtil.ItemCallback<Item>() {
        override fun areItemsTheSame(old: Item, new: Item) = old.id == new.id
        override fun areContentsTheSame(old: Item, new: Item) = old == new
    }
}

// ✅ object 声明:Result 包装
sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val e: Throwable) : Result<Nothing>()
    
    // 匿名对象:扩展函数
    companion object {
        inline fun <T> runCatching(block: () -> T): Result<T> {
            return try {
                Success(block())
            } catch (e: Exception) {
                Error(e)
            }
        }
    }
}

面试加分点

  • 生命周期差异:object 声明是类加载时创建,object 表达式是执行到时才创建
  • Java 互调 :object 表达式生成的类名是 ClassName$1ClassName$2

6.6 Kotlin 构造函数的 init 顺序

核心回答 :Kotlin 构造函数执行顺序:主构造函数 → init 块(按出现顺序)→ 次构造函数。参数属性在 init 之前初始化。

kotlin 复制代码
class MyClass(val name: String) {
    init {
        println("init 1, name = $name")  // name 已初始化
    }
    
    val upperName = name.uppercase()  // 在 init 之前执行
    
    init {
        println("init 2, upperName = $upperName")  // upperName 也已初始化
    }
    
    constructor(extra: String) : this(extra.split(" ").first()) {
        println("secondary constructor")
    }
}

Android 实战场景

kotlin 复制代码
class UserViewModel(
    private val repository: UserRepository  // 主构造:依赖注入
) : ViewModel() {
    
    private val _state = MutableStateFlow<UserState>(UserState.Loading)
    
    init {
        // init 中访问 repository,这是安全的
        viewModelScope.launch {
            loadUsers()
        }
    }
    
    // ✅ 带默认值的次构造函数
    constructor(id: String) : this(DefaultRepository()) {
        // 先执行 DefaultRepository() 初始化
        // 再执行 init 块
        // 最后执行这里的逻辑
    }
}

// ✅ 多个 init 块:分段初始化逻辑
class ArticleDetailViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    
    // 1. 先获取 SavedStateHandle 中的数据
    private val articleId: String = savedStateHandle["article_id"] ?: ""
    
    init {
        // 2. 如果有缓存ID,加载缓存
        if (articleId.isNotEmpty()) {
            loadArticle(articleId)
        }
    }
    
    // 3. 带默认值的构造
    constructor() : this(SavedStateHandle())
}

面试加分点

  • 属性初始化顺序:主构造函数参数 → 属性声明处 → init 块

  • 构造顺序图

    plaintext

    kotlin 复制代码
    class Foo(x: Int) {
        val a = x + 1       // 第1步
        init { ... }        // 第2步
        val b = a + 1       // 第3步
    }
  • 次构造函数必须调用主构造 :使用 this()super()

七、Android 相关 Kotlin 特性

7.1 Hilt 依赖注入:这些注解到底怎么配合?

核心回答 :Hilt 是 Dagger 在 Android 的专用版,核心注解:@HiltAndroidApp@AndroidEntryPoint@Inject@Module@Provides

less 复制代码
// Application
@HiltAndroidApp
class MyApp : Application()

// Activity(或其他 EntryPoint)
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject lateinit var viewModel: MainViewModel
}

// ViewModel
@HiltViewModel
class MainViewModel @Inject constructor(
    private val repository: UserRepository  // 自动注入
) : ViewModel()

// Module
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    
    @Provides
    @Singleton
    fun provideOkHttp(): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor { chain ->
                chain.request().newBuilder()
                    .addHeader("Content-Type", "application/json")
                    .build()
                    .let { chain.proceed(it) }
            }
            .build()
    }
    
    @Provides
    @Singleton
    fun provideRetrofit(okHttp: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .client(okHttp)
            .build()
    }
}

Android 实战场景

kotlin 复制代码
// ✅ Repository 层注入
class UserRepository @Inject constructor(
    private val api: ApiService,
    private val database: UserDao
) {
    suspend fun getUser(id: String): User {
        return try {
            val remote = api.getUser(id)
            database.insert(remote)
            remote
        } catch (e: Exception) {
            database.getUser(id) ?: throw e
        }
    }
}

// ✅ Qualifier:同类型多个实例
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptor

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class LoggingInterceptor

@Module @InstallIn(SingletonComponent::class)
object InterceptorModule {
    @Provides
    @AuthInterceptor
    fun provideAuthInterceptor(): OkHttpInterceptor = AuthInterceptorImpl()
    
    @Provides
    @LoggingInterceptor
    fun provideLoggingInterceptor(): OkHttpInterceptor = LoggingInterceptor()
}

// ✅ AssistedInject 场景
class DetailViewModel @AssistedInject constructor(
    @Assisted private val itemId: String,
    private val repository: ItemRepository
) : ViewModel()

面试加分点

  • @Binds vs @Provides@Binds 适用于接口绑定,@Provides 适用于无法在接口标注的场景
  • Scope :默认 @Singleton 是全局单例,可自定义 @ActivityScoped 等
  • Dagger/Hilt 编译 :用 @Inject constructor 代替 @Provides 时,构造参数必须是可以被注入的类型

7.2 Kotlin 协程在 Android 中的最佳实践

核心回答 :用 viewModelScope 管理 ViewModel 生命周期,用 lifecycleScope 管理 Activity/Fragment,用 repeatOnLifecycle 防止内存泄漏。

kotlin 复制代码
// ✅ ViewModel 中
class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            // 自动绑定 ViewModel 生命周期
            // ViewModel 销毁时自动取消
        }
    }
}

// ✅ Activity/Fragment 中
class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        lifecycleScope.launch {
            // 绑定到 STARTED,onStop 时暂停,onResume 时恢复
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.data.collect { data ->
                    updateUI(data)
                }
            }
        }
    }
}

Android 实战场景

kotlin 复制代码
// ✅ Flow + repeatOnLifecycle 完整示例
class UserListFragment : Fragment() {
    
    private val viewModel: UserListViewModel by viewModels()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // ✅ 生命周期安全的 Flow 收集
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { state ->
                    when (state) {
                        is UiState.Loading -> showLoading()
                        is UiState.Success -> showUsers(state.data)
                        is UiState.Error -> showError(state.message)
                    }
                }
            }
        }
        
        // ✅ 或使用 KTX 扩展
        viewModel.uiState.onEach { state -> 
            updateUI(state)
        }.launchIn(viewLifecycleOwner.lifecycleScope)
    }
}

// ✅ 一次性事件用 SharedFlow
class UserListViewModel : ViewModel() {
    
    private val _oneTimeEvent = MutableSharedFlow<UiEvent>()
    val oneTimeEvent: SharedFlow<UiEvent> = _oneTimeEvent.asSharedFlow()
    
    fun onUserClicked(user: User) {
        viewModelScope.launch {
            _oneTimeEvent.emit(UiEvent.NavigateToDetail(user.id))
        }
    }
}

面试加分点

  • 为什么不用 GlobalScope:生命周期不受控制,容易内存泄漏
  • Dispatchers.Main.immediate:避免在主线程分发任务时的额外开销
  • NonCancellable :用 withContext(NonCancellable) { } 让取消期间继续执行

八、综合实战题

8.1 MVVM + 协程 + Flow 完整架构实现

核心回答:典型架构:Repository 提供数据源 → ViewModel 处理逻辑 → UI 收集 StateFlow。

scss 复制代码
UI (Activity/Fragment) 
    ↓ 观察
ViewModel (StateFlow) 
    ↓ 调用
UseCase (suspend)
    ↓ 调用
Repository (Flow) 
    ↓ 获取
DataSource (Room/Retrofit)

完整代码示例

kotlin 复制代码
// ========== Domain 层 ==========
data class User(val id: String, val name: String, val email: String)

interface UserRepository {
    suspend fun getUser(id: String): User
    fun observeUsers(): Flow<List<User>>
}

// ========== Data 层 ==========
class UserRepositoryImpl(
    private val api: ApiService,
    private val dao: UserDao
) : UserRepository {
    
    override suspend fun getUser(id: String): User {
        return api.getUser(id).toEntity()
    }
    
    override fun observeUsers(): Flow<List<User>> {
        return dao.getAllUsers().asFlow().map { entities ->
            entities.map { it.toDomain() }
        }
    }
}

// ========== UseCase 层 ==========
class GetUserUseCase(private val repository: UserRepository) {
    suspend operator fun invoke(userId: String): Result<User> {
        return try {
            Result.success(repository.getUser(userId))
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

// ========== ViewModel ==========
@HiltViewModel
class UserDetailViewModel @Inject constructor(
    private val getUserUseCase: GetUserUseCase,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    
    private val userId: String = savedStateHandle["user_id"] ?: ""
    
    private val _uiState = MutableStateFlow<UserDetailUiState>(UserDetailUiState.Loading)
    val uiState: StateFlow<UserDetailUiState> = _uiState.asStateFlow()
    
    init {
        loadUser()
    }
    
    private fun loadUser() {
        viewModelScope.launch {
            getUserUseCase(userId)
                .onSuccess { user ->
                    _uiState.value = UserDetailUiState.Success(user)
                }
                .onFailure { e ->
                    _uiState.value = UserDetailUiState.Error(e.message ?: "未知错误")
                }
        }
    }
    
    fun refresh() {
        loadUser()
    }
}

// ========== UI 层 ==========
@Composable
fun UserDetailScreen(viewModel: UserDetailViewModel) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
    when (val state = uiState) {
        is UserDetailUiState.Loading -> CircularProgressIndicator()
        is UserDetailUiState.Success -> UserDetailContent(state.user)
        is UserDetailUiState.Error -> ErrorContent(state.message)
    }
}

面试加分点

  • 单向数据流:State 不可变、Event 单向传递、副作用用 Channel/SharedFlow
  • UseCase 的价值:业务逻辑复用、测试方便、解耦 ViewModel 和 Data 层
  • SavedStateHandle:进程被系统杀死后恢复状态

九、最新技术趋势题(加分题)

9.1 Kotlin 2.0 和 K2 编译器:带来了什么变化?

核心回答 :K2 编译器是 Kotlin 历史上最大的架构升级,编译速度提升 2-3 倍,语义更准确,FE10 前端重写。

kotlin 复制代码
// K2 编译器的改进点

// 1. 编译速度提升
// K2 使用 FIR (Front-End Intermediate Representation)
// 不再依赖 Java 前端,实现增量编译优化

// 2. 语义准确性提升
// sealed interface(1.9已有)
sealed interface Result<out T> {
    data class Success<T>(val data: T) : Result<T>
    data class Error(val message: String) : Result<Nothing>
}

// 3. KSP vs KAPT
// KSP (Kotlin Symbol Processing) 比 KAPT 快 2-4 倍
// Hilt、Room 2.0+ 已支持 KSP

Android 实战场景

kotlin 复制代码
// ✅ 使用 KSP 加速构建
plugins {
    id("com.google.devtools.ksp") version "2.0.0-1.0.21"
}

dependencies {
    // Room KSP
    ksp("androidx.room:room-compiler:2.6.1")
    
    // Hilt KSP (2.48+)
    ksp("com.google.dagger:hilt-compiler:2.48")
}

// ✅ K2 兼容性注意事项
// K2 会更严格地检查某些语义
// 例如:隐式 receivers 的歧义检测更严格
class A {
    fun foo() = 1
}

class B {
    fun foo() = 2
}

class C : A(), B()

fun test(c: C) {
    // K2 会要求明确指定调用哪个 foo
    c.A::foo()  // 可能需要显式指定
}

面试加分点

  • K2 迁移时间表:Kotlin 2.0 稳定版已发布,但 KAPT 插件仍在维护
  • Compose + K2:Compose 编译器已迁移到 KSP,性能大幅提升
  • Gradle 8.x:配合 Kotlin 2.0,构建速度显著提升

9.2 Kotlin Multiplatform(KMP)现状

核心回答 :KMP 让 Kotlin 代码运行在 Android、iOS、Web、Server。2024年 Beta,2025年稳定中。

kotlin 复制代码
// ✅ 共享业务逻辑
expect class PlatformLogger() {
    fun log(message: String)
}

actual class PlatformLogger() {
    actual fun log(message: String) {
        // 平台特定实现
        console.log(message)  // JS
        // NSLog(message)      // iOS
    }
}

// ✅ 平台特定实现
// androidMain, iosMain, jsMain, jvmMain

面试加分点

  • Compose Multiplatform:共享 UI 代码(JetBrains 主推)
  • 与 Flutter/React Native 的区别:KMP 复用原生 UI,更接近原生体验
  • 适用场景:SDK、中间件、核心算法共享

总结

Kotlin 面试的核心考点就这些:

  1. 语法层面:val/var、空安全、扩展函数、Lambda
  2. 原理层面:inline/reified 原理、协程状态机、泛型擦除
  3. 实践层面:Android 协程最佳实践、Flow vs LiveData、Hilt
  4. 高级层面:字节码分析、K2 编译器、Multiplatform

面试技巧

  • 问到原理时,试着画状态机或写伪代码
  • 对比类问题,用表格展示,一目了然
  • 结合 Android 场景回答,比纯语法题加分
相关推荐
plainGeekDev2 小时前
Kotlin 泛型与扩展:out/in 搞不懂?扩展函数到底扩展了啥?
kotlin
plainGeekDev2 小时前
Kotlin 特殊类型篇:密封类比枚举好使在哪?Nothing 到底是个啥?
kotlin
沅霖4 小时前
Android Studio Java工程开发环境,怎么切换到Kotlin开发环境
android·kotlin·android studio
Kapaseker5 小时前
Kotlin SharedFlow 的三个参数到底有啥用
android·kotlin
阿巴斯甜5 小时前
by 和by lazy 懒加载
kotlin
三少爷的鞋7 小时前
Android 架构系列之MVVM 和 MVI 算架构吗?
android·kotlin
只可远观1 天前
Android 自动埋点(页面打开 / 关闭 + 点击事件)完整方案
android·kotlin
aqi001 天前
FFmpeg开发笔记(一百零二)国产的音视频移动开源工具FFmpegAndroid
android·ffmpeg·kotlin·音视频·直播·流媒体
阿巴斯甜1 天前
子协程的异常传播(CoroutineExceptionHandler ):
kotlin