"用到一个类才会触发它的初始化"是常见说法,但"用到"到底指什么、有没有例外,真实跑一遍最清楚——顺带用 javap 看穿一个编译器在背后悄悄做的事。
上一篇: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 不是。
不只是"不触发初始化",编译期常量在字节码层面真实做得更彻底。
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 类"这个动作,自然也就没有触发初始化的理由。
每一行都是真实运行这段代码,通过静态初始化块是否打印来判断的结果。