Kotlin 面试题全面解析:从基础到进阶

Kotlin 面试题全面解析:从基础到进阶

前言

随着 Kotlin 在 Android 开发、服务端开发以及跨平台领域的广泛应用,越来越多的公司在面试中加入了 Kotlin 相关的考察。本文精心整理了 40+ 道高频 Kotlin 面试题,涵盖基础语法、面向对象、函数式编程、协程、集合与泛型等核心主题,并附上详细解析和代码示例,帮助你在面试中脱颖而出。


目录

  1. 基础语法篇
  2. 空安全篇
  3. 面向对象篇
  4. [函数与 Lambda 篇](#函数与 Lambda 篇)
  5. 集合与泛型篇
  6. 协程篇
  7. [Kotlin 与 Java 互操作篇](#Kotlin 与 Java 互操作篇)
  8. 高级特性篇
  9. 实战与设计模式篇

一、基础语法篇

Q1:valvar 有什么区别?val 就是常量吗?

答:

  • val(value):只读变量 ,一旦赋值后不能重新赋值,类似 Java 的 final
  • var(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 valval 的区别:

特性 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 返回 truenull == "hello" 返回 false,不会抛 NPE。


Q4:Kotlin 中的类型转换怎么做?asas? 有什么区别?

答:

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 有什么不同?

答:

whenswitch超级增强版

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:UnitNothingAny 分别是什么?

答:

这三个类型是 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:lateinitlazy 有什么区别?

答:

这是高频面试题,两者都是延迟初始化的手段,但应用场景完全不同:

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 的 classdata classsealed classenum classobjectabstract classinterface 各自的特点和区别?

答:

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 的要求和自动生成了什么?

答:

要求:

  • 主构造函数至少有一个参数
  • 主构造函数参数必须标记为 valvar
  • 不能是 abstractopensealedinner

自动生成的方法(仅基于主构造函数中的属性):

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 classequals()hashCode()toString()copy()componentN() 只考虑主构造函数中的属性,类体中声明的属性不参与!


Q12:sealed classenum class 的区别?sealed classsealed 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
实例数量 每个值固定一个实例 子类可以有多个实例
子类状态 所有值相同的属性 每个子类可以有不同的属性
子类类型 都是同一类型 可以是 classdata classobject
适用场景 有限、固定的值集合 有限的类型层次,但每种类型有不同数据

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:objectcompanion 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. 次构造函数

执行顺序总结:

  1. 主构造函数参数传入
  2. 属性初始化和 init 块按照声明顺序依次执行
  3. 次构造函数体执行

Q15:Kotlin 中 openfinalabstract 的含义?为什么默认是 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》的建议------"要么为继承而设计并提供文档,要么就禁止继承"

  1. 安全性:防止被意外继承和重写导致的脆弱基类问题
  2. 性能final 方法可以被 JVM 内联优化
  3. 明确意图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("这行不会执行")
}

noinlinecrossinline

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:SequenceIterable(集合)有什么区别?什么时候用 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
只需要部分结果(firsttake Sequence
需要索引访问 Iterable(集合)
简单的单步操作 Iterable(集合)

Q22:Kotlin 泛型中的 inout 是什么?和 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:launchasync 的区别?

答:

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)?coroutineScopesupervisorScope 的区别?

答:

结构化并发是指协程的生命周期被限定在一个明确的作用域内,确保:

  1. 不会泄漏协程
  2. 父协程会等待所有子协程完成
  3. 异常能够正确传播
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() 时,先调用 Baseinit 块。此时 Derivedsize 还未初始化(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 classequals()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 操作 性能优化

面试建议

  1. 理解原理:不仅要知道怎么用,更要知道为什么这样设计、编译后是什么样
  2. 对比思维 :能够清晰对比相似概念的异同(如 launch vs asynclateinit vs lazy
  3. 实战经验:结合实际项目场景描述使用体验和踩过的坑
  4. 代码展示:在白板/编辑器上能够流畅写出正确的 Kotlin 代码
  5. 深入协程 :协程几乎是 Kotlin 面试的必考内容,要深入理解 suspend 的本质

本文涵盖了 Kotlin 面试中最常见的 40+ 道题目。建议收藏后反复研读,并结合实际编码练习加深理解。祝你面试顺利!🎉

后记

2026年5月18日于上海,在claude opus 4.6辅助下完成。

相关推荐
小林望北4 小时前
Kotlin 协程的挂起(suspend)原理
kotlin·挂起·kotlin协程·suspend
沐知全栈开发4 小时前
TypeScript Map 对象
开发语言
2601_957418804 小时前
Android相机有线连接全栈解决方案:PTP/MTP协议深度实现与应用实践
android·数码相机
yujunl4 小时前
U9开发模式之一门面模式的理解
开发语言
Chase_______4 小时前
【Java基础核心知识点全解·第0篇】Java开发环境搭建指南:JDK + IDEA 从安装配置到运行 HelloWorld
java·开发语言·intellij-idea
布吉岛的石头4 小时前
Java 程序员第 19 阶段:大模型Agent智能体入门:拆解自主任务编排原理
java·开发语言·人工智能
私人珍藏库4 小时前
【Android】Solid文件管理器3.5.2 安卓文件管理器
android·人工智能·app·工具·软件·多功能
三品吉他手会点灯4 小时前
C语言学习笔记 - 37.数据类型 - scanf函数的基本用法
c语言·开发语言·笔记·学习
70asunflower4 小时前
Python 开发实用技巧集锦
开发语言·python