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)
  • 属性初始化器:fullNameinitials
  • init 块:两个 init 初始化逻辑
  • 次构造函数:constructor(lastName: String) : this("", lastName)

3. 构造函数与 init 块的区别

✅ 主构造函数不能直接写逻辑

与次构造函数不同,Kotlin 的主构造函数不能直接包含可执行代码。为了在初始化过程中执行逻辑,我们需要借助属性初始化器或 init 块。

✅ 初始化顺序

Kotlin 会按照它们在类体中出现的顺序,依次执行:

  1. 属性初始化器(Property Initializers)
  2. init 块(Initializer Blocks)

例如,当我们执行以下代码:

val p = Person("ali", "dehghani")

输出结果如下:

Initializing full name
You're ali dehghani
Initializing initials
You're initials are AD

说明初始化顺序为:

  1. fullName 属性初始化器
  2. 第一个 init
  3. initials 属性初始化器
  4. 第二个 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 仓库


原始标题:The Difference Between init Block and Constructor in Kotlin