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 块,除非用Synchronized或BlockingScheduler - 进阶: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 会自动生成 equals、hashCode、toString、copy、componentN 函数。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 实现
-
错误场景:
golateinit 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
rustwhen (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")
}
}
面试加分点:
-
扩展函数不能重写 :因为是静态分发,看的是变量的静态类型而非运行时类型
kotlinopen 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 使用 SupervisorJob :
viewModelScope.launch失败不会影响其他 launch - CoroutineExceptionHandler:全局异常处理器,用于记录日志、上报
- CancellationException:协程取消时会抛出这个异常,通常不需要捕获
3.6 Flow vs LiveData:该怎么选?
核心回答 :LiveData 是Android 专属 ,只能在主线程观察;Flow 是Kotlin 协程原生,支持背压、任意线程、更多操作符。
表格
| 特性 | LiveData | Flow |
|---|---|---|
| 线程安全 | 主线程观察 | 可配置 |
| 背压处理 | 无 | 有(buffer、collectLatest等) |
| 操作符 | 有限 | 丰富(map、filter、flatMap...) |
| Android 生命周期 | 自动绑定 | 需要 repeatOnLifecycle |
| null 支持 | 可观察 null | 用 null 或 flowOf(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 函数内部 ,
withContext、delay、yield等 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/outkotlinfun 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 泛型擦除后,编译器会生成桥接方法保持多态
csharpclass Node<T> { T data; void setData(T data) { } } // 擦除后生成桥接方法:void setData(Object data) -
类型Token解决方案 :用
TypeToken或reified获取运行时类型
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实现懒加载kotlinobject HeavyResource { val data by lazy { loadData() } } -
线程安全 :JVM 保证
<clinit>线程安全 -
Java 互调 :
@JvmStatic、@JvmField影响生成的 Java 代码
6.3 委托 by:属性委托和类委托有什么区别?
核心回答:
- 属性委托 :
by lazy、by 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(默认)、PUBLICATION、NONE -
ReadOnlyProperty vs ReadWriteProperty:区分只读和可写属性
-
map 委托:直接用 Map 存储属性
kotlinclass 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$1、ClassName$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
kotlinclass 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 面试的核心考点就这些:
- 语法层面:val/var、空安全、扩展函数、Lambda
- 原理层面:inline/reified 原理、协程状态机、泛型擦除
- 实践层面:Android 协程最佳实践、Flow vs LiveData、Hilt
- 高级层面:字节码分析、K2 编译器、Multiplatform
面试技巧:
- 问到原理时,试着画状态机或写伪代码
- 对比类问题,用表格展示,一目了然
- 结合 Android 场景回答,比纯语法题加分