首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

【Java 每日一題】final 修飾的靜態變量 vs 普通靜態變量:編譯期常量的秘密

     編輯:Java 綜合教學

今日題目

package Dec2016;

public class Ques1215 {
    static final int num1 = 2;
    static final int num2 = 3;
    static int num3 = 2;
    static int num4 = 3;

    public static void main(String[] args) {
        int result1 = num1 * num2; // 行 1
        int result2 = num3 * num4; // 行 2
    }
}

問題:主程序中行 1 和行 2 在字節碼層面有什麼本質區別?

核心答案

行 1 (num1 * num2) 在編譯期即被解析為常量 6,直接嵌入字節碼;
行 2 (num3 * num4) 在運行期執行乘法指令計算結果。

深度解析:宏變量 (Compile-time Constant) 機制

JVM 規範定義的編譯期常量條件

變量必須同時滿足:

  1. final 修飾
  2. 聲明時顯式初始化
  3. 值為編譯期常量表達式:
  • 基本類型字面量 (2, 3.14f, 'a', true)
  • 字符串字面量 ("hello")
  • 基本算術運算 (2 + 3, 5 * 10)
  • 字符串拼接 ("a" + "b")
  • 不包含:變量引用、方法調用、類實例創建、強制類型轉換 (如 (int) 2.5)

編譯器行為差異

代碼編譯期處理運行期字節碼
static final int a = 2;標記為 ConstantValue 屬性bipush 2 (直接推入常量)
static final int b = a + 1;計算為 3,標記 ConstantValuebipush 3
static int c = 2;無特殊處理getstatic c → iconst_2 → putstatic
int r = a * b;常量折疊:計算 6,內聯到使用處bipush 6 (無乘法指令!)
int r = c * d;無法優化getstatic c → getstatic d → imul → istore

驗證實驗:javap 反編譯

> javac Ques1215.java
> javap -c Ques1215

// 行 1 字節碼
  0: bipush        6       // 直接推入常量 6,無乘法運算
  2: istore_1

// 行 2 字節碼
  3: getstatic     #2      // 獲取 num3
  6: getstatic     #3      // 獲取 num4
  9: imul                    // 整數乘法指令
 10: istore_2

延伸思考:這有什麼實際影響?

性能層面

  • 編譯期常量零運行期開銷:無字段讀取、無運算指令、無內存訪問
  • JIT 編譯器對非常量字段也會優化 (逃逸分析、標量替換),但不如編譯期內聯徹底

語義層面:修改常量值的陷阱

// A.jar
public class Consts {
    public static final int VERSION = 1;
}

// B.jar 依賴 A.jar
public class App {
    public static void main(String[] args) {
        System.out.println(Consts.VERSION); // 編譯期內聯為 1
    }
}

// 修改 A.jar: VERSION = 2,重新打包 A.jar,但不重新編譯 B.jar
// 運行 B.jar 輸出仍為 1!因為常量已內聯到 B.jar 字節碼中

最佳實踐:對外發布的 API 常量,若未來可能變更,不要用 static final 基本類型/字符串,改用 public static int getVersion() 或非 final 字段 + 文檔約定。

非基本類型的 final 陷阱

static final List LIST = new ArrayList<>(); // 引用不可變,但對象可變
LIST.add("hello"); // ✅ 編譯通過,運行正常

static final Date DATE = new Date(); // 引用不可變,但 Date 可變
DATE.setTime(0); // ✅ 危險!破壞不可變性假設

總結

  • static final + 編譯期常量表達式 = 宏變量,編譯期內聯,零開銷
  • 普通 static 字段 = 運行期讀取/寫入,靈活但有開銷
  • 發布 API 時慎用基本類型/字符串 static final,除非真正不可變 (如 Math.PI)
  • 引用類型 final 僅保證引用不變,不保證對象不可變

下期預告:String.intern() 與字符串常量池的那些事兒。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved