Java编译期常量折叠是指javac将满足public static final、基本类型或String、编译期常量表达式三个条件的变量直接替换为字面值,嵌入字节码;修改常量后依赖类须重编译才生效。

Java 编译器(javac)对满足条件的 static final 常量表达式会直接计算并替换为字面值,这个过程不依赖 JVM,也不发生在运行时——它就发生在你敲下 javac 的那一刻。
哪些表达式会被编译期折叠?
必须同时满足三个硬性条件,缺一不可:
-
public static final(接口中字段默认满足,但类中若省略public,外部类就看不到该折叠效果) - 类型是基本类型(
int、boolean、char等)或String - 初始化表达式是编译期常量表达式:只含字面量、已折叠的
final变量、简单运算符(+、&&、?:等),不含方法调用、new、非final变量引用
例如:public static final int TIMEOUT = 5 * 60; 会被折叠为 300;public static final String PATH = "/api/" + VERSION;(若 VERSION 本身也是编译期常量)会被折叠为完整路径字符串。
反例:public static final long TS = System.currentTimeMillis(); 不会折叠——哪怕你加了 final,只要含方法调用,就出局。
立即学习“Java免费学习笔记(深入)”;
为什么改了常量值,其他类却没更新?
因为折叠后,调用方字节码里根本没引用那个字段,而是直接嵌入了字面值。比如 A 类定义了 public static final int CODE = 200;,B 类写了 if (A.CODE == 200),编译后 B 类的字节码实际是 if (200 == 200)。
这时如果你只修改 A 类的 CODE 为 201 并重编译 A.class,B.class 仍用旧值——除非你也重新编译 B 类。
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
常见误判场景:
- 用 IDE 的 “Find Usages” 查不到 B 类对
A.CODE的引用(它已不存在) - 用反射读取
A.class字段得到的是当前类加载时的值,但 B 类逻辑早已固化为旧字面值 - 打包工具(如 Maven Shade)若未触发全量重编译,也会遗留旧值
怎么验证某个常量是否真被折叠了?
最可靠方式是看字节码,而不是源码或反编译后的 Java 代码:
- 运行
javap -c YourClass,定位到使用该常量的方法 - 如果看到
iconst_5、bipush 300或ldc "hello world",说明已折叠 - 如果看到
getstatic YourClass/CODE,说明没折叠(比如少了public,或用了运行时方法)
注意:某些反编译器(如 JD-GUI)会把 ldc "xxx" “美化”成 Const.NAME,造成假象——务必以 javap 输出为准。
String 拼接的特殊性与陷阱
String 是唯一能参与折叠的引用类型,但前提是拼接操作数全是编译期常量:
-
final String a = "x"; final String b = "y"; String s = a + b;→ ✅ 折叠为"xy" -
final String a = "x"; String b = "y"; String s = a + b;→ ❌b非final,不折叠,运行时走StringBuilder -
public static final String S = someMethod();→ ❌ 即使someMethod()总返回固定字符串,方法调用本身禁止折叠
折叠后的字符串一定在字符串常量池中,且唯一;未折叠的则可能产生新对象——这点直接影响 == 判断和内存占用。
真正容易被忽略的点在于:折叠是编译器单次决策,它不感知后续变更。一旦发生,调用方就和定义方解耦了。这不是 bug,是设计使然——但要求你在模块化、多仓库协作或热更新场景下,必须把“重编译依赖方”当作发布流程的一部分,而不是可选项。

















