重新思考 Java 中的領域原始類型與 Project Valhalla
多年來,Java 開發者一直面臨型別安全與效能之間令人沮喪的取捨。一方面,使用領域原始類型——編碼業務約束的型別(例如 PositiveInt 取代原始的 int)——讓編譯器能夠拒絕無效狀態,防止錯誤。另一方面,將原始型別包裝在類別中會產生帶有標頭與指標的堆積物件,導致大量記憶體開銷以及在效能關鍵的「熱迴圈」中出現快取未命中的情況。
過去的經驗法則是僅在系統邊界精緻化型別,核心邏輯則回退使用原始型別以維持速度。Project Valhalla 透過引入值類別改變了這個等式,允許 JVM 將包裝類別直接展平至暫存器、封閉物件或陣列槽位中。
包裝類別的效能稅
要了解為何需要 Valhalla,我們必須檢視標準 Java 包裝類別的記憶體佈局。一個簡單的 class PositiveInt { final int v; } 在 HotSpot 上通常佔用 16 位元組(12 位元組的標頭加上 4 位元組的整數)。此外,這些物件的陣列並不儲存實際的值,而是 4 位元組的參考,指向散佈在堆積中的物件。
對於處理數百萬事件的串流處理器而言,這種架構是災難性的。包裝類別的記憶體開銷是原始 int 的四倍,且每次存取都需要「指標追蹤」,常導致快取線載入並可能產生快取未命中。這種開銷是開發者傳統上在熱路徑中避免使用精緻型別的原因。
實作精緻型別
在 Scala 等語言中,精緻型別可以在編譯時以謂詞縮小。Java 缺乏此原生機制,意味著必須在建構時檢查約束。典型的實作涉及一個基礎的 Refined<T> 類別以及具體的延伸:
private static class PositiveInt extends Refined<Integer> {
public PositiveInt(int value) {
super(i -> i > 0, value);
}
}
雖然這提供了任何 PositiveInt 實例皆有效的靜態保證,但原始型別的裝箱仍是瓶頸。這正是 Project Valhalla 的 value 關鍵字發揮作用的地方。
迎向 Project Valhalla 與值類別
值類別是沒有身分識別的物件。它們具備行為與不變條件,但儲存方式如同原始型別。使用 value 關鍵字時,開發者告訴 JVM 此型別沒有身分,允許 JVM 在任何出現該物件的地方內嵌其欄位。
以下示範使用 Valhalla 預覽(JEP 401)實作領域原始類型的範例:
public value class PositiveInt implements RefinedInt<PositiveInt> {
private final int value;
public PositiveInt(int value) {
if (value <= 0) {
throw new IllegalArgumentException("must be positive: " + value);
}
this.value = value;
}
@Override public int value() { return value; }
}
透過使用像 RefinedInt 這樣的專門介面取代通用的 Refined<T>,即可避免裝箱。最終得到的型別在安全性上與類別相同,卻具備原始型別的效能。
擴展至多欄位型別
此模式不僅限於單一值的包裝類別。包含 Latitude 與 Longitude(皆為以 double 為基礎的值類別)的 Coordinate 值類別,使 JVM 能在每個槽位中儲存 16 位元組連續的 double。相較之下,等價的具身分類別則會儲存指向分散於堆積的物件的參考,每個物件都有自己的標頭。
數字上的影響
在 64 位元 HotSpot(使用壓縮 oops)上的基準測試顯示,10 個元素陣列的記憶體占用差異相當顯著:
int[10]:56 位元組(純原始型別)PositiveInt[10](身分類別):216 位元組(具領域意識但成本高)PositiveInt[10](值類別):56 位元組(低成本且具領域意識)
值類別取得與純原始型別完全相同的記憶體佈局與快取行為,實質上消除了領域建模的「效能稅」。
採用時的關鍵考量
雖然值類別是一項強大的工具,但它們帶來了多項語意上的變化,開發者必須加以考量:
1. 相等語意
值類別以值而非指標比較。對值類別使用 == 運算子會測試欄位逐一的可替代性,等同於實作良好的 equals() 方法。將現有的具身分類別遷移為 value 類別時,任何依賴參照相等性的程式碼行為都會在不知情的情況下改變。
2. 可為 null 性
值類別不可為 null。Project Valhalla 正在引入受限於 null 的參考(例如 PositiveInt! 表示非 null,PositiveInt? 表示可為 null),讓編譯器能在熱路徑中消除 null 檢查。
3. 泛型差距
目前,由於類型擦除,List<PositiveInt> 與 Optional<PositiveInt> 仍會產生裝箱。泛型專門化尚未完整推出;因此,在關鍵路徑中為了取得最佳效能,應使用具型別的陣列(PositiveInt[])而非集合。
4. 框架整合
大多數 Java 框架(Jackson、JPA、Bean Validation)預期的是原始型別或 String。值型別需要輕量的適配器。例如,針對 PositiveInt 的 Jackson 反序列化器只需要將 p.getIntValue() 的呼叫包裝在新的 PositiveInt 建構子中。
結論
Project Valhalla 正在彌合高階領域建模與低階效能之間的鴻溝。透過允許型別「像類別般編寫卻像 int 一樣運作」,Java 正在移除在整個應用程式中使用強大領域原始類型的最後障礙。雖然仍處於預覽階段,值類別承諾未來能讓安全性與正確性(在型別系統中編碼)不再帶來效能懲罰。