类与对象的底层机制
从 Java 的"类"到 Kotlin 的"类":一次精心的重构
在 Java 的世界里,定义一个简单的数据持有类需要大量的"仪式感"代码——声明字段、写构造函数、写 getter/setter、可能还要写 toString()。Kotlin 的类设计目标很明确:消除所有可以由编译器代劳的样板代码,同时在字节码层面保持与 Java 完全兼容。
这意味着你在 Kotlin 中写出的每一行简洁语法,背后都有编译器在默默生成对应的 JVM 字节码。本文的任务就是揭开这层编译器的"魔法面纱",让你清清楚楚地知道每一行 Kotlin 代码在 JVM 上到底变成了什么。
主构造函数与次构造函数
主构造函数:类头的一部分
Kotlin 最显著的语法特征之一就是主构造函数(Primary Constructor)直接写在类头上。这不仅是语法糖,更是一种设计声明——它在强调:"这些参数是构造这个对象所必需的核心信息。"
class User(val name: String, val age: Int)
就这一行,编译器会为你生成以下内容:
// 编译后等效 Java
public final class User {
@NotNull
private final String name; // 属性的 backing field
private final int age; // 属性的 backing field
public User(@NotNull String name, int age) {
Intrinsics.checkNotNullParameter(name, "name"); // 空安全守卫
super();
this.name = name;
this.age = age;
}
@NotNull
public final String getName() { return this.name; } // getter
public final int getAge() { return this.age; } // getter
// val 属性没有 setter
}
注意几个关键细节:
val参数编译为private final字段 +getter,没有settervar参数编译为private字段 +getter+setter- 没有
val/var的参数仅仅是构造函数参数,不会生成属性和访问器 - 类默认是
final的——这个设计后面会详细展开
把主构造函数想象成建筑的"地基图纸"——它定义了这栋楼最核心的结构参数。你可以在此基础上加楼层(属性)、加装修(方法),但地基参数一旦定好,就是整栋楼的根本。
次构造函数:必须委托给主构造函数
Kotlin 允许定义次构造函数(Secondary Constructor),但有一个硬性约束:每个次构造函数最终都必须直接或间接地委托给主构造函数。这与 Java 不同——Java 允许多个互相独立的构造函数。
class User(val name: String, val age: Int) {
var email: String = ""
// 次构造函数,必须委托给主构造函数
constructor(name: String, age: Int, email: String) : this(name, age) {
this.email = email
}
}
编译后的字节码(等效 Java):
public final class User {
private final String name;
private final int age;
private String email;
// 主构造函数
public User(@NotNull String name, int age) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
this.age = age;
this.email = ""; // 属性初始化器在主构造函数中执行
}
// 次构造函数——先调用主构造函数,再执行自己的逻辑
public User(@NotNull String name, int age, @NotNull String email) {
this(name, age); // 委托给主构造函数
this.email = email; // 次构造函数的自有逻辑
}
}
为什么强制委托? 因为 Kotlin 的初始化逻辑(属性初始化器、init 块)全部编织在主构造函数中。如果允许次构造函数跳过主构造函数,这些初始化逻辑就不会执行,对象的状态就可能不一致。强制委托是编译器层面的安全保障。
默认参数:消灭 Java 式的构造函数重载
在 Java 中,为了提供灵活的构造方式,你经常需要写出一串"望远镜式"的构造函数重载:
// Java:望远镜式构造函数
public User(String name) { this(name, 0); }
public User(String name, int age) { this(name, age, ""); }
public User(String name, int age, String email) { ... }
Kotlin 用默认参数一招搞定:
class User(
val name: String,
val age: Int = 0,
val email: String = ""
)
编译器会生成一个带有位掩码的合成构造函数来处理默认值:
// 编译器生成的合成构造函数(简化版)
// 第三个参数是位掩码,标记哪些参数使用了默认值
public User(String name, int age, String email, int mask, DefaultConstructorMarker marker) {
if ((mask & 0x2) != 0) age = 0; // 第 2 个参数使用默认值
if ((mask & 0x4) != 0) email = ""; // 第 3 个参数使用默认值
this(name, age, email);
}
如果你需要让 Java 代码也能像"多个构造函数重载"那样调用这个类,可以加上 @JvmOverloads 注解——编译器会自动生成多个重载构造函数。
init 块的执行时机与顺序
init 不是构造函数——它与构造函数"融合"
init 块是 Kotlin 中执行初始化逻辑的地方,但它在字节码中并不是一个独立的方法。编译器会将所有 init 块的代码按声明顺序编织进主构造函数中。
class Configuration(val host: String) {
val url: String
init {
println("第一个 init 块:验证 host")
require(host.isNotBlank()) { "host 不能为空" }
}
val port: Int = 8080
init {
println("第二个 init 块:构建 url")
url = "https://$host:$port"
}
}
编译后的字节码顺序(等效 Java):
public final class Configuration {
private final String url;
private final int port;
private final String host;
public Configuration(@NotNull String host) {
Intrinsics.checkNotNullParameter(host, "host");
super();
// ① 主构造函数参数赋值
this.host = host;
// ② 第一个 init 块(按源码顺序)
System.out.println("第一个 init 块:验证 host");
// require 逻辑...
// ③ 属性初始化器(port = 8080,按源码中的声明位置)
this.port = 8080;
// ④ 第二个 init 块(按源码顺序)
System.out.println("第二个 init 块:构建 url");
this.url = "https://" + host + ":" + this.port;
}
}
核心规则:属性初始化器和 init 块交错执行
关键认知:属性初始化器和 init 块不是分阶段执行的,而是按照它们在源码中出现的顺序,自上而下依次执行。编译器把它们全部"摊平"进主构造函数的方法体中。
这意味着属性声明的位置很重要:
class Trap {
init {
// ❌ 编译错误:此时 value 还没有初始化
// println(value)
}
val value: Int = 42
init {
println(value) // ✅ 正确:value 已经在上面初始化了
}
}
用一张时序图来理解整个初始化流程:
创建对象 → 调用主构造函数
│
├─ ① 调用父类构造函数(super())
├─ ② 按源码顺序执行属性初始化器和 init 块
│ ├─ 属性初始化器 A
│ ├─ init 块 1
│ ├─ 属性初始化器 B
│ └─ init 块 2
└─ ③ 如果是次构造函数调用,执行次构造函数的自有逻辑
属性访问器:Kotlin 属性的本质是方法对
属性 ≠ 字段
这是理解 Kotlin 面向对象模型最重要的认知之一:Kotlin 的属性(Property)不是 Java 的字段(Field)。属性 = backing field + getter(+ setter)。你看到的每一次属性访问,在字节码中都是一次方法调用。
class Temperature {
var celsius: Double = 0.0
set(value) {
require(value >= -273.15) { "温度不能低于绝对零度" }
field = value // field 是编译器提供的关键字,指向 backing field
}
// 计算属性:没有 backing field,每次访问都是纯计算
val fahrenheit: Double
get() = celsius * 9 / 5 + 32
}
编译后的字节码(等效 Java):
public final class Temperature {
private double celsius; // backing field:只有 celsius 有
// fahrenheit 没有 backing field!
public final double getCelsius() {
return this.celsius;
}
public final void setCelsius(double value) {
if (!(value >= -273.15)) {
throw new IllegalArgumentException("温度不能低于绝对零度");
}
this.celsius = value; // 直接写入 backing field
}
public final double getFahrenheit() {
return this.celsius * 9.0 / 5.0 + 32.0; // 纯计算,无字段存储
}
}
backing field 的生成规则
编译器不是对每个属性都生成 backing field。规则很简单:
| 场景 | 是否生成 backing field |
|---|---|
使用默认 getter/setter(var name: String = "") |
✅ |
自定义 getter/setter 中引用了 field 关键字 |
✅ |
纯计算属性(val x get() = ...,且未引用 field) |
❌ |
把 Kotlin 属性想象成一个"有门禁的保险箱"。
backing field是保险箱里的实际物品,getter和setter是门禁系统。有些"属性"其实没有保险箱——它们每次都是现场计算(比如fahrenheit),就像一个实时汇率看板,不存储数据,每次查看都是实时计算的结果。
@JvmField:绕过访问器,直接暴露字段
如果你需要与 Java 框架交互(如序列化框架直接读写字段),可以用 @JvmField 注解跳过 getter/setter 的生成:
class Config {
@JvmField var debug: Boolean = false
}
编译后,debug 会直接暴露为一个 public 字段,没有 getter/setter。这在 Android 开发中与某些注解处理器配合时特别有用。
object 关键字:一个关键字,三种身份
Kotlin 的 object 关键字根据上下文有三种完全不同的含义。理解它们在字节码中的实现,也就理解了 Kotlin 如何在 JVM 上实现这些高级抽象。
对象声明(Object Declaration):线程安全的单例
object DatabaseManager {
private val connections = mutableListOf<Connection>()
fun getConnection(): Connection {
// ...
}
}
编译后的字节码(等效 Java):
public final class DatabaseManager {
@NotNull
public static final DatabaseManager INSTANCE; // 单例引用
private static final List connections;
// 私有构造函数——外部不能 new
private DatabaseManager() {}
// 类加载时初始化——JVM 保证线程安全
static {
DatabaseManager var0 = new DatabaseManager();
INSTANCE = var0;
connections = new ArrayList();
}
@NotNull
public final Connection getConnection() { ... }
}
线程安全的保障来自 JVM 的类加载机制。 JVM 规范(§5.5)保证了以下几点:
- 每个类最多只被初始化一次
- 类初始化(
<clinit>方法)由 JVM 持有初始化锁,多线程环境下只有一个线程能执行<clinit> - 其他线程会被阻塞,直到初始化完成
这就是为什么 Kotlin 的 object 声明是天然线程安全的单例——它利用了 JVM 已有的、经过二十多年实战检验的类加载机制,而不是自己用 synchronized 或双重检查锁来实现。
把 JVM 的类加载器想象成物业管理员——当第一位住户(线程 A)搬进新公寓楼时,管理员锁上大门,亲自监督所有基础设施的安装(电、水、网络),确保一切就绪后才开门让其他住户进入。其他住户(线程 B、C)在门外等候时,不需要自己操心"电装好了吗?水通了吗?"
伴生对象(Companion Object):不是 Java 的 static
这是 Kotlin 初学者最常见的误解之一:companion object 不等于 Java 的 static。它本质上是一个嵌套在类内部的单例对象,只是被编译器赋予了"可以用类名直接调用"的语法糖。
class UserRepository {
companion object {
private const val TAG = "UserRepository"
fun create(): UserRepository {
println("$TAG: 创建实例")
return UserRepository()
}
}
}
// 调用方式看起来像静态方法
val repo = UserRepository.create()
编译后的字节码(等效 Java):
public final class UserRepository {
@NotNull
private static final String TAG = "UserRepository"; // const 直接内联
// ① 伴生对象被编译为一个内部类
public static final class Companion {
@NotNull
public final UserRepository create() {
System.out.println("UserRepository: 创建实例");
return new UserRepository();
}
private Companion() {}
}
// ② 外部类持有伴生对象的静态引用
@NotNull
public static final Companion Companion = new Companion();
}
关键观察:
companion object编译为内部类UserRepository$Companion,而不是静态方法- 外部类通过
public static final Companion Companion字段持有它的引用 - 调用
UserRepository.create()实际上等于UserRepository.Companion.create()——是实例方法调用,不是静态方法调用
这个差异有实际影响:
| 特性 | Java static |
Kotlin companion object |
|---|---|---|
| 本质 | 类级别方法/字段 | 内部单例对象的实例方法 |
| 能否实现接口 | ❌ | ✅ |
| 能否被继承/覆写 | ❌ | ✅(通过接口) |
| 能否持有状态 | 能(静态字段) | 能(对象属性) |
| 运行时开销 | 无 | 额外的对象分配(Companion 实例) |
伴生对象可以实现接口,这给了它远超 Java static 的表达能力:
interface Factory<T> {
fun create(): T
}
class User private constructor(val name: String) {
companion object : Factory<User> {
override fun create(): User = User("default")
}
}
// 可以将伴生对象作为接口的实例传递
fun <T> buildObject(factory: Factory<T>): T = factory.create()
val user = buildObject(User) // User 的伴生对象被当作 Factory<User> 实例使用
如果你确实需要在字节码中生成真正的 Java 静态方法(比如为了让 Java 代码调用更方便),使用 @JvmStatic 注解:
class Logger {
companion object {
@JvmStatic
fun log(message: String) { println(message) }
}
}
此时编译器会在外部类上额外生成一个静态方法,它内部委托给伴生对象:
// 编译器额外生成的静态方法
public static final void log(@NotNull String message) {
Companion.log(message); // 委托给伴生对象
}
对象表达式(Object Expression):比 Java 匿名内部类更强
Kotlin 的对象表达式是 Java 匿名内部类的升级版,它有一个关键的超能力:可以捕获并修改外部作用域中的可变变量。
fun countClicks(button: Button) {
var clickCount = 0 // 可变局部变量
button.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View) {
clickCount++ // 在 Java 匿名内部类中,这行会编译错误!
println("点击次数:$clickCount")
}
})
}
在 Java 中,匿名内部类只能捕获 final 或"事实不可变(effectively final)"的局部变量。Kotlin 突破了这个限制——编译器使用 Ref 包装类来实现:
// 编译后的字节码逻辑(简化版)
public static void countClicks(Button button) {
// 可变变量被包装进 IntRef 对象
final IntRef clickCount = new IntRef(); // kotlin.jvm.internal.IntRef
clickCount.element = 0;
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// 修改的是 IntRef 对象内部的 element 字段
clickCount.element++;
System.out.println("点击次数:" + clickCount.element);
}
});
}
编译器的策略很巧妙:IntRef 对象本身是 final 的(满足 JVM 的要求),但它里面的 element 字段是可变的。这就像一个信封——信封本身不换,但里面的信可以随时替换。
对象表达式与 Java 匿名内部类的完整对比:
| 特性 | Java 匿名内部类 | Kotlin 对象表达式 |
|---|---|---|
捕获 final 变量 |
✅ | ✅ |
捕获并修改 var 变量 |
❌ | ✅(通过 Ref 包装) |
| 同时实现多个接口 | ❌ | ✅ |
| 实现类 + 接口 | ❌(只能一个) | ✅ |
可见性修饰符
四种修饰符的语义
Kotlin 提供了四种可见性修饰符,与 Java 的体系既有相似也有本质差异:
| 修饰符 | Kotlin 中的含义 | Java 对应 |
|---|---|---|
public |
所有地方可见(默认) | public |
private |
仅在声明所在的文件/类内可见 | private |
protected |
类及其子类可见(不包括同包) | protected(包括同包) |
internal |
同模块内可见 | 无直接对应 |
两个值得注意的差异:
- 默认可见性:Kotlin 默认
public,Java 默认包级私有(package-private)。Kotlin 团队认为,如果一个声明没有明确限制访问范围,应该假定它是公开 API 的一部分——这迫使开发者主动思考"这个 API 需要限制访问吗?" protected不包含同包访问:Java 的protected允许同包的其他类访问,这常常导致意外的耦合。Kotlin 收紧了这一点。
internal:一个 JVM 没有的可见性
JVM 字节码没有"模块"的概念——它只认识 public、protected、private 和包级私有(package-private)。那 Kotlin 的 internal 在字节码中是怎么实现的?
答案是名称混淆(Name Mangling):
// Kotlin 源码
internal fun doInternalWork() {
println("这是模块内部的工作")
}
编译后的字节码:
// 可见性变成了 public(因为 JVM 没有 internal)
// 但方法名被混淆了!
public static final void doInternalWork$app_main() {
System.out.println("这是模块内部的工作");
}
编译器将模块名(如 app_main)作为后缀追加到方法名上。这产生了两个效果:
- Kotlin 编译器层面:编译器通过
.kotlin_module元数据文件记录了internal标记,其他模块的 Kotlin 代码尝试调用时会被编译器直接拒绝 - Java 互操作层面:虽然方法在 JVM 层面是
public的,但那个丑陋的混淆名字(doInternalWork$app_main)会劝退绝大多数 Java 开发者
如果你需要更强的隔离——让 Java 代码彻底"看不见"你的
internal成员——可以使用@JvmSynthetic注解,编译器会在字节码中标记该成员为ACC_SYNTHETIC,Java 编译器会完全忽略它。
继承与多态:open、override、final 的设计哲学
为什么 Kotlin 默认 final
在 Java 中,所有类默认可以被继承,所有非 private 方法默认可以被覆写。这看似"灵活",实际上埋了一个经典的地雷——脆弱基类问题(Fragile Base Class Problem)。
脆弱基类问题:当基类的作者修改了一个看似无害的实现细节时,远处一个你完全不知道的子类可能因此崩溃。这是因为基类无法预知所有可能的覆写行为,子类也无法预知基类未来的演进方向。
《Effective Java》第 19 条给出了明确建议:"要么为继承而设计,并写好文档;要么就禁止继承。" Kotlin 在语言层面直接践行了这一原则——类和方法默认 final,只有显式标记 open 才允许继承/覆写。
// 默认 final:不能被继承
class ImmutablePoint(val x: Int, val y: Int)
// 显式 open:允许继承
open class Shape(val color: String) {
open fun area(): Double = 0.0 // 允许子类覆写
fun describe() = "一个${color}的图形" // 不允许覆写(默认 final)
}
class Circle(color: String, val radius: Double) : Shape(color) {
override fun area(): Double = Math.PI * radius * radius
// override fun describe() = ... // ❌ 编译错误:describe 不是 open 的
}
字节码层面的 final 与 open
在 JVM 字节码中,这个行为通过 ACC_FINAL 标志位实现:
// class ImmutablePoint → 带有 ACC_FINAL 标志
// Kotlin 为其生成:
public final class ImmutablePoint { ... }
// class Shape(标记 open)→ 没有 ACC_FINAL 标志
public class Shape { ... }
// Shape.area()(标记 open)→ 没有 ACC_FINAL
public double area() { return 0.0; }
// Shape.describe()(默认 final)→ 带有 ACC_FINAL
public final String describe() { return "一个" + this.color + "的图形"; }
ACC_FINAL 不仅是访问控制——它还是一个性能信号。JVM 的 JIT 编译器看到 final 方法时,知道这个方法不会被覆写,可以进行以下优化:
- 去虚拟化(Devirtualization):将虚方法调用(
invokevirtual)优化为直接调用,省去方法表查找 - 方法内联(Inlining):将方法体直接"拷贝"到调用点,省去方法调用的开销
override 默认仍然是 open 的
一个容易忽视的细节:覆写后的方法默认仍然是 open 的,子类的子类还可以继续覆写它。如果要终止覆写链,必须显式用 final 修饰:
open class Animal {
open fun sound() = "..."
}
open class Dog : Animal() {
override fun sound() = "汪汪" // 隐式 open——Dog 的子类还可以覆写
}
class GuideDog : Dog() {
override fun sound() = "轻声汪" // 合法——因为 Dog.sound() 是 open 的
}
open class Cat : Animal() {
final override fun sound() = "喵喵" // 显式 final——Cat 的子类不能再覆写
}
// class PersianCat : Cat() {
// override fun sound() = "优雅喵" // ❌ 编译错误:Cat.sound() 是 final 的
// }
抽象类 vs 接口:Kotlin 接口的 DefaultImpls 秘密
两者的本质区别
在概念上,抽象类和接口的区别很直观:
| 特性 | 抽象类 | 接口 |
|---|---|---|
| 可以有构造函数 | ✅ | ❌ |
| 可以持有状态(backing field) | ✅ | ❌ |
| 可以有默认方法实现 | ✅ | ✅ |
| 一个类能继承/实现几个 | 1 个 | 多个 |
可以有非 public 成员 |
✅ | 仅 private(Kotlin 1.5+) |
设计决策的核心是:
- 抽象类:当多个类有共同的状态和行为时使用,它代表了一种"是什么"的关系(is-a),例如
Animal→Dog - 接口:当多个不相关的类需要共同的能力时使用,它代表了一种"能做什么"的关系(can-do),例如
Clickable、Drawable
Kotlin 接口的默认方法实现
Kotlin 接口可以包含方法的默认实现,这在 Java 8 之后看起来和 Java 接口的 default 方法很像,但它们的编译策略有重要差异:
interface Clickable {
fun click() // 抽象方法
fun showRipple() = println("显示波纹效果") // 默认实现
}
interface Focusable {
fun setFocus(focused: Boolean) = println("焦点状态:$focused")
}
class Button : Clickable, Focusable {
override fun click() = println("按钮被点击")
// showRipple() 和 setFocus() 使用默认实现
}
历史模式:DefaultImpls 内部类
在早期 Kotlin 版本和针对 Java 6/7 的编译中,编译器使用一个名为 DefaultImpls 的静态内部类来存放默认方法实现:
// Clickable 接口的编译产物(DefaultImpls 模式)
public interface Clickable {
void click();
void showRipple();
// 默认实现被放到这个静态内部类中
public static final class DefaultImpls {
public static void showRipple(Clickable $this) {
System.out.println("显示波纹效果");
}
}
}
// Button 类的编译产物
public final class Button implements Clickable, Focusable {
public void click() { System.out.println("按钮被点击"); }
// 编译器自动生成的桥接方法——委托给 DefaultImpls
public void showRipple() {
Clickable.DefaultImpls.showRipple(this);
}
}
注意 DefaultImpls.showRipple(Clickable $this) 的第一个参数是接口实例——它本质上是一个静态方法,通过参数接收"this"引用。
现代模式:原生 JVM default 方法
在现代 Kotlin(目标 JVM 8+)中,编译器默认使用 JVM 原生的 default 方法,同时为了二进制兼容性可能仍然生成 DefaultImpls:
// 现代编译产物
public interface Clickable {
void click();
// 原生 JVM default 方法
default void showRipple() {
System.out.println("显示波纹效果");
}
}
编译器选项 -jvm-default 控制具体行为:
| 选项 | 生成 default 方法 |
生成 DefaultImpls |
适用场景 |
|---|---|---|---|
enable(默认) |
✅ | ✅(兼容性桥接) | 需要保持二进制兼容 |
no-compatibility |
✅ | ❌ | 新项目,不需要兼容旧代码 |
disable |
❌ | ✅ | 必须兼容 Java 6/7 |
接口中的属性:没有 backing field
接口中声明的属性不能有 backing field——这是接口"不能持有状态"这一规则的技术体现:
interface Named {
val name: String // 抽象属性——实现类必须提供
val greeting: String // 带默认 getter 的属性——本质是计算属性
get() = "你好,我是 $name"
}
class Person(override val name: String) : Named
// person.greeting 会调用接口中定义的默认 getter
编译后,greeting 的 getter 被放进 DefaultImpls(或编译为 default 方法),而 name 的实际存储由实现类 Person 提供。
综合实战:字节码全景验证
让我们用一段综合性的代码来串联本文所有核心知识点:
// ① internal 可见性 + object 声明(单例)
internal object AppConfig {
const val VERSION = "1.0.0" // 编译期常量——直接内联到调用点
var debugMode = false
}
// ② open 类 + 主构造函数 + init 块 + 属性访问器
open class Component(val id: String) {
val createdAt: Long
init {
createdAt = System.currentTimeMillis()
println("组件 $id 已创建")
}
open fun render(): String = "<component id='$id'/>"
}
// ③ 继承 + companion object + 覆写
class Button(id: String, val label: String) : Component(id) {
companion object {
private var instanceCount = 0
fun totalInstances(): Int = instanceCount
}
init {
instanceCount++
}
override fun render(): String = "<button id='$id'>$label</button>"
// final override——终止覆写链
final override fun toString(): String = "Button($id, $label)"
}
// ④ 接口 + 默认实现
interface Clickable {
fun onClick()
fun feedback() = println("交互反馈")
}
// ⑤ 对象表达式 + 捕获可变变量
fun setupButton(): Button {
var clickCount = 0
val btn = Button("btn-1", "确认")
val listener = object : Clickable {
override fun onClick() {
clickCount++ // 通过 IntRef 包装实现
println("点击 ${btn.label} 第 $clickCount 次")
}
}
listener.onClick()
return btn
}
这段代码涉及的编译产物清单:
| Kotlin 构造 | 字节码产物 |
|---|---|
internal object AppConfig |
public final class AppConfig(名称不混淆,但成员方法名混淆) |
const val VERSION |
不生成字段——值直接内联到所有使用处 |
init { createdAt = ... } |
代码被编织进 Component 的构造函数 |
companion object |
生成 Button$Companion 内部类 + Button.Companion 静态字段 |
override fun render() |
普通虚方法调用(invokevirtual) |
final override fun toString() |
生成带 ACC_FINAL 的方法 |
object : Clickable { ... } |
生成匿名内部类 XXX$1,捕获 IntRef 和 Button 引用 |
设计哲学总结
纵观 Kotlin 类与对象的整套设计,可以提炼出三条核心哲学:
1. 显式优于隐式
- 继承必须显式
open——阻止意外的覆写 - 覆写必须显式
override——不可能"恰好"和父类方法同名 - 可见性默认
public——迫使开发者主动限缩访问范围
2. 编译器代劳一切可自动化的工作
- 主构造函数自动生成属性和访问器
init块自动编织进构造函数object声明自动处理线程安全的单例模式- 默认参数自动生成带位掩码的合成构造函数
3. 在 JVM 上保持零额外开销
val编译为final字段const val直接内联- 属性访问器是标准的 getter/setter
object单例利用 JVM 类加载机制,无需额外的同步代码
理解了这些设计选择背后的"为什么",你在使用这些特性时就不再是凭直觉猜测行为,而是清楚地知道每一行代码在 JVM 上的确切表现——这就是写出零缺陷代码的第一步。