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

 更新时间:2026年05月20日 08:58:06   作者:希望永不加班  
在Java开发中,常量是我们每天都会接触的概念,从接口超时时间、业务枚举值,到全局配置参数,常量的合理使用能提升代码可读性、可维护性,甚至优化程序性能,但很多同学只知道用final修饰常量,却分不清「编译期常量」和「运行时常量」的本质区别,本文给大家详细说说

引言

在Java开发中,“常量”是我们每天都会接触的概念——从接口超时时间、业务枚举值,到全局配置参数,常量的合理使用能提升代码可读性、可维护性,甚至优化程序性能。但很多同学只知道用final修饰常量,却分不清「编译期常量」和「运行时常量」的本质区别。

比如:同样是static final修饰的变量,为什么有的能直接被引用而不触发类初始化?有的修改后必须重新编译所有引用类?有的加了transient却无效?

一、什么是Java常量?

Java中的常量,本质是「初始化后不可修改的变量」,核心约束由final关键字实现——final修饰的变量,一旦完成初始化,就无法重新赋值(基础类型不可改值,引用类型不可改引用地址)。

根据「值确定的时机」,常量被分为两大类型:编译期常量(Compile-time Constant)和运行时常量(Run-time Constant),二者的底层实现、使用规则、性能表现差异极大,也是面试中常被深挖的考点。

补充:Oracle官方文档明确规定,常量的核心判定标准是「值是否能在编译阶段确定」,这也是区分两种常量的核心依据,后续所有知识点都围绕这一核心展开。

二、编译期常量(Compile-time Constant)—— 编译期确定值

2.1 定义与核心特征

编译期常量,指的是「在Java代码编译阶段就能确定其最终值」的常量,无需等到程序运行,编译器就能明确其具体值,并对其进行优化(如常量折叠)。

核心特征(必须同时满足,缺一不可):

  • 修饰符:必须用 static final 共同修饰(接口中的字段默认被public static final修饰,因此接口中的常量默认都是编译期常量);
  • 数据类型:只能是「基本数据类型」(byte、short、int、long、float、double、boolean、char)或「String类型」,不能是引用类型(如Integer、Object、数组等);
  • 初始化值:必须是「编译期可计算的常量表达式」,不能依赖运行时的计算结果(如方法调用、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 = "Java常量";
// 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; // 逻辑运算(不包含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<String> 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; // 仅final,无static,属于运行时常量
// 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修饰)存储在方法区的运行时常量池,但值是运行时确定的;
  • 类初始化:访问静态运行时常量时,会触发其所在类的初始化(因为需要运行时确定值,必须加载类);实例运行时常量需实例化对象后才能访问,会触发对象初始化。

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类的MAX_NUM = 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,序列化后仍能获取到常量值。

正确做法:若想排除常量的序列化,不要用transient(对编译期常量无效),可实现Externalizable接口,手动控制不写入该字段。

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

错误示例:

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

原因:局部final变量的初始化值若依赖运行时计算,就是运行时常量,编译器无法对其进行常量折叠优化;只有局部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底层优化和开发细节,也是大厂面试中区分“初级开发者”和“中级开发者”的关键考点。

很多开发者因为分不清二者,导致出现“常量修改不生效”“类初始化异常”“序列化漏洞”等问题,看完这篇,基本能避开所有高频坑,同时应对所有相关面试题。

以上就是Java编译期常量与运行时常量的区别详解的详细内容,更多关于Java编译期常量与运行时常量的资料请关注脚本之家其它相关文章!

相关文章

  • Android Studio中创建java工程的完整步骤

    Android Studio中创建java工程的完整步骤

    Android Studio创建java工程是非常麻烦的,因为Android Studio没有提供直接创建java工程的方法,下面这篇文章主要给大家介绍了关于Android Studio中创建java工程的完整步骤,需要的朋友可以参考下
    2024-01-01
  • SpringBoot整体读取多个配置属性及其相关操作方法

    SpringBoot整体读取多个配置属性及其相关操作方法

    本文介绍SpringBoot中@ConfigurationProperties用于批量读取配置属性,对比@Value,涵盖其用法、配置选项、List处理及校验功能,支持宽松绑定与属性转换,需相关依赖启用,感兴趣的朋友一起看看吧
    2025-07-07
  • Java压缩文件夹最实用简单的方法

    Java压缩文件夹最实用简单的方法

    在本篇内容里小编给大家整理的是一篇关于Java压缩文件夹最实用简单的方法以及相关实例,有需要的朋友们可以跟着学习下。
    2022-11-11
  • Java字节流和字符流总结IO流!

    Java字节流和字符流总结IO流!

    下面小编就为大家带来一篇Java IO流字节流和字符流的实例讲解。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2021-07-07
  • Spring Boot中单例类实现对象的注入方式

    Spring Boot中单例类实现对象的注入方式

    这篇文章主要介绍了Spring Boot中单例类实现对象的注入方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08
  • java的时间类汇总(齐全)

    java的时间类汇总(齐全)

    这篇文章主要介绍了java的时间类汇总(齐全),文章围绕主题展开详细的内容介绍,具有一定的参考价值,感兴趣的小伙伴可以参考一下
    2022-09-09
  • 浅谈Java三目运算

    浅谈Java三目运算

    本文给大家主要介绍的是java中三目运算的详细介绍,并附上2个示例,希望对大家理解三目运算能够有所帮助。
    2015-03-03
  • 浅谈使用java解析和生成JSON

    浅谈使用java解析和生成JSON

    在www.json.org上公布了很多JAVA下的json构造和解析工具,其中google-gson和org.json比较简单,两者使用上差不多但还是有些区别。下面我们就来分别介绍下用他们构造和解析Json数据的方法示例。
    2015-08-08
  • Mybatis SQL数量限制插件的实现

    Mybatis SQL数量限制插件的实现

    本文介绍了一种应对大数据查询风险的Mybatis插件,通过配置限制查询结果数量,并支持动态调整和特殊接口绕过,具有一定的参考价值,感兴趣的可以了解一下
    2025-07-07
  • idea中一键执行maven和应用重启实现过程

    idea中一键执行maven和应用重启实现过程

    文章介绍了如何在IntelliJ IDEA中配置组合命令,实现一键执行Maven的`clean install`和应用重启,从而简化多模块项目的开发流程,通过在Run/DebugConfigurations中添加启动前任务并配置相应的Maven命令,可以自动完成清理、构建和启动应用,提高开发效率
    2025-12-12

最新评论