委托与属性原理
# 05.委托与属性原理
# 目录介绍
- 1. 案例引入
- 2. 架构概览
- 3. 属性委托字节码
- 4. lazy三种模式
- 5. 类委托字节码
- 6. 内建委托家族
- 7. 自定义属性委托
- 8. 委托性能代价
- 9. 事故案例复盘
- 10. 综合案例串讲
# 1. 案例引入
# 1.1 一段反常代码
以下代码是一个真实事故——某 SDK 用 by lazy(LazyThreadSafetyMode.NONE) 做全局配置的懒加载,线上偶发 NullPointerException:
// AppConfig.kt
object AppConfig {
val instance: Config by lazy(LazyThreadSafetyMode.NONE) { // ⚠ NONE 模式
parseConfig()
}
}
// 多个线程同时启动时:
Thread { AppConfig.instance.timeout }.start()
Thread { AppConfig.instance.timeout }.start() // 偶发返回 null
上报数据:
| 指标 | 数据 |
|---|---|
| Crash 上报期间 | 3 天(灰度 5%) |
| 崩溃次数 | 214 次 |
| 崩溃概率 | ~0.02%(DAU 10 万时约 20 次/天) |
| 崩溃线程 | 100% 非主线程 |
| 崩溃现场 | Config.timeout 抛 NPE,但 Config 类字段声明是非空 Int |
最诡异的地方在于——timeout 在 Config 类里明明是 val timeout: Int = 30,怎么可能取到 null 触发 NPE?(其实是装箱后的 Integer 引用取到 null)。
# 1.2 顺藤摸到根因
表象:Config? 声明的字段返回 null?还是 parseConfig() 里的字段没初始化就被读到?
反编译 AppConfig.instance 的 getter 字节码:
public static final Config getInstance() {
Lazy lazy = instance$delegate; ; 拿 Lazy 对象
KProperty prop = $$delegatedProperties[0]; ; 拿属性元数据
return (Config) lazy.getValue(); ; 委托取值
}
再深入 UnsafeLazyImpl 源码(NONE 模式对应的实现):
internal class UnsafeLazyImpl<out T>(initializer: () -> T) : Lazy<T> {
private var initializer: (() -> T)? = initializer
private var _value: Any? = UNINITIALIZED_VALUE
// ⚠ 注意:_value 和 initializer 都是普通字段,无 @Volatile
override val value: T
get() {
if (_value === UNINITIALIZED_VALUE) {
_value = initializer!!()
initializer = null
}
@Suppress("UNCHECKED_CAST")
return _value as T
}
}
问题定位:JMM(Java Memory Model)下,普通字段的写入不保证对其他线程立即可见:
Time Thread A Thread B
t0 读 _value == UNINITIALIZED_VALUE
t1 调 initializer() 分配 Config 对象 obj
t2 obj.timeout = 30(构造函数内)
t3 _value = obj 读 _value(可能看到 obj,也可能仍是 UNINITIALIZED)
↓
此时 obj 已经写入,但 obj.timeout 的初始化
可能因为**指令重排序**尚未完成
读到 obj.timeout = 0 (int 默认值)
或读到装箱 Integer 引用 = null
根因:NONE 模式没有 volatile、没有 synchronized、没有 happens-before 语义——多线程读到部分构造对象是完全合法的 JMM 行为。
# 1.3 我们要回答什么
看到这里,我们至少积累了 6 个疑问:
by lazy编译后到底生成了什么?多少个类、多少个字段?SYNCHRONIZED / PUBLICATION / NONE三种模式各自的实现有什么区别?- 属性委托
by与类委托by是同一个关键字,语义是同一回事吗? Delegates.observable的通知机制怎么工作?为什么容易泄漏?KProperty<*>是什么?为什么每次 getter 调用都要传它?- 类委托与 Java 动态代理
Proxy.newProxyInstance差在哪?
带着这 6 个问题,进入正文。
# 2. 架构概览
# 2.1 委托两大家族
Kotlin `by` 关键字
├── 类委托 (Class Delegation)
│ └── class C : I by impl { ... }
│ ├── 编译器为每个接口方法生成转发桩
│ └── 编译期完成,零反射
└── 属性委托 (Property Delegation)
└── val prop: T by delegate
├── 编译器生成 backing field 存储 delegate
└── getter/setter 内调用 delegate.getValue()/setValue()
两者对比:
| 维度 | 类委托 | 属性委托 |
|---|---|---|
| 语法位置 | class ... : Interface by expr | val/var name: T by expr |
| 委托对象职责 | 实现某个接口 | 提供 getValue/setValue |
| 编译器生成 | 接口方法转发桩 | getter/setter + delegate 字段 |
| 一个类可否多个 | 可(每个接口一个 delegate) | 可(每个属性一个 delegate) |
| 典型用途 | 组合替代继承(Repo by impl) | by lazy / observable / Map 取值 |
同一个关键字承担两个语义——这是 Kotlin 语言设计的取舍。
# 2.2 为什么这么切
疑惑:为什么 Kotlin 把两种截然不同的语义都用 by 表达?
论证:
- 两者都是**"把行为委托给另一个对象"**——语义在直觉上一致(把动作交给别人做)
- 都通过编译期代码生成实现,不依赖反射——保持性能可预测
- 都体现"组合优于继承"的设计哲学——GoF 十大经典设计模式之一
- 让 API 使用者一眼看出"这不是继承,是委托"——降低阅读复杂度
深层动机:Effective Java 第 18 条明确指出"Favor composition over inheritance"(组合优于继承)。但 Java 里实现委托语法负担极重——每个接口方法都要手写转发桩(几十个方法就是几十行样板代码)。Kotlin 用 by 关键字把这个模式变成语言级支持。
结论:by 是 Kotlin 对"组合模式"的语言级支持——把 GoF 的委托模式变成一个关键字,让"组合优于继承"从口号变成能落地的语法。
# 3. 属性委托字节码
# 3.1 getValue约定方法
val name: String by delegate
约定:delegate 对象必须提供 getValue(thisRef, property): T 方法(var 还要 setValue)。这不是接口约束,是编译期约定——只要有匹配签名的 operator 函数即可。
约定签名:
// 只读属性 val
operator fun getValue(thisRef: R, property: KProperty<*>): T
// 可写属性 var(还需要)
operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
关键词:
operator—— Kotlin 让编译器识别为"约定方法"的标记thisRef: R—— 属性所在类的引用(顶层属性时为Nothing?)property: KProperty<*>—— 属性元数据(名字、返回类型、注解等)T—— 属性类型
约定不是接口的价值:可以让任何对象(如 Map、java.util.Properties、自定义 DSL 对象)通过扩展函数加上 getValue 变成合法委托对象,不需要继承特定接口。
// 标准库为 Map 加的扩展函数
operator fun <V, V1 : V> Map<in String, V>.getValue(thisRef: Any?, property: KProperty<*>): V1 =
getOrImplicitDefault(property.name) as V1
一行扩展函数,Map 就能作为委托对象——这就是"约定优于配置"的威力。
# 3.2 编译器生成字段
class Foo {
val name: String by MyDelegate()
}
反编译(简化):
public final class Foo {
// 委托对象字段
private final MyDelegate name$delegate = new MyDelegate();
// 类初始化时构造属性元数据数组(这一段在 <clinit> 里)
static final KProperty[] $$delegatedProperties = {
(KProperty) new PropertyReference1Impl(
Reflection.getOrCreateKotlinClass(Foo.class), // 所属类
"name", // 属性名
"getName()Ljava/lang/String;" // 签名
)
};
// 编译器生成的 getter
public final String getName() {
return name$delegate.getValue(this, $$delegatedProperties[0]);
}
}
生成物清单:
| 生成物 | 数量 | 内存占用 | 说明 |
|---|---|---|---|
xxx$delegate 实例字段 | 每属性 1 个 | 4-8 字节引用 | 存放委托对象 |
$$delegatedProperties 静态数组 | 每类 1 个 | 数组本身 + 每属性 1 个 KProperty | 缓存属性元数据 |
KProperty 静态实例 | 每属性 1 个 | 一次生成,全局共享 | 反射需要 |
| getter/setter 方法 | 每属性 1-2 个 | 字节码若干 | 转发到 delegate |
观察:没有生成新类——所有生成物都放在原类中。这与 by lazy 里的 Lazy<T> 匿名对象不是一回事(by lazy 里额外分配的是 delegate 对象,不是新类)。
# 3.3 KProperty是什么
KProperty<*> 是 kotlin.reflect 里的接口,代表属性的元信息:
public interface KProperty<out V> : KCallable<V> {
public val name: String // "name"
public val getter: Getter<V> // getter 引用
public val annotations: List<Annotation> // 注解
// ...
}
每个委托属性会被编译器"注册"一个 KProperty 实例——存在类的静态数组 $$delegatedProperties 里。
为什么每次 getter 都要传 KProperty?
考虑一个通用委托:
class Logger : ReadOnlyProperty<Any?, String> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
println("read ${property.name}") // ← 需要知道属性名
return "value"
}
}
class Config {
val timeout: String by Logger()
val retries: String by Logger()
}
同一个 Logger 实例可能被多个属性委托复用——所以运行时必须传入 KProperty 告知"你现在为哪个属性工作"。这是委托机制通用性的必要代价。
编译器优化:optimizedCallableReferences 编译选项(Kotlin 1.4+ 默认)让 KProperty 生成更精简——用轻量的 PropertyReference1Impl 而非重量的反射对象。
# 3.4 setValue对称结构
var 属性的 by 委托同时生成 setter,内部调用 delegate.setValue(this, property, newValue):
class Foo {
var name: String by DelegateVar()
}
// 反编译等价于:
public final void setName(String value) {
this.name$delegate.setValue(this, $$delegatedProperties[0], value);
}
读写对称:val 生成 getter;var 生成 getter + setter。约定方法一一对应。
只写委托不存在——Kotlin 没有"write-only property"概念。
# 4. lazy三种模式
# 4.1 SYNCHRONIZED默认
默认模式——用 synchronized 保证多线程只初始化一次:
val name: String by lazy { compute() } // 默认 SYNCHRONIZED
内部实现(SynchronizedLazyImpl 源码简化):
private class SynchronizedLazyImpl<out T>(
initializer: () -> T, lock: Any? = null
) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE // ← @Volatile
private val lock = lock ?: this
override val value: T
get() {
val v1 = _value // ← volatile 读
if (v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return v1 as T // ← 快路径,无锁
}
return synchronized(lock) {
val v2 = _value // ← 双重检查
if (v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") v2 as T
} else {
val typedValue = initializer!!()
_value = typedValue // ← volatile 写
initializer = null // 释放 lambda 引用(可回收捕获的对象)
typedValue
}
}
}
override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE
}
关键设计 4 点:
- @Volatile 字段 —— 保证多线程可见性(happens-before)
- 双重检查锁模式 —— 快路径无锁,慢路径加锁;避免每次 get 都走 monitorenter
- UNINITIALIZED_VALUE 哨兵 —— 让
null也能作为合法的初始化结果(区分"未初始化"和"初始化为 null") - initializer = null —— 初始化后释放 lambda 引用,让 lambda 捕获的对象可被 GC
# 4.2 PUBLICATION实现
多线程可并发初始化,但只有一个结果被"发布":
val name by lazy(LazyThreadSafetyMode.PUBLICATION) { compute() }
SafePublicationLazyImpl 源码简化:
private class SafePublicationLazyImpl<out T>(initializer: () -> T) : Lazy<T> {
@Volatile private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
private val final: Any = Any() // CAS 目标标记
override val value: T
get() {
val v1 = _value
if (v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") return v1 as T
}
val initializerValue = initializer
if (initializerValue != null) {
val newValue = initializerValue()
if (valueUpdater.compareAndSet(this, UNINITIALIZED_VALUE, newValue)) {
initializer = null // 只有胜利者能置空
return newValue
}
}
@Suppress("UNCHECKED_CAST") return _value as T
}
companion object {
private val valueUpdater = AtomicReferenceFieldUpdater.newUpdater(
SafePublicationLazyImpl::class.java, Any::class.java, "_value"
)
}
}
关键机制:AtomicReferenceFieldUpdater.compareAndSet 让并发的初始化竞争,胜者的值被采用,其它值被丢弃。
代价与风险:
| 项 | 说明 |
|---|---|
| 无锁 | 相比 SYNCHRONIZED 少一次 monitorenter,更快 |
| initializer 可能被多次调用 | 竞争的多个线程各调一次 |
| 副作用要小心 | 例如 initializer 里发日志、加计数器会重复 |
| 内存开销 | 多一个 AtomicReferenceFieldUpdater 静态字段 |
典型场景:initializer 是纯函数、无副作用、多次执行结果一致——例如解析常量、加载资源。
# 4.3 NONE性能最优
单线程模式——不加锁,只用普通字段:
val name by lazy(LazyThreadSafetyMode.NONE) { compute() }
UnsafeLazyImpl 源码全文(很短):
internal class UnsafeLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
private var _value: Any? = UNINITIALIZED_VALUE // ← 无 @Volatile
override val value: T
get() {
if (_value === UNINITIALIZED_VALUE) {
_value = initializer!!()
initializer = null
}
@Suppress("UNCHECKED_CAST") return _value as T
}
override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE
}
性能优势(相比 SYNCHRONIZED):
| 项 | NONE | SYNCHRONIZED | 相对性能 |
|---|---|---|---|
| 字段读 | 普通 load | volatile load | +10-30% |
| 字段写 | 普通 store | volatile store(带内存屏障) | +50-100% |
| 无内存屏障 | ✅ | ❌(volatile 写有 StoreLoad 屏障) | 显著 |
| 无 monitorenter | 快路径都无 | 快路径也无(双检) | 差异极小 |
风险:如 1.1 案例所示——多线程访问未初始化的属性时,字段写入尚未 flush 到主内存,另一线程可能看到 null 甚至部分初始化对象。JMM 允许指令重排序是 NONE 模式的根本坑。
唯一合法场景:
- 私有属性 + 确定单线程访问
- 在
Activity.onCreate/Fragment.onViewCreated里的 View 绑定 - 局部作用域的 lazy(虽然极少见,但合法)
# 4.4 双重检查锁真相
为什么 SYNCHRONIZED 需要双重检查?
假设只用一层检查:
override val value: T
get() = synchronized(lock) { // ← 每次都要 monitorenter/exit
if (_value === UNINITIALIZED) {
_value = initializer!!()
}
_value as T
}
问题:即使值已经初始化,每次 get 都要 monitorenter/exit——热点场景性能不可接受。JMH 实测下差 5-10 倍。
双重检查解决方案:
1. 快路径:无锁读 _value(volatile load)
2. 已初始化 → 直接返回(99.99% 走这一路)
3. 未初始化 → monitorenter
4. 慢路径:再检查 _value(可能其他线程已初始化)
5. 仍未初始化 → 调用 initializer 并存值(volatile store)
为什么 _value 必须是 @Volatile?
反例(如果 _value 不是 volatile):
Time Thread A Thread B
t0 _value == UNINITIALIZED
t1 进入 synchronized 块
t2 _value = new Object() { field = 42 }
t3 离开 synchronized 块(内存屏障) 读 _value(快路径,无锁)
t4 可能看到旧值 UNINITIALIZED
但即使看到 obj,obj.field 可能仍是 0
volatile 提供的 happens-before:_value = xxx 之前的所有写入(包括 obj 构造函数里的字段初始化)对读到 _value = xxx 的线程都可见。
Kotlin SYNCHRONIZED 的实现就是"教科书级"双检锁范式——@Volatile + 双重检查,与 Effective Java 第 83 条推荐的 Java 版本一模一样。
# 5. 类委托字节码
# 5.1 接口方法转发
interface Repo { fun save(): Int; fun load(): Int }
class RepoImpl : Repo { override fun save() = 1; override fun load() = 2 }
class LoggingRepo(val delegate: Repo) : Repo by delegate
反编译 LoggingRepo:
public final class LoggingRepo implements Repo {
private final Repo $$delegate_0;
public LoggingRepo(Repo delegate) {
this.$$delegate_0 = delegate;
}
// ↓ 编译器自动生成的转发桩
public int save() { return $$delegate_0.save(); }
public int load() { return $$delegate_0.load(); }
}
关键:编译器为每个接口方法生成一个"转发桩"——零反射、零动态开销、类型安全。
转发桩的字节码非常薄:
public int save();
ALOAD 0
GETFIELD LoggingRepo.$$delegate_0 : Repo
INVOKEINTERFACE Repo.save ()I
IRETURN
3 条指令——JIT 稳态后可以完全内联,与直接调用 delegate.save() 无异。
# 5.2 delegate字段生成
字段名规则:$$delegate_N,N 从 0 递增(支持多接口委托给多个对象):
interface A { fun a() }
interface B { fun b() }
class ImplA : A { override fun a() { } }
class ImplB : B { override fun b() { } }
class Multi(implA: A, implB: B) : A by implA, B by implB
反编译:
public final class Multi implements A, B {
private final A $$delegate_0; // ← 第一个委托
private final B $$delegate_1; // ← 第二个委托
public Multi(A implA, B implB) {
this.$$delegate_0 = implA;
this.$$delegate_1 = implB;
}
public void a() { $$delegate_0.a(); }
public void b() { $$delegate_1.b(); }
}
多委托的能力——Kotlin 借此实现"多重继承"的组合替代方案。这在 Java 里需要手写几十个转发方法。
# 5.3 override覆盖机制
选择性 override 接口方法即可只增强特定方法:
class LoggingRepo(val delegate: Repo) : Repo by delegate {
override fun save(): Int {
println("before save")
return delegate.save() // 或调用 $$delegate_0.save()
}
// load 由编译器生成的转发桩处理
}
混合行为:
| 方法 | 处理 |
|---|---|
save() | 用户覆写,加日志 |
load() | 编译器生成,直接转发 |
观察:用户只写关心的方法,其他默认转发——这是组合模式最简洁的表达。
优先级规则:如果同时有 override 和自动转发,override 胜出(编译器不会为已 override 的方法再生成转发桩)。
# 5.4 与Proxy动态代理
| 维度 | Kotlin 类委托 | Java Proxy.newProxyInstance |
|---|---|---|
| 生成时机 | 编译期 | 运行时字节码生成(ProxyGenerator) |
| 性能 | 直接方法调用(3 条字节码) | 反射 + InvocationHandler.invoke(),约 10-100 倍慢 |
| 类型安全 | 编译期强类型 | 弱类型(Object[] args) |
| 语法负担 | 一个 by 关键字 | 匿名内部类 + 反射 API |
| 生成 class 数量 | 无新 class(转发方法内嵌) | 运行时生成 $Proxy0.class |
| 能否动态选择委托对象 | ❌ 编译期固定接口 | ✅ 运行时可拦截任意方法 |
| 典型用途 | 组合替代继承 | AOP、Mock、RPC 客户端 |
性能实测(一次接口方法调用):
| 方式 | 耗时 (ns) | 相对慢 |
|---|---|---|
| 直接调用 | 1-2 | 1x |
| Kotlin 类委托 | 2-3 | 1.5x |
Java 反射 Method.invoke | 30-60 | 30x |
| Java Proxy | 40-80 | 40x |
| ByteBuddy/CGLIB 生成子类 | 5-10 | 5x |
结论:类委托是编译期方案,几乎零开销;Proxy 是运行时方案,功能更灵活但性能显著更差。选择时按需权衡。
# 6. 内建委托家族
# 6.1 observable源码
var name: String by Delegates.observable("init") { prop, old, new ->
println("$old -> $new")
}
内部实现(ObservableProperty 源码简化):
abstract class ObservableProperty<T>(initialValue: T) : ReadWriteProperty<Any?, T> {
private var value = initialValue
protected open fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = true
protected open fun afterChange(property: KProperty<*>, oldValue: T, newValue: T): Unit = Unit
override fun getValue(thisRef: Any?, property: KProperty<*>): T = value
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
val oldValue = this.value
if (!beforeChange(property, oldValue, value)) return
this.value = value
afterChange(property, oldValue, value) // 触发监听
}
}
Delegates.observable 的完整实现:
public inline fun <T> Delegates.observable(
initialValue: T,
crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit
): ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) =
onChange(property, oldValue, newValue)
}
观察:observable 用模板方法模式——ObservableProperty 抽象基类定义骨架,用户传入的 lambda 通过 afterChange 参与。这是 GoF 设计模式在标准库的典型应用。
# 6.2 vetoable否决实现
var age by Delegates.vetoable(0) { _, _, new -> new >= 0 }
age = 20 // ✅ 通过,被设置
age = -1 // ❌ 被否决,age 保持 20
beforeChange 返回 false 时不写入新值——用于拒绝无效赋值。
实现:
public inline fun <T> Delegates.vetoable(
initialValue: T,
crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean
): ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) {
override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean =
onChange(property, oldValue, newValue)
}
observable vs vetoable:
| 特性 | observable | vetoable |
|---|---|---|
| 覆写的方法 | afterChange | beforeChange |
| lambda 返回值 | Unit | Boolean(是否允许) |
| 触发时机 | 写入后通知 | 写入前决定 |
| 适用场景 | 变化监听(UI 刷新) | 校验拒绝(不合法值不写入) |
同一个 ObservableProperty 基类衍生出两种能力——组合的美感。
# 6.3 notNull保护委托
var name: String by Delegates.notNull()
name.length // 未赋值前访问抛异常
name = "K" // 之后正常
用途:延迟初始化非空 var(相当于 lateinit var 的 API 版本)。
实现极其简单:
private class NotNullVar<T : Any>() : ReadWriteProperty<Any?, T> {
private var value: T? = null
override fun getValue(thisRef: Any?, property: KProperty<*>): T =
value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.")
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
}
notNull vs lateinit var 对比:
| 项 | Delegates.notNull | lateinit var |
|---|---|---|
| 语法 | by Delegates.notNull() | lateinit var |
| 支持类型 | 任意非空类型(包括基本类型的装箱) | 只支持非空引用类型(不能是 Int/Long/Boolean 等基本类型) |
| 检查开销 | 每次 getter 判空 | 通过字段 null 判断 + throw UninitializedPropertyAccessException |
| 委托对象开销 | 每属性一个委托对象 | 无额外对象 |
| 反射查询 | KProperty | ::name.isInitialized |
| 推荐 | 只在需要基本类型装箱时用 | 优先选择 |
建议:普通对象类型用 lateinit,只在需要 Int/Long 等不能 lateinit 的场景用 Delegates.notNull()。
# 6.4 Map委托取值
class User(map: Map<String, Any>) {
val name: String by map
val age: Int by map
}
val u = User(mapOf("name" to "K", "age" to 20))
println(u.name) // "K"
println(u.age) // 20
用 Map.getValue 约定从 map 中读取属性——常见于 JSON、Preferences 等场景。
标准库为 Map 加的扩展函数:
public inline operator fun <V, V1 : V> Map<in String, V>.getValue(
thisRef: Any?, property: KProperty<*>
): V1 {
@Suppress("UNCHECKED_CAST")
return getOrImplicitDefault(property.name) as V1
}
// var 版本用 MutableMap
public inline operator fun <V> MutableMap<in String, in V>.setValue(
thisRef: Any?, property: KProperty<*>, value: V
) { this.put(property.name, value) }
关键:属性名 = Map 的 key。所以 val name: String by map 会读 map["name"]。
典型应用场景:
// 从 JSON 反序列化的 map 创建对象
val json: Map<String, Any> = parseJson(text)
val user = User(json) // 属性自动从 map 取
// 从 SharedPreferences 读设置
class Settings(prefs: SharedPreferences) {
val theme: String by prefs.all
val fontSize: Int by prefs.all
}
风险:类型不匹配时 ClassCastException——因为 getValue 用了 unchecked cast。生产环境要配合数据校验。
# 7. 自定义属性委托
# 7.1 ReadWriteProperty接口
自定义可读写属性委托的最简写法——实现 ReadWriteProperty<Any?, T>:
class LoggedProp<T>(private var value: T) : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, prop: KProperty<*>): T {
println("get ${prop.name} = $value")
return value
}
override fun setValue(thisRef: Any?, prop: KProperty<*>, new: T) {
println("set ${prop.name}: $value -> $new")
value = new
}
}
class Config {
var timeout by LoggedProp(30)
}
val c = Config()
c.timeout = 60 // set timeout: 30 -> 60
val t = c.timeout // get timeout = 60
两个标准接口:
| 接口 | 用途 |
|---|---|
ReadOnlyProperty<R, T> | val 属性(只需 getValue) |
ReadWriteProperty<R, T> | var 属性(需要 getValue + setValue) |
不实现这两个接口也可以——只要有 operator fun getValue/setValue 匹配签名即可(约定优于接口)。
# 7.2 provideDelegate高级
provideDelegate 让委托的创建也可参与——比如在属性绑定时做校验:
class Preference<T>(val default: T) {
operator fun provideDelegate(
thisRef: Any?, prop: KProperty<*>
): ReadWriteProperty<Any?, T> {
// ← 在属性初始化时调用一次(不是每次 getter)
checkPreference(prop.name) // 校验属性名合法性
return PreferenceImpl(prop.name, default)
}
}
class Settings {
var name by Preference("default") // provideDelegate 被调用,返回真正的委托
}
执行时机:
类初始化时
↓
name$delegate = Preference("default").provideDelegate(this, KProperty)
↓ (返回 PreferenceImpl)
↓
每次 name 的 getter/setter
↓
name$delegate.getValue(this, KProperty)
核心价值:provideDelegate 让委托知道自己在绑定给谁——可以做校验、注册、初始化等一次性工作。
典型场景:
// 数据库属性绑定
class DBColumn<T>(val type: String) {
operator fun provideDelegate(
thisRef: Table, prop: KProperty<*>
): ReadWriteProperty<Table, T> {
// 在类初始化时注册列到 schema
thisRef.registerColumn(prop.name, type)
return ColumnImpl(prop.name)
}
}
# 7.3 委托的委托
委托可以委托给另一个委托——高级组合能力:
class DebugDelegate<T>(private val inner: ReadWriteProperty<Any?, T>) : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, prop: KProperty<*>): T {
val v = inner.getValue(thisRef, prop)
println("[DEBUG] read ${prop.name} = $v")
return v
}
override fun setValue(thisRef: Any?, prop: KProperty<*>, new: T) {
println("[DEBUG] write ${prop.name} = $new")
inner.setValue(thisRef, prop, new)
}
}
class Config {
// 包装 observable:先 debug 再触发 observable
var timeout: Int by DebugDelegate(
Delegates.observable(30) { _, old, new -> updateUI(old, new) } as ReadWriteProperty<Any?, Int>
)
}
使用场景:给现有委托加装横切关注点(日志、埋点、性能计时)——装饰器模式在委托层的应用。
# 7.4 Preferences示例
完整例子——Android SharedPreferences 的属性委托封装:
class PreferenceDelegate<T>(
private val prefs: SharedPreferences,
private val key: String,
private val default: T
) : ReadWriteProperty<Any?, T> {
@Suppress("UNCHECKED_CAST")
override fun getValue(thisRef: Any?, prop: KProperty<*>): T = when (default) {
is String -> prefs.getString(key, default) as T
is Int -> prefs.getInt(key, default) as T
is Long -> prefs.getLong(key, default) as T
is Boolean -> prefs.getBoolean(key, default) as T
is Float -> prefs.getFloat(key, default) as T
else -> throw IllegalArgumentException("Unsupported type: ${default!!::class}")
}
override fun setValue(thisRef: Any?, prop: KProperty<*>, value: T) {
prefs.edit().apply {
when (value) {
is String -> putString(key, value)
is Int -> putInt(key, value)
is Long -> putLong(key, value)
is Boolean -> putBoolean(key, value)
is Float -> putFloat(key, value)
else -> throw IllegalArgumentException("Unsupported type: ${value!!::class}")
}
}.apply()
}
}
// 使用
class AppSettings(prefs: SharedPreferences) {
var theme by PreferenceDelegate(prefs, "theme", "light")
var fontSize by PreferenceDelegate(prefs, "font_size", 14)
var notifEnabled by PreferenceDelegate(prefs, "notif_enabled", true)
}
// 调用方
val settings = AppSettings(context.getSharedPreferences("app", 0))
settings.theme = "dark" // 自动写入 SharedPreferences
val cur = settings.theme // 自动从 SharedPreferences 读
收益:
- 消除了大量
getString/putString + edit/apply样板代码 - 类型安全(编译期确定)
- 属性名统一为 key,避免手写字符串常量
- 加缓存、加变化监听都很容易
这就是委托机制的实用价值——把重复的样板代码收敛到语言层。
# 8. 委托性能代价
# 8.1 一次方法转发
类委托:每次调用多一次虚方法调用——通常被 JIT 内联,几乎零开销。
实测(JMH benchmark,一次接口方法调用):
| 方式 | 耗时 (ns) | 说明 |
|---|---|---|
| 直接方法调用 | 1.2 | 基准 |
| Kotlin 类委托(未内联) | 2.5 | 加一次虚方法调用 |
| Kotlin 类委托(JIT 稳态后) | 1.3 | JIT 内联,与直接调用无异 |
| Java Proxy | 45 | 反射 + InvocationHandler |
结论:稳态下类委托接近零开销。冷路径(JIT 未触发)时有约 1 ns 的额外开销。
# 8.2 KProperty创建
属性委托:每次 getter 都要传 KProperty 引用;KProperty 由编译器生成、静态字段缓存——一次创建,重复使用。
内存开销:
class Foo {
val a by MyDelegate() // 生成 KProperty(a)
val b by MyDelegate() // 生成 KProperty(b)
val c by MyDelegate() // 生成 KProperty(c)
}
静态字段 $$delegatedProperties 里存 3 个 KProperty 实例——类加载时创建一次,永远存在。
每属性开销:
PropertyReference1Impl对象:约 48 字节- 静态数组一格:8 字节
- 总计:每属性 ~56 字节
10000 个委托属性 = 560 KB 静态元数据——通常不是瓶颈。
# 8.3 lazy对象额外开销
by lazy { } 生成一个 Lazy<T> 实现类的实例——每个属性一个对象。
内存对比(每属性):
| 方案 | 内存开销 |
|---|---|
手写 @Volatile var _cache: T? = null + 双检 | 8 字节引用 |
by lazy { } (SYNCHRONIZED) | SynchronizedLazyImpl ~40 字节 + lambda ~16 字节 = ~56 字节 |
by lazy(PUBLICATION) { } | SafePublicationLazyImpl ~48 字节 + lambda ~16 字节 |
by lazy(NONE) { } | UnsafeLazyImpl ~32 字节 + lambda ~16 字节 |
10 万个 lazy 属性 = 5 MB 额外对象——在极端场景下需要考虑手写方案,一般情况不必优化。
替代方案:热点路径可以自己实现,一般路径 by lazy 更清晰:
// 手写等价物
class Foo {
@Volatile private var _cache: Config? = null
val cfg: Config
get() {
_cache?.let { return it }
return synchronized(this) {
_cache ?: parseConfig().also { _cache = it }
}
}
}
# 8.4 JIT能否消除
结论汇总:
| 委托类型 | JIT 能否消除 | 说明 |
|---|---|---|
类委托 by delegate | ✅ 完全消除 | 转发桩只有 3 条字节码,稳态后与直接调用无异 |
by lazy 快路径 | ✅ 大部分消除 | Lazy.getValue 是 inline,SynchronizedLazyImpl.value 的 volatile 读不能消除但便宜 |
by lazy 慢路径 | ❌ 无法消除 | synchronized 块必然存在 |
by Delegates.observable | ⚠️ 部分消除 | getValue 简单可以内联,setValue 里的 lambda 调用可能保留 |
自定义 ReadWriteProperty | ⚠️ 取决于实现 | 简单实现被内联,复杂实现保留调用 |
关键:kotlin.Lazy 接口的 getValue 就是 inline 扩展函数:
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
因此 cfg 的读取字节码等价于:
GETFIELD instance$delegate
INVOKEVIRTUAL Lazy.getValue() ; ← 会被 JIT 内联到具体子类的 value getter
稳态下 lazy 委托几乎零开销——除了 volatile 读的一次内存屏障(约 1-2 ns)。
# 9. 事故案例复盘
# 9.1 lazy_NONE死锁
NONE 模式的可见性问题——即使不"死锁",也可能读到未完全构造的对象。
典型现象:字段类型是 Config(非空),运行时读到 null——因为其它线程看到了未初始化的 Config 引用。
JMM 允许的重排序:
_value = Config() // 逻辑上等于:
// 1. 分配 Config 对象内存
// 2. 调用构造函数初始化字段
// 3. _value = 对象地址
// 编译器/CPU 可能重排为:
// 1. 分配 Config 对象内存
// 3. _value = 对象地址(此时字段尚未初始化)
// 2. 调用构造函数初始化字段
其他线程看到步骤 3 后但步骤 2 前的状态:
_value已经不是 UNINITIALIZED- 但
_value.timeout仍是 Int 默认值 0 - 或
_value.name仍是 null(引用类型默认值)
这就是**"部分构造对象"**的经典 JMM 陷阱——volatile 或 synchronized 是唯一的防护手段。
其他线程读到 null 后触发装箱转型 NPE:
val timeout: Int = config.timeout // 如果 timeout 字段被优化为 Integer
// Integer 引用为 null
// 自动拆箱触发 NPE
修复方案 3 选一:
// 方案 A:改成默认的 SYNCHRONIZED
val instance: Config by lazy { parseConfig() }
// 方案 B:手写 volatile + 双检
@Volatile private var _cache: Config? = null
val instance: Config
get() = _cache ?: synchronized(this) {
_cache ?: parseConfig().also { _cache = it }
}
// 方案 C:类初始化时立即赋值(放弃 lazy)
val instance: Config = parseConfig()
推荐方案 A——默认 SYNCHRONIZED 就是为并发准备的,除非有强性能需求,不要用 NONE。
# 9.2 observable泄漏Fragment
class MyFragment : Fragment() {
var user: User by Delegates.observable(User("init")) { _, _, _ ->
updateUI() // 隐式引用 Fragment.this
}
}
问题:Lambda 捕获 this,只要 user 变量还活着(或委托对象还活着),Fragment 就无法回收。
为什么容易泄漏?
反编译上面代码:
public final class MyFragment$user$1 extends Lambda implements Function3<...> {
final MyFragment this$0; // ← 捕获 Fragment
public MyFragment$user$1(MyFragment f) { this$0 = f; }
public Object invoke(...) {
this$0.updateUI();
return Unit.INSTANCE;
}
}
Lambda 匿名类持有 MyFragment 引用——委托对象 → lambda → Fragment,如果委托对象被静态字段或长生命周期对象持有,Fragment 就永远回收不了。
正确姿势 3 种:
// 方案 A:用 WeakReference
class MyFragment : Fragment() {
private val userDelegate = ObservableDelegate<User>(User("init")) { new ->
weakThis.get()?.updateUI()
}
var user: User by userDelegate
private val weakThis = WeakReference(this)
}
// 方案 B:viewLifecycleOwner 自动清理
class MyFragment : Fragment() {
var user: User by Delegates.observable(User("init")) { _, _, _ ->
if (view != null) updateUI() // 防御性检查
}
}
// 方案 C:Flow / StateFlow(推荐现代方案)
class MyFragment : Fragment() {
private val _user = MutableStateFlow(User("init"))
var user: User
get() = _user.value
set(value) { _user.value = value }
override fun onViewCreated(v: View, s: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
_user.collect { updateUI(it) }
}
}
}
}
推荐方案 C——用协程 + Flow 替代 observable,生命周期自动管理,无泄漏风险。
# 9.3 类委托绕过override
class Repo : IRepo {
override fun save(): Int { println("Repo.save"); return 1 }
fun helper(): Int = save() // ⚠ 内部调用 save
}
class Logging(delegate: Repo) : IRepo by delegate {
override fun save(): Int { println("Logging.save"); return 999 }
// helper 不是 IRepo 接口方法,Logging 没有它
}
val logging = Logging(Repo())
logging.save() // 输出 "Logging.save",返回 999 ✅
logging.helper() // ⚠ 编译不通过(helper 不在 IRepo 接口中)
// 假设让 helper 也在 IRepo 接口中:
interface IRepo { fun save(): Int; fun helper(): Int }
// 那么:
val logging = Logging(Repo())
logging.helper() // 输出 "Repo.save"(不是 Logging.save!)返回 1
根因图解:
logging.helper()
↓
$$delegate_0.helper() ; 转发到 Repo 实例
↓
Repo.helper() { return save() } ; 这里的 save() 是 Repo 自己的 save
↓
Repo.save() ; 返回 1,不经过 Logging.save
核心问题:类委托只做接口方法的转发——Repo 内部方法之间的调用看不到 Logging 的存在。this 指针在 Repo 里就是 Repo,不是 Logging。
对比继承的差异:
// 如果用继承(假设 Repo 是 open class)
class Logging : Repo() {
override fun save(): Int { return 999 }
}
val logging = Logging()
logging.helper() // 输出 "Logging.save" 返回 999
// 因为 helper 里的 save() 走虚方法分派,命中 Logging.save
继承和委托的核心差异表:
| 特性 | 继承 | 委托 |
|---|---|---|
| this 指针 | 是子类实例 | 委托的是"另一个对象" |
| 内部方法调用 | 走虚方法分派(多态) | 在委托对象内闭环 |
| 覆写生效范围 | 所有子类方法调用 | 只对外部直接调用生效 |
| 灵活性 | 需要 open/final 标记 | 无需修改被委托类 |
结论:委托不是继承的替代品——两者有本质差异。选择时按需权衡:
- 需要"内部调用感知覆写" → 用继承
- 需要"零侵入组合" → 用委托
- 大部分场景优先考虑委托(Effective Java 第 18 条)
# 10. 综合案例串讲
# 10.1 案例真相揭晓
回到 1.1 节的 6 个疑问:
by lazy生成一个Lazy<T>实现字段 +KProperty静态引用——每属性一个对象,约 56 字节。SYNCHRONIZED用 volatile + 双重检查锁;PUBLICATION用 CAS 允许并发但只发布一个;NONE无同步,只能单线程访问。- 属性委托
by与类委托by是不同的编译期机制——只是关键字复用。前者委托 getter/setter,后者委托接口方法。 Delegates.observable用继承ObservableProperty的模板方法模式——Lambda 里若引用this就容易泄漏。KProperty是编译器生成的属性元数据引用——每属性一个静态实例,运行时用于反射与委托传参。- 类委托是编译期方法转发,Java Proxy 是运行时字节码动态生成——前者更快、更类型安全,后者更灵活。
案例根因:lazy(NONE) 无 volatile / synchronized 保证——线程 A 完成初始化后写 _value 字段,但字段写入未提供 happens-before 保证;线程 B 读到部分初始化的 Config 对象,其内部字段仍为默认值 0 / null,装箱后拆箱触发 NPE。
修复:
- 改成默认的
by lazy { }(SYNCHRONIZED)——最简单 - 或用
volatile字段 + 手写双重检查——性能有需求时 - 绝不用
NONE除非能证明单线程访问——如 View 绑定、Activity 生命周期内的 lazy
# 10.2 一个lazy委托一生
写下 val cfg: Config by lazy { parseConfig() }
↓
kotlinc 编译期:
├─ 生成 cfg$delegate: Lazy<Config> 字段
├─ 生成 $$delegatedProperties[N] 静态 KProperty 数组
├─ 类初始化:cfg$delegate = LazyKt.lazy { parseConfig() }
│ └─ 默认调用 kotlin.LazyKt__LazyJVMKt.lazy(initializer)
│ └─ 返回 SynchronizedLazyImpl(initializer, null)
└─ 生成 getCfg() { return cfg$delegate.getValue(this, prop) }
└─ Lazy.getValue 是 inline,实际调用 SynchronizedLazyImpl.value getter
↓
运行期首次调用 getCfg():
├─ SynchronizedLazyImpl.value getter
├─ 快路径检查:_value volatile 读 == UNINITIALIZED?是
├─ synchronized(lock) 加锁
├─ 慢路径检查:_value == UNINITIALIZED?是
├─ 调用 initializer()(用户 lambda)
├─ 存入 _value(volatile 写,触发 happens-before)
├─ initializer = null(释放 lambda 引用)
└─ 释放锁,返回结果
↓
后续调用:
├─ 快路径检查:_value != UNINITIALIZED(99.99% 命中)
└─ 直接返回 _value(一次 volatile 读,约 1-2 ns)
↓
JIT 稳态后:
├─ Lazy.getValue 内联
├─ SynchronizedLazyImpl.value getter 内联
└─ 最终字节码几乎等价于 return this.cfg$delegate._value(带 volatile 语义)
一次编译,三个阶段消费——编译期生成、类加载期实例化、运行期快慢双路径。
# 10.3 设计哲学回扣
- 组合优于继承(Effective Java Item 18):
by关键字让"has-a"变成一等公民——一个关键字消除大量样板代码 - 约定优于配置:
getValue/setValue是约定不是接口——灵活性与类型安全并存;任何对象加上扩展函数就能变成委托 - 编译期完成,零反射:Kotlin 委托机制的运行时开销极小——不像 Java Proxy 那样牺牲性能换灵活
- 让危险模式显式化:
LazyThreadSafetyMode.NONE强迫开发者显式选择——不做默默假设,避免误用 - 标准库示范:
ObservableProperty用模板方法模式,Delegates.observable/vetoable用继承衍生——是 GoF 设计模式的教科书应用
# 10.4 委托速查表
| 场景 | 推荐 |
|---|---|
| 单线程延迟初始化 | by lazy(NONE) |
| 多线程延迟初始化 | by lazy { } |
| 无副作用可并发初始化 | by lazy(PUBLICATION) |
| 属性变化监听 | Delegates.observable 或 StateFlow |
| 属性变化否决 | Delegates.vetoable |
| 无初始值非空 var(引用类型) | lateinit var |
| 无初始值非空 var(基本类型) | Delegates.notNull() |
| 从 Map 取值 | by map |
| 从 SharedPreferences 取值 | 自定义 PreferenceDelegate |
| 组合多接口 | 类委托 by delegateN |
| 需要拦截创建 | provideDelegate |
| 装饰现有委托 | 委托的委托(装饰器模式) |
lazy 三模式选择速查:
| 场景 | 模式 |
|---|---|
| 确定单线程访问(如 View 绑定) | NONE |
| 多线程访问 + 有副作用 | SYNCHRONIZED(默认) |
| 多线程访问 + 纯函数 initializer | PUBLICATION |
| 不确定 | SYNCHRONIZED(安全默认) |
委托机制口诀:
by有两义:类委托委托方法,属性委托委托 get/setgetValue/setValue是约定:不是接口,operator修饰即可lazy默认安全:SYNCHRONIZED是防御性设计- 委托不是继承:内部方法调用不走委托覆写
- 零反射不代表零开销:委托对象、KProperty 都要内存
下一步:至此 Kotlin 精通模块的 5 篇核心原理专栏全部完成。回到 模块总览 温习学习路线,或者拿一个真实 Android 项目实践你新学到的一切——每次看到 ?. by lazy data class launch inline,你应该都能"看见"它们编译后的样子。