bigywater.tech
Java 内部原理 · 35

类到底什么时候被初始化:真实验证编译期常量根本不会触发它

"用到一个类才会触发它的初始化"是常见说法,但"用到"到底指什么、有没有例外,真实跑一遍最清楚——顺带用 javap 看穿一个编译器在背后悄悄做的事。

CONST → 静态块没执行
真实验证:读取一个 static final 编译期常量字段,所在类的静态初始化块真实完全没有被执行
NON_CONST → 静态块真实执行了
对照组:同样返回值 42,但要真实调用一个方法才能算出来,读取时所在类真实被初始化了

上一篇:Thread.interrupt() 真实语义:忙等循环真的不会被打断

思路:两个字段,返回值一样,来源不一样

一个字段是编译期就能确定值的常量,另一个字段的值要真实调用方法才能算出来——虽然运行时结果都是 42。

ClassInitDemo.java static class HasConstant { static final int CONST = 42; // 编译期常量 static { System.out.println("HasConstant 的静态初始化块真实执行了"); } } static class HasNonConstant { static final int NON_CONST = computeAtRuntime(); // 得真实调用方法 static { System.out.println("HasNonConstant 的静态初始化块真实执行了"); } static int computeAtRuntime() { return 40 + 2; } }

真实验证:静态初始化块,一个执行了一个没执行

分别读取两个字段,观察各自的静态初始化块有没有真实打印。

真实实测
=== 访问 HasConstant.CONST 之前,什么都没打印 ===
读取 HasConstant.CONST = 42
(上面这行读取完之后,再检查有没有触发初始化)

=== 访问 HasNonConstant.NON_CONST ===
HasNonConstant 的静态初始化块真实执行了
读取 HasNonConstant.NON_CONST = 42

真实结果一目了然:HasConstant 的静态初始化块自始至终都没有真实执行过,哪怕代码里已经读取了 HasConstant.CONST;HasNonConstant 的静态初始化块则在读取 NON_CONST 之前真实执行了。两个字段最终值都是 42,唯一的区别是:CONST 是 Java 语言规范定义的"编译期常量"(final + 基本类型/String + 初始化表达式在编译期就能确定),NON_CONST 不是。

真实验证:javap 揭示更彻底的真相——常量被直接内联了

不只是"不触发初始化",编译期常量在字节码层面真实做得更彻底。

真实实测 · javap -c 反编译主方法片段
11: ldc           #23   // String 读取 HasConstant.CONST = 42
13: invokevirtual #15   // Method java/io/PrintStream.println:(Ljava/lang/String;)V
整行 "读取 HasConstant.CONST = " + HasConstant.CONST 在编译期就被真实折叠成了一个完整的字符串常量,字节码里根本没有出现任何指向 HasConstant 类的引用——编译器直接把值抄了过来,连"读取"这个动作本身都不存在了

这解释了为什么静态初始化块不会执行:JVM 触发类初始化的前提是代码里真实出现了对这个类的"主动使用"(比如 getstatic 读取它的非常量字段、调用它的静态方法、new 它的实例等);而编译期常量在编译阶段就已经被真实替换成字面量,运行时的字节码里根本不存在"访问 HasConstant 类"这个动作,自然也就没有触发初始化的理由。

这个细节在真实项目里意味着什么?最直接的坑是升级依赖库时:如果一个库里定义了 public static final int VERSION = 1 这样的常量,你的代码编译时把这个值真实内联进了自己的字节码——之后哪怕库升级到 VERSION = 2,只替换库的 jar 包、不重新编译自己的代码,读到的依然是内联进去的旧值 1,因为运行时压根不会再真实去读库里的那个字段。这也是很多"看起来该更新的值,实际没更新"的诡异 bug 的真实根源之一。

分步演示:什么样的访问真实会触发,什么不会

每一行都是真实运行这段代码,通过静态初始化块是否打印来判断的结果。

静态初始化真实是惰性的;访问编译期常量表达式真实完全不会触发类初始化,因为值已经被编译器直接内联了
这一步新验证的场景

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,反编译用的是 javap -c。
    • 和系列 字符串拼接编译产物 那篇一样,都是"真实反编译揭示编译器背后做了什么"这个主题,建议对照读。
    • 没有涉及:类初始化的完整触发条件清单(JLS 12.4.1 里列出的 6 种主动使用场景)、类加载的完整生命周期(加载、验证、准备、解析、初始化)——这些留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号