1. 概述

本文将探讨如何在 Kotlin 中使用 companion object(伴生对象) 实现类的静态初始化。

我们会先回顾 Java 中的静态初始化机制,然后对比在 Kotlin 中如何实现相同效果。最后,通过分析生成的字节码,揭示 Kotlin 背后的实现原理。这对于理解 Kotlin 与 JVM 的交互非常有帮助,也能避免一些“踩坑”。

2. 静态初始化

在 Java 中,我们使用 static 初始化块 来执行类加载时的初始化逻辑:

static {
    // 在这里放置静态初始化代码
}

Kotlin 并没有直接提供 static 关键字,也不支持类似 Java 的 static 块。✅ 但这并不意味着无法实现静态初始化。

Kotlin 的解决方案是:在类的 companion object 中定义 init。这种方式等价于 Java 的 static 初始化。

示例代码如下:

class Static {
    
    companion object {
        lateinit var answer: String
        
        init {
            answer = "42"
            println("Initialized")
        }
    }
}

⚠️ 注意:

  • answercompanion object 中的变量,但可以通过 Static.answer 直接访问。
  • init 块会在类被 JVM 加载时执行,且仅执行一次。
  • lateinit 用于延迟初始化非空变量,此处适合静态场景。

这背后的机制是:JVM 加载 Static 类时,会自动触发其伴生对象的初始化,进而执行 init 块中的逻辑。

3. 字节码解析

为了深入理解 Kotlin 的实现方式,我们查看其生成的字节码。

编译 Kotlin 文件

使用 kotlinc 编译:

$ kotlinc Static.kt

查看字节码

使用 javap 反编译生成的 .class 文件:

$ javap -c -p -v Static

输出关键部分如下:

Compiled from "Static.kt"
public final class com.baeldung.staticinit.Static {
  public static java.lang.String answer;
  
  public static final Static$Companion Companion;
}

从中可以得出几个重要结论:

伴生对象的成员变为静态字段

  • answer 被编译为 public static String answer,位于外层类中。

伴生对象本身是一个静态内部类

InnerClasses:
  public static final #15= #37 of #2; // Companion=class Static$Companion of class Static
  • companion object 被编译为 Static$Companion,是一个 static final 内部类。

伴生对象是单例

public static final Static$Companion Companion;
  • 编译器生成一个名为 Companion 的静态字段,持有 Static$Companion 的唯一实例。

init 块被编译为 static 初始化块

最关键的部分来了 —— init 块对应的字节码:

static {};
    Code:
       0: new           #37     // class Static$Companion
       3: dup
       4: aconst_null
       5: invokespecial #40     // Method Static$Companion."<init>":(LDefaultConstructorMarker;)V
       8: putstatic     #42     // Field Companion:LStatic$Companion;
      11: ldc           #44     // String 42
      13: putstatic     #20     // Field answer:LString;
      16: ldc           #46     // String Initialized
      18: astore_0
      19:iconst_0
      20: istore_1
      21: getstatic     #52     // Field System.out:LPrintStream;
      24: aload_0
      25: invokevirtual #58     // Method PrintStream.println:(LObject;)V
      28: return

解读这段字节码:

  1. 创建伴生对象实例(index 0–5):

    • 使用 new 创建 Static$Companion 实例。
    • 调用其构造方法(带 DefaultConstructorMarker 参数,Kotlin 特有)。
  2. 保存为静态字段(index 8):

    • 将实例赋值给 Companion 静态字段,确保单例。
  3. 执行初始化逻辑(index 11 后):

    • 设置 answer = "42"
    • 执行 println("Initialized")

⚠️ 重点总结:

尽管 Kotlin 没有 static 块语法,但 companion object 中的 init 块最终会被编译为 JVM 的 static {} 初始化块。

也就是说,在字节码层面,Java 和 Kotlin 的静态初始化行为完全一致。

4. 结论

本文通过对比 Java 与 Kotlin 的静态初始化机制,展示了 Kotlin 如何通过 companion object + init 块实现等效功能。

并通过字节码分析证实:Kotlin 的伴生对象初始化最终被编译为标准的 JVM 静态初始化流程,包括:

  • 静态字段
  • 静态内部类
  • static {} 初始化块

这种设计既保持了 JVM 兼容性,又提供了更符合面向对象理念的语法封装。

所有示例代码均可在 GitHub 上获取:https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-2


原始标题:Static Initialization Block in Kotlin