Kotlin 面试题全面解析:从基础到进阶
前言
随着 Kotlin 在 Android 开发、服务端开发以及跨平台领域的广泛应用,越来越多的公司在面试中加入了 Kotlin 相关的考察。本文精心整理了 40+ 道高频 Kotlin 面试题,涵盖基础语法、面向对象、函数式编程、协程、集合与泛型等核心主题,并附上详细解析和代码示例,帮助你在面试中脱颖而出。
目录
- 基础语法篇
- 空安全篇
- 面向对象篇
- [函数与 Lambda 篇](#函数与 Lambda 篇)
- 集合与泛型篇
- 协程篇
- [Kotlin 与 Java 互操作篇](#Kotlin 与 Java 互操作篇)
- 高级特性篇
- 实战与设计模式篇
一、基础语法篇
Q1:val 和 var 有什么区别?val 就是常量吗?
答:
val(value):只读变量 ,一旦赋值后不能重新赋值,类似 Java 的finalvar(variable):可变变量,可以重新赋值
⚠️ 重要澄清:val 不等于常量!
kotlin
val list = mutableListOf(1, 2, 3)
// list = mutableListOf(4, 5, 6) // ❌ 不能重新赋值
list.add(4) // ✅ 但可以修改其内容!
println(list) // [1, 2, 3, 4]
val 只保证引用不可变 ,但引用指向的对象内容可能是可变的。
真正的编译时常量应该使用 const val:
kotlin
const val MAX_COUNT = 100 // 编译时常量,必须是顶层或 object 中的基本类型/String
class Config {
val runtimeVal = System.currentTimeMillis() // 运行时确定的只读变量
// const val x = System.currentTimeMillis() // ❌ const val 必须编译时确定
}
const val 与 val 的区别:
| 特性 | const val |
val |
|---|---|---|
| 赋值时机 | 编译时 | 运行时 |
| 允许的类型 | 基本类型和 String | 任意类型 |
| 允许的位置 | 顶层、object、companion object | 任意位置 |
| 编译后 | 内联替换 | 生成字段+getter |
Q2:Kotlin 的基本数据类型和 Java 有什么不同?
答:
Kotlin 中没有原始类型(primitive types)的概念,一切皆对象:
kotlin
val a: Int = 42 // 看起来是对象
val b: Double = 3.14
val c: Boolean = true
但在编译后,Kotlin 编译器会自动优化:
- 不可空类型(如
Int)编译为 Java 的基本类型(int) - 可空类型(如
Int?)编译为 Java 的包装类型(Integer)
kotlin
val a: Int = 42 // 编译为 int
val b: Int? = 42 // 编译为 Integer
// 这会影响同一性判断
val c: Int = 10000
val d: Int? = c
val e: Int? = c
println(d == e) // true(结构相等,比较值)
println(d === e) // false(引用相等,可能是不同的 Integer 对象)
Q3:== 和 === 的区别是什么?
答:
==:结构相等 (Structural Equality),等价于 Java 的equals()===:引用相等 (Referential Equality),等价于 Java 的==
kotlin
val str1 = String("Hello".toCharArray())
val str2 = String("Hello".toCharArray())
println(str1 == str2) // true ------ 内容相同
println(str1 === str2) // false ------ 不是同一个对象
data class User(val name: String)
val u1 = User("Kotlin")
val u2 = User("Kotlin")
println(u1 == u2) // true ------ data class 自动重写了 equals
println(u1 === u2) // false ------ 两个不同的实例
📌 面试加分点: Kotlin 的
==是 null 安全的。null == null返回true,null == "hello"返回false,不会抛 NPE。
Q4:Kotlin 中的类型转换怎么做?as 和 as? 有什么区别?
答:
kotlin
// 不安全的类型转换
val obj: Any = "Hello"
val str: String = obj as String // ✅ 成功
// val num: Int = obj as Int // ❌ 抛出 ClassCastException
// 安全的类型转换
val str2: String? = obj as? String // ✅ "Hello"
val num2: Int? = obj as? Int // ✅ null(转换失败返回 null)
// 智能类型转换(Smart Cast)
fun printLength(obj: Any) {
if (obj is String) {
// 编译器自动将 obj 转换为 String,无需手动 cast
println("长度: ${obj.length}")
}
}
Q5:Kotlin 的 when 表达式和 Java 的 switch 有什么不同?
答:
when 是 switch 的超级增强版:
kotlin
// 1. 基本用法
when (x) {
1 -> println("一")
2, 3 -> println("二或三") // 多个值匹配
in 4..10 -> println("4到10") // 范围匹配
!in 11..20 -> println("不在11到20之间")
else -> println("其他")
}
// 2. 类型匹配
when (obj) {
is String -> println(obj.length) // 自动智能转换
is Int -> println(obj + 1)
is List<*> -> println(obj.size)
}
// 3. 无参数形式(替代 if-else if 链)
when {
score >= 90 -> grade = "A"
score >= 80 -> grade = "B"
score >= 60 -> grade = "C"
else -> grade = "F"
}
// 4. 作为表达式(有返回值)
val result = when (statusCode) {
200 -> "成功"
404 -> "未找到"
500 -> "服务器错误"
else -> "未知状态" // 作为表达式时 else 通常必须存在
}
// 5. 捕获 when 的主语
when (val response = executeRequest()) {
is Success -> println(response.data)
is Error -> println(response.message)
}
与 Java switch 的关键区别:
- 不需要
break,不会贯穿(fall-through) - 可以匹配任意表达式,不限于常量
- 可以作为表达式返回值
- 支持类型检查、范围检查等
Q6:Unit、Nothing 和 Any 分别是什么?
答:
这三个类型是 Kotlin 类型系统的基石:
Any ← 所有非空类型的根类型
/ | \
Int String ...
\ | /
Nothing ← 所有类型的子类型(底类型)
Any? ← 所有类型的根类型(包括可空)
/ | \
Int? String? ...
\ | /
Nothing? ← 唯一的值是 null
Any: 所有非空类型的超类型,类似 Java 的 Object
kotlin
fun process(value: Any) {
println(value) // 可以接收任何非空值
}
Unit: 表示"无有意义的返回值",类似 Java 的 void,但 Unit 是一个真正的类型,有且仅有一个实例
kotlin
fun greet(): Unit { // 返回类型可以省略
println("Hello")
}
// Unit 是真正的类型,可以用在泛型中
val callback: () -> Unit = { println("done") }
Nothing: 表示"永远不会返回"的类型,没有任何实例
kotlin
// 函数永远不会正常返回
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
// 无限循环
fun infinite(): Nothing {
while (true) { /* ... */ }
}
// Nothing 的实际应用:让编译器推断类型
val result = data ?: throw IllegalStateException("data is null")
// throw 表达式的类型是 Nothing,Nothing 是所有类型的子类型
// 所以 result 的类型就是 data 的类型
二、空安全篇
Q7:Kotlin 是如何解决空指针问题的?
答:
Kotlin 在类型系统层面区分了可空类型和非空类型:
kotlin
var a: String = "hello" // 非空类型,不能赋值 null
var b: String? = "hello" // 可空类型,可以赋值 null
// a = null // ❌ 编译错误
b = null // ✅
// b.length // ❌ 编译错误:可空类型不能直接调用方法
处理可空类型的四种方式:
kotlin
val name: String? = getNullableName()
// 方式1:安全调用操作符 ?.
val length: Int? = name?.length
// 方式2:Elvis 操作符 ?:
val length: Int = name?.length ?: 0
// 方式3:非空断言 !!(危险!)
val length: Int = name!!.length // 如果 name 为 null,抛出 NPE
// 方式4:条件检查(智能转换)
if (name != null) {
println(name.length) // 编译器自动推断 name 为非空
}
Q8:?.、?:、!!、?.let 分别是什么?各自的使用场景?
答:
kotlin
val user: User? = findUser(id)
// 1. ?. 安全调用:如果为 null 则整个表达式返回 null
val city: String? = user?.address?.city
// 2. ?: Elvis 操作符:左边为 null 时使用右边的默认值
val name: String = user?.name ?: "Anonymous"
val validated = user ?: throw IllegalStateException("User not found")
val early = user ?: return // 结合 return 提前退出
// 3. !! 非空断言:强制转为非空,为 null 则抛 KotlinNullPointerException
val name: String = user!!.name // 尽量避免使用!
// 4. ?.let:不为 null 时执行代码块
user?.let { nonNullUser ->
println("用户名: ${nonNullUser.name}")
sendEmail(nonNullUser.email)
}
// 5. 组合使用
user?.let {
println("找到用户: ${it.name}")
} ?: println("用户不存在")
最佳实践:
- 优先使用
?.和?:的组合 !!仅在你 100% 确定不为 null 时使用(或在测试代码中)?.let适合需要对非空值执行多步操作的场景
Q9:lateinit 和 lazy 有什么区别?
答:
这是高频面试题,两者都是延迟初始化的手段,但应用场景完全不同:
kotlin
// lateinit:延迟初始化 var 变量
class UserActivity : AppCompatActivity() {
lateinit var adapter: UserAdapter // 声明时不初始化
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = UserAdapter() // 稍后初始化
// 检查是否已初始化
if (::adapter.isInitialized) {
adapter.loadData()
}
}
}
// lazy:懒加载 val 变量
class Config {
val databaseConnection: Connection by lazy {
println("创建数据库连接...") // 只在第一次访问时执行
DriverManager.getConnection("jdbc:mysql://localhost/db")
}
}
对比:
| 特性 | lateinit |
by lazy |
|---|---|---|
| 修饰对象 | var |
val |
| 初始化时机 | 手动赋值 | 第一次访问时自动初始化 |
| 可用类型 | 非空、非基本类型 | 任意类型 |
| 线程安全 | 不保证 | 默认线程安全(LazyThreadSafetyMode) |
| 是否可重新赋值 | ✅ 可以 | ❌ 不可以 |
| 空安全 | 不支持可空类型 | 支持可空类型 |
| 使用位置 | 类属性 | 类属性、局部变量、顶层属性 |
| 检测初始化 | ::prop.isInitialized |
不需要(自动处理) |
by lazy 的线程安全模式:
kotlin
// 默认:同步锁,线程安全(性能稍低)
val a by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { compute() }
// 发布模式:允许多线程同时初始化,但只有一个值生效
val b by lazy(LazyThreadSafetyMode.PUBLICATION) { compute() }
// 无锁:不保证线程安全(性能最高,单线程使用)
val c by lazy(LazyThreadSafetyMode.NONE) { compute() }
三、面向对象篇
Q10:Kotlin 的 class、data class、sealed class、enum class、object、abstract class、interface 各自的特点和区别?
答:
kotlin
// 1. 普通类
class Person(val name: String, var age: Int) {
fun introduce() = "我是 $name,今年 $age 岁"
}
// 2. 数据类 - 自动生成 equals/hashCode/toString/copy/componentN
data class User(val name: String, val email: String)
// 3. 密封类 - 受限的类层次结构
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
data object Loading : Result<Nothing>()
}
// 4. 枚举类
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF);
fun toHex() = "#${rgb.toString(16).padStart(6, '0')}"
}
// 5. object - 单例
object DatabaseManager {
fun connect() { /* ... */ }
}
// 6. 抽象类
abstract class Shape {
abstract fun area(): Double
fun describe() = "这是一个形状,面积为 ${area()}" // 可以有具体实现
}
// 7. 接口
interface Clickable {
fun click()
fun showOff() = println("I'm clickable!") // 可以有默认实现
}
核心区别总结:
| 类型 | 可实例化 | 可继承 | 状态 | 主要用途 |
|---|---|---|---|---|
class |
✅ | 需要 open |
✅ | 通用类 |
data class |
✅ | ❌ (默认 final) | ✅ | 数据载体 |
sealed class |
❌ | ✅ (受限) | ✅ | 受限类型层次 |
enum class |
❌ | ❌ | ✅ | 有限枚举值 |
object |
❌ (单例) | 需要 open |
✅ | 单例 |
abstract class |
❌ | ✅ | ✅ | 模板基类 |
interface |
❌ | ✅ | ❌ (无字段*) | 能力契约 |
Q11:data class 的要求和自动生成了什么?
答:
要求:
- 主构造函数至少有一个参数
- 主构造函数参数必须标记为
val或var - 不能是
abstract、open、sealed、inner类
自动生成的方法(仅基于主构造函数中的属性):
kotlin
data class User(val name: String, val age: Int) {
var nickname: String = "" // ⚠️ 不参与自动生成的方法!
}
val user1 = User("Alice", 25)
val user2 = User("Alice", 25)
// 1. equals() / hashCode() - 基于 name 和 age
println(user1 == user2) // true(nickname 不参与比较)
// 2. toString()
println(user1) // User(name=Alice, age=25)(nickname 不包含)
// 3. copy() - 浅拷贝,可选修改部分属性
val user3 = user1.copy(age = 26)
println(user3) // User(name=Alice, age=26)
// 4. componentN() - 解构声明
val (name, age) = user1
println("$name is $age years old")
⚠️ 面试易错点:
data class的equals()、hashCode()、toString()、copy()和componentN()只考虑主构造函数中的属性,类体中声明的属性不参与!
Q12:sealed class 和 enum class 的区别?sealed class 和 sealed interface 的区别?
答:
sealed class vs enum class:
kotlin
// enum:每个枚举值是单例,且类型完全相同
enum class Direction {
NORTH, SOUTH, EAST, WEST // 每个值都是 Direction 的实例
}
// sealed:子类可以有不同的类型、不同的属性、多个实例
sealed class NetworkResult {
data class Success(val data: String, val code: Int) : NetworkResult()
data class Error(val message: String) : NetworkResult()
data object Loading : NetworkResult()
}
| 特性 | enum class |
sealed class |
|---|---|---|
| 实例数量 | 每个值固定一个实例 | 子类可以有多个实例 |
| 子类状态 | 所有值相同的属性 | 每个子类可以有不同的属性 |
| 子类类型 | 都是同一类型 | 可以是 class、data class、object |
| 适用场景 | 有限、固定的值集合 | 有限的类型层次,但每种类型有不同数据 |
sealed class vs sealed interface:
kotlin
sealed interface Error {
data class NetworkError(val code: Int) : Error
data class DatabaseError(val query: String) : Error
}
sealed interface Cacheable
// sealed interface 允许多继承
data class CachedNetworkError(val code: Int) : Error, Cacheable
sealed interface 在 Kotlin 1.5 引入,允许子类实现多个 sealed interface,更加灵活。
Q13:object、companion object、匿名对象的区别?
答:
kotlin
// 1. object 声明 - 单例
object AppConfig {
var debug = false
fun init() { /* ... */ }
}
AppConfig.init() // 直接通过类名调用
// 2. companion object - 伴生对象(类似 Java 的 static)
class User private constructor(val name: String) {
companion object Factory { // 名称可省略
fun create(name: String): User = User(name)
const val MAX_AGE = 150
}
}
val user = User.create("Alice") // 通过类名调用
// 也可以:User.Factory.create("Alice")
// 3. 匿名对象 - 替代 Java 的匿名内部类
val listener = object : View.OnClickListener {
override fun onClick(v: View?) {
println("Clicked!")
}
}
// 匿名对象也可以不实现任何接口
val point = object {
val x = 10
val y = 20
}
println("${point.x}, ${point.y}")
核心区别:
| 特性 | object |
companion object |
匿名对象 |
|---|---|---|---|
| 本质 | 单例类 | 类的伴生单例 | 临时对象表达式 |
| 有名字 | ✅ | ✅ (可选) | ❌ |
| 实例数量 | 全局唯一 | 每个类唯一 | 每次创建新实例 |
| 可以继承/实现 | ✅ | ✅ | ✅ |
| 使用场景 | 全局单例 | 工厂方法、常量 | 回调、临时实现 |
Q14:Kotlin 中的构造函数有哪些?init 块的执行时机是什么?
答:
kotlin
class Person(
val name: String, // 主构造函数参数 + 属性声明
var age: Int
) {
// 属性初始化
val nameLength = name.length.also { println("1. 属性初始化: nameLength = $it") }
// 初始化块(可以有多个,按声明顺序执行)
init {
println("2. init 块 1: name=$name, age=$age")
require(age >= 0) { "年龄不能为负数" }
}
val upperName = name.uppercase().also { println("3. 属性初始化: upperName = $it") }
init {
println("4. init 块 2")
}
// 次构造函数:必须委托给主构造函数
constructor(name: String) : this(name, 0) {
println("5. 次构造函数")
}
}
// 调用 Person("Kotlin") 的输出顺序:
// 1. 属性初始化: nameLength = 6
// 2. init 块 1: name=Kotlin, age=0
// 3. 属性初始化: upperName = KOTLIN
// 4. init 块 2
// 5. 次构造函数
执行顺序总结:
- 主构造函数参数传入
- 属性初始化和
init块按照声明顺序依次执行 - 次构造函数体执行
Q15:Kotlin 中 open、final、abstract 的含义?为什么默认是 final?
答:
kotlin
// Kotlin 中类和方法默认是 final(不可继承/重写)
class Animal { // 默认 final,不可继承
fun eat() { } // 默认 final,不可重写
}
// 需要 open 关键字才能继承/重写
open class Animal {
open fun eat() { println("吃东西") } // 可以被重写
fun breathe() { println("呼吸") } // 不可被重写
open fun sleep() { println("睡觉") }
}
class Dog : Animal() {
override fun eat() { println("啃骨头") }
// override fun breathe() { } // ❌ 编译错误
final override fun sleep() { println("趴着睡") } // 阻止进一步重写
}
// abstract 类似 Java
abstract class Shape {
abstract fun area(): Double // 没有实现,子类必须重写
open fun describe() { } // 有实现,子类可选重写
fun id() = hashCode() // 不可重写
}
为什么默认 final?
这是 Kotlin 的一个重要设计决策,来自《Effective Java》的建议------"要么为继承而设计并提供文档,要么就禁止继承":
- 安全性:防止被意外继承和重写导致的脆弱基类问题
- 性能 :
final方法可以被 JVM 内联优化 - 明确意图 :
open明确表示"这个类/方法是为继承设计的"
四、函数与 Lambda 篇
Q16:什么是扩展函数?它的实现原理是什么?有什么限制?
答:
扩展函数允许为已有类添加新函数,无需继承或装饰器:
kotlin
// 定义扩展函数
fun String.addExclamation(): String {
return this + "!" // this 指向接收者对象
}
println("Hello".addExclamation()) // Hello!
// 实用示例
fun Int.isEven(): Boolean = this % 2 == 0
fun List<Int>.secondOrNull(): Int? = if (size >= 2) this[1] else null
编译原理: 扩展函数本质上是静态方法,接收者对象作为第一个参数:
java
// 上面的 Kotlin 扩展函数编译后等价于:
public static String addExclamation(String receiver) {
return receiver + "!";
}
关键限制:
kotlin
open class Parent {
open fun greet() = "Parent"
}
class Child : Parent() {
override fun greet() = "Child"
}
// 扩展函数是静态解析的,不支持多态!
fun Parent.ext() = "Parent extension"
fun Child.ext() = "Child extension"
val obj: Parent = Child()
println(obj.greet()) // "Child" ------ 成员函数支持多态
println(obj.ext()) // "Parent extension" ------ 扩展函数看声明类型!
// 扩展函数不能访问私有/受保护成员
class Secret {
private val code = "1234"
}
// fun Secret.reveal() = code // ❌ 无法访问 private 成员
// 成员函数优先级高于扩展函数
class MyClass {
fun foo() = "member"
}
fun MyClass.foo() = "extension" // ⚠️ 会被忽略
println(MyClass().foo()) // "member"
Q17:高阶函数是什么?Kotlin 中常用的标准高阶函数有哪些?
答:
高阶函数是以函数作为参数或返回值的函数:
kotlin
// 函数作为参数
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
val sum = calculate(5, 3) { a, b -> a + b } // 8
val product = calculate(5, 3) { a, b -> a * b } // 15
// 函数作为返回值
fun getOperation(type: String): (Int, Int) -> Int {
return when (type) {
"add" -> { a, b -> a + b }
"mul" -> { a, b -> a * b }
else -> { _, _ -> 0 }
}
}
常用标准库高阶函数(作用域函数):
kotlin
data class User(var name: String, var age: Int, var email: String = "")
val user = User("Alice", 25)
// let - 常用于空安全处理,it 引用,返回 lambda 结果
val length = user.name?.let {
println("name is $it")
it.length // 返回值
}
// run - 适合初始化+计算,this 引用,返回 lambda 结果
val info = user.run {
email = "$name@example.com"
"Name: $name, Age: $age" // 返回值
}
// with - 类似 run 但不是扩展函数
val description = with(user) {
"User $name is $age years old"
}
// apply - 适合对象配置,this 引用,返回对象本身
val configured = User("Bob", 30).apply {
email = "bob@example.com"
age = 31
}
// also - 适合附加操作(日志、验证),it 引用,返回对象本身
val validated = User("Charlie", 28).also {
println("创建用户: ${it.name}")
require(it.age > 0) { "年龄必须为正" }
}
记忆口诀:
| 函数 | 引用方式 | 返回值 | 使用场景 |
|---|---|---|---|
let |
it |
Lambda 结果 | 空安全、转换 |
run |
this |
Lambda 结果 | 初始化 + 计算 |
with |
this |
Lambda 结果 | 对已有对象的多个操作 |
apply |
this |
对象本身 | 对象配置 |
also |
it |
对象本身 | 附加操作(日志、验证) |
Q18:inline 函数是什么?为什么需要 inline?什么时候不该用?
答:
问题背景: Lambda 在 Kotlin 中编译后会生成匿名类对象,每次调用都会创建对象,造成性能开销。
inline 的作用: 将函数体和 Lambda 在编译时内联到调用处,消除 Lambda 的对象创建开销。
kotlin
// 不使用 inline
fun regularFun(action: () -> Unit) {
println("Before")
action()
println("After")
}
// 使用 inline
inline fun inlineFun(action: () -> Unit) {
println("Before")
action()
println("After")
}
fun main() {
inlineFun {
println("Action")
}
// 编译后等价于直接把代码"粘贴"过来:
// println("Before")
// println("Action")
// println("After")
}
inline 的额外能力 ------ 非局部返回:
kotlin
inline fun inlineFun(action: () -> Unit) {
action()
}
fun main() {
inlineFun {
println("Hello")
return // ✅ 非局部返回:从 main 函数返回!(因为代码被内联了)
}
println("这行不会执行")
}
noinline 和 crossinline:
kotlin
// noinline:阻止特定 Lambda 参数被内联
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
inlined()
notInlined() // 可以被存储到变量中
val stored = notInlined // ✅ 如果内联了就不能这样做
}
// crossinline:禁止非局部返回,但仍然内联
inline fun bar(crossinline action: () -> Unit) {
val runnable = Runnable { action() } // 在不同的执行上下文中调用
runnable.run()
}
什么时候该用/不该用 inline:
| 该用 | 不该用 |
|---|---|
| 函数接收 Lambda 参数 | 函数体很大(会增大字节码) |
| Lambda 被频繁调用 | 没有 Lambda 参数 |
需要 reified 类型参数 |
Lambda 需要被存储到变量中 |
Q19:reified 关键字是什么?解决了什么问题?
答:
由于 JVM 的类型擦除 ,泛型类型信息在运行时不可用。reified 配合 inline 函数可以在运行时保留泛型类型信息:
kotlin
// ❌ 不使用 reified:无法直接使用泛型类型
fun <T> isType(value: Any): Boolean {
// return value is T // ❌ 编译错误:Cannot check for instance of erased type
return false
}
// ✅ 使用 reified
inline fun <reified T> isType(value: Any): Boolean {
return value is T // ✅ 可以直接使用 T
}
println(isType<String>("Hello")) // true
println(isType<Int>("Hello")) // false
// 实用示例:简化 Android startActivity
inline fun <reified T : Activity> Context.startActivity() {
val intent = Intent(this, T::class.java) // 可以获取 T 的 Class
startActivity(intent)
}
// 使用
startActivity<DetailActivity>() // 无需传入 DetailActivity::class.java
// 实用示例:JSON 反序列化
inline fun <reified T> String.fromJson(): T {
return Gson().fromJson(this, T::class.java)
}
val user = """{"name":"Alice","age":25}""".fromJson<User>()
五、集合与泛型篇
Q20:Kotlin 中可变集合和不可变集合的区别?
答:
kotlin
// 不可变集合(只读)
val list: List<Int> = listOf(1, 2, 3)
val set: Set<String> = setOf("a", "b", "c")
val map: Map<String, Int> = mapOf("one" to 1, "two" to 2)
// list.add(4) // ❌ 编译错误:没有 add 方法
// 可变集合
val mutableList: MutableList<Int> = mutableListOf(1, 2, 3)
val mutableSet: MutableSet<String> = mutableSetOf("a", "b", "c")
val mutableMap: MutableMap<String, Int> = mutableMapOf("one" to 1)
mutableList.add(4) // ✅
// ⚠️ 注意:不可变只是接口层面的,底层实现可能是可变的
val readOnly: List<Int> = mutableListOf(1, 2, 3)
// readOnly.add(4) // ❌ 编译错误
(readOnly as MutableList<Int>).add(4) // ⚠️ 运行时可能成功但不推荐!
创建方式汇总:
| 不可变 | 可变 |
|---|---|
listOf() |
mutableListOf() / arrayListOf() |
setOf() |
mutableSetOf() / hashSetOf() / linkedSetOf() |
mapOf() |
mutableMapOf() / hashMapOf() / linkedMapOf() |
emptyList() |
- |
Q21:Sequence 和 Iterable(集合)有什么区别?什么时候用 Sequence?
答:
这是一道考察性能优化意识的经典题。
kotlin
val list = (1..1_000_000).toList()
// Iterable(集合):每一步操作生成一个中间集合(Eager,饿汉式)
val result1 = list
.filter { it % 2 == 0 } // 创建一个新 List(~50万元素)
.map { it * 2 } // 再创建一个新 List(~50万元素)
.take(10) // 再创建一个新 List(10个元素)
// Sequence:逐元素处理,不创建中间集合(Lazy,懒汉式)
val result2 = list.asSequence()
.filter { it % 2 == 0 } // 返回 Sequence,不立即执行
.map { it * 2 } // 返回 Sequence,不立即执行
.take(10) // 返回 Sequence,不立即执行
.toList() // 终端操作,才真正开始处理
处理方式对比:
Iterable(横向处理):
[1,2,3,4,5,...] → filter全部 → [2,4,6,...] → map全部 → [4,8,12,...] → take(10)
Sequence(纵向处理):
元素1: filter(1)=false → 跳过
元素2: filter(2)=true → map(2)=4 → take → 收集
元素3: filter(3)=false → 跳过
元素4: filter(4)=true → map(4)=8 → take → 收集
... 收集到10个就停止!
使用建议:
| 场景 | 推荐 |
|---|---|
| 小集合(<1000元素) | Iterable(集合) |
| 大集合 + 链式操作 | Sequence |
只需要部分结果(first、take) |
Sequence |
| 需要索引访问 | Iterable(集合) |
| 简单的单步操作 | Iterable(集合) |
Q22:Kotlin 泛型中的 in、out 是什么?和 Java 的 ? extends / ? super 有什么对应关系?
答:
这是 Kotlin 泛型的型变(Variance) 机制:
kotlin
// out = 协变(Covariance):只能作为输出(生产者)
// 对应 Java 的 <? extends T>
interface Producer<out T> {
fun produce(): T // ✅ T 作为返回值
// fun consume(t: T) // ❌ 编译错误:T 不能作为参数
}
// in = 逆变(Contravariance):只能作为输入(消费者)
// 对应 Java 的 <? super T>
interface Consumer<in T> {
fun consume(t: T) // ✅ T 作为参数
// fun produce(): T // ❌ 编译错误:T 不能作为返回值
}
// 不变(Invariance):既能输入又能输出
interface MutableList<T> {
fun get(index: Int): T // T 作为输出
fun add(element: T) // T 作为输入
}
实际应用:
kotlin
// List 是协变的:List<out E>
// 所以 List<String> 是 List<Any> 的子类型
val strings: List<String> = listOf("a", "b")
val anys: List<Any> = strings // ✅ 协变
// MutableList 是不变的:MutableList<E>
val mutableStrings: MutableList<String> = mutableListOf("a", "b")
// val mutableAnys: MutableList<Any> = mutableStrings // ❌ 不变
// Comparable 是逆变的:Comparable<in T>
// 所以 Comparable<Any> 是 Comparable<String> 的子类型
fun sort(list: List<String>, comparator: Comparable<Any>) { /* ... */ }
记忆口诀:PECS(Producer Extends, Consumer Super)
- out = 生产者 = 只读 = Java
? extends - in = 消费者 = 只写 = Java
? super
Q23:什么是星投影(Star Projection)*?
答:
星投影 * 类似于 Java 的 ?(通配符),表示"不关心具体类型":
kotlin
// 当你不知道或不关心泛型的具体类型时使用
fun printAll(list: List<*>) {
for (item in list) {
println(item) // item 的类型是 Any?
}
}
printAll(listOf(1, 2, 3))
printAll(listOf("a", "b", "c"))
// 星投影的含义取决于型变
// 对于 out 类型(如 List<out E>):List<*> 等价于 List<out Any?>
// 对于 in 类型(如 Comparable<in T>):Comparable<*> 等价于 Comparable<in Nothing>
// 对于不变类型(如 MutableList<E>):
// - 读取时等价于 MutableList<out Any?>
// - 写入时等价于 MutableList<in Nothing>(不能写入)
val mutableList: MutableList<*> = mutableListOf(1, "two", 3.0)
val item = mutableList[0] // ✅ 类型是 Any?
// mutableList[0] = 42 // ❌ 不能写入,因为不知道具体类型
六、协程篇
Q24:什么是协程?和线程有什么区别?
答:
协程是一种轻量级的并发方案,可以在不阻塞线程的情况下挂起和恢复执行。
kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
// 启动 10 万个协程
val jobs = List(100_000) {
launch {
delay(1000)
print(".")
}
}
jobs.forEach { it.join() }
// ✅ 轻松运行,因为协程非常轻量
// 如果用线程启动 10 万个,很可能 OOM
}
核心区别:
| 特性 | 线程 | 协程 |
|---|---|---|
| 调度 | OS 内核调度 | 用户态调度(协程框架) |
| 开销 | 每个线程 ~1MB 栈空间 | 每个协程几十 KB |
| 切换成本 | 高(上下文切换) | 低(函数调用级别) |
| 阻塞 | 阻塞线程 | 挂起,不阻塞线程 |
| 数量 | 通常几百到几千 | 轻松几万到几十万 |
| 编程模型 | 回调/Future | 顺序式代码(看起来同步) |
Q25:launch 和 async 的区别?
答:
kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
// launch:启动协程,返回 Job,不带返回值("发射后不管")
val job: Job = launch {
delay(1000)
println("launch 完成")
}
job.join() // 等待完成
// async:启动协程,返回 Deferred<T>,带返回值
val deferred: Deferred<Int> = async {
delay(1000)
42 // 返回值
}
val result: Int = deferred.await() // 等待结果
println("async 结果: $result")
// async 的典型应用:并发请求
val time = measureTimeMillis {
val user = async { fetchUser() } // 并发执行
val orders = async { fetchOrders() } // 并发执行
println("User: ${user.await()}, Orders: ${orders.await()}")
}
println("耗时: ${time}ms") // ~1000ms 而非 ~2000ms
}
suspend fun fetchUser(): String { delay(1000); return "Alice" }
suspend fun fetchOrders(): String { delay(1000); return "[Order1, Order2]" }
| 特性 | launch |
async |
|---|---|---|
| 返回类型 | Job |
Deferred<T> |
| 获取结果 | 无返回值 | await() 获取结果 |
| 异常处理 | 立即传播 | 在 await() 时抛出 |
| 使用场景 | 不需要结果的异步操作 | 需要结果的并发计算 |
Q26:协程的调度器(Dispatcher)有哪些?各自的作用?
答:
kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
// Dispatchers.Main - 主线程(Android UI 线程)
// 用于 UI 更新、轻量操作
launch(Dispatchers.Main) {
textView.text = "更新 UI"
}
// Dispatchers.IO - IO 线程池(默认 64 个线程)
// 用于网络请求、文件读写、数据库操作
launch(Dispatchers.IO) {
val data = fetchFromNetwork()
val file = readFile()
}
// Dispatchers.Default - CPU 密集型线程池(线程数 = CPU 核心数)
// 用于排序、解析 JSON、复杂计算
launch(Dispatchers.Default) {
val sorted = hugeList.sorted()
val parsed = Json.parse(bigJsonString)
}
// Dispatchers.Unconfined - 不指定线程
// 在调用者线程中立即执行,直到第一个挂起点
// 一般不在生产代码中使用
launch(Dispatchers.Unconfined) {
println("Thread: ${Thread.currentThread().name}") // 可能是 main
delay(100)
println("Thread: ${Thread.currentThread().name}") // 可能变成别的线程
}
// 切换调度器
launch(Dispatchers.IO) {
val data = fetchData() // IO 线程
withContext(Dispatchers.Main) { // 切换到主线程
updateUI(data)
}
}
}
Q27:什么是结构化并发(Structured Concurrency)?coroutineScope 和 supervisorScope 的区别?
答:
结构化并发是指协程的生命周期被限定在一个明确的作用域内,确保:
- 不会泄漏协程
- 父协程会等待所有子协程完成
- 异常能够正确传播
kotlin
// coroutineScope:任何一个子协程失败 → 取消所有其他子协程
suspend fun fetchUserAndOrders() = coroutineScope {
val user = async { fetchUser() } // 子协程1
val orders = async { fetchOrders() } // 子协程2
// 如果 fetchUser() 抛异常,fetchOrders() 也会被取消
Pair(user.await(), orders.await())
}
// supervisorScope:子协程的失败不影响其他子协程
suspend fun loadDashboard() = supervisorScope {
val user = async { fetchUser() } // 子协程1
val recommendations = async { fetchRecs() } // 子协程2
val news = async { fetchNews() } // 子协程3
// 即使 fetchRecs() 失败,其他两个继续运行
val userData = try { user.await() } catch (e: Exception) { null }
val recsData = try { recommendations.await() } catch (e: Exception) { emptyList() }
val newsData = try { news.await() } catch (e: Exception) { emptyList() }
}
| 特性 | coroutineScope |
supervisorScope |
|---|---|---|
| 子协程失败时 | 取消所有其他子协程 | 其他子协程不受影响 |
| 适用场景 | 所有任务都必须成功 | 部分任务可以容错 |
| 异常传播 | 向上传播 | 不向上传播(需手动处理) |
Q28:suspend 函数的本质是什么?它在编译后变成了什么?
答:
suspend 函数在编译后会通过 CPS (Continuation Passing Style) 变换:
kotlin
// 我们写的代码
suspend fun fetchUser(): User {
val token = getToken() // 挂起点1
val user = getUser(token) // 挂起点2
return user
}
// 编译器变换后(伪代码)
fun fetchUser(continuation: Continuation<User>): Any? {
val sm = continuation as? FetchUserStateMachine
?: FetchUserStateMachine(continuation)
when (sm.label) {
0 -> {
sm.label = 1
val result = getToken(sm) // 传入状态机
if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
// 如果没有挂起,继续执行
}
1 -> {
val token = sm.result as String
sm.label = 2
val result = getUser(token, sm)
if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
}
2 -> {
val user = sm.result as User
sm.continuation.resume(user)
}
}
}
核心概念:
- Continuation(续体): 封装了"协程剩余要执行的代码"和"当前状态"
- 状态机: 每个
suspend函数编译为一个状态机,挂起点是状态切换点 - COROUTINE_SUSPENDED: 特殊标记值,表示协程已挂起
📌 关键理解:
suspend函数并不一定会挂起!只有遇到真正的异步操作时才会挂起。suspend关键字只是赋予了函数"挂起的能力"。
Q29:Flow 是什么?和 Channel 有什么区别?
答:
Flow 是协程版的"响应式流",用于处理异步数据流:
kotlin
// 创建 Flow
fun numbersFlow(): Flow<Int> = flow {
for (i in 1..5) {
delay(100)
emit(i) // 发射数据
}
}
// 收集 Flow
fun main() = runBlocking {
numbersFlow()
.filter { it % 2 == 0 }
.map { it * it }
.collect { value ->
println(value) // 4, 16
}
}
// StateFlow - 状态管理(类似 LiveData)
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun loadUser() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val user = repository.fetchUser()
_uiState.value = UiState.Success(user)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message)
}
}
}
}
// SharedFlow - 事件广播(一次性事件)
class EventBus {
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events.asSharedFlow()
suspend fun emit(event: Event) { _events.emit(event) }
}
Flow vs Channel:
| 特性 | Flow | Channel |
|---|---|---|
| 类型 | 冷流(Cold) | 热流(Hot) |
| 生产者 | 有人收集时才开始产生 | 不管有没有接收者都在产生 |
| 消费模式 | 可多次收集,每次重新开始 | 一个元素只能被一个接收者消费 |
| 使用场景 | 数据转换流、UI 状态 | 生产者-消费者模式、任务分发 |
Q30:协程中如何处理异常?
答:
kotlin
// 方式1:try-catch
launch {
try {
riskyOperation()
} catch (e: Exception) {
println("捕获异常: ${e.message}")
}
}
// 方式2:CoroutineExceptionHandler(全局异常处理)
val handler = CoroutineExceptionHandler { _, exception ->
println("全局捕获: ${exception.message}")
}
val scope = CoroutineScope(SupervisorJob() + handler)
scope.launch {
throw RuntimeException("Boom!")
}
// 方式3:async 的异常在 await() 时抛出
val deferred = scope.async {
throw RuntimeException("Async Boom!")
}
try {
deferred.await()
} catch (e: Exception) {
println("Await 捕获: ${e.message}")
}
// 方式4:supervisorScope + 单独 try-catch
supervisorScope {
val job1 = launch {
try { failingTask() } catch (e: Exception) { /* 单独处理 */ }
}
val job2 = launch {
successfulTask() // 不受 job1 影响
}
}
⚠️ 常见陷阱:
kotlin
// ❌ 这样 catch 不到 launch 内部的异常!
try {
launch {
throw RuntimeException("Boom!") // 异常不会被外层 try-catch 捕获
}
} catch (e: Exception) {
println("这里捕获不到")
}
// ✅ 应该在协程内部 catch
launch {
try {
throw RuntimeException("Boom!")
} catch (e: Exception) {
println("这里可以捕获")
}
}
七、Kotlin 与 Java 互操作篇
Q31:Kotlin 如何调用 Java 代码?有哪些需要注意的地方?
答:
kotlin
// 1. 平台类型(Platform Type)
// Java 返回的类型在 Kotlin 中是"平台类型",既不是 String 也不是 String?
// Kotlin 编译器不会强制空安全检查
val javaString = JavaClass.getString() // 类型是 String!(平台类型)
// 你需要自己判断是否可能为 null
// 2. Java getter/setter 自动映射为属性
// Java: user.getName() / user.setName("Alice")
// Kotlin: user.name = "Alice"
// 3. SAM 转换(Single Abstract Method)
// Java 的函数式接口可以用 Lambda
button.setOnClickListener { view ->
println("Clicked")
}
// 4. 受检异常
// Java 的受检异常在 Kotlin 中不需要声明或捕获
val file = File("test.txt")
file.readText() // 不需要 try-catch IOException
// 5. Java 的 void 在 Kotlin 中是 Unit
Q32:Java 如何调用 Kotlin 代码?常用的 Kotlin-Java 互操作注解?
答:
kotlin
// @JvmStatic - 在 companion object 中生成真正的 static 方法
class AppUtils {
companion object {
@JvmStatic
fun doSomething() { /* ... */ }
}
}
// Java: AppUtils.doSomething() // 有注解
// Java: AppUtils.Companion.doSomething() // 无注解
// @JvmField - 暴露为公共字段,不生成 getter/setter
class Config {
@JvmField
val TAG = "Config"
}
// Java: config.TAG // 直接访问字段
// @JvmOverloads - 为有默认参数的函数生成重载方法
class Builder {
@JvmOverloads
fun build(name: String, age: Int = 0, email: String = "") { /* ... */ }
}
// Java 中可以调用:
// build("Alice")
// build("Alice", 25)
// build("Alice", 25, "alice@email.com")
// @JvmName - 自定义在 Java 中看到的方法名
@file:JvmName("StringUtils") // 改变文件级函数的类名
package com.example
fun String.isPalindrome(): Boolean = this == this.reversed()
// Java: StringUtils.isPalindrome("abc")
// @Throws - 声明受检异常(供 Java 调用者知道需要 catch)
@Throws(IOException::class)
fun readFile(path: String): String {
return File(path).readText()
}
八、高级特性篇
Q33:什么是委托属性(Delegated Properties)?如何自定义委托?
答:
委托属性将属性的 getter/setter 逻辑委托给另一个对象:
kotlin
// 标准库提供的委托
// 1. lazy
val heavyData: String by lazy { loadFromDatabase() }
// 2. observable
var name: String by Delegates.observable("初始值") { prop, old, new ->
println("${prop.name}: $old → $new")
}
// 3. vetoable(可以拒绝修改)
var age: Int by Delegates.vetoable(0) { _, _, new ->
new >= 0 // 只接受非负数
}
// 4. Map 委托
class User(map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
val user = User(mapOf("name" to "Alice", "age" to 25))
println(user.name) // Alice
// 自定义委托
class LoggingDelegate<T>(private var value: T) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
println("读取 ${property.name} = $value")
return value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
println("设置 ${property.name}: $value → $newValue")
value = newValue
}
}
var count: Int by LoggingDelegate(0)
count = 5 // 输出: 设置 count: 0 → 5
println(count) // 输出: 读取 count = 5,然后打印 5
// Android 中常见的委托:SharedPreferences 委托
class PreferenceDelegate(
private val prefs: SharedPreferences,
private val key: String,
private val defaultValue: String
) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return prefs.getString(key, defaultValue) ?: defaultValue
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
prefs.edit().putString(key, value).apply()
}
}
// 使用
var username: String by PreferenceDelegate(prefs, "username", "Guest")
Q34:Kotlin 的协变和逆变在实际开发中如何应用?
答:
kotlin
// 实际案例1:事件系统
sealed class Event
class ClickEvent(val x: Int, val y: Int) : Event()
class SwipeEvent(val direction: String) : Event()
// out T - 只读容器是协变的
interface EventProducer<out T : Event> {
fun produce(): T
}
val clickProducer: EventProducer<ClickEvent> = object : EventProducer<ClickEvent> {
override fun produce() = ClickEvent(0, 0)
}
val eventProducer: EventProducer<Event> = clickProducer // ✅ 协变
// in T - 只写容器是逆变的
interface EventConsumer<in T : Event> {
fun consume(event: T)
}
val generalConsumer: EventConsumer<Event> = object : EventConsumer<Event> {
override fun consume(event: Event) { println("Processing event") }
}
val clickConsumer: EventConsumer<ClickEvent> = generalConsumer // ✅ 逆变
// 实际案例2:类型安全的构建器
class ListBuilder<in T> {
private val list = mutableListOf<@UnsafeVariance T>()
fun add(item: T) { list.add(item) }
}
Q35:typealias 是什么?有哪些使用场景?
答:
kotlin
// 1. 简化复杂类型签名
typealias UserClickHandler = (User, View, Int) -> Unit
typealias StringMap = Map<String, String>
typealias Predicate<T> = (T) -> Boolean
fun filterUsers(users: List<User>, predicate: Predicate<User>): List<User> {
return users.filter(predicate)
}
// 2. 提高可读性
typealias UserId = Long
typealias Email = String
fun sendEmail(userId: UserId, email: Email) { /* ... */ }
// 3. 简化泛型
typealias NetworkResult<T> = Result<Pair<Int, T>> // Result<Pair<状态码, 数据>>
// 4. 简化内部类引用
typealias OnItemClick = RecyclerViewAdapter.OnItemClickListener
// ⚠️ 注意:typealias 不创建新类型!只是别名
typealias Meters = Double
typealias Kilometers = Double
val distance: Meters = 100.0
val km: Kilometers = distance // ✅ 这是合法的,因为本质上都是 Double
Q36:内联类(value class)是什么?解决了什么问题?
答:
kotlin
// 问题:typealias 不提供类型安全
typealias UserId = Long
typealias OrderId = Long
fun findUser(id: UserId) { /* ... */ }
fun findOrder(id: OrderId) { /* ... */ }
val userId: UserId = 1L
val orderId: OrderId = 2L
findUser(orderId) // ⚠️ 编译通过!但逻辑上是错误的
// 解决方案:value class(内联类)
@JvmInline
value class UserId(val value: Long)
@JvmInline
value class OrderId(val value: Long)
fun findUser(id: UserId) { /* ... */ }
fun findOrder(id: OrderId) { /* ... */ }
val userId = UserId(1L)
val orderId = OrderId(2L)
// findUser(orderId) // ❌ 编译错误!类型安全!
// 性能优势:编译后不会创建包装对象(大多数情况下)
// UserId(1L) 在运行时就是一个 long 值 1L
value class 的限制:
- 只能有一个主构造函数参数
- 不能有
init块 - 不能被继承
- 不能有带有幕后字段的属性
九、实战与设计模式篇
Q37:用 Kotlin 实现单例模式有哪几种方式?
答:
kotlin
// 方式1:object 声明(最推荐)
object Singleton {
var count = 0
fun doSomething() { /* ... */ }
}
// 方式2:companion object + 私有构造函数
class Singleton private constructor() {
companion object {
val instance: Singleton by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
Singleton()
}
}
}
// 方式3:带参数的单例(常见面试题!)
class Database private constructor(val url: String) {
companion object {
@Volatile
private var instance: Database? = null
fun getInstance(url: String): Database {
return instance ?: synchronized(this) {
instance ?: Database(url).also { instance = it }
}
}
}
}
// 方式4:枚举单例
enum class EnumSingleton {
INSTANCE;
fun doSomething() { /* ... */ }
}
Q38:用 Kotlin 实现建造者模式?
答:
kotlin
// 方式1:传统 Builder 模式
class HttpRequest private constructor(
val url: String,
val method: String,
val headers: Map<String, String>,
val body: String?
) {
class Builder {
private var url: String = ""
private var method: String = "GET"
private var headers: MutableMap<String, String> = mutableMapOf()
private var body: String? = null
fun url(url: String) = apply { this.url = url }
fun method(method: String) = apply { this.method = method }
fun header(key: String, value: String) = apply { headers[key] = value }
fun body(body: String) = apply { this.body = body }
fun build(): HttpRequest {
require(url.isNotEmpty()) { "URL must not be empty" }
return HttpRequest(url, method, headers, body)
}
}
}
val request = HttpRequest.Builder()
.url("https://api.example.com")
.method("POST")
.header("Content-Type", "application/json")
.body("""{"name": "Kotlin"}""")
.build()
// 方式2:Kotlin 惯用方式 - 使用默认参数 + apply(更简洁)
data class HttpRequest(
val url: String,
val method: String = "GET",
val headers: Map<String, String> = emptyMap(),
val body: String? = null
)
val request = HttpRequest(
url = "https://api.example.com",
method = "POST",
headers = mapOf("Content-Type" to "application/json"),
body = """{"name": "Kotlin"}"""
)
// 方式3:DSL 风格
class RequestBuilder {
var url: String = ""
var method: String = "GET"
private val headers = mutableMapOf<String, String>()
var body: String? = null
fun headers(block: HeaderBuilder.() -> Unit) {
HeaderBuilder().apply(block).build().forEach { (k, v) -> headers[k] = v }
}
fun build() = HttpRequest(url, method, headers, body)
}
fun request(block: RequestBuilder.() -> Unit): HttpRequest {
return RequestBuilder().apply(block).build()
}
// 使用 DSL
val req = request {
url = "https://api.example.com"
method = "POST"
headers {
"Content-Type" to "application/json"
"Authorization" to "Bearer token"
}
body = """{"name": "Kotlin"}"""
}
Q39:Kotlin 中如何实现观察者模式?
答:
kotlin
// 方式1:使用 Delegates.observable
class UserViewModel {
var name: String by Delegates.observable("") { _, old, new ->
println("name changed: $old → $new")
notifyListeners()
}
private val listeners = mutableListOf<(String) -> Unit>()
fun addListener(listener: (String) -> Unit) {
listeners.add(listener)
}
private fun notifyListeners() {
listeners.forEach { it(name) }
}
}
// 方式2:使用 Flow(推荐)
class UserRepository {
private val _users = MutableStateFlow<List<User>>(emptyList())
val users: StateFlow<List<User>> = _users.asStateFlow()
fun addUser(user: User) {
_users.value = _users.value + user
}
}
// 在 UI 层收集
lifecycleScope.launch {
repository.users.collect { users ->
adapter.submitList(users)
}
}
Q40:什么是 Kotlin DSL?如何编写类型安全的 DSL?
答:
DSL(Domain Specific Language)是为特定领域设计的"迷你语言":
kotlin
// 示例:HTML DSL
fun html(block: HTML.() -> Unit): HTML = HTML().apply(block)
class HTML {
private val children = mutableListOf<String>()
fun head(block: Head.() -> Unit) {
children.add(Head().apply(block).render())
}
fun body(block: Body.() -> Unit) {
children.add(Body().apply(block).render())
}
fun render() = "<html>\n${children.joinToString("\n")}\n</html>"
}
class Head {
var title = ""
fun render() = " <head><title>$title</title></head>"
}
class Body {
private val elements = mutableListOf<String>()
fun h1(text: String) { elements.add(" <h1>$text</h1>") }
fun p(text: String) { elements.add(" <p>$text</p>") }
fun render() = " <body>\n${elements.joinToString("\n")}\n </body>"
}
// 使用
val page = html {
head {
title = "Kotlin DSL"
}
body {
h1("Hello, DSL!")
p("This is a type-safe builder")
}
}
println(page.render())
// <html>
// <head><title>Kotlin DSL</title></head>
// <body>
// <h1>Hello, DSL!</h1>
// <p>This is a type-safe builder</p>
// </body>
// </html>
DSL 实现的关键 Kotlin 特性:
- 带接收者的 Lambda(
T.() -> Unit) - 扩展函数
- 中缀函数(
infix) - 运算符重载
@DslMarker注解(限制作用域)
kotlin
// @DslMarker 防止外层作用域泄漏
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class Body { /* ... */ }
@HtmlDsl
class HTML { /* ... */ }
// 这样在 body { } 内部就不能直接调用 html 的方法
十、综合题与常见陷阱
Q41:以下代码的输出是什么?(考察属性初始化顺序)
kotlin
open class Base {
open val size: Int = 0
init { println("Base size = $size") }
}
class Derived : Base() {
override val size: Int = 10
init { println("Derived size = $size") }
}
fun main() {
Derived()
}
答:
Base size = 0
Derived size = 10
解析: 执行 Derived() 时,先调用 Base 的 init 块。此时 Derived 的 size 还未初始化(override val size 背后是一个被重写的 getSize() 方法),但由于 Derived 的属性初始化还没有运行,返回的是 Int 的默认值 0。这是一个经典的在构造函数中调用可重写方法的陷阱。
Q42:以下代码有什么问题?
kotlin
data class User(val name: String) {
var loginCount = 0
}
val u1 = User("Alice").apply { loginCount = 5 }
val u2 = User("Alice").apply { loginCount = 10 }
println(u1 == u2) // ?
println(u1.copy()) // ?
答:
u1 == u2 → true // loginCount 不参与 equals 比较!
u1.copy() // User(name=Alice),且 loginCount 重置为 0!
data class 的 equals()、copy() 只基于主构造函数参数 。loginCount 在类体中声明,不参与这些自动生成的方法。copy() 只复制主构造函数参数,loginCount 会使用默认值 0。
Q43:解释以下协程代码的行为
kotlin
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("Job: sleeping $i ...")
delay(500L)
}
} finally {
println("Job: I'm running finally")
delay(1000L) // 这行会执行吗?
println("Job: After delay in finally") // 这行呢?
}
}
delay(1300L)
println("Main: I'm tired of waiting!")
job.cancelAndJoin()
println("Main: Now I can quit.")
}
答:
Job: sleeping 0 ...
Job: sleeping 1 ...
Job: sleeping 2 ...
Main: I'm tired of waiting!
Job: I'm running finally
Main: Now I can quit.
"After delay in finally" 不会打印! 因为协程被取消后处于"取消中"状态,在 finally 块中调用 delay() 等挂起函数会立即抛出 CancellationException。
如果需要在 finally 中执行挂起操作:
kotlin
finally {
withContext(NonCancellable) {
delay(1000L) // 这样可以正常执行
println("Cleanup done")
}
}
面试技巧与总结
面试高频题 Top 10
| 排名 | 题目 | 考察点 |
|---|---|---|
| 1 | val vs var vs const val |
基础概念 |
| 2 | 空安全机制(?. ?: !! let) |
核心特性 |
| 3 | lateinit vs by lazy |
延迟初始化 |
| 4 | data class 特性与陷阱 |
面向对象 |
| 5 | 协程 launch vs async |
并发编程 |
| 6 | 协程调度器 | 并发编程 |
| 7 | 扩展函数原理和限制 | 语言机制 |
| 8 | inline/reified |
性能优化 |
| 9 | sealed class 的使用场景 |
类型系统 |
| 10 | Sequence vs List 操作 |
性能优化 |
面试建议
- 理解原理:不仅要知道怎么用,更要知道为什么这样设计、编译后是什么样
- 对比思维 :能够清晰对比相似概念的异同(如
launchvsasync、lateinitvslazy) - 实战经验:结合实际项目场景描述使用体验和踩过的坑
- 代码展示:在白板/编辑器上能够流畅写出正确的 Kotlin 代码
- 深入协程 :协程几乎是 Kotlin 面试的必考内容,要深入理解
suspend的本质
本文涵盖了 Kotlin 面试中最常见的 40+ 道题目。建议收藏后反复研读,并结合实际编码练习加深理解。祝你面试顺利!🎉
后记
2026年5月18日于上海,在claude opus 4.6辅助下完成。