展开菜单
首页 精品内容 本月促销 装机必备 Windows macOS软件 IOS软件 Android AI PDF教程 专题
全部分类

当前位置:

首页 > 编程开发 > Java编译期常量与运行时常量的区别详解

Java编译期常量与运行时常量的区别详解

编译期常量由staticfinal修饰,值在编译阶段确定,仅限基本类型或String,访问不触发类初始化并支持常量折叠优化。运行时常量值依赖运行时计算,访问会触发类初始化,无编译优化。两者在底层存储、修改影响范围及使用场景上存在显著差异。

引言

聊到Ja va开发,“常量”这两个字几乎每天都离不开。从接口超时时间到业务枚举值,再到全局配置参数,把常量用好了,代码的可读性和可维护性都能上一个台阶,甚至还能顺带优化一下性能。但说实话,很多开发者只知道用final来修饰常量,却讲不清“编译期常量”和“运行时常量”到底差在哪。

Ja va编译期常量与运行时常量的区别详解

举个例子:同样是static final修饰的变量,为什么有的被引用时压根不触发类初始化?有的改个值就必须重新编译所有引用它的类?还有的加了transient却完全没效果?这些问题如果答不上来,说明对常量的理解还停留在表面。

一、什么是Ja va常量?

Ja va里的常量,说白了就是“初始化之后再也不许改的变量”,这个“不许改”靠的是final关键字——基础类型不能再赋值,引用类型不能再指向别的对象。就这么简单。

不过,按照值确定的时间点,常量其实可以分成两大类:编译期常量(Compile-time Constant)和运行时常量(Run-time Constant)。这两者在底层实现、使用规则、性能表现上差异巨大,也是面试官最爱深挖的考点。

Oracle官方文档写得非常清楚:判断常量的核心标准就是“值是否能在编译阶段确定”。后面所有的知识点都是围绕这个核心展开的,记住了这一条,后面的内容就好理解了。

二、编译期常量(Compile-time Constant)——编译时就把值定死

2.1 定义与核心特征

所谓编译期常量,就是“在Ja va代码编译阶段,编译器就能算出确切值”的常量。程序根本不用跑起来,编译器就已经知道它等于多少了,甚至还能顺手做一些优化(比如常量折叠)。

要成为编译期常量,必须同时满足以下条件,一个都不能少:

  • 修饰符:必须用 static final 一起修饰(接口里的字段默认就是 public static final,所以接口中的常量默认都是编译期常量);
  • 数据类型:只能是基本类型(byte、short、int、long、float、double、boolean、char)或 String,不能是引用类型(比如 IntegerObject、数组之类的);
  • 初始化值:必须是“编译期能算出来的常量表达式”,不能依赖运行时的计算结果(比如方法调用、new对象、随机数等);
  • 底层存储:编译后,这个值会直接嵌入到调用类的字节码里,同时也会进入方法区的运行时常量池,运行时根本不用去原类里读;
  • 类初始化:访问编译期常量时,不会触发它所在类的初始化——因为值在编译期就定死了,不需要加载类就能拿到。

2.2 合法与非法示例

合法示例(全满足,属于编译期常量):

// 1. 基本类型字面量(最常见)
public static final int MAX_AGE = 100;
public static final boolean FLAG = true;
public static final char CH = 'A';
public static final double PI = 3.1415926;
// 2. String字面量
public static final String NAME = "Ja va常量";
// 3. 编译期可计算的表达式(只包含编译期常量和合法运算符)
public static final int SUM = 10 + 20; // 编译期就算出30
public static final String COMBINE = "Hello" + "World"; // 编译期直接拼成"HelloWorld"
public static final int DIFF = MAX_AGE - 50; // 引用了别的编译期常量,照样能算
public static final boolean LOGIC = FLAG && true; // 逻辑运算也OK(但不包含instanceof、++/--)
// 4. 接口中的常量(默认public static final)
interface Constant {
    String URL = "https://xxx.com"; // 编译期常量
    int TIMEOUT = 3000;
}

非法示例(不满足条件,不是编译期常量):

// 1. 引用类型(包装类、枚举都不行)
public static final Integer NUM = 100; // Integer是引用类型,排除
public static final List LIST = new ArrayList<>(); // 引用类型,排除
public static final EnumType TYPE = EnumType.A; // 枚举也是引用类型,排除
// 2. 初始化值依赖运行时计算
public static final int RANDOM = new Random().nextInt(); // 运行时才随机,排除
public static final String UUID = UUID.randomUUID().toString(); // 方法调用,运行时才确定
public static final int CURRENT_TIME = (int) System.currentTimeMillis(); // 运行时获取时间
// 3. 缺少static修饰(只有final,没资格当编译期常量)
public final int AGE = 20; // 这是运行时常量
// 4. 使用了非法运算符(++/--)
public static final int COUNT = 10++; // ++是运行时自增,编译期算不了,直接报错

2.3 底层原理:编译期优化(常量折叠)

编译器对编译期常量有一个看家本领:常量折叠(Constant Folding)。在编译阶段,所有涉及编译期常量的表达式都会被直接算出结果,然后用这个结果替换掉原来的表达式。运行时代码根本不用再算一遍,性能自然就上去了。

来看个例子:

public class CompileConstant {
    public static final int A = 5;
    public static final int B = 10;
    public static final int C = A * B + 1; // 表达式:5*10+1
}

编译之后去反编译字节码就会发现,C 的值已经被直接替换成 51 了,原来的 A * B + 1 这个表达式直接被编译器干掉了。运行时拿到的是现成的 51,不再需要任何计算。这就是常量折叠的威力。

同理,字符串拼接也一样:"Hello" + "World" 在编译期就会直接变成 "HelloWorld",运行时根本不需要拼接操作。

2.4 关键特性:访问不触发类初始化

这是编译期常量最核心的特性,也是面试高频考点。因为值已经嵌入到调用类的字节码里了,访问时压根不需要加载常量所在的类,所以类的初始化(包括静态代码块、静态变量赋值这些)统统都不会执行。

看代码:

// 常量类
public class ConstantClass {
    // 编译期常量
    public static final String COMPILE_CONST = "编译期常量";
    // 静态代码块(类初始化时执行)
    static {
        System.out.println("ConstantClass 被初始化了");
    }
}
// 测试类
public class Test {
    public static void main(String[] args) {
        // 访问编译期常量
        System.out.println(ConstantClass.COMPILE_CONST);
    }
}

运行结果:只输出 编译期常量,完全看不到 ConstantClass 被初始化了 那句话。

原因很简单:JVM 发现这是个编译期常量,直接从当前类的字节码里就拿出了值,根本不需要去加载 ConstantClass,自然也就不会触发初始化。

三、运行时常量(Run-time Constant)——运行时才见分晓

3.1 定义与核心特征

运行时常量,顾名思义,“在程序运行阶段(类加载或者对象实例化时)才能确定最终值”。编译阶段压根不知道它等于多少,所以编译器也没法做常量折叠之类的优化。

运行时常量的特征就宽松多了,满足任意一条即可:

  • 修饰符:可以只用 final(实例常量),也可以用 static final(但初始化值依赖运行时计算);
  • 数据类型:基本类型、String、引用类型(Integer、Object、数组、枚举等)都可以;
  • 初始化值:依赖运行时的计算结果,比如方法调用、new对象、读配置文件、随机数等;
  • 底层存储:实例常量(仅 final)跟着对象走,存在堆里;静态运行时常量(static final)存在方法区的运行时常量池,但值是运行时才确定的;
  • 类初始化:访问静态运行时常量时,会触发其所在类的初始化(因为必须加载类才能确定值);实例运行时常量则需要先 new 对象(触发对象初始化)才能访问。

3.2 常见示例

// 1. 仅final修饰的实例常量(运行时常量)
public class RuntimeConstant {
    // 实例常量,每次new对象时初始化,每个对象可以不同
    public final int INSTANCE_CONST;
    // 在构造方法里初始化(运行时才确定)
    public RuntimeConstant(int value) {
        this.INSTANCE_CONST = value;
    }
}
// 2. static final修饰,但初始化值依赖运行时计算
public class RuntimeConstant2 {
    // 运行时常量:值由方法调用确定(运行时算)
    public static final int RANDOM_NUM = new Random().nextInt(100);
    // 运行时常量:值从配置文件读(运行时加载)
    public static final String CONFIG_VALUE = readConfig("config.key");
    // 运行时常量:引用类型(枚举)
    public static final EnumType TYPE = EnumType.B;
    // 运行时常量:包装类(引用类型)
    public static final Integer WRAP_NUM = 100;
    // 静态代码块(访问时会触发)
    static {
        System.out.println("RuntimeConstant2 被初始化了");
    }
    // 模拟读配置文件的方法(运行时执行)
    private static String readConfig(String key) {
        return "config_value";
    }
}
// 3. 局部final变量(也是运行时常量)
public class RuntimeConstant3 {
    public void test() {
        // 局部final变量,方法执行时初始化,属于运行时常量
        final int LOCAL_CONST = 100;
        // 局部final变量,值由参数决定(运行时传入)
        final String LOCAL_STR = new String("局部常量");
    }
}

3.3 关键特性:访问触发类/对象初始化

与编译期常量正好相反,运行时常量的值需要在运行时才能拿到,所以访问它时一定会触发对应的初始化:

  • 静态运行时常量(static final):访问时触发所在类的初始化(静态代码块、静态变量赋值都会执行);
  • 实例运行时常量(仅 final):必须先 new 对象(触发对象初始化),才能访问这个常量。

验证一下:

public class Test {
    public static void main(String[] args) {
        // 访问静态运行时常量,触发类初始化
        System.out.println(RuntimeConstant2.RANDOM_NUM);
    }
}

运行结果:

RuntimeConstant2 被初始化了
45(随机值,每次可能不同)

原因很清楚:RANDOM_NUM 的值是通过 new Random().nextInt(100) 在运行时算出来的,所以访问它时必须先加载 RuntimeConstant2 类,自然就触发了类初始化,静态代码块也就跟着执行了。

四、编译期常量与运行时常量 核心区别

为了方便对比,整理了一张表格,把两者在各维度上的差异讲得明明白白。

对比维度

编译期常量

运行时常量

核心定义

编译阶段确定值,编译器可优化

运行阶段确定值,编译器无法优化

修饰符要求

必须是 static final 共同修饰

可仅 final,也可 static final(值依赖运行时)

数据类型

仅基本类型 + String类型

基本类型、String、引用类型(枚举、包装类等)均可

初始化值要求

编译期可计算的常量表达式(字面量、合法运算)

可依赖运行时计算(方法调用、new对象、配置读取等)

底层存储

值嵌入调用类字节码,同时存入运行时常量池

静态:运行时常量池;实例:堆内存

类初始化触发

访问时不触发所在类初始化

静态:访问时触发类初始化;实例:new对象时触发

编译器优化

支持常量折叠,减少运行时开销

无优化,运行时计算值

transient修饰效果

无效(编译期常量会被直接嵌入字节码,不受transient影响)

有效(引用类型的运行时常量,加transient可排除序列化)

修改后影响范围

修改后需重新编译所有引用类(否则引用旧值)

修改后仅需重新编译自身类,引用类无需重新编译

典型使用场景

全局固定值(如PI、接口地址、枚举字面量)

动态配置(如配置文件读取、随机值、对象唯一标识)

编译期常量:编译时确定值,不触发类初始化,可优化,修改需全量编译;
运行时常量:运行时确定值,触发初始化,无优化,修改仅需编译自身。

五、如何选择两种常量?

很多开发者习惯性地到处用 static final,结果踩了不少坑——比如“常量修改了却不生效”、“莫名其妙触发类初始化”等等。归根结底,就是没选对常量类型。下面结合实际开发,给出明确的选型建议。

5.1 优先使用编译期常量的场景

  • 值固定不变,而且在编译期就能确定(比如数学常量、固定的业务基准值、接口地址);
  • 需要被多个类引用,且希望减少运行时开销(常量折叠能提升性能);
  • 不希望触发类初始化(比如工具类里的常量,避免无谓的类加载)。

示例:工具类中的常量定义

public class MathUtil {
    // 编译期常量:固定不变,可优化
    public static final double PI = 3.1415926;
    public static final int DEFAULT_SCALE = 2;
    public static final String EMPTY_STR = "";
}

5.2 优先使用运行时常量的场景

  • 值需要动态确定(比如从配置文件读、数据库查、随机生成、方法返回值);
  • 常量是引用类型(枚举、包装类、数组、对象);
  • 每个对象需要独立的常量值(比如对象唯一标识、实例级别的固定配置);
  • 常量值可能会修改,又不想每次改完都重新编译所有引用类(降低维护成本)。

示例:配置类中的运行时常量

public class ConfigConstant {
    // 运行时常量:从配置文件读取(动态确定值)
    public static final String DB_URL = ConfigLoader.load("db.url");
    public static final int DB_PORT = Integer.parseInt(ConfigLoader.load("db.port"));
    // 运行时常量:引用类型(枚举)
    public static final DataSourceType DATA_SOURCE_TYPE = DataSourceType.MYSQL;
    // 实例运行时常量:每个对象独立值
    public final String INSTANCE_ID;
    public ConfigConstant(String instanceId) {
        this.INSTANCE_ID = instanceId;
    }
}

六、注意事项

1:误以为“static final修饰的都是编译期常量”

这个误区非常普遍。有人觉得只要加上了 static final,就一定是编译期常量,其实不然。

// 错误:以为这是编译期常量,其实是运行时常量
public static final Integer NUM = 100;
public static final String UUID = UUID.randomUUID().toString();

原因在于:Integer 是引用类型,UUID 的值由方法调用确定(运行时),所以两个都是运行时常量。访问它们会触发类初始化,也不会被常量折叠。

判断是否为编译期常量,不能只看修饰符,还得看“数据类型”和“初始化值是否能在编译期确定”。

2:编译期常量修改后,引用类未重新编译,导致旧值残留

场景:类A定义了编译期常量 MAX_NUM = 100,类B引用了 A.MAX_NUM。后来把A类的常量改成 200,但只重新编译了A类,没编译B类。结果运行时B类拿到的还是旧值 100。

原因:编译期常量的值会嵌入到引用类的字节码里。B类编译时,字节码里已经写死了 100。A类改了之后,如果不重新编译B类,B类永远用的是嵌入的那个旧值。

避坑方案:修改编译期常量后,必须重新编译所有引用它的类。如果常量值可能频繁修改,最好直接改成运行时常量(比如从配置文件读取)。

3:用transient修饰编译期常量,误以为能排除序列化

public class SerializeTest implements Serializable {
    // 错误:transient对编译期常量无效
    private transient static final String SECRET = "123456";
}

原因:编译期常量的值是直接嵌入字节码的,transient 根本拦不住它。序列化后依然能获取到这个值。

正确做法:如果真想排除某个常量的序列化,不要指望 transient(它对编译期常量无效)。可以实现 Externalizable 接口,手动控制序列化逻辑,不写这个字段。

4:局部final变量误认为是编译期常量

public void test() {
    final int a = new Random().nextInt();
    // 错误:以为a是编译期常量,其实是运行时常量
    System.out.println(a + 10);
}

原因:局部 final 变量的初始化值如果依赖运行时计算,那它就是运行时常量,编译器不会做常量折叠。只有初始化值是字面量或编译期可计算表达式时,才会被优化。

5:接口中的常量不是编译期常量

interface MyConstant {
    // 错误:以为这是编译期常量,其实是运行时常量
    String CONFIG = readConfig();
    static String readConfig() {
        return "config";
    }
}

原因:接口中的常量默认是 public static final,但初始化值 readConfig() 是方法调用(运行时确定),所以是运行时常量。访问时会触发接口的初始化。

七、全文总结

1. 两种常量的核心区别:值确定的时机(编译期 vs 运行期);

2. 编译期常量:static final + 基本类型/String + 编译期表达式,不触发类初始化,支持常量折叠;

3. 运行时常量:可以仅 final,支持引用类型,值依赖运行时计算,触发初始化;

4. 坑点核心:不要混淆 static final 和编译期常量,修改编译期常量需要全量编译;

5. 选型原则:固定值用编译期,动态值用运行期;

6. 面试关键:类初始化触发、常量折叠、transient 效果、修改后影响范围。

编译期常量与运行时常量,乍看是个很基础的概念,但背后藏着JVM底层的优化逻辑和不少开发细节。大厂面试中,这道题往往是区分“初级开发者”和“中级开发者”的分水岭。

很多开发者因为分不清这两者,吃过“常量修改不生效”“类初始化异常”“序列化漏洞”之类的亏。把这篇文章吃透,基本就能避开所有高频坑了。

本文内容来源于互联网,如有侵权请联系删除。
作者最新文章
编程开发
相关文章 更多
精品专题 更多
本月促销

正软商城本月促销专区,汇集办公、设计、安全、影音、系统工具及AI软件等正版软件优惠活动,提供限时折扣、特价授权和优惠购买信息,活动库存及价格以页面实时展示为准。

装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

macOS软件

正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

IOS软件

正软商城iOS软件专区,精选适用于iPhone和iPad的办公、学习、影音、设计、效率及AI应用,提供功能介绍、适用设备、系统要求和正版获取方式等信息。

AI

正软商城AI软件专区,汇集AI写作、AI绘画、AI视频、AI办公、AI编程、AI翻译、智能客服和数据分析等人工智能工具,提供功能介绍、适用平台、收费方式及正版购买信息。

PDF教程

正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

Mac软件 更多
灵活计算器
灵活计算器

灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

赤友清理大师
赤友清理大师

赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

极度公式
极度公式

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

图几
图几

图几是一款适用于 macOS 的截图、标注与美化工具,支持离线操作保障隐私。界面整理和高频系统操作被放到一起考虑,桌面或窗口内容一多时,管理起来会更省心。

密码键盘
密码键盘

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。

思源笔记
思源笔记

思源笔记是一款本地笔记软件,提供所见即所得的编辑方式,为长文写作带来顺滑的体验。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

Office 365 简体中文
Office 365 简体中文

一款文字处理软件,一种订阅式的跨平台办公软件,基于云平台提供多种服务,通过将 Excel 和 Outlook 等应用与 OneDrive 和 Microsoft Teams 等强大的云服务相结合,Office 365 可让任何人使用任何设备随时随地创建和共享内容。

WALTR PRO
WALTR PRO

WALTR是一款电脑至iOS文件传输转换工具,操作简单,快速实现文件识别与传送。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

CodeExpander
CodeExpander

CodeExpander 是一款快捷短语输入增强工具,通过键入缩写自动展开为自定义文段,提升工作效率。任务管理和过程控制会更完整,持续下载、批量同步或需要稳定传输流程的场景会更适合它。

Mountain Duck
Mountain Duck

Mountain Duck 是一款能将多个网盘挂载到本地的工具,像本地磁盘一样使用网盘。清理链路的完整性会更好一些,做应用卸载、残留处理和空间整理时,通常能少走很多手动排查步骤。

Menuist
Menuist

Menuist 是一款面向 macOS 的 Finder 右键菜单增强工具,主要用来补充新建文件、快捷导航等常用操作,让日常文件管理和访问路径时更高效、更顺手。

Mole
Mole

Mole 是一款专为 Mac 设计的深度清理优化工具,涵盖缓存清理、应用管理及实时状态监控等功能。清理链路的完整性会更好一些,做应用卸载、残留处理和空间整理时,通常能少走很多手动排查步骤。

WINDOWS 更多
Windows 10
Windows 10

Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

极度公式
极度公式

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

密码键盘
密码键盘

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。

思源笔记
思源笔记

思源笔记是一款本地笔记软件,提供所见即所得的编辑方式,为长文写作带来顺滑的体验。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

傲梅轻松备份
傲梅轻松备份

傲梅轻松备份是一款专业易用的数据备份软件,为重要数据提供安全保障。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。

Office 365 简体中文
Office 365 简体中文

一款文字处理软件,一种订阅式的跨平台办公软件,基于云平台提供多种服务,通过将 Excel 和 Outlook 等应用与 OneDrive 和 Microsoft Teams 等强大的云服务相结合,Office 365 可让任何人使用任何设备随时随地创建和共享内容。

Wise Folder Hider Pro
Wise Folder Hider Pro

Wise Folder Hider Pro 是一款专业级文件和文件夹隐藏加密软件,为私密数据添加多重保护。高频操作更强调就近处理,浏览、整理和跨目录移动文件时,来回切换和重复点击都会少很多。

WALTR PRO
WALTR PRO

WALTR是一款电脑至iOS文件传输转换工具,操作简单,快速实现文件识别与传送。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

CodeExpander
CodeExpander

CodeExpander 是一款快捷短语输入增强工具,通过键入缩写自动展开为自定义文段,提升工作效率。任务管理和过程控制会更完整,持续下载、批量同步或需要稳定传输流程的场景会更适合它。

PinStack
PinStack

PinStack是一款轻量级的Windows平台剪贴板管理工具,优化您的剪贴板使用体验。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

Mountain Duck
Mountain Duck

Mountain Duck 是一款能将多个网盘挂载到本地的工具,像本地磁盘一样使用网盘。清理链路的完整性会更好一些,做应用卸载、残留处理和空间整理时,通常能少走很多手动排查步骤。

Seer
Seer

Seer是一款在Win平台下的空格键功能增强效率工具,只需轻敲空格键,就能预览几乎任何格式的文件。它更适合把零散的小功能集中起来使用,处理高频琐碎任务时会更省事。