一、基础语法与特性
1. val 和 var 的区别?
val:声明不可变变量(只读),类似 Java 的final。var:声明可变变量。- 推荐优先使用
val,提高代码安全性。
2. lateinit 和 by lazy 的区别?
| 特性 | lateinit |
by lazy |
|---|---|---|
| 类型 | 只能用于 var,且不能是基本类型 |
只能用于 val |
| 初始化 | 手动赋值(延迟到运行时) | 第一次访问时初始化 |
| 线程安全 | 否(需手动保证) | 是(默认 LazyThreadSafetyMode.SYNCHRONIZED) |
| 使用场景 | 依赖注入、Android onCreate() 中初始化 |
单例、耗时对象初始化 |
kotlin
深色版本
class MyClass {
lateinit var name: String // 必须在使用前初始化
val data by lazy { expensiveOperation() } // 第一次访问时计算
}
3. == 和 === 的区别?
==:结构相等(调用equals())。===:引用相等(内存地址相同)。- 对于基本类型,
===会进行值比较(装箱优化)。
4. data class 有什么作用?自动生成哪些方法?
- 用于数据载体类,自动提供:
equals()/hashCode()toString()(格式:User(name=John, age=30))copy()(复制并可修改部分属性)componentN()(解构支持)
- 要求主构造函数至少有一个参数。
kotlin
深色版本
data class User(val name: String, val age: Int)
5. sealed class(密封类)的作用?
- 表示受限的类继承结构,所有子类必须在同一个文件中定义。
- 在
when表达式中可以穷尽检查,无需else分支。
kotlin
深色版本
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val exception: Exception) : Result()
}
fun handle(result: Result) = when(result) {
is Success -> println(result.data)
is Error -> println(result.exception)
// 不需要 else,编译器知道已覆盖所有情况
}
二、空安全(Null Safety)
6. Kotlin 如何解决空指针异常(NPE)?
- 类型系统区分可空类型(
String?)和非空类型(String)。 - 非空类型不能赋值为
null。 - 可空类型必须进行空检查才能使用。
7. 安全调用(?.)、Elvis 操作符(?:)、非空断言(!!)的区别?
kotlin
深色版本
val name: String? = getName()
// 安全调用:如果 name 不为 null,调用 toUpperCase()
val upper = name?.toUpperCase()
// Elvis 操作符:提供默认值
val result = name ?: "Unknown"
// 非空断言:强制认为不为 null,可能抛出 NPE
val mustHave = name!!.length
8. 如何安全地进行类型转换?
- 使用
as?(安全转换):
kotlin
深色版本
val str: String? = obj as? String
三、函数与高阶函数
9. 什么是高阶函数?举例说明。
- 函数可以作为参数或返回值。
kotlin
深色版本
fun operate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
val result = operate(2, 3) { x, y -> x + y }
10. inline 函数的作用?
- 内联:编译时将函数体插入调用处,避免创建匿名类和对象(提升性能)。
- 常用于高阶函数(如
let、apply、run)。 - 使用
noinline可阻止部分参数内联,crossinline限制return。
11. 常用作用域函数(let、run、with、apply、also)的区别?
| 函数 | 上下文对象 | 返回值 | 适用场景 |
|---|---|---|---|
let |
it |
Lambda 结果 | 空安全调用、局部变量 |
run |
this |
Lambda 结果 | 多个操作、初始化 |
with |
this |
Lambda 结果 | 调用对象多个方法 |
apply |
this |
对象自身 | 对象配置(Builder 模式) |
also |
it |
对象自身 | 副作用(如日志) |
kotlin
深色版本
val user = User()
.apply { name = "John"; age = 30 } // 配置
.also { println("Created: $it") } // 副作用
四、协程(Coroutines)
12. 什么是协程?与线程的区别?
- 协程:轻量级线程,由用户态调度,挂起不阻塞线程。
- 线程:操作系统调度,阻塞会占用线程资源。
- 协程开销小,可创建成千上万个。
13. launch 和 async 的区别?
launch:启动协程,返回Job,用于不返回结果的场景。async:启动协程,返回Deferred<T>,用于需要返回结果的场景,需调用.await()。
kotlin
深色版本
// launch:只关心执行
scope.launch { fetchData() }
// async:需要结果
val deferred = scope.async { fetchData() }
val result = deferred.await()
14. 协程的上下文(CoroutineContext)包含哪些元素?
Job:协程的生命周期。Dispatcher:指定运行线程(如Dispatchers.IO、Main)。CoroutineExceptionHandler:处理未捕获异常。CoroutineName:协程名称(调试用)。
15. viewModelScope 和 lifecycleScope 的作用?
viewModelScope:在ViewModel中使用,ViewModel销毁时自动取消协程。lifecycleScope:在Activity/Fragment中使用,生命周期结束时取消。- 都属于 AndroidX Lifecycle 库,避免内存泄漏。
五、面向对象与继承
16. Kotlin 类默认是 final 的,如何允许继承?
- 使用
open关键字:
kotlin
深色版本
open class Animal // 允许继承
class Dog : Animal() // 继承
17. object 关键字有哪些用法?
- 单例模式:
kotlin深色版本
object NetworkManager { fun request() { ... } } - 伴生对象(Companion Object):
kotlin深色版本
class MyClass { companion object { const val TAG = "MyClass" } } - 对象表达式(匿名内部类):
kotlin深色版本
val listener = object : OnClickListener { ... }
六、集合与函数式编程
18. map、flatMap、filter、reduce 的作用?
kotlin
深色版本
val list = listOf(1, 2, 3)
list.map { it * 2 } // [2, 4, 6]
list.filter { it > 1 } // [2, 3]
list.reduce { acc, i -> acc + i } // 6
val nested = listOf(listOf(1,2), listOf(3,4))
nested.flatMap { it } // [1, 2, 3, 4] (展平)
19. MutableList 和 List 的区别?
List:只读接口(不能添加/删除)。MutableList:可变接口。
七、与 Java 互操作
20. Kotlin 如何调用 Java 代码?有哪些注意事项?
- 大部分无缝互操作。
- 注意:
- Java 的
null在 Kotlin 中是可空类型。 - Java 集合在 Kotlin 中可能是可变的。
- 使用
@JvmOverloads、@JvmStatic、@JvmField控制 JVM 字节码生成。
- Java 的
21. @JvmOverloads 的作用?
- 让 Kotlin 函数支持默认参数,在 Java 中生成多个重载方法。
kotlin
深色版本
@JvmOverloads
fun greet(name: String = "World", age: Int = 0) { ... }
Java 中可调用:greet(), greet("John"), greet("John", 25)。
八、进阶问题
22. tailrec 关键字的作用?
- 标记尾递归函数,编译器将其优化为循环,避免栈溢出。
kotlin
深色版本
tailrec fun factorial(n: Int, acc: Int = 1): Int =
if (n <= 1) acc else factorial(n - 1, n * acc)
23. 协程取消是协作式的,如何检查取消?
- 使用
ensureActive()或检查isActive:
kotlin
深色版本
while (isActive) {
// 执行任务
}
24. Channel 和 Flow 的区别?
Channel:冷流,生产者-消费者队列,支持挂起。Flow:热流,冷数据流,用于异步数据序列,支持背压。
总结
Kotlin 面试重点:
- 空安全:
?、!!、?:、?. - 协程:
launch/async、scope、异常处理 - 函数式编程:高阶函数、
let/apply等作用域函数 - 语法糖:
data class、sealed class、object - Android 特有:
viewModelScope、lifecycleScope
建议结合实际项目经验,解释这些特性如何提升代码质量、可读性和安全性
一、深入理解 Kotlin 编译与字节码
25. const val 和 val 的区别?什么情况下必须用 const?
val:运行时赋值(可以是函数调用结果)。const val:编译期常量,必须是基本类型或String,且值在编译时确定。const可用于注解、when分支、默认参数。
kotlin
深色版本
const val TAG = "MyActivity" // ✅ 可用于注解
val version = getVersion() // ❌ 不能用于注解
@SomeAnnotation(TAG) // 只有 const val 可以
fun log(message: String = TAG) { ... } // 默认参数也需 const
26. @JvmStatic 和 @JvmField 的作用?
@JvmStatic:在伴生对象中,生成 静态方法(而非静态内部类的实例方法)。@JvmField:将属性暴露为 public 字段,不生成 getter/setter。
kotlin
深色版本
class Utils {
companion object {
@JvmStatic fun doWork() { ... } // Java 中可直接 Utils.doWork()
@JvmField val NAME = "Utils" // Java 中可直接 Utils.NAME
}
}
27. Kotlin 的默认参数是如何在字节码中实现的?
- 编译器会生成多个重载方法(除非加
@JvmOverloads)。 - 调用时,未传参的位置由编译器插入默认值。
- 在 Java 中调用带默认参数的 Kotlin 函数,必须使用
@JvmOverloads才能生成重载。
二、协程进阶与实战
28. 协程取消后,如何确保资源正确释放(如文件、网络连接)?
- 使用
try { ... } finally { ... }或use语句。 finally块中的代码总是执行,即使协程被取消。
kotlin
深色版本
scope.launch {
val file = openFile()
try {
while (isActive) {
// 读取文件
}
} finally {
file.close() // 即使被 cancel(),也会执行
}
}
29. withContext(Dispatcher.IO) 和 launch(Dispatcher.IO) 的区别?
withContext:挂起函数,切换上下文并返回结果,常用于函数内部。launch:启动新协程,不阻塞当前协程,通常用于“开火即忘”(fire and forget)。
kotlin
深色版本
// 推荐:在 suspend 函数中使用 withContext
suspend fun fetchData(): String {
return withContext(Dispatcher.IO) {
// 耗时操作
}
}
// launch 用于启动独立任务
scope.launch { fetchData() }
30. 什么是“协程泄露”(Coroutine Leak)?如何避免?
- 定义:协程启动后未被正确取消或等待,导致资源浪费或内存泄漏。
- 场景:
- 在
ViewModel中启动launch但未绑定viewModelScope。 async启动后未调用await()或cancel()。
- 在
- 避免:
- 使用结构化并发(
CoroutineScope)。 - 优先使用
viewModelScope/lifecycleScope。 - 对
async的结果必须处理。
- 使用结构化并发(
三、泛型与类型系统
31. in 和 out 关键字的作用?(型变)
out T(协变):T只作为返回值(生产者),List<Cat>是List<Animal>的子类型。in T(逆变):T只作为参数(消费者),Comparator<Animal>是Comparator<Cat>的子类型。
kotlin
深色版本
interface Producer<out T> {
fun get(): T // T 是输出
}
interface Consumer<in T> {
fun consume(item: T) // T 是输入
}
32. reified 类型参数的作用?
- 允许在内联函数中使用
T::class或is T,因为类型在编译时已知。
kotlin
深色版本
inline fun <reified T> getInstance(): T {
return when (T::class) {
String::class -> "Hello" as T
Int::class -> 42 as T
else -> throw IllegalArgumentException()
}
}
四、DSL(领域特定语言)
33. Kotlin 如何构建 DSL?@DslMarker 的作用?
- 使用 lambda with receiver 和 高阶函数 构建 DSL。
@DslMarker确保 DSL 的层级结构,防止跨层级调用。
kotlin
深色版本
@DslMarker
annotation class HtmlTagMarker
@HtmlTagMarker
class Tag(val name: String) {
fun render() = "<$name>$content</$name>"
private var content = ""
fun content(text: String) { content += text }
}
fun tag(name: String, init: Tag.() -> Unit): Tag {
val tag = Tag(name)
tag.init() // 在 Tag 的上下文中执行
return tag
}
// 使用 DSL
val html = tag("div") {
content("Hello")
// 无法调用其他 Tag 的方法,因为 @DslMarker 限制了作用域
}
这是 Kotlin 构建类型安全 HTML、Gradle 脚本、Ktor 路由等的基础。
五、性能与最佳实践
34. Sequence 和 List 的操作有何不同?何时使用 Sequence?
List操作:立即执行,每步生成新集合(中间集合开销大)。Sequence操作:惰性求值,类似 Java Stream,只有在toList()或forEach()时才执行。
kotlin
深色版本
// List:三次遍历,生成两个中间列表
list.filter { it > 0 }.map { it * 2 }.first { it > 10 }
// Sequence:一次遍历,无中间集合
sequence.filter { it > 0 }.map { it * 2 }.first { it > 10 }
- 使用场景:大数据集、复杂链式操作、早期终止(如
first)。
35. 如何避免 Kotlin 的“过度语法糖”导致代码可读性下降?
- 原则:
- 不滥用
apply/also嵌套。 - 复杂逻辑避免单行 lambda。
- 团队统一编码规范。
- 优先选择清晰而非“聪明”的代码。
- 不滥用
六、Android 与 Kotlin 特有
36. by viewModels() 和 by activityViewModels() 的原理?
- 基于 委托属性(
Delegates)和ViewModelProvider。 by调用getValue(),内部通过ViewModelProvider获取ViewModel。- 自动绑定生命周期。
37. Kotlin 的 when 表达式比 Java switch 强在哪里?
- 支持任意类型(对象、字符串、枚举)。
- 支持范围(
in 1..10)、类型检查(is String)、条件(-> value > 0)。 - 可作为表达式返回值。
kotlin
深色版本
val result = when (x) {
in 1..10 -> "Small"
is String -> "It's a string"
else -> "Unknown"
}
七、开放性问题(考察设计能力)
38. 如何用 Kotlin 实现一个简单的依赖注入(DI)容器?
- 使用
object存储单例。 - 使用高阶函数注册和获取实例。
- 利用委托属性延迟初始化。
39. 如何设计一个线程安全的缓存(Cache)?
- 使用
ConcurrentHashMap。 - 结合
@Volatile和双重检查锁定(Double-Checked Locking)。 - 或使用
synchronized块。
40. Kotlin 多平台(KMP)的了解?
- 一套代码,多平台共享(Android、iOS、Web、Desktop)。
- 共享业务逻辑、网络、数据层。
- 平台特定代码通过
expect/actual声明。
总结:Kotlin 面试准备策略
| 层次 | 考察点 | 准备建议 |
|---|---|---|
| 基础 | val/var、空安全、函数 |
熟练语法,能手写 |
| 核心 | 协程、作用域函数、集合 | 理解原理,能解释区别 |
| 进阶 | 泛型、DSL、字节码 | 了解编译机制,有实战经验 |
| 实战 | 内存泄漏、性能优化、架构 | 结合项目讲清楚“为什么” |
最后建议:
- 准备 1-2 个 Kotlin 特性优化项目代码 的例子(如用
sealed class替代枚举,用协程替代回调)。 - 了解 Kotlin 1.9+ 的新特性(如
K2编译器、required关键字等)。
Kotlin 不仅是语法糖,更是一种设计哲学。面试官希望看到你如何用 Kotlin 写出更安全、更简洁、更易维护的代码