Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Kotlin 语言核心

Kotlin 是现代 Android 的第一语言。你会用,但要把“会用“升级到“讲得清原理“。本篇覆盖中级面试高频语法点的底层机制

一、空安全(Null Safety)

Kotlin 把 null 检查提前到编译期。

  • val a: String 不可空;val b: String? 可空。
  • 安全调用 b?.length:b 为 null 时整体返回 null。
  • Elvis b?.length ?: 0:为 null 时给默认值。
  • 非空断言 b!!:为 null 时抛 NPE(慎用)。
  • 平台类型 String!:来自 Java 的类型,Kotlin 不知其可空性,调用方负责

易错:lateinit var 用于非空且延迟初始化(只能用于 var、非基本类型),访问前未初始化抛 UninitializedPropertyAccessException,可用 ::x.isInitialized 判断。by lazy 用于 val,线程安全延迟初始化。

二、扩展函数 / 扩展属性

fun String.lastChar(): Char = this[length - 1]

原理(高频追问):扩展函数编译成静态方法,接收者作为第一个参数传入。所以:

  • 扩展函数是静态分发,不是多态——调用哪个由声明类型决定,不是运行时类型。
  • 不能真正修改类、不能访问 private 成员。
  • 扩展属性没有 backing field,只能定义 get/set。

三、高阶函数与 inline

inline fun <T> measure(block: () -> T): T { ... }
  • Lambda 默认会被编译成 Function 对象,有对象创建开销。
  • inline 把函数体和 lambda 直接内联到调用处,消除对象分配,还能让 lambda 内的 return 直接返回外层函数(非局部返回)。
  • noinline:某个 lambda 参数不内联。
  • crossinline:禁止该 lambda 非局部返回(用于会在别处调用的场景)。
  • reified:配合 inline,让泛型类型在运行时可见(T::class),解决泛型擦除。
  • 边界:reified 只让内联函数调用点能拿到 T 的运行时类型,不能恢复集合元素的完整泛型实参;例如仍无法把 List<String>List<Int> 的元素类型当作普通运行时类型安全区分。

四、作用域函数(let/run/with/apply/also)

函数引用对象返回值典型用途
letitlambda 结果非空判断后操作 x?.let { }
runthislambda 结果配置对象并计算结果
withthislambda 结果对一个对象多次操作(非扩展)
applythis对象本身初始化配置 Paint().apply { }
alsoit对象本身副作用(打日志)不改链式

记忆法:返回结果用 let/run/with,返回自身用 apply/also;用 it 是 let/also,用 this 是 run/with/apply。

五、class 家族

  • data class:自动生成 equals/hashCode/toString/copy/componentN。注意 copy 是浅拷贝;只有主构造参数参与生成。
  • sealed class / sealed interface:密封类型,子类受限在同一模块。配合 when 可穷尽分支(无需 else),适合表达状态(Loading/Success/Error)。
  • object:单例;companion object 伴生对象(类级别成员,可实现接口、可命名)。
  • enum:枚举,可带属性和方法。
  • 嵌套 vs 内部类:Kotlin 嵌套类默认是静态的;加 inner 才持有外部类引用。

六、委托(Delegation)

委托的心智模型是:我不亲自做,把某件事交给另一个对象做。它解决的是“复用行为”问题,不是“继承层级”问题。

Kotlin 里有两类常见委托:

类型解决什么问题典型写法面试关键词
类委托把接口实现转交给另一个对象class B(a: A) : A by a组合优于继承、编译器生成转发
属性委托把 getter/setter 的通用逻辑抽出去val x by lazy { }getValue / setValue 约定

1. 类委托:组合优于继承

interface DataSource {
    fun query(): String
}

class RemoteDataSource : DataSource {
    override fun query(): String = "remote"
}

class LoggingDataSource(
    private val real: DataSource
) : DataSource by real {
    override fun query(): String {
        println("before query")
        return real.query()
    }
}

: DataSource by real 的意思是:如果 LoggingDataSource 没有自己实现某个 DataSource 方法,编译器就自动生成转发代码,把调用交给 real

这比继承更灵活:你可以包装不同实现、增强行为、替换数据源,而不用把类层级设计得很深。Android 里常见于 Repository 包装、缓存层包装、埋点/日志包装、测试 fake 实现替换。

易错点:外层类 override 的成员,不会改变委托对象内部自己的调用逻辑。委托对象在执行自己的方法时,访问的是它自己的成员,不是外层包装类 override 后的成员。

2. 属性委托:把属性访问逻辑抽出去

val config by lazy {
    loadConfigFromDisk()
}

var name: String by Delegates.observable("unknown") { property, old, new ->
    println("${property.name}: $old -> $new")
}

属性委托的本质是编译器把属性访问改写为对委托对象的调用:

class StringDelegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "value of ${property.name}"
    }
}

val title: String by StringDelegate()

读取 title 时,实际调用的是 StringDelegate.getValue(...)。如果是 var,还需要 operator fun setValue(...)

常见标准库委托:

  • by lazy { }:首次访问才初始化。默认 SYNCHRONIZED,多线程安全但有锁开销;单线程场景可考虑 LazyThreadSafetyMode.NONE
  • Delegates.observable:属性变化时回调,适合状态监听。
  • Delegates.notNull():非空但延迟赋值,访问前未赋值会抛异常。
  • by map:属性名作为 key,从 Map 中读值,常见于 JSON/配置映射示例。

Android 落地可以这样理解:ViewBinding 委托、SharedPreferences/DataStore 属性包装、页面参数校验、配置中心读取,本质都是把重复的 getter/setter 或初始化逻辑集中管理。

七、泛型型变

泛型型变解决的是:泛型类型之间能不能保持原来的父子关系

先看一个反直觉点:

val strings: MutableList<String> = mutableListOf("a")
// val anys: MutableList<Any> = strings // 编译不允许

虽然 StringAny 的子类,但 MutableList<String> 不是 MutableList<Any> 的子类。否则下面这种写法就会破坏类型安全:

val strings: MutableList<String> = mutableListOf("a")
val anys: MutableList<Any> = strings // 假设允许
anys.add(123)                        // 往 String 列表塞 Int
val s: String = strings[1]           // 运行时炸掉

所以 Kotlin 默认让泛型不型变(invariant)。只有当编译器能确认安全时,才允许你声明 outin

关键字心智模型只能做什么典型例子Java 类比
out T生产者 Producer主要把 T 读出来List<out T> / Source<out T>? extends T
in T消费者 Consumer主要把 T 写进去 / 传进去Comparator<in T> / Sink<in T>? super T
不加既读又写精确类型MutableList<T>普通泛型
*不关心具体类型安全读取为上界,不能安全写入具体 TList<*>?

1. out:只生产,所以可以协变

interface Source<out T> {
    fun next(): T
}

val stringSource: Source<String> = object : Source<String> {
    override fun next(): String = "hello"
}

val anySource: Source<Any> = stringSource // 安全:String 一定也是 Any

Source<out T> 只把 T 作为返回值“吐出来”,外部不会把错误类型塞进去,所以 Source<String> 可以当成 Source<Any> 使用。

2. in:只消费,所以可以逆变

interface Sink<in T> {
    fun accept(value: T)
}

val anySink: Sink<Any> = object : Sink<Any> {
    override fun accept(value: Any) = println(value)
}

val stringSink: Sink<String> = anySink // 安全:能消费 Any,当然能消费 String

Sink<in T> 只接收 T,不把具体 T 返回给你。一个能处理 Any 的消费者,当然也能处理 String,所以赋值方向和 out 反过来。

3. 什么时候用什么

记住 PECS: Producer Extends, Consumer Super。Kotlin 写法就是:生产者用 out,消费者用 in

  • 只从容器里读 T:考虑 out T
  • 只往对象里传入 T:考虑 in T
  • 既要读又要写具体 T:通常不要加型变,保持 T 不型变。
  • 不知道具体类型,只想安全遍历或打印:用 * 星投影。

面试表达可以这样说:型变不是为了“更高级”,而是为了在不牺牲类型安全的前提下,让 API 参数更灵活。


高频面试题

Q1:===== 的区别? == 比较值(调用 equals),=== 比较引用。Java 的 == 对应 Kotlin 的 ===

Q2:lateinitby lazy的区别? lateinit 用于 var、非空、可多次赋值、不能用于基本类型、由开发者负责初始化时机;lazy 用于 val、首次访问自动初始化、线程安全可配置。

Q3:扩展函数能被重写吗?为什么? 不能。扩展函数是静态分发,编译成静态方法,调用哪个由声明类型决定而非运行时类型,所以没有多态。

Q4:inline 一定能提升性能吗? 不一定。inline 消除 lambda 对象分配,适合高阶函数;但内联会增大字节码,对大函数滥用反而增加体积、降低性能。Kotlin 编译器会对大 inline 函数告警。

Q5:Kotlin 的 UnitNothingAny 区别? Any 是所有非空类型的根(类比 Object);Unit 表示无返回值(类比 void,但是真实对象);Nothing 表示永不返回(抛异常或死循环),是所有类型的子类型。

Q6:data class 用作 HashMap 的 key 安全吗? 安全(自动生成了 hashCode/equals),但若字段可变,作为 key 后修改字段会导致查找失败 —— 应保证 key 不可变。

Q7:Kotlin 委托的本质是什么? 类委托本质是编译器为接口方法生成转发代码,把调用交给被委托对象;属性委托本质是把 getter/setter 改写为 getValue / setValue 调用。它的价值是用组合复用行为,减少继承和重复样板代码。

Q8:为什么 MutableList<String> 不能赋给 MutableList<Any>? 因为 MutableList 既能读也能写。如果允许赋值,就可以通过 MutableList<Any> 往原本的 MutableList<String> 里写入 Int,破坏类型安全。只读的生产者可以用 out,只写/消费的对象可以用 in,既读又写通常保持不型变。