Java編譯期常量與運行時常量的區(qū)別詳解
引言
在Java開發(fā)中,“常量”是我們每天都會接觸的概念——從接口超時時間、業(yè)務枚舉值,到全局配置參數(shù),常量的合理使用能提升代碼可讀性、可維護性,甚至優(yōu)化程序性能。但很多同學只知道用final修飾常量,卻分不清「編譯期常量」和「運行時常量」的本質區(qū)別。
比如:同樣是static final修飾的變量,為什么有的能直接被引用而不觸發(fā)類初始化?有的修改后必須重新編譯所有引用類?有的加了transient卻無效?
一、什么是Java常量?
Java中的常量,本質是「初始化后不可修改的變量」,核心約束由final關鍵字實現(xiàn)——final修飾的變量,一旦完成初始化,就無法重新賦值(基礎類型不可改值,引用類型不可改引用地址)。
根據(jù)「值確定的時機」,常量被分為兩大類型:編譯期常量(Compile-time Constant)和運行時常量(Run-time Constant),二者的底層實現(xiàn)、使用規(guī)則、性能表現(xiàn)差異極大,也是面試中常被深挖的考點。
補充:Oracle官方文檔明確規(guī)定,常量的核心判定標準是「值是否能在編譯階段確定」,這也是區(qū)分兩種常量的核心依據(jù),后續(xù)所有知識點都圍繞這一核心展開。
二、編譯期常量(Compile-time Constant)—— 編譯期確定值
2.1 定義與核心特征
編譯期常量,指的是「在Java代碼編譯階段就能確定其最終值」的常量,無需等到程序運行,編譯器就能明確其具體值,并對其進行優(yōu)化(如常量折疊)。
核心特征(必須同時滿足,缺一不可):
- 修飾符:必須用
static final共同修飾(接口中的字段默認被public static final修飾,因此接口中的常量默認都是編譯期常量); - 數(shù)據(jù)類型:只能是「基本數(shù)據(jù)類型」(byte、short、int、long、float、double、boolean、char)或「String類型」,不能是引用類型(如Integer、Object、數(shù)組等);
- 初始化值:必須是「編譯期可計算的常量表達式」,不能依賴運行時的計算結果(如方法調(diào)用、new對象、隨機值等);
- 底層存儲:編譯后,常量值會直接嵌入到調(diào)用類的字節(jié)碼中,同時存入方法區(qū)的運行時常量池,無需在運行時從原類中讀??;
- 類初始化:訪問編譯期常量時,不會觸發(fā)其所在類的初始化(因為值已在編譯期確定,無需加載類即可獲取)。
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(); // 方法調(diào)用,運行時確定 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 底層原理:編譯期優(yōu)化(常量折疊)
編譯器對編譯期常量有一個核心優(yōu)化:常量折疊(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
}編譯后,反編譯字節(jié)碼會發(fā)現(xiàn),C 的值已經(jīng)被直接替換為 51,原表達式 A * B + 1 會被編譯器刪除。也就是說,運行時程序直接使用 51,無需再計算表達式,這就是常量折疊的優(yōu)化效果。
補充:字符串拼接的優(yōu)化的也是同理,"Hello" + "World" 會在編譯期直接拼接為"HelloWorld",運行時無需執(zhí)行字符串拼接操作。
2.4 關鍵特性:訪問不觸發(fā)類初始化
這是編譯期常量最核心的特性,也是面試高頻考點—— 因為編譯期常量的值已經(jīng)嵌入到調(diào)用類的字節(jié)碼中,訪問時無需加載其所在的類,因此不會觸發(fā)類的初始化(不會執(zhí)行靜態(tài)代碼塊、靜態(tài)變量初始化等操作)。
示例:
// 常量類
public class ConstantClass {
// 編譯期常量
public static final String COMPILE_CONST = "編譯期常量";
// 靜態(tài)代碼塊(類初始化時執(zhí)行)
static {
System.out.println("ConstantClass 被初始化了");
}
}
// 測試類
public class Test {
public static void main(String[] args) {
// 訪問編譯期常量
System.out.println(ConstantClass.COMPILE_CONST);
}
}運行結果:僅輸出 編譯期常量,不會輸出 ConstantClass 被初始化了。
原因:訪問編譯期常量時,JVM無需加載 ConstantClass,直接從當前類的字節(jié)碼中獲取常量值,因此不會觸發(fā)類的初始化。
三、運行時常量(Run-time Constant)—— 運行期確定值
3.1 定義與核心特征
運行時常量,指的是「在程序運行階段(類加載或對象實例化時)才能確定其最終值」的常量,編譯階段無法確定具體值,編譯器無法對其進行常量折疊等優(yōu)化。
核心特征(滿足任意一條即可,無需同時滿足):
- 修飾符:可以僅用
final修飾(實例常量),也可以用static final修飾(但初始化值依賴運行時計算); - 數(shù)據(jù)類型:可以是基本類型、String類型,也可以是引用類型(如Integer、Object、數(shù)組、枚舉等);
- 初始化值:依賴運行時的計算結果,如方法調(diào)用、new對象、讀取配置文件、隨機值等;
- 底層存儲:實例常量(僅final修飾)跟隨對象存儲在堆內(nèi)存中;靜態(tài)運行時常量(static final修飾)存儲在方法區(qū)的運行時常量池,但值是運行時確定的;
- 類初始化:訪問靜態(tài)運行時常量時,會觸發(fā)其所在類的初始化(因為需要運行時確定值,必須加載類);實例運行時常量需實例化對象后才能訪問,會觸發(fā)對象初始化。
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 {
// 運行時常量:值由方法調(diào)用確定(運行時計算)
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;
// 靜態(tài)代碼塊(訪問時會觸發(fā)執(zhí)行)
static {
System.out.println("RuntimeConstant2 被初始化了");
}
// 讀取配置文件的方法(運行時執(zhí)行)
private static String readConfig(String key) {
// 模擬讀取配置文件
return "config_value";
}
}
// 3. 局部final變量(運行時常量)
public class RuntimeConstant3 {
public void test() {
// 局部final變量,方法執(zhí)行時初始化,屬于運行時常量
final int LOCAL_CONST = 100;
// 局部final變量,值由參數(shù)確定(運行時傳入)
final String LOCAL_STR = new String("局部常量");
}
}3.3 關鍵特性:訪問觸發(fā)類/對象初始化
與編譯期常量相反,運行時常量的值需要在運行時確定,因此訪問時會觸發(fā)對應的初始化操作:
- 靜態(tài)運行時常量(static final修飾):訪問時會觸發(fā)其所在類的初始化(執(zhí)行靜態(tài)代碼塊、靜態(tài)變量初始化);
- 實例運行時常量(僅final修飾):需要先new對象(觸發(fā)對象初始化),才能訪問該常量。
實戰(zhàn)驗證(延續(xù)上面的示例):
public class Test {
public static void main(String[] args) {
// 訪問靜態(tài)運行時常量,觸發(fā)類初始化
System.out.println(RuntimeConstant2.RANDOM_NUM);
}
}運行結果:
RuntimeConstant2 被初始化了 45(隨機值,每次運行可能不同)
原因:RANDOM_NUM 是靜態(tài)運行時常量,值由 new Random().nextInt(100) 確定(運行時計算),因此訪問時必須加載 RuntimeConstant2 類,觸發(fā)類初始化,執(zhí)行靜態(tài)代碼塊。
四、編譯期常量與運行時常量 核心區(qū)別
為了方便大家記憶和對比,整理了一張詳細的對比表,覆蓋定義、修飾符、底層、性能、初始化等核心維度,同時補充實戰(zhàn)中的關鍵差異:
對比維度 | 編譯期常量 | 運行時常量 |
核心定義 | 編譯階段確定值,編譯器可優(yōu)化 | 運行階段確定值,編譯器無法優(yōu)化 |
修飾符要求 | 必須是 | 可僅 |
數(shù)據(jù)類型 | 僅基本類型 + String類型 | 基本類型、String、引用類型(枚舉、包裝類等)均可 |
初始化值要求 | 編譯期可計算的常量表達式(字面量、合法運算) | 可依賴運行時計算(方法調(diào)用、new對象、配置讀取等) |
底層存儲 | 值嵌入調(diào)用類字節(jié)碼,同時存入運行時常量池 | 靜態(tài):運行時常量池;實例:堆內(nèi)存 |
類初始化觸發(fā) | 訪問時不觸發(fā)所在類初始化 | 靜態(tài):訪問時觸發(fā)類初始化;實例:new對象時觸發(fā) |
編譯器優(yōu)化 | 支持常量折疊,減少運行時開銷 | 無優(yōu)化,運行時計算值 |
transient修飾效果 | 無效(編譯期常量會被直接嵌入字節(jié)碼,不受transient影響) | 有效(引用類型的運行時常量,加transient可排除序列化) |
修改后影響范圍 | 修改后需重新編譯所有引用類(否則引用舊值) | 修改后僅需重新編譯自身類,引用類無需重新編譯 |
典型使用場景 | 全局固定值(如PI、接口地址、枚舉字面量) | 動態(tài)配置(如配置文件讀取、隨機值、對象唯一標識) |
編譯期常量:編譯時確定值,不觸發(fā)類初始化,可優(yōu)化,修改需全量編譯;
運行時常量:運行時確定值,觸發(fā)初始化,無優(yōu)化,修改僅需編譯自身。
五、如何選擇兩種常量?
很多開發(fā)者濫用static final,導致出現(xiàn)“常量修改后不生效”“類初始化異常”等問題,核心是沒選對常量類型。結合企業(yè)級開發(fā)實踐,給出明確的選型建議:
5.1 優(yōu)先使用編譯期常量的場景
- 值固定不變,且在編譯期就能確定(如數(shù)學常量、固定的業(yè)務基準值、接口固定地址);
- 需要被多個類引用,且希望減少運行時開銷(常量折疊優(yōu)化,提升性能);
- 不需要觸發(fā)類初始化(如工具類中的常量,避免不必要的類加載)。
示例:工具類中的常量定義
public class MathUtil {
// 編譯期常量:固定不變,可優(yōu)化
public static final double PI = 3.1415926;
public static final int DEFAULT_SCALE = 2;
public static final String EMPTY_STR = "";
}5.2 優(yōu)先使用運行時常量的場景
- 值需要動態(tài)確定(如從配置文件讀取、數(shù)據(jù)庫查詢、隨機生成、方法返回值);
- 常量是引用類型(如枚舉、包裝類、數(shù)組、對象);
- 每個對象需要獨立的常量值(如對象的唯一標識、實例級別的固定配置);
- 常量值可能會修改,且不想重新編譯所有引用類(降低維護成本)。
示例:配置類中的運行時常量
public class ConfigConstant {
// 運行時常量:從配置文件讀?。▌討B(tài)確定值)
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的值由方法調(diào)用確定(運行時),因此這兩個都是運行時常量,訪問時會觸發(fā)類初始化,且不支持常量折疊。
正確做法:判斷是否為編譯期常量,不僅看修飾符,還要看「數(shù)據(jù)類型」和「初始化值是否可編譯期確定」。
2:編譯期常量修改后,引用類未重新編譯,導致舊值殘留
場景:類A定義了編譯期常量MAX_NUM = 100,類B引用了A.MAX_NUM;修改A類的MAX_NUM = 200,僅重新編譯A類,未編譯B類,運行時B類仍使用舊值100。
原因:編譯期常量的值會嵌入到引用類的字節(jié)碼中,B類編譯后,字節(jié)碼中已經(jīng)是100,修改A類后,若不重新編譯B類,B類會一直使用嵌入的舊值。
避坑方案:修改編譯期常量后,必須重新編譯所有引用該常量的類;若常量值可能頻繁修改,建議改為運行時常量(從配置文件讀取)。
3:用transient修飾編譯期常量,誤以為能排除序列化
錯誤示例:
public class SerializeTest implements Serializable {
// 錯誤:transient修飾編譯期常量,無效
private transient static final String SECRET = "123456";
}原因:編譯期常量會被直接嵌入字節(jié)碼,序列化時不受transient影響,即使加了transient,序列化后仍能獲取到常量值。
正確做法:若想排除常量的序列化,不要用transient(對編譯期常量無效),可實現(xiàn)Externalizable接口,手動控制不寫入該字段。
4:局部final變量誤認為是編譯期常量
錯誤示例:
public void test() {
final int a = new Random().nextInt();
// 錯誤:認為a是編譯期常量,實際是運行時常量
System.out.println(a + 10);
}原因:局部final變量的初始化值若依賴運行時計算,就是運行時常量,編譯器無法對其進行常量折疊優(yōu)化;只有局部final變量的初始化值是字面量或編譯期可計算表達式,才會被編譯器優(yōu)化。
5:接口中的常量不是編譯期常量
錯誤示例:
interface MyConstant {
// 錯誤:認為這是編譯期常量,實際是運行時常量
String CONFIG = readConfig();
static String readConfig() {
return "config";
}
}原因:接口中的常量默認是public static final,但初始化值readConfig()是方法調(diào)用(運行時確定),因此是運行時常量,訪問時會觸發(fā)接口的初始化。
七、全文總結
1. 兩種常量的核心區(qū)別:值確定的時機(編譯期 vs 運行期);
2. 編譯期常量:static final + 基本/String + 編譯期表達式,不觸發(fā)類初始化,支持常量折疊;
3. 運行時常量:可僅final,支持引用類型,值依賴運行時計算,觸發(fā)初始化;
4. 坑點核心:不要混淆static final和編譯期常量,修改編譯期常量需全量編譯;
5. 選型原則:固定值用編譯期,動態(tài)值用運行期;
6. 面試關鍵:類初始化觸發(fā)、常量折疊、transient效果、修改后影響范圍。
編譯期常量與運行時常量,看似簡單,卻藏著JVM底層優(yōu)化和開發(fā)細節(jié),也是大廠面試中區(qū)分“初級開發(fā)者”和“中級開發(fā)者”的關鍵考點。
很多開發(fā)者因為分不清二者,導致出現(xiàn)“常量修改不生效”“類初始化異常”“序列化漏洞”等問題,看完這篇,基本能避開所有高頻坑,同時應對所有相關面試題。
以上就是Java編譯期常量與運行時常量的區(qū)別詳解的詳細內(nèi)容,更多關于Java編譯期常量與運行時常量的資料請關注腳本之家其它相關文章!
相關文章
Eclipse 導出可執(zhí)行Java工程/可執(zhí)行Jar文件(包含第三方Jar包)
這篇文章主要介紹了Eclipse 導出可執(zhí)行Java工程/可執(zhí)行Jar文件(包含第三方Jar包)的相關資料,需要的朋友可以參考下2016-11-11
新建springboot項目時,entityManagerFactory報錯的解決
這篇文章主要介紹了新建springboot項目時,entityManagerFactory報錯的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-01-01
如何自定義springboot-starter日志組件供各個服務使用(系統(tǒng)日志優(yōu)化)
文章介紹了如何將各個微服務的接口調(diào)用日志邏輯優(yōu)化為一個可共享的Spring Boot Starter,通過自定義注解和自動裝配機制實現(xiàn),本文給大家介紹的非常詳細,感興趣的朋友跟隨小編一起看看吧2025-01-01
Java實現(xiàn)限定時間CountDownLatch并行場景
本文將結合實例代碼,介紹Java實現(xiàn)限定時間CountDownLatch并行場景,文中通過示例代碼介紹的非常詳細,需要的朋友們下面隨著小編來一起學習學習吧2021-07-07
java多線程編程之向線程傳遞數(shù)據(jù)的三種方法
在多線程的異步開發(fā)模式下,數(shù)據(jù)的傳遞和返回和同步開發(fā)模式有很大的區(qū)別。由于線程的運行和結束是不可預料的,因此,在傳遞和返回數(shù)據(jù)時就無法象函數(shù)一樣通過函數(shù)參數(shù)和return語句來返回數(shù)據(jù)2014-01-01
關于通過java調(diào)用datax,返回任務執(zhí)行的方法
今天小編就為大家分享一篇關于通過java調(diào)用datax,返回任務執(zhí)行的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-08-08
Java參數(shù)校驗詳解之使用@Valid注解和自定義注解進行參數(shù)驗證
在后端開發(fā)中,參數(shù)校驗是非常普遍的,下面這篇文章主要給大家介紹了關于Java參數(shù)校驗詳解之使用@Valid注解和自定義注解進行參數(shù)驗證的相關資料,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2024-06-06

