01 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)
| 函数 | 引用对象 | 返回值 | 典型用途 |
|---|---|---|---|
let | it | lambda 结果 | 非空判断后操作 x?.let { } |
run | this | lambda 结果 | 配置对象并计算结果 |
with | this | lambda 结果 | 对一个对象多次操作(非扩展) |
apply | this | 对象本身 | 初始化配置 Paint().apply { } |
also | it | 对象本身 | 副作用(打日志)不改链式 |
记忆法:返回结果用 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)
- 类委托:
class B(b: Base) : Base by b,把接口实现委托给成员,组合优于继承。 - 属性委托:
by lazy { }:首次访问时计算,默认SYNCHRONIZED线程安全。Delegates.observable:值变化时回调。Delegates.notNull():非空但延迟赋值。by map:从 Map 读取属性。- 自定义委托需实现
getValue/setValue。
七、泛型型变
- out T(协变):只能作为输出(生产者),
List<out T>,List<String>可赋给List<Any>。 - in T(逆变):只能作为输入(消费者),
Comparator<in T>。 *星投影:不关心具体类型时使用。- PECS:Producer-Extends(out),Consumer-Super(in)。面试可用一句话落地:只从容器里读
T用out,只往容器里写T用in;既要读又要写具体T时通常不要加型变,否则编译器会限制不安全操作。
高频面试题
Q1:== 和 === 的区别?
== 比较值(调用 equals),=== 比较引用。Java 的 == 对应 Kotlin 的 ===。
Q2:lateinit 和 by lazy的区别?
lateinit 用于 var、非空、可多次赋值、不能用于基本类型、由开发者负责初始化时机;lazy 用于 val、首次访问自动初始化、线程安全可配置。
Q3:扩展函数能被重写吗?为什么? 不能。扩展函数是静态分发,编译成静态方法,调用哪个由声明类型决定而非运行时类型,所以没有多态。
Q4:inline 一定能提升性能吗? 不一定。inline 消除 lambda 对象分配,适合高阶函数;但内联会增大字节码,对大函数滥用反而增加体积、降低性能。Kotlin 编译器会对大 inline 函数告警。
Q5:Kotlin 的 Unit、Nothing、Any 区别?
Any 是所有非空类型的根(类比 Object);Unit 表示无返回值(类比 void,但是真实对象);Nothing 表示永不返回(抛异常或死循环),是所有类型的子类型。
Q6:data class 用作 HashMap 的 key 安全吗? 安全(自动生成了 hashCode/equals),但若字段可变,作为 key 后修改字段会导致查找失败 —— 应保证 key 不可变。