1. 概述
在 Kotlin 中,类的初始化逻辑可以通过多种方式实现:主构造函数(primary constructor)、次构造函数(secondary constructor)、属性初始化器(property initializer)以及 init
块。虽然它们都用于初始化,但它们的执行顺序和使用场景有所不同。
本文将通过一个示例类来说明这些初始化逻辑之间的区别,并借助字节码分析它们在底层是如何工作的。
2. 示例类
我们以一个 Person
类为例,包含主构造函数、两个 init
块、两个属性初始化器以及一个次构造函数:
class Person(val firstName: String, val lastName: String) {
private val fullName: String = "$firstName $lastName".trim()
.also { println("Initializing full name") }
init {
println("You're $fullName")
}
private val initials = "${firstName.firstOrEmpty()}${lastName.firstOrEmpty()}".trim()
.also { println("Initializing initials") }
init {
println("You're initials are $initials")
}
constructor(lastName: String) : this("", lastName) {
println("I'm secondary")
}
private fun String.firstOrEmpty(): Char = firstOrNull()?.toUpperCase() ?: ' '
}
这个类展示了以下几种初始化机制:
- 主构造函数:
Person(val firstName: String, val lastName: String)
- 属性初始化器:
fullName
和initials
init
块:两个init
初始化逻辑- 次构造函数:
constructor(lastName: String) : this("", lastName)
3. 构造函数与 init 块的区别
✅ 主构造函数不能直接写逻辑
与次构造函数不同,Kotlin 的主构造函数不能直接包含可执行代码。为了在初始化过程中执行逻辑,我们需要借助属性初始化器或 init
块。
✅ 初始化顺序
Kotlin 会按照它们在类体中出现的顺序,依次执行:
- 属性初始化器(Property Initializers)
init
块(Initializer Blocks)
例如,当我们执行以下代码:
val p = Person("ali", "dehghani")
输出结果如下:
Initializing full name
You're ali dehghani
Initializing initials
You're initials are AD
说明初始化顺序为:
fullName
属性初始化器- 第一个
init
块 initials
属性初始化器- 第二个
init
块
3.1 字节码分析
虽然 Kotlin 的主构造函数不能直接写逻辑,但在编译时,Kotlin 编译器会将所有属性初始化器和 init
块合并到主构造函数中。
我们可以通过反编译查看生成的字节码:
>> kotlinc Person.kt
>> javap -c -p com.baeldung.initblock.Person
简化后的字节码大致如下:
public com.baeldung.initblock.Person(java.lang.String, java.lang.String);
Code:
// 初始化 firstName 和 lastName
13: invokespecial #20
16: aload_0
17: aload_1
18: putfield #23
21: aload_0
22: aload_2
23: putfield #25
// fullName 属性初始化
27: new #27
30: dup
31: invokespecial #28
...
99: ldc #57
101: astore 8
103: iconst_0
104: istore 9
106: getstatic #63
109: aload 8
111: invokevirtual #69
// 第一个 init 块
123: putfield #78
126: nop
127: ldc #80
129: aload_0
130: getfield #78
133: invokestatic #84
136: astore_3
137: iconst_0
138: istore 4
140: getstatic #63
143: aload_3
144: invokevirtual #69
从字节码可以看出,主构造函数中包含了所有属性初始化器和 init
块的逻辑。也就是说,Kotlin 将这些初始化逻辑统一编译进了主构造函数中。
3.2 次构造函数的执行顺序
与主构造函数不同,次构造函数可以包含自己的初始化逻辑,但必须通过 this(...)
调用主构造函数(显式或隐式)。
这意味着:
✅ 次构造函数中的逻辑会在主构造函数的所有初始化完成后执行。
我们执行以下代码:
val p = Person("dehghani")
输出如下:
Initializing full name
You're dehghani
Initializing initials
You're initials are D
I'm secondary
可以看到,主构造函数的初始化逻辑先执行,最后才执行次构造函数中的打印语句。
反编译后的字节码如下:
public com.baeldung.initblock.Person(java.lang.String);
Code:
// 调用主构造函数
10: invokespecial #109
// 次构造函数中的打印逻辑
13: ldc #111
18: getstatic #63
21: aload_2
22: invokevirtual #69
✅ 次构造函数中的代码总是在主构造函数之后执行。
4. 总结
初始化方式 | 是否能写逻辑 | 执行顺序 | 备注 |
---|---|---|---|
属性初始化器 | ✅ | 按声明顺序 | 可用于初始化属性值 |
init 块 |
✅ | 按声明顺序 | 用于执行初始化逻辑 |
主构造函数 | ❌ | 最先执行 | 不可直接写逻辑,但包含所有初始化器和 init 块 |
次构造函数 | ✅ | 在主构造函数之后 | 必须调用主构造函数 |
⚠️ 踩坑提醒
- ❌ 不要在主构造函数中写逻辑,否则会编译报错。
- ⚠️ 多个
init
块时,执行顺序取决于它们在类体中的顺序。 - ⚠️ 次构造函数的逻辑总是在主构造函数之后执行,因此不要在次构造函数中依赖尚未初始化的属性。
✅ 推荐使用方式
- ✅ 对于简单属性初始化,使用属性初始化器即可。
- ✅ 对于需要依赖构造参数的复杂逻辑,使用
init
块。 - ✅ 次构造函数用于提供多个构造方式,但要记得调用主构造函数。
如需查看完整示例代码,可以访问:GitHub 仓库