第 11 章 类与属性:构造顺序、backing field 与显式 backing field
本章目标
- 精确掌握 Kotlin 对象的初始化顺序------并能解释一个真实的"字段还是 0"的 bug;
- 理解属性系统的三层结构:属性 / 访问器 / backing field;
- 掌握 📌 显式 backing field(2.3 实验 → 2.4 稳定) 的语法、五条限制、以及它与"backing property"惯用法的取舍;
- 吃透属性委托的约定与
provideDelegate;- 搞清属性在字节码层面的所有细节(
@JvmField、isXxx命名、internal混淆)。
11.1 类声明全解剖
kotlin
@Serializable // 注解
abstract class Repository<T : Entity>( // 修饰符 + 类名 + 泛型约束
protected val tableName: String, // 主构造器参数(同时是属性)
private val dialect: Dialect, // 主构造器参数(私有属性)
cacheSize: Int = 1000, // 主构造器参数(仅参数,不是属性)
) : BaseRepository<T>(tableName), Queryable by SqlQueryable() { // 超类 + 接口 + 委托
// ── 属性 ─────────────────────────────────
protected val cache: MutableMap<String, T> = HashMap(cacheSize) // 有 backing field
val isEmpty: Boolean get() = cache.isEmpty() // 无 backing field(计算属性)
// ── 初始化块 ─────────────────────────────
init {
require(tableName.isNotBlank()) { "table name required" }
}
// ── 成员函数 ─────────────────────────────
abstract suspend fun findById(id: String): T?
open fun evict(id: String) { cache.remove(id) }
// ── 次构造器 ─────────────────────────────
constructor(tableName: String) : this(tableName, PostgresDialect(), 500)
// ── 伴生对象 ─────────────────────────────
companion object {
const val DEFAULT_CACHE_SIZE = 1000
fun <T : Entity> of(table: String): Repository<T> = ...
}
}
声明顺序的推荐规范(Kotlin 官方编码约定):
markdown
1. 属性声明与 init 块
2. 次构造器
3. 方法声明
4. companion object
且属性声明不要与方法交错------按语义分组放在前面。
11.2 主构造器:参数 vs 属性
这是新手最容易混淆的地方:
kotlin
class Person(name: String, val age: Int) {
val displayName: String = name.uppercase() // name 只是参数,可用于初始化
// fun greet() = println(name) // ❌ 编译错误:name 在构造器之外不可见
fun greet() = println(displayName) // ✅
}
| 写法 | 生成字段 | 生成 getter | 作用域 |
|---|---|---|---|
class A(x: Int) |
❌ | ❌ | 仅主构造器 + init 块 + 属性初始化器 |
class A(val x: Int) |
✅ private final int x |
✅ getX() |
全类可见 |
class A(var x: Int) |
✅ private int x |
✅ getX() + setX() |
全类可见 |
class A(private val x: Int) |
✅ | ✅ 但 private | 全类可见,外部不可访问 |
🔬 为什么"仅参数"的形式存在 :它让你能在构造时用参数计算属性,但不保留参数本身------减少对象大小、避免暴露内部实现。
kotlin
class Circle(radius: Double) { // 不存 radius,省 8 字节
val area: Double = PI * radius * radius // 构造时算好
val circumference: Double = 2 * PI * radius
}
⚠️ 但如果参数被 Lambda 捕获,它会被提升为字段:
kotlin
class Handler(name: String) {
val callback: () -> Unit = { println(name) } // ← name 被捕获 → 生成 private final String name 字段!
}
🧪 实验 11-1:验证字段提升
bash
javap -p out/book/ch11/Handler.class
# private final java.lang.String name; ← 出现了!
# private final kotlin.jvm.functions.Function0 callback;
11.3 初始化顺序:精确规则 + 一个真实的 bug
<init> 的执行顺序
kotlin
① super() 调用(父类的 <init> 完整执行,包括父类的属性初始化与 init 块)
② 本类主构造器的属性赋值(val/var 参数 → this.field = param)
③ 本类的属性初始化器 与 init 块 ------ **严格按源码文本顺序交错执行**
④ 本类主构造器的函数体(如果有)
⑤ 次构造器:委托 this(...) 完成后,执行次构造器自己的函数体
🧪 实验 11-2:亲眼看到顺序
kotlin
open class Base(x: Int) {
val a = trace("Base.a", 1)
init { trace("Base.init", 0) }
val b = trace("Base.b", 2)
private fun trace(s: String, v: Int): Int { println("$s = $v"); return v }
}
class Derived(x: Int) : Base(x) {
val c = traceD("Derived.c", 3)
init { traceD("Derived.init", 0) }
val d = traceD("Derived.d", 4)
private fun traceD(s: String, v: Int): Int { println("$s = $v"); return v }
}
fun main() { Derived(0) }
输出:
text
Base.a = 1
Base.init = 0
Base.b = 2
Derived.c = 3
Derived.init = 0
Derived.d = 4
完全按文本顺序,父类先于子类。
⚠️ 真实的 bug:在构造器里调用 open 方法
kotlin
open class Base {
init {
println("Base init, value = ${value()}") // 调用虚方法!
}
open fun value(): Int = 0
}
class Derived : Base() {
private val x: Int = 42
override fun value(): Int = x
}
fun main() { Derived() }
输出:Base init, value = 0 ------ 不是 42!
🔬 原因 :Base.<init> 执行时,Derived.x 还没有被赋值 (步骤 ③ 还没轮到 Derived)。value() 通过虚分派调到 Derived.value(),读取 x,此时 x 是 int 字段的零值 0。
🚀 这是《Effective Java》第 19 条"要么为继承而设计并写文档,要么禁止继承"的经典案例。 Kotlin 的应对是默认 final ------如果 Base 不加 open,value() 就不能被重写,问题不存在。
三条防御措施:
- 不要在构造器/
init块里调用open成员; - 如果必须,用模板方法 + 工厂函数:
kotlin
class Service private constructor(val config: Config) {
init { validate() } // validate 是 private final,安全
private fun validate() { ... }
companion object {
fun create(raw: RawConfig): Service {
val cfg = parse(raw) // 所有复杂逻辑在构造之前完成
return Service(cfg)
}
}
}
- 用 Detekt 规则拦截(
ConstructorParameterNaming之外,可自定义规则检测 init 块中的 open 调用)。
⚠️ 另一个陷阱:默认参数表达式引用未初始化的属性
kotlin
class Config(
val retries: Int = defaultRetries(), // ① 默认值表达式
) {
val timeout: Int = 30 // ② 属性初始化器
private fun defaultRetries() = timeout * 2 // ⚠️ 此时 timeout 还是 0!
}
println(Config().retries) // 0,不是 60!
🔬 原因 :默认参数表达式在步骤 ②(属性赋值)之前 求值,此时 timeout 字段还是零值。
🚀 修复:
kotlin
class Config(val timeout: Int = 30, val retries: Int = timeout * 2) // ✅ 参数顺序保证
// 或
class Config(timeout: Int = 30) {
val timeout: Int = timeout
val retries: Int = timeout * 2 // ✅ 在属性初始化器里计算
}
11.4 次构造器
kotlin
class Connection private constructor(
val host: String,
val port: Int,
val timeoutMs: Int,
) {
constructor(host: String) : this(host, 5432, 3000) {
println("简单构造器:使用默认端口和超时") // ← 次构造器体,最后执行
}
constructor(url: String) : this(parseHost(url), parsePort(url), 5000)
companion object {
fun fromUrl(url: String) = Connection(url) // 工厂函数(更推荐)
}
}
🚀 现代 Kotlin 的最佳实践是"少用次构造器,多用默认参数 + 工厂函数":
kotlin
// ✅ 更好
class Connection(
val host: String,
val port: Int = 5432,
val timeoutMs: Int = 3000,
) {
companion object {
fun fromUrl(url: String): Connection {
val (h, p) = parse(url)
return Connection(h, p, timeoutMs = 5000)
}
}
}
理由:
- 工厂函数有名字 (
fromUrl比Connection(url)表达力强); - 工厂函数可以返回子类型或缓存实例;
- 默认参数比多个构造器更灵活(可以只覆盖某一个);
- 次构造器会让初始化顺序更难推理。
📌 JDK 25 的 JEP 513(Flexible Constructor Bodies) 让 Java 也能在 super() 之前写语句(参数校验、字段预赋值)。Kotlin 的模型不同(主构造器 + init 块 + 次构造器委托),所以这条 JEP 主要影响混编工程里 Java 侧的写法。
11.5 属性系统的三层结构
kotlin
┌─────────────────────────────────────────────────────┐
│ 属性(Property)------ Kotlin 语言层的概念 │
│ val/var name: Type │
├─────────────────────────────────────────────────────┤
│ 访问器(Accessors)------ getter(必有)+ setter(var) │
│ 可以自定义,可以改可见性,可以加注解 │
├─────────────────────────────────────────────────────┤
│ backing field ------ JVM 字段(可能不存在!) │
│ private final Type name │
└─────────────────────────────────────────────────────┘
什么时候没有 backing field
编译器只在"需要存储值"时才生成字段:
kotlin
class Rectangle(val w: Int, val h: Int) {
val area: Int get() = w * h // ❌ 无字段(纯计算)
val isSquare: Boolean get() = w == h // ❌ 无字段
var name: String = "" // ✅ 有字段(默认访问器需要存储)
set(value) { field = value.trim() } // 用了 field → 必须有字段
val derived: String get() = name.uppercase() // ❌ 无字段
}
🔬 判断规则 :如果属性的访问器里用了 field 关键字,或使用了默认访问器实现,就生成 backing field;否则不生成。
field 关键字
kotlin
var counter: Int = 0
set(value) {
if (value < 0) throw IllegalArgumentException("negative")
field = value // ← 只能在访问器里用
println("set to $field")
}
⚠️ field 只在 声明该属性的类内部、且只在访问器里可用。
11.6 📌 显式 backing field(2.3 实验 → 2.4 稳定)
它解决的长期痛点
Kotlin 有个经典的"backing property"惯用法,写起来很啰嗦:
kotlin
// 旧写法:暴露只读视图,内部可变
class ShoppingCart {
private val _items = mutableListOf<String>() // ① 私有可变字段
val items: List<String> get() = _items // ② 公开只读属性 + 手写 getter
fun addItem(item: String) { _items.add(item) } // ③ 内部用 _items
}
三个问题:
- 需要两个声明(
_items和items); _items的命名约定只是惯例,不是语言保证;get() = _items每次访问都走一次方法调用(虽然会被内联)。
新写法
kotlin
class ShoppingCart {
val items: List<String>
field = mutableListOf() // ← 显式 backing field
fun addItem(item: String) {
items.add(item) // ✅ 类内部:items 被智能转换为 MutableList<String>
}
fun removeItem(item: String) {
items.remove(item) // ✅
}
}
也可以显式声明 backing field 的类型:
kotlin
val items: List<String>
field: MutableList<String> = mutableListOf()
🔬 语义:
- 在声明类内部 ,
items解析为 backing field 的类型 (MutableList<String>),所以能调用add/remove; - 在类外部 ,
items是属性声明的类型 (List<String>),只能读; - 只有一个声明、一个字段、一个 getter。
🧪 实验 11-3:看字节码
bash
kotlinc Cart.kt -d out && javap -p -c out/ShoppingCart.class
text
public final class ShoppingCart {
private final java.util.List items; // backing field(类型是 List,实例是 ArrayList)
public final java.util.List getItems(); // getter
public ShoppingCart();
public final void addItem(java.lang.String);
public final void removeItem(java.lang.String);
}
对比旧写法,少了一个 _items 字段 ,且 addItem 里直接 getfield items 而非 getfield _items。
五条限制(务必记住)
📌 属性只有在满足以下全部条件时才能有显式 backing field:
| # | 限制 | 原因 |
|---|---|---|
| 1 | 不能有自定义 getter | field = ... 本身就替代了 getter 的存储职责 |
| 2 | 必须是 val(只读) |
可变属性的 setter 语义与"内部可变、外部只读"冲突 |
| 3 | 不能是 open |
子类可能重写 getter,破坏类型收窄的假设 |
| 4 | 不能是委托属性 (by) |
委托属性没有自己的字段 |
| 5 | 不能是 const val |
编译期常量已被内联,无字段可言 |
另外两条对 backing field 本身的约束:
- backing field 的类型必须是属性类型的子类型 (
MutableList<String> <: List<String>✅); - backing field 必须是
private可见性(由编译器保证,你无法改)。
什么时候仍然用"backing property"
当违反任一限制时(最常见的是需要自定义 getter):
kotlin
class UserDirectory {
private val _users = mutableListOf("sarah", "mike")
val users: List<String>
get() = _users.sorted() // ← 自定义 getter:每次返回排序后的副本
fun addUser(name: String) { _users.add(name) }
}
🚀 决策规则:
- 需要"内部可变 + 外部只读",且不需要转换 → 显式 backing field(更简洁);
- 需要在读取时做转换/校验/缓存/日志 → backing property (
_x+ 自定义 getter); - 需要惰性计算 →
by lazy; - 需要变更通知 →
Delegates.observable。
⚠️ 注意显式 backing field 的一个微妙之处 :因为外部拿到的是同一个可变对象的只读视图,所以:
kotlin
val cart = ShoppingCart()
val items: List<String> = cart.items
cart.addItem("Apple")
println(items) // [Apple] ← items 也变了!因为它是同一个 ArrayList 的视图
这不是不可变,只是"只读接口"。 如果需要真正的不可变快照,getter 必须复制:
kotlin
val items: List<String> get() = _items.toList() // 每次复制,O(n)
👉 这与第 1 章讲的 Collections.unmodifiableList 是同一个道理:"只读"和"不可变"是两件事。
11.7 const val 编译期常量
kotlin
const val MAX_LOGIN_ATTEMPTS = 3 // 顶层
object Config { const val VERSION = "1.0" } // object 内
class S { companion object { const val X = 1 } } // companion 内
三个条件:
- 位于顶层 /
object/companion object; - 类型是
String或基本类型; - 不能有自定义 getter。
🔬 字节码 :public static final 字段 + ConstantValue 属性。
📌 官方文档明确指出:编译期常量仍然有 backing field,因此可以通过反射访问。 但引用方不会读这个字段------值已被内联进引用方的常量池。
⚠️ 升级陷阱(第 6 章已提):
kotlin
// 库 v1
object Limits { const val MAX = 100 }
// 使用方(已编译)
val x = Limits.MAX // 字节码:sipush 100 ← 值被内联,没有 getstatic!
// 库 v2:MAX 改成 200
// 使用方不重新编译 → 仍然是 100 💥
🚀 库设计准则 :公开的可调参数用普通 val(走 getter),真正的数学/协议常量才用 const val。
kotlin
object Limits {
const val HTTP_OK = 200 // ✅ 永不变
val maxRetries = 3 // ✅ 走 getter,可热更新
}
注解参数必须用 const val(因为注解值必须是编译期常量):
kotlin
const val SUBSYSTEM_DEPRECATED = "This subsystem is deprecated"
@Deprecated(SUBSYSTEM_DEPRECATED)
fun processLegacyOrders() { }
11.8 lateinit 完整规则
可以用在哪里
- ✅ 顶层属性
- ✅ 局部变量(📌 1.2+)
- ✅ 类体中的属性
不能用的场景
- ❌ 主构造器参数(
class A(lateinit var x: String)非法) - ❌ 有自定义 getter/setter 的属性
- ❌ 可空类型(
lateinit var x: String?非法) - ❌ 基本类型 (
Int/Long/Boolean/Char/Float/Double/Short/Byte)→ 用Delegates.notNull<Int>()
isInitialized 的限制
kotlin
class WeatherStation {
lateinit var latestReading: String
fun printReading() {
if (this::latestReading.isInitialized) println(latestReading)
else println("No reading available")
}
}
📌 只能在"能访问该属性"的地方用 isInitialized :同类、外层类、或同文件的顶层属性。不能检查别的类的 lateinit 属性(因为它编译成 private 字段 + public getter,无法从外部访问字段)。
⚠️ isInitialized 的实现是反射 (读取字段的 null 状态),所以不要在热路径频繁调用。
🔬 字节码与线程安全
kotlin
lateinit var view: View
text
private View view; // 普通字段,初始 null
public final View getView() {
View v = this.view;
if (v == null) Intrinsics.throwUninitializedPropertyAccessException("view");
return v;
}
public final void setView(View v) {
Intrinsics.checkNotNullParameter(v, "<set-?>");
this.view = v;
}
⚠️ 字段不是 volatile:
- 线程 A 调用
setView,线程 B 调用getView; - 没有 happens-before 关系(除非有其他同步);
- B 可能读到
null→ 抛UninitializedPropertyAccessException; - 更糟:B 可能读到一个半构造的
View(如果View的字段不是 final)。
🚀 多线程初始化请用 by lazy(默认 SYNCHRONIZED 带 volatile) ,或者显式用 @Volatile:
kotlin
@Volatile lateinit var config: Config // ✅ 但注意 lateinit + volatile 的组合在某些版本有限制,需实测
11.9 属性委托:约定与 provideDelegate
三个约定
kotlin
class Delegate {
// ① getValue:读
operator fun getValue(thisRef: Any?, property: KProperty<*>): String = ...
// ② setValue:写(var 才需要)
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { ... }
// ③ provideDelegate:在委托对象创建时介入(可选)
operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): ReadOnlyProperty<Any?, String> = ...
}
val x: String by Delegate()
🔬 by 的编译结果:
text
private final Delegate x$delegate; // 存的是委托对象,不是值!
public final String getX() {
return x$delegate.getValue(this, /* KProperty */ Reflection.property(...));
}
⚠️ 每次读取属性都调用 getValue ,并传一个 KProperty 对象(通常是缓存的单例,但仍有方法调用开销)。
provideDelegate 的价值
它让你在委托创建时做校验,而不是在每次读写时:
kotlin
class NotNullVar<T : Any>(private var value: T?) {
operator fun getValue(thisRef: Any?, prop: KProperty<*>) =
value ?: throw IllegalStateException("${prop.name} not initialized")
operator fun setValue(thisRef: Any?, prop: KProperty<*>, v: T) { value = v }
}
class Validator {
operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): NotNullVar<String> {
// 在对象构造时就检查注解、名字约定等
require(prop.name.startsWith("_")) { "delegated property must start with _" }
return NotNullVar(null)
}
}
标准委托的实现剖析
kotlin
// Delegates.notNull
public fun <T : Any> notNull(): ReadWriteProperty<Any?, T> = NotNullVar()
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 }
}
// Delegates.observable
public inline fun <T> observable(initialValue: T, crossinline onChange: (prop, old, new) -> Unit)
: ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) =
onChange(property, oldValue, newValue)
}
// Delegates.vetoable ------ beforeChange 返回 false 则拒绝赋值
// Delegates.map ------ 从 Map 读取属性值(用于动态配置)
Delegates.map 的妙用:
kotlin
class Config(map: Map<String, Any?>) {
val host: String by map
val port: Int by map
val debug: Boolean by map
}
val c = Config(mapOf("host" to "localhost", "port" to 8080, "debug" to false))
⚠️ key 缺失时抛 NoSuchElementException;类型不匹配时抛 ClassCastException。
自定义委托实战:一次性初始化 + 清空引用
kotlin
/** 读取一次后清空引用,帮助 GC 回收大对象 */
class ConsumeOnce<T : Any>(initial: T?) {
private var value: T? = initial
operator fun getValue(thisRef: Any?, prop: KProperty<*>): T =
value?.also { value = null } ?: throw IllegalStateException("${prop.name} already consumed")
}
class Parser(input: String) {
private val rawInput by ConsumeOnce(input) // 大字符串,用完就放
fun parse(): Ast { val src = rawInput; ... } // 之后 rawInput 的引用被清空
}
11.10 可见性修饰符
| 修饰符 | 类成员 | 顶层声明 |
|---|---|---|
public(默认) |
到处可见 | 到处可见 |
private |
类内(含成员) | 同文件内 |
protected |
类内 + 子类 | ❌ 不可用 |
internal |
同模块内可见 | 同模块内可见 |
⚠️ Kotlin 没有 package-private (Java 的默认可见性)。这是有意的:包结构应该是逻辑分组 ,不应该是访问控制边界(Java 里"为了访问而把类放同一个包"是常见的坏味道)。
🔬 internal 的字节码真相(第 3 章已提,这里给证据):
kotlin
// module name = "ch11-classes"
class Widget {
internal fun helper() = 42
internal val state = 0
}
text
public final class Widget {
private final int state;
public final int helper$ch11_classes(); // ← public + 名称混淆!
public final int getState$ch11_classes(); // ← getter 也混淆
public Widget();
}
⚠️ 四个后果:
- Java 代码可以调用
helper$ch11_classes()(IDE 会警告); - 反射可以访问(因为是 public);
- 改 Gradle 模块名会破坏二进制兼容(方法名里嵌了模块名);
internal不是真正的封装,只是"编译器 + IDE 的提示"。
🚀 需要真正封装时 :用 private + 明确的公开 API;或者用 JPMS 模块(module-info.java)。
访问器的可见性
kotlin
class BankAccount(initial: Int) {
var balance: Int = initial
private set // 外部只读,内部可写
fun deposit(amount: Int) { if (amount > 0) balance += amount }
}
⚠️ 访问器的可见性不能比属性更宽:
kotlin
private var x = 0
public set // ❌ 编译错误
11.11 嵌套类与内部类
kotlin
class Outer {
private val secret = 42
class Nested { // 静态嵌套类(不持有 Outer 引用)
// fun f() = secret // ❌ 无法访问 Outer 的成员
fun g() = "nested"
}
inner class Inner { // 内部类(持有 Outer 引用)
fun f() = secret // ✅ 可以访问
}
}
Outer.Nested() // 直接构造
Outer().Inner() // 需要外部实例
🔬 字节码差异:
text
public final class Outer$Nested { ... } // 无 outer 字段
public final class Outer$Inner {
final Outer this$0; // ← 持有外部引用!
public Outer$Inner(Outer outer) { this.this$0 = outer; }
}
⚠️ inner class 的内存泄漏风险:它持有外部类引用,如果内部类实例被长期持有(如注册到全局监听器、放进静态集合),外部类就无法被 GC。
🚀 准则 :默认用 class(嵌套,静态) ,只有真的需要访问外部实例时才用 inner。这与 Java 的默认相反(Java 的内部类默认持有外部引用,静态嵌套类要写 static)------Kotlin 的默认更安全。
Android 上的经典案例:Handler 写成非静态内部类 → Activity 泄漏。
11.12 属性在 JVM 上的表示与 Java 互操作
命名规则
| Kotlin 属性 | Java 侧看到的 getter | setter |
|---|---|---|
val name: String |
getName() |
--- |
var name: String |
getName() |
setName(String) |
val isActive: Boolean |
isActive() |
--- |
var isActive: Boolean |
isActive() |
setActive(boolean) ⚠️ |
val isEmpty: Boolean |
isEmpty() |
--- |
⚠️ is 前缀的特殊规则 :以 is 开头的 Boolean 属性,getter 不加 get 前缀,setter 把 is 换成 set。所以 isActive → isActive() / setActive()。
🚀 这个不对称会导致 Java 侧的困惑 (bean.isActive() 但 bean.setActive(true)),也是某些 JavaBean 内省工具出问题的原因。建议 Boolean 属性避免用 is 前缀,或者接受这个约定。
@JvmField:暴露为字段
kotlin
class Config {
@JvmField val maxRetries = 3 // 直接暴露 public 字段,无 getter
}
java
// Java
int n = config.maxRetries; // 直接字段访问
⚠️ @JvmField 的约束 :属性必须有 backing field、不能是 private(否则没意义)、不能有自定义访问器、不能是 open/override/const/lateinit。
🚀 何时用:
- 与 Java 库互操作且对方期望字段(如某些框架的配置类);
- 极致性能场景(省一次方法调用,但 JIT 通常已经内联了 getter,收益微乎其微)。
一般不要用------它放弃了属性访问器的未来扩展能力(比如以后想加校验,就得改字段为属性 → 二进制不兼容)。
顶层属性
kotlin
// file: Config.kt, package book.ch11
val appName = "Book"
fun version() = "1.0"
text
public final class book.ch11.ConfigKt {
private static final String appName = "Book";
public static final String getAppName();
public static final String version();
}
java
// Java
String n = ConfigKt.getAppName();
📌 用 @file:JvmName("AppConfig") 改类名:
kotlin
@file:JvmName("AppConfig")
package book.ch11
val appName = "Book"
java
String n = AppConfig.getAppName(); // 更专业
🚀 所有暴露给 Java 的 Kotlin 文件都应该加 @file:JvmName ,否则 XxxKt 这种名字很难看且难猜。
11.13 本章小结
- 初始化顺序 :
super()→ 主构造器属性赋值 → 属性初始化器与init块按文本顺序交错 → 主构造器体 → 次构造器体。 - 两个真实 bug :① 构造器里调用
open方法 → 读到子类未初始化的零值字段;② 默认参数表达式引用尚未初始化的属性 → 得到 0。Kotlin 默认final恰好防住了 ①。 - 主构造器参数
x: Int与属性val x: Int完全不同:前者只在构造期可见(除非被 Lambda 捕获而提升为字段)。 - 属性 = 属性声明 + 访问器 + 可选的 backing field。计算属性没有字段。
- 📌 显式 backing field(2.4 稳定) 用一行
field = mutableListOf()取代了_items+get() = _items的三行惯用法;但只读接口 ≠ 不可变对象 (外部拿到的仍是同一个 ArrayList 的视图)。五条限制 :无自定义 getter、必须val、非open、非委托、非const。 const val会被内联进引用方常量池 → 库的公开可调参数应该用普通val。lateinit不支持基本类型、非 volatile、isInitialized只能在可访问该属性的地方用 。多线程初始化优先by lazy。- 属性委托存的是委托对象而非值 ,每次读都调
getValue(有开销);provideDelegate让你在创建时校验。 internal编译为public+ 名称混淆(name$moduleName),不是真封装,且改模块名破坏二进制兼容。- 嵌套类默认是静态的(与 Java 相反),这个默认更安全(避免内存泄漏)。
11.14 练习题
11-1. 下面代码输出什么?为什么?
kotlin
open class A {
open val x: Int = 1
init { println("A.init x=$x") }
}
class B : A() {
override val x: Int = 2
init { println("B.init x=$x") }
}
fun main() { B() }
答案要点
text
A.init x=1
B.init x=2
分析:
B()触发A.<init>;A.<init>执行this.x = 1(对A.x的字段赋值)------⚠️ 注意:A.x被 override 成属性访问器,实际上A的初始化会调用setX?不,A的x有 backing field,override val x在B中也有自己的 backing field。这是两个不同的字段!A的init块读x→ 走 getter → 虚分派到B.getX()→ 读B.x字段 → 此时还是 0?
🔬 实际输出是 A.init x=1 ,因为 Kotlin 对 open val x = 1 的处理是:A 里生成字段 x 和 getter getX();B 里生成字段 x 和 override 的 getX() 返回 B.x。A.init 里的 x 读取会调用 getX()(虚方法)→ 返回 B.x = 0。
⚠️ 所以真实输出应该是:
text
A.init x=0 ← B.x 还没初始化!
B.init x=2
这正是 11.3 讲的"构造器里访问 open 成员"陷阱的属性版本。
教训 :不要在 init 块或属性初始化器里读取 open 属性。修复方案:把 A 的 x 改成 final(去掉 open),或者把依赖 x 的逻辑移到工厂函数/延迟执行。
这个练习的价值在于:你以为你懂了初始化顺序,但属性 override 会引入虚分派,让"文本顺序"的直觉失效。请务必亲手跑一遍验证。
11-2. 用显式 backing field 重写下面的类,并指出重写后语义上的一个变化。
kotlin
class Registry {
private val _entries = mutableMapOf<String, Entry>()
val entries: Map<String, Entry> get() = _entries
fun register(name: String, e: Entry) { _entries[name] = e }
}
答案要点
kotlin
class Registry {
val entries: Map<String, Entry>
field = mutableMapOf()
fun register(name: String, e: Entry) { entries[name] = e }
}
语义变化:
- 字段数量 :从 2 个(
_entries和entries其实是同一个字段?不------旧写法只有_entries一个字段,entries是纯 getter)变成 1 个(entries的 backing field)。所以内存占用相同(都是一个引用字段)。 - 访问路径 :旧写法
registry.entries走 getter 返回_entries;新写法 getter 返回 backing field。行为等价。 - 类内部的可变性 :旧写法必须用
_entries[name] = e;新写法可以直接entries[name] = e(编译器智能转换为MutableMap)。这是最大的可读性收益。 - ⚠️ 两者都不是不可变 :外部拿到的
Map是同一个LinkedHashMap的只读视图,可以强转为MutableMap修改(第 1/16 章的陷阱)。
如果需要真不可变 :val entries: Map<String, Entry> get() = _entries.toMap()(每次复制,O(n)),或用 backing property 模式。
11-3. 为什么显式 backing field 不允许自定义 getter?如果我想"内部可变 + 读取时排序",应该怎么做?
答案要点
为什么不允许 :显式 backing field 的核心语义是"类内部访问该属性名时,解析为 backing field 的更具体类型 "(一种编译器智能转换)。如果同时存在自定义 getter,就产生了歧义:类内部的 items 到底走 getter 还是直接读字段?两者可能返回不同的值(getter 可能做了转换/复制)。Kotlin 选择禁止这种组合以保持语义清晰。
替代方案(backing property):
kotlin
class UserDirectory {
private val _users = mutableListOf<String>()
val users: List<String> get() = _users.sorted() // 读取时排序
fun addUser(name: String) { _users.add(name) }
}
优化:如果排序很频繁且列表变化少,加缓存:
kotlin
private var sortedCache: List<String>? = null
val users: List<String>
get() = sortedCache ?: _users.sorted().also { sortedCache = it }
fun addUser(name: String) { _users.add(name); sortedCache = null }
⚠️ 这个缓存不是线程安全的,需要 @Volatile 或 synchronized。
11-4. (🧪 动手)验证 lateinit 在多线程下的可见性问题。设计一个能大概率复现失败的实验。
答案要点
kotlin
class Holder { lateinit var payload: BigObject }
class BigObject { val data = LongArray(1_000_000) { it } }
fun main() {
repeat(1000) { round ->
val h = Holder()
var failed = false
val writer = thread { h.payload = BigObject() }
val reader = thread {
try { if (h::payload.isInitialized) h.payload.data[999_999] }
catch (e: Throwable) { failed = true }
}
writer.join(); reader.join()
if (failed) println("round $round failed")
}
}
说明 :由于 x86 的强内存模型和 JIT 的行为,这个实验不一定能复现------这正是并发 bug 可怕的地方(在开发机上"没问题",在生产环境的 ARM/高负载下出问题)。
理论分析:
lateinit字段是普通字段,写入没有 release 语义,读取没有 acquire 语义;- 根据 JMM,reader 线程可能:① 看到 null(抛
UninitializedPropertyAccessException);② 看到非 null 引用但对象的字段未初始化(读到data为 null 或数组元素为 0); - 情况 ② 之所以不会发生在
final字段上,是因为 JMM 对final有安全发布保证(第 30 章)。
正确做法 :by lazy(内部 @Volatile + 双检查)、或 @Volatile 字段 + 手动 null 检查、或用不可变对象 + AtomicReference。
11-5. const val 和普通顶层 val 在字节码层面有什么区别?各自的适用场景与风险?
答案要点
const val X = 5 |
val x = 5(顶层) |
|
|---|---|---|
| 字段 | public static final int X + ConstantValue 属性 |
private static final int x(无 ConstantValue) |
| 访问器 | ❌ 无 | ✅ getX() 静态方法 |
| 引用方字节码 | 值被内联 (iconst_5),无 getstatic/invokestatic |
invokestatic getX() |
| 性能 | 最快(零调用) | JIT 内联后同样快 |
| 升级风险 | ⚠️ 改值后引用方必须重新编译 | ✅ 改值后引用方无需重编译(JIT 会去优化并重编译) |
| 可用于注解参数 | ✅ | ❌ |
| 支持类型 | 仅 String 和基本类型 |
任意 |
| 反射可见 | ✅(有 backing field) | ✅ |
适用场景:
const val:协议常量、数学常数、注解参数、真正的编译期已知值;- 普通
val:库的公开可调参数、运行时计算的值、需要未来能改的配置。
库设计准则 :公开 API 中的"可能变化的值"一律用普通 val,避免升级时的隐形破坏。
11.15 面试题精选
Q1:Kotlin 的属性和 Java 的字段 + getter/setter 有什么区别?
答题框架:
- 语法层 :属性是一等概念,访问用
obj.x而非obj.getX(); - 实现层:属性 = 可选的 backing field + getter(必有)+ setter(var);可以只有 getter(计算属性,无字段);
- 能力 :可以自定义访问器、改访问器可见性(
private set)、加注解到访问器、用委托(by)、📌 用显式 backing field(2.4+)实现"内部可变外部只读"; - 互操作 :Kotlin 属性生成符合 JavaBean 规范的 getter/setter(
is前缀有特殊规则);@JvmField可直接暴露字段;Java 的 getter/setter 在 Kotlin 里被合成为属性访问; - 风险 :
const val内联导致升级陷阱;internal属性名混淆;lateinit非 volatile。
Q2:Kotlin 对象的初始化顺序是什么?有什么陷阱?
答题框架 :见 11.3。核心是"super() → 构造器属性 → 初始化器与 init 块按文本顺序 → 构造器体",加两个陷阱(open 成员调用、默认参数引用未初始化属性),加防御措施(默认 final、工厂函数、参数顺序)。
Q3:lateinit 和 by lazy 怎么选?
见第 6 章 Q3 + 11.8。补充:lateinit 不支持基本类型与可空类型、非 volatile、可用 isInitialized 检查(但有作用域限制);lazy 默认 SYNCHRONIZED(volatile + 双检查),有三种模式可调,会多分配一个 Lazy 对象。
Q4:什么是显式 backing field?它解决了什么问题?有什么限制?
答题框架:
- 是什么 :📌 Kotlin 2.3 实验、2.4 稳定的语法,
val items: List<String>+field = mutableListOf(),让属性的 backing field 类型可以比属性声明类型更具体。 - 解决什么 :取代
private val _items = mutableListOf()+val items: List<String> get() = _items的三行惯用法;且类内部访问该属性名会被智能转换为 backing field 类型,可以直接调用可变方法。 - 五条限制 :无自定义 getter、必须
val、非open、非委托属性、非const;backing field 类型必须是属性类型的子类型且 private。 - 注意 :它提供的是"只读接口",不是不可变------外部拿到的仍是同一个可变对象。
- 替代 :需要自定义 getter 时用 backing property(
_x模式)。
下一章预告 :第 12 章《特殊类家族》------data/sealed/value/enum/object/companion。我们会讲:data class 的 equals 为什么不看父类字段、value class 什么时候会装箱(以及那个会让 Java 调用方崩溃的名称混淆 )、sealed 为什么比 enum 更适合演化、object 的线程安全保证来自哪一行字节码。