static final变量若是编译期常量(基本类型/String+字面量或常量表达式),则值被内联,不触发类初始化;否则仍执行静态块。

静态 final 变量的本质是编译期常量
当一个变量同时被 static 和 final 修饰,且其值在编译时就能完全确定(如字面量字符串、数字、常量表达式),JVM 编译器会将其识别为“编译期常量”。这类变量的值会被直接内联到所有引用它的调用处,存入调用类自身的常量池中。这意味着:调用方代码根本不会触发定义该变量的类加载和初始化。
例如:
-
public static final String MSG = "OK";→ 是编译期常量,System.out.println(OtherClass.MSG);不会执行OtherClass的静态块 -
public static final String TIME = System.currentTimeMillis() + "";→ 不是编译期常量(依赖运行时方法),仍会触发类初始化 -
public static final int MAX = 100 * 2;→ 是编译期常量(常量折叠),等价于200
代码块初始化逻辑只在类真正加载时执行
静态代码块(static {})属于类初始化阶段的一部分,仅在 JVM 第一次主动使用该类(如首次访问非编译期常量的静态成员、创建实例、反射等)时触发。而 static final 编译期常量绕过了这个过程。
常见误判场景:
- 以为只要写了
static { System.out.println("init"); },每次读取static final字段就打印——实际只在类被迫初始化时才执行 - 把配置读取、资源加载等重逻辑放在静态块里,却用
static final String CONFIG = loadFromProps();(但loadFromProps()非编译期可求值),结果每次访问都触发初始化,影响性能
不同编译环境的差异主要体现在常量判定上
是否被当作编译期常量,取决于编译器能否在 .java → .class 转换阶段确认其值。这与 JDK 版本、编译器实现(javac / ECJ)、以及是否启用优化有关,但主流 JDK(8~21)行为基本一致。
关键判断依据:
- 类型必须是基本类型或
String - 声明时必须有字面量或编译期可计算的常量表达式(如
1+2、"a"+"b") - 不能调用任何方法(包括
String.valueOf()、Integer.toString()等) - 不能引用未被
final修饰的变量(哪怕那个变量本身值没变)
实战建议:按意图选择初始化方式
不要为了“看起来像常量”而滥用 static final。根据实际需求选型:
- 需要真·不可变、无副作用、可跨类内联的值 → 用
public static final 基本类型/String+ 字面量 - 需要延迟初始化或含副作用(如日志、IO、单例构建)→ 改用
private static volatile Holder或static {}+ 普通static变量 - 需要线程安全的懒汉式初始化 → 优先考虑静态内部类或
java.util.concurrent.ConcurrentHashMap缓存,而非靠final伪装










