本文主要介绍 Kotlin 中的一些初始化顺序、函数内联、内联相关关键字(noinline、crossinline、reified)、枚举、可空类型以及委托机制等内容。
一、Kotlin 的初始化顺序
在 Kotlin 中,不论是顶层类还是继承层次里的「子类」,它自己的属性初始化表达式和 init {} 代码块,都会在 该类的主构造函数 完成期间执行完毕,之后才轮到它的次级构造函数体。先给一张总顺序表,再用代码验证。
┌── 主构造阶段 ───────────────┐ ┌─ 次级构造函数体 ─┐
① 主构造参数默认值
② 所有属性初始化表达式 <-------都在这里一次性完成
③ 所有 init { } 代码块
└───────────────┬───────────┘
│—— this(...) 委托调用
┌───────────────▼───────────┐
④ 次级构造函数代码块 ← 只有这一步属于“次构造体”
└───────────────────────────┘
用打印验证执行顺序
class Demo(val p: Int = log("param")) { // ①
val a = log("prop a") // ②
val b = log("prop b") // ②
init { log("init A") } // ③
init { log("init B") } // ③
constructor() : this(0) { // ④
log("secondary body")
}
}
fun log(tag: String): Int {
println(tag)
return 0
}
fun main() { Demo() }
输出结果
param
prop a
prop b
init A
init B
secondary body
可以看到:
param、prop a、prop b、init A、init B——全部在进入次级构造体之前完成。secondary body(次构造函数体)最后才执行,一次且仅一次。
-
主构造函数与次级构造函数
-
执行顺序:
Kotlin 规定必须先执行主构造函数,再执行次级构造函数中的代码。因此,次级构造函数必须先委托调用主构造函数。 -
成员变量和 init 代码块:
成员变量的初始化和 init 代码块的执行顺序是按照它们在类中声明的顺序进行的。 -
后续处理:
紧接着就是属性的初始化。如果是 data class,还会自动生成并初始化一些方法,例如:equals()hashCode()toString()copy()componentsN()系列函数
备注:在 data class 中,
==表示调用equals()方法,而===则用于比较引用地址。如果重写了equals()方法,==会比较对象的内容(按你定义的逻辑)。如果没有重写equals()方法,==实际上会比较对象的引用,类似于===运算符。
-
二、中缀函数(infix)
Kotlin 支持中缀函数,使得调用方式更加自然。可以使用空格直接调用函数了,而不是用 "."。以下以示例说明如何将普通函数改写为中缀函数:
普通函数:
fun <T> T?.showInfo(default: String = "对象为空"): T? {
println(this ?: default)
return this
}
fun main() {
val a: String? = null
a.showInfo()
}
改为中缀函数:
infix fun <T> T?.showInfo(default: String): T? {
println(this ?: default)
return this
}
fun main() {
val a: String? = null
a showInfo ("对象为空!")
}
说明:中缀函数要求函数名位于变量两侧,使得代码看起来更像自然语言表达,调用时函数关键字处于中间。Kotlin 的 infix 函数不支持默认值参数。如果你需要默认值,请使用普通函数或提供函数的重载。
三、内联函数(inline)及相关关键字
由于 Java 对函数类型的原生支持有限,Kotlin 提供了内联函数(inline)以优化 lambda 表达式的性能,避免每次调用时创建新的函数对象,并降低调用堆栈深度。具体说明如下:
1. inline 的作用
-
内联优化:
内联函数在编译时会将函数体直接插入到调用处,避免函数调用开销。例如:fun hello(postAction: () -> Unit) { println("Hello!") postAction() } // 调用处 fun main() { hello { println("Bye!") } }不使用内联的时候,编译出来的结果是这样的:
fun main() { val post = object : Function0<Unit> { override fun invoke() { return println("Bye!") } } }
使用内联的结果相当于:
fun main() {
println("Hello!")
println("Bye!")
}
这样就不再创建 lambda 对象,性能更高,尤其在循环和高频调用场景中效果显著。最好是在频繁使用的高阶函数处使用inline,否则可能造成包体积的负优化等。
2. noinline
用在参数上的关键字,局部关闭内联优化。
- 作用:
对于内联函数的参数,默认情况下 lambda 表达式会被内联。如果不希望某个 函数类型的 参数被内联,则可以使用noinline修饰。函数类型的参数本质上是一个对象,可以把对象当做函数来调用,这也是最常见的做法,但是同时也可以当做对象来用,比如当做返回值。但是当我们把这些函数都做成内联的时候,他们就不再是对象了:
inline fun hello(preAction: () -> Unit, postAction: () -> Unit): ()->Unit {
preAction() // 做前置工作
println("Hello!")
postAction() // 做点后续工作
return postAction // 需要返回值
}
fun main() {
hello({
println("Emm...")
}, {
println("Bye!")
})
}
// 因为hello有inline关键字,那么实际的代码就变成了这样:
fun main() {
println("Emm...")
println("Hello!")
println("Bye!")
// postAction 这一行就会报错。
}
如果还想要返回其中的函数型类型的参数的话,比如preAction或者是postAction,那么就需要在他们前边加上noinline 关键字。
inline fun hello(preAction: () -> Unit, noinline postAction: () -> Unit): ()->Unit {
preAction() // 做前置工作
println("Hello!")
postAction() // 做点后续工作
return postAction // 需要返回值
}
// 上边的代码编译之后的效果
fun main() {
println("Emm...")
println("Hello!")
val postAction = ({
println("Bye!")
}).invoke()
postAction
}
注意:被 noinline 修饰的 函数类型的参数 将不会参与内联优化,因而需要实际创建函数对象。
3. crossinline
用在参数上的关键字。
✦ 一句话结论
crossinline 禁止 Lambda 里的“非局部 return” ,既能保持参数被内联(inline 带来的零开销),又能安全地把该 Lambda 放到其他线程、回调或高阶函数中执行。
1 为什么需要 crossinline
| 场景 | 问题 | crossinline 解决 |
|---|---|---|
| 内联函数把 Lambda 直接展开到调用处 | Lambda 里写 return 会尝试直接返回到外层调用者 | 如果 Lambda 被异步调用,这个 “跳出去” 行为会让编译器报错 |
| 异步执行或转手传递 Lambda | 但仍想保留 inline 带来的性能 | 加 crossinline,禁止 Lambda 用 return 跳出,让它安全“逃逸” |
2 代码示例
在内联函数中,删除crossinline之后,无法在一个异步线程中执行函数参数了。这是因为kotlin是为了避免这样一种情况,所才从语法层面完全禁止这种调用:
doSomething 被标记为 inline,这意味着它的参数 action 理论上可以在 Lambda 里写裸 return 来“跳出” doSomething:
doSomething {
println("before")
return // 按规则:应直接结束 doSomething 的调用
}
问题出在执行位置
Thread {
action() // ← 这里并不是在 doSomething 的 *同一栈帧* 中执行
return@Thread
}.start()
action()被包进Runnable,真正执行要等新线程启动。- 如果
action里写了裸return,它会试图回到已经不存在的doSomething栈帧 → 非法跳转。 - 编译器因此禁止这种调用,并给出错误提示。
crossinline禁止 在action内部出现裸return,从源头杜绝“跨线程跳栈”。这样编译器就放心让我们把action延迟 到别的线程/回调里执行。
inline fun doSomething(crossinline action: () -> Unit) {
// Lambda 被封装进 Runnable,稍后在新线程执行
Thread {
action() // ✅ 可以调用
// action() 内部不能写 return;只能用 return@action 或省略,只结束 Runnable.run(),不会企图跳到 doSomething
}.start()
}
fun main() {
doSomething {
println("Running in another thread")
// return // ❌ 编译错误:crossinline 禁止非局部 return
return@doSomething // ✅ 可以,局部返回
}
}
要点
- 如果去掉
crossinline,编译器会因“Lambda 可能在函数返回后执行”而拒绝编译。 crossinline让 Lambda 依旧 inline,但把return改限制为只能局部返回(用return@label)。
编译器这么限制是为了 类型 & 控制流安全:
如果允许裸return,Lambda 可能在未来某个时刻(甚至另一个线程)才执行,
这时外层函数早就返回,栈帧已不存在,直接跳回去会破坏 JVM 的正常执行。
3 与其他关键字对比
| 关键字 | 是否内联 | 是否允许非局部 return | 典型用途 |
|---|---|---|---|
inline(默认) | ✔︎ | ✔︎ | 普通高阶函数,Lambda 当场执行 |
noinline | ✖︎ | ✖︎ | 需要把 Lambda 当对象存储或返回 |
crossinline | ✔︎ | ✖︎ | Lambda 异步/延迟调用,仍想省掉对象开销 |
4. reified
- 作用:
Kotlin 中泛型在运行时会被擦除,无法直接获取具体类型。通过内联函数结合reified关键字,可以在函数内部获取泛型的实际类型。
示例1:
inline fun <reified T> test(t: T) {
val c = T::class.java // 编译成功,能获取到 T 对应的 Class 对象
}
注意:只有在内联函数中,使用 reified 才有意义,因为编译器需要在调用处将泛型信息写入代码中。reified 关键字通常与 inline 函数一起使用,它可以让泛型类型参数在运行时保留具体的类型信息。通常由于类型擦除(type erasure),泛型在运行时是不可见的,但是使用 reified 后,你可以在函数体内直接使用类型参数,例如做类型检查或者获取 KClass 对象。
示例2
inline fun <reified T> isInstance(value: Any): Boolean {
return value is T
}
fun main() {
println(isInstance<String>("Hello")) // 输出 true
println(isInstance<Int>("Hello")) // 输出 false
}
在上面的代码中,由于 T 被标记为 reified,所以在函数体内可以直接使用 is T 来判断传入的值是否为该类型。如果没有 reified 标记,就无法在运行时获取 T 的类型信息,从而无法进行这样的操作。
注意事项
- 仅适用于 inline 函数:
reified类型参数只能用于inline函数,因为只有在内联的场景下,编译器才能将泛型的具体类型信息嵌入到调用处的代码中。 - 类型安全: 使用
reified可以帮助你写出更加类型安全的代码,因为你可以在运行时进行类型判断,而无需显式传递Class或者KClass对象。
四、枚举(Enum)
-
常用静态方法:
values():返回枚举中所有值的数组。valueOf(String):根据名称返回对应的枚举常量。
-
成员属性:
name:枚举常量的名称。ordinal:枚举常量的序号。
-
其它:
- 枚举支持匿名内部类,可以为枚举常量重写方法。
- 枚举可以实现接口,但不能继承其他类。
下面是一个综合示例,展示了枚举的常用静态方法、成员属性、匿名内部类以及实现接口的用法:
// 定义一个接口,枚举将实现这个接口
interface Operation {
fun apply(a: Int, b: Int): Int
}
// 定义一个枚举类实现 Operation 接口
enum class MathOperation : Operation {
// 每个枚举常量通过匿名内部类重写 apply 方法,实现各自的运算逻辑
ADD {
override fun apply(a: Int, b: Int) = a + b
},
SUBTRACT {
override fun apply(a: Int, b: Int) = a - b
},
MULTIPLY {
override fun apply(a: Int, b: Int) = a * b
},
DIVIDE {
override fun apply(a: Int, b: Int) = if (b != 0) a / b else throw IllegalArgumentException("除数不能为0")
}
}
fun main() {
// 使用 values() 方法,获取枚举中所有的常量
val operations = MathOperation.values()
println("枚举中的所有运算:")
for (op in operations) {
// 使用 name 和 ordinal 属性
println("名称:${op.name},序号:${op.ordinal}")
}
// 使用 valueOf() 方法,根据名称获取枚举常量
val opName = "MULTIPLY"
val op = MathOperation.valueOf(opName)
val result = op.apply(6, 7)
println("\n使用 valueOf() 获取 '$opName' 后,6 和 7 的运算结果为:$result")
// 演示枚举实现接口的调用
println("\n通过接口调用运算方法:")
for (operation in MathOperation.values()) {
println("${operation.name} 运算:6 和 7 的结果是 ${operation.apply(6, 7)}")
}
}
说明
-
常用静态方法:
MathOperation.values()返回枚举中所有常量的数组。MathOperation.valueOf(opName)根据给定名称返回对应的枚举常量。
-
成员属性:
- 每个枚举常量都有
name属性,表示常量的名称。 ordinal属性表示枚举常量在枚举中的序号(从 0 开始)。
- 每个枚举常量都有
-
匿名内部类:
- 每个枚举常量通过大括号内的代码块,重写了
apply方法,实现了各自不同的运算逻辑。
- 每个枚举常量通过大括号内的代码块,重写了
-
实现接口:
- 枚举类
MathOperation实现了接口Operation,所以所有枚举常量都必须提供apply方法的实现。 - 注意,枚举不能继承其他类,因为它们默认继承自
Enum类,但可以实现一个或多个接口。
- 枚举类
五、可空类型与基本类型
- Kotlin 中不存在原始基本类型,所有数据类型都是对象。
- 对于可空类型,编译后会转换为相应的包装类型;而非可空类型则尽可能使用 JVM 原生的基本类型,这样可以获得更好的性能。
- 这种设计使得类型转换操作更加透明,必须由开发者主动处理类型转换问题。
六、委托(Delegation)
Kotlin 的委托机制分为类委托和属性委托,分别对应 Java 中的代理模式和属性访问代理。
1. 类委托
示例 1:基础接口委托
通过实现接口,并在类内部持有一个同类型成员,可以将大部分接口实现委托出去。
// 定义一个接口
interface Printer {
fun print(message: String)
}
// 实现接口的一个具体类
class SimplePrinter : Printer {
override fun print(message: String) {
println("Printing: $message")
}
}
// 通过接口委托,把 Printer 的实现委托给传入的 delegate 对象
class DelegatingPrinter(private val delegate: Printer) : Printer by delegate
fun main() {
val printer = SimplePrinter()
val delegatingPrinter = DelegatingPrinter(printer)
delegatingPrinter.print("Hello, Kotlin!")
}
说明:
DelegatingPrinter 使用 : Printer by delegate 把 Printer 接口的大部分实现委托给了 delegate 对象(这里是 SimplePrinter 的实例),从而无需重复实现所有方法。
示例 2:添加扩展功能
如果需要在委托的基础上添加额外的功能,可以在委托的同时重写部分方法。
// 接口同上
interface Printer {
fun print(message: String)
}
// 基础实现类
class SimplePrinter : Printer {
override fun print(message: String) {
println("Printing: $message")
}
}
// 扩展类,通过委托实现接口,并在部分方法中添加额外功能
class LoggingPrinter(private val delegate: Printer) : Printer by delegate {
// 重写 print 方法,在调用委托对象之前和之后做一些日志输出
override fun print(message: String) {
println("Log: 开始打印")
delegate.print(message)
println("Log: 打印结束")
}
}
fun main() {
val printer = SimplePrinter()
val loggingPrinter = LoggingPrinter(printer)
loggingPrinter.print("Hello, Kotlin with logging!")
}
说明:
在 LoggingPrinter 中,使用 : Printer by delegate 委托了接口实现,但重写了 print 方法,从而在调用实际打印前后添加了日志功能。这展示了如何在委托的基础上进行局部定制。
示例 3:局部定制与功能扩展
通过委托机制,可以只定制接口中的部分方法,而其他方法则保持原有实现不变。下面以数据存储为例,展示如何在加载数据时添加缓存功能。
// 定义数据存储接口
interface DataStore {
fun save(data: String)
fun load(): String
}
// 文件存储的具体实现
class FileDataStore : DataStore {
override fun save(data: String) {
println("将数据保存到文件:$data")
}
override fun load(): String {
println("从文件中加载数据")
return "file data"
}
}
// 缓存存储类:委托了 DataStore 接口,但重写了 load 方法,实现缓存功能
class CachingDataStore(private val delegate: DataStore) : DataStore by delegate {
private var cache: String? = null
override fun load(): String {
if (cache == null) {
cache = delegate.load()
}
println("返回缓存数据:$cache")
return cache!!
}
}
fun main() {
val fileStore = FileDataStore()
val cachingStore = CachingDataStore(fileStore)
// 第一次调用 load,会从文件中加载
println(cachingStore.load())
// 后续调用 load,则直接返回缓存的数据
println(cachingStore.load())
// save 方法没有重写,依然使用委托对象的实现
cachingStore.save("New data")
}
说明:
在 CachingDataStore 中,我们对 load 方法进行了局部定制,添加了缓存逻辑,而 save 方法则完全委托给 FileDataStore。这样既复用了原有功能,又能根据需要增加新的特性。
2. 属性委托
- 说明:
Kotlin 还支持将属性的 getter 和 setter 委托给另一个对象。这种机制称为属性委托。常见的委托有lazy、observable、vetoable等。如果自定义的话,委托类需要使用operator修饰符来实现getValue()和setValue()方法,其参数必须包含委托属性所属的类(Owner)和属性信息(KProperty)。
2.1 延迟属性 (Lazy)
延迟属性在首次访问时进行初始化,并将结果缓存起来。
示例:
val lazyValue: String by lazy {
println("Computing the value...")
"Hello"
}
fun main() {
println(lazyValue) // 第一次访问时会计算并打印 "Computing the value..." 和 "Hello"
println(lazyValue) // 后续访问直接返回缓存值,不再计算
}
2.2 可观察属性 (Observable)
通过 Delegates.observable 可以在属性变化时自动调用回调函数。
示例:
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("<no name>") { prop, old, new ->
println("属性 ${prop.name} 由 $old 变为 $new")
}
}
fun main() {
val user = User()
user.name = "Alice" // 触发回调,打印:属性 name 由 <no name> 变为 Alice
user.name = "Bob" // 触发回调,打印:属性 name 由 Alice 变为 Bob
}
2.3 可限制修改属性 (Vetoable)
Delegates.vetoable 允许在属性值改变前进行验证,决定是否接受这个新值。
示例:
import kotlin.properties.Delegates
class User {
var age: Int by Delegates.vetoable(0) { _, old, new ->
new >= old // 只有新值大于或等于旧值时才允许修改
}
}
fun main() {
val user = User()
user.age = 18
println(user.age) // 输出 18
user.age = 16 // 由于新值小于旧值,修改被拒绝
println(user.age) // 依然输出 18
}
2.4 自定义属性委托
这种委托只能用于顶层属性、类成员属性或扩展属性,不能用于方法内部的局部变量,因为局部变量没有生成标准的 get/set 方法。
属性委托 (delegated property) 的本质:
把读取 (get) 与写入 (set) 的实现交给“委托对象”——一个实现了 getValue/setValue 运算符函数的对象。
这个“对象”通常是你 new 出来的一个 小类实例,而不是某个已存在的属性。
- 示例1:简单自定义属性委托
1. 委托实现
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/** 确保属性始终 ≥ 0,违法时抛 IllegalArgumentException */
class NonNegativeInt(init: Int = 0) : ReadWriteProperty<Any?, Int> {
private var field = init
override fun getValue(thisRef: Any?, property: KProperty<*>): Int = field
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
require(value >= 0) {
"${property.name} must be non-negative (got $value)"
}
field = value
}
}
使用方式
class Player {
var score by NonNegativeInt() // 默认为 0
}
fun main() {
val p = Player()
p.score = 10 // ✅ OK
p.score = -3 // ❌ IllegalArgumentException
}
要点
- 业务类只写一行
var score by NonNegativeInt(),
校验逻辑完全藏在委托里。 - 若后期要改成「最大值 9999」或加埋点,只需改委托,不动业务代码。
-
示例2:自定义委托实现
// **写法差异** 只体现在 *代码层面的可读性、复用性*: // - `Simple07` **直接声明运算符函数**,轻量、语法最短; // - `StringDelegate` **实现标准接口 `ReadWriteProperty`**,范型可配、IDE 能补全、更通用。 class Owner { var text: String by Simple07() var text2: Int by StringDelegate() } class Simple07 { private var str: String = "Default" operator fun getValue(owner: Owner, property: KProperty<*>): String { println("Simple07 getValue 执行啦") return str } operator fun setValue(owner: Owner, property: KProperty<*>, value: String) { println("Simple07 setValue 执行啦") str = value } } class StringDelegate : ReadWriteProperty<Owner, Int> { private var number: Int = 10000 override fun getValue(thisRef: Owner, property: KProperty<*>): Int { println("ReadWriteProperty getValue 执行啦") return number } override fun setValue(thisRef: Owner, property: KProperty<*>, value: Int) { println("ReadWriteProperty setValue 执行啦") number = value } }
如何使用:
fun main() {
val o = Owner()
println("初始 text = ${o.text}") // 触发 Simple07.getValue
o.text = "Hello"
println("修改后 text = ${o.text}") // 再次触发 getValue
println("初始 text2 = ${o.text2}") // 触发 StringDelegate.getValue
o.text2 = 42
println("修改后 text2 = ${o.text2}")// 触发 setValue + getValue
}
-
示例3:对外只读、对内可修改的集合委托
如果需要实现一个集合,对外暴露只读接口,但内部可修改,可以使用属性委托:class Model2 { // 对外暴露不可修改的 List,通过委托到内部的 MutableList val data: List<String> by ::_data // 内部使用 MutableList,可以修改数据 private val _data: MutableList<String> = mutableListOf() fun load() { _data.add("Hello") // 内部可以修改 } } fun main() { val model = Model2() // model.data.add("Hello") // 编译错误,因为 data 是只读 List // val localValue: String by lazy { "Hello, World!" } // 从 Kotlin 1.4 开始,局部委托属性是允许的。 println(model.data) }
注意:委托只能用于属性委托或类委托,从 Kotlin 1.4 开始,局部委托属性是允许的。之前的版本会报错。
七、Nothing
Nothing 是所有类型的子类型。它有3个作用。
- 第一个作用,作为函数的返回值,表达这个函数永不返回,抛异常或者无限循环的场景
fun foreverRepeat(): Nothing {
while (true) {
// 无限循环的代码
}
}
- 第二个主要用途,用来作为泛型变量的一个通用的空白占位置:
val emptyList: List<Nothing> = listOf()
var apples: List<Apple> = emptyList
var users: List<User> = emptyList
var phones: List<Phone> = emptyList
var images: List<Image> = emptyList
val emptyProducer: Producer<Nothing> = Producer()
var appleProducer: Producer<Apple> = emptyProducer
var userProducer: Producer<User> = emptyProducer
var phoneProducer: Producer<Phone> = emptyProducer
var imageProducer: Producer<Image> = emptyProducer
- 第三个用途,让代码从语法层面得到解释,完善语法,比如return 返回的就是Nothing.
一些问题
1、不同高阶函数的一些区别
fun runAsUser(block: (Int.() -> Unit)) {
1.block() // 或者写成 value.block()
}
fun runAsUser(block : (Double)->Unit) {
block()
}
上边的两种写法编译之后是这样的:
public final void runAsUser(@NotNull Function1 block) {
Intrinsics.checkNotNullParameter(block, "block");
block.invoke(1);
}
public final void runAsUser(@NotNull Function1 block) {
Intrinsics.checkNotNullParameter(block, "block");
block.invoke(1.0);
}
可见block: (Int.() -> Unit,block : (Double)->Unit 编译之后并没有区别,只是一个有隐式的receiver,而另一个没有罢了。
学习速查
| 问题 | 答案 | |
|---|---|---|
| 1 | Kotlin 中对象的完整初始化顺序? | ① 主构造函数参数默认值 → ② 属性声明顺序 → ③ init 块顺序 → ④ 次级构造函数体(次级构造函数必须先委托主构造函数)。 栈示例:<主构造> → prop1 → prop2 → initA → initB → <次级构造体> |
| 2 | infix 函数的两条语法限制? | ① 必须是成员函数或扩展函数;② 只能有一个参数且不能带默认值。 |
| 3 | inline 带来哪两层性能收益? | ① 消除函数调用栈;② 避免为每次 Lambda 调用生成 Function 对象(减少堆分配 + GC)。 |
| 4 | 什么时候给 inline 参数加 noinline? | 当该 Lambda 需要作为对象存储、返回或传递时,否则被内联后就不再是对象。 |
| 5 | crossinline 解决了什么问题? | 保留内联零开销的同时,禁止 Lambda 的非局部 return,从而可安全地在异步线程/回调里执行该 Lambda。 |
| 6 | 如何同时用 noinline+crossinline? | 把一个 Lambda 返回(noinline)给外部缓存,另一个 Lambda 异步执行(crossinline);两者共存同一个 inline 函数。 |
| 7 | 为什么 reified 只能出现在 inline 函数中? | 编译器需在调用点注入字节码以保留实际泛型类型;非内联函数没有此展开机会。 |
| 8 | Kotlin 枚举常量如何重写接口方法? | 编译器把每个常量生成成 匿名内部类,这些类继承同一个 Enum 子类并各自覆盖方法。 |
| 9 | Int? 何时会装箱? | 只要出现可空类型或泛型擦除场景就会 boxed;纯 Int 非可空且非泛型时编译为 int。边界例:listOf<Int?>() 里的元素会装箱。 |
| 10 | 类委托 vs. Decorator 最大区别? | 类委托由 语言级语法自动转发全部接口方法,可选局部重写;Decorator 需手工转发,样板多。 |
| 11 | 写一个磁盘缓存属性委托的核心思路? | getValue:内存缓存为空→读文件→缓存;setValue:写文件→刷新缓存→发送事件。 |
| 12 | 为什么 List<Nothing> 能赋值给 List<Apple>? | Nothing 是一切类型的协变子类型,充当“永不产生元素”的生产者占位符,类型安全。 |
| 13 | (Int.() -> Unit) 与 (Int) -> Unit 编译后差异? | 前者多了隐藏接收者参数,字节码签名仍是 Function1<Integer, Unit>,但可重载因 Kotlin 语义不同。 |
| 14 | Delegates.observable 与 StateFlow 区别? | 前者同步回调、无背压、不具备冷流语义;后者是热流、支持多订阅、具备背压与协程取消。 |
| 15 | 局部 by lazy 线程安全等级? | 使用 LazyThreadSafetyMode.SYNCHRONIZED — 即与全局 lazy 相同:首次访问加锁,保证多线程安全,局部变量作用域结束即被 GC。 |