← 返回 Siami 首頁

Project Valhalla 完整解密:Java 十二年磨一劍,JDK 28 終於讓物件『寫得像 class、跑得像 int』

▲ 590 💬 15
Project Valhalla 完整解密:Java 十二年磨一劍,JDK 28 終於讓物件『寫得像 class、跑得像 int』

編按:本文綜合整理自 JVM Weekly vol. 180The RegisterTheNextWebOpenJDK JEP 401,並加入 Siami 編輯部觀點與分析。

為什麼這件事重要

Valhalla 不只是 Java 平台再添一個語法糖。它動到 Java 自 1995 年以來最根本的物件假設:「每個物件都有 identity」。這條假設支撐了 equals/hashCode 的整個體系、synchronized 的鎖定模型,以及所有依賴「== 比較位址」這類直覺的程式碼。一旦 value class 讓程式設計師能宣告「這個東西不需要 identity」,等於是把整個物件模型的基石鬆開。

對高效能 Java 領域(資料密集、向量運算、ML、遊戲、金融、影像 codec)來說,Valhalla 提供了「不必犧牲抽象就拿到原生型別密度」的途徑。長年以來,這些領域的工程師被迫在「寫 Color class 易讀但慢」與「手寫 byte 陣列快但危險」之間二選一。Valhalla 把這個二元對立拆掉。

但影響不只在高效能端。當 Integer、Long、Double 等原生型別 wrapper 在 preview 模式變成 value class,所有使用 boxed Integer 的集合(List 除外,但 int[] 和包成 value class 的 Integer[])都會自動享受 scalarization 與 heap flattening 的紅利。這等於是對地球上每一支 Java 程式碼做了一次隱形升級——雖然大多數團隊要到 JDK 29 LTS(2027 年 9 月)才會用到穩定版。


事件背景:一個 19.7 萬行的 Pull Request

這次的變更規模極為龐大,整合期間其餘的 commiters 被要求暫緩提交大型變更。光是這一個 Pull Request 就橫跨 1,816 個檔案、新增超過 19.7 萬行程式碼。不過先別急著開香檳:它是 preview 特性,預設關閉,而且 Brian Goetz 立刻出來踩煞車,說這只是「Valhalla 的第一部分」。Goetz 加了一句精闢觀察——之前那些喊「他們永遠不會出貨」的人,現在會無縫切換成「但他們沒出貨最重要的部分」(社群裡流傳多年的玩笑是:我們會先抵達北歐神話裡那個死後世界 Valhalla,才看得到這個專案完工)。

You have to earn your own haters.(要贏得自己的仇恨者。)所以現在是講完整故事的好時機。本期就是一個大型深度報導,預設讀者從未追蹤過 Valhalla 的工作:從 2014 年的問題出發,經歷多次構想演化(其中不少走進了垃圾桶),一路到 JDK 28 究竟會端出什麼。泡杯咖啡吧——本期特地在這個時機推出。

Valhalla 從第一天起就高舉一句口號:「寫起來像 class,跑起來像 int。」(Codes like a class, works like an int.)這一句話就抓到了整個專案的精髓:希望能用一般 class 的寫法——帶方法、帶建構式驗證、欄位命名合理——但讓 JVM 能像處理原生型別一樣高效處理它們。要理解為何這是個問題,得回頭看 Java 的根基。在這套語言中,除了八個原生型別(int、long、double、boolean 等),其他全部都是參考型別。當你寫 Point p = new Point(1, 2),p 不是一個點。p 是一個指標、一張寄物櫃的票:heap 某處擺著一個物件,而你手裡拿的是寫著位址的紙條。每次想讀欄位,JVM 都得「跑去寄物處」走一趟指標(指標間接)。單一物件沒什麼。問題在規模。Heap 上每個物件都有自己的 header(十來 bytes 的 metadata:包含 JVM 用來辨識型別與是否有人在同步鎖定的資訊)。順帶一提,這正是 Project Lilliput 近期在處理的問題,協助縮小物件 header。但 header 大小不是全部。每個物件都要配置、之後由 GC 回收。物件散落在 heap 各處,一個 100 萬個 Point 的陣列,實際上是 100 萬張寄物票指向散落在倉庫各處的 100 萬個箱子。

Brian Goetz 在《State of Valhalla》文件裡把這種記憶體佈局稱為「fluffy」:膨鬆、臃腫。夢想中的佈局是 dense(密集):資料並排躺著。為什麼密度重要?因為硬體跑得比 Java 快。1995 年記憶體存取的成本大約等同於一次 CPU 運算。今天 CPU 比主記憶體快兩個數量級,整段差距由快取填補。處理器以 cache line(通常是 64 bytes)為單位讀取記憶體。資料如果密集且連續,一個 cache line 就能帶進大量有用數值。如果我們跳來跳去走指標,每次存取都有 cache miss 風險,慢了可能 100 倍。這就是參考局部性(locality of reference),也是這整場遊戲真正的賭注。「但 JVM 有 escape analysis。」總有眼尖的人跳出來說。沒錯:虛擬機能辨識某些物件從未「逃出」一段程式碼範圍,因此根本不配置它。從程式設計師角度看,物件好像存在,實際上欄位被攤平到一般變數或 CPU 暫存器。最理想的情況下,配置成本和後續 GC 工作直接歸零。麻煩在於這個優化既不可預測又脆弱。它只在 JIT 編譯器能高度掌握物件完整流向時生效。但只要物件落入另一個 class 的欄位、存進陣列、傳進更複雜的方法、或超出 JIT 能分析的程式碼邊界,整套戲法就失效了。原始碼一字不改,但效能行為可能劇烈變化。這正是為什麼有經驗的 JVM 工程師把 escape analysis 視為加分題,而非專案的基礎。如果應用程式的效能仰賴某個特定 JIT 版本是否套用這個優化,很容易掉進難以預測的效能回退陷阱。小幅重構、升級 JDK、改變程式碼結構,都可能讓物件被打回 heap,配置成本與 GC 工作全部回歸。剩下的唯一選項就是蠻力解法:放棄物件,手動編碼資料。與其寫一個 Color class,不如直接抓三個 byte r、g、b。這不是學術範例——這種做法多年來用於遊戲引擎、圖形函式庫、影像處理系統、資料庫、分析引擎與 HPC 程式碼,每個 byte、每次配置都要斤斤計較。問題是,速度的代價是安全性與可讀性。我們失去欄位名稱、私有狀態、驗證、方法。JEP 401 給了一個簡單範例:用「原始」色彩 byte 工作的開發者可能誤把 RGB 看成 BGR,紅藍對調,整張圖悄悄毀掉。一個 class 不會允許這種事發生。一個裸 int?當然會。正是這種「要嘛方便 class、要嘛快速原生型別」的二元對立,是 Valhalla 試圖抹除的。官方記錄上 Project Valhalla 始於 2014 年。James Gosling 當時形容它是「六個 PhD 綁成一個死結」,這毫不誇張。有趣的是,這個構想比專案本身更古老:Java 的創造者在語言第一版就想做 value types,但 1995 年放棄了,因為問題太難。目標訂得很宏偉:讓程式設計模型與現代硬體的效能特性重新對齊。換句話說,讓程式設計師能宣告自己的型別——在記憶體中像原生型別一樣平坦密集,但看起來和用起來就像普通 class。說起來容易,做起來難。接下來幾年,團隊打造了五個不同的原型,每個原型探勘問題的不同面向。故事最有趣的部分從這裡開始——因為要理解 Valhalla 現在的樣貌,必須看看多少構想中途陣亡。早期原型走的方向現在稱為「Q World」。它假設新 value type 是與物件截然不同的物種,有獨立的型別描述子、獨立的 bytecodes、獨立的頂層型別,就像原生型別一樣。聽起來合理:既然要跑得像 int,就用 int 的方式表示。問題是,這種分隔讓整個 JVM 型別系統淹沒在額外的複雜度裡——每件事都得做兩個變體。突破發生在約 2019 年的原型「L World」。名稱來自 value type 開始與物件參考共用同一個「L 載體」(L 描述子,即 JVM 用於一般參考的同一個)。團隊原本預期這種統一會太難,但出乎意料地順利,沒有重大妥協,還順手解決了前幾輪留下的一整串問題。


數據解讀 / 質疑

19.7 萬行 code、1,816 個檔案的單一 PR 規模,是 OpenJDK 史上罕見的整合。對比 Java 8 lambda expressions 當年也不過幾萬行,Valhalla 一次整合就抵得上一個中型語言特性的總和。Brian Goetz 與 Dan Smith 之所以要求其他 committers 暫緩大型提交,正是因為這個 PR 觸及了太多基礎——任何並行的型別系統重構都可能和它衝突。

但「Valhalla 進入 JDK 28」這個標題是半真。它只是 preview,預設關閉,下一個 LTS(JDK 29,2027 年 9 月)大概率仍是 preview。最完整的紅利——specialized generics(讓 ArrayList 真正扁平化)與 null 限制型別——要等更後面的版本。

值得注意的還有社群反應。Hacker News 上 torginus 的留言一語道破:「撇開 Java 社群不願承認 .NET 存在的事不談,這跟 .NET struct 差在哪?Value type、generic specialization、boxing——快速掃一眼好像做了同樣的選擇。」JVM Weekly 的回答是:C# struct 有 identity、允許 mutate,模型較重;Valhalla 的 value object 沒有 identity,由 JVM 自行裁量記憶體佈局,「對人更簡單、對機器更自由」。這個辯護在功能面成立,但 12 年才端出第一期,社群耐心正在被消耗——Goetz 自己都說,「『他們永遠不會出貨』的人現在會無縫切換成『但他們沒出貨最重要的部分』」。


核心問題根源:Java 為什麼需要 Valhalla

L World 還產生了一個根本性的「啊哈」,塑造了後續所有東西:語言模型與 JVM 模型不必 100% 重疊。L World 是虛擬機的正確模型,但你可以把它當作翻譯目標,在語言層提供程式設計師更方便的介面。這種分層成了專案其餘部分的關鍵。也就是在那時,工作分成兩期的計畫逐漸成形:先做 value class(當時還叫別的名字,後面會說明),再做 specialized generics。Generics 部分會在第 6 節回頭講,那是另一段更長的故事。如果你曾試著讀 Valhalla 的資料、被一堵矛盾的術語牆彈開,不是你的錯。命名在這裡變過好幾次,而且不是表面修飾:每次命名變動背後都對應著模型的變動。讓我們追一遍,因為這是這個特性如何被設計出來的最佳示範。

第 1 階段:value types。 最早的術語。含糊,因為當時還不清楚這些東西究竟該長成什麼樣。

第 2 階段:inline classes。 約 2019-2020 年確立了一個區分,本質上沿用至今:class 分成 identity class(有 identity,也就是至今熟悉的一切)和新的 inline class(沒有 identity)。正是在這個時期,「codes like a class, works like an int」這句口號誕生,基本約束也定了:inline class 預設為 final、欄位 final、無法在上面 synchronized。


十二年構想演化:五個原型、四個階段命名

第 3 階段:「primitive classes」與雙重投影模型。 這裡開始有趣了,因為這正是被大幅縮減的想法。在 2021 年的《State of Valhalla》文件中,Valhalla 承諾了三件事:value objects、primitive classes、specialized generics。「primitive class」的構想是:單一型別會有兩個投影——value 變體(平坦、絕不 null、行為像原生型別)和參考變體(一個允許 null 的 box)。各個版本曾寫成 Point.val/Point.ref,後來又試驗 Point! 與 Point? 語法。模型很強大,但心智負擔沉重。程式設計師必須日常在同型別的兩種形式之間切換,搞懂何時會發生轉換。團隊秉持「即使犧牲效能天花板,也要為使用者簡化模型」的原則,最終拆解了這種二元對立。

第 4 階段(今日):「value classes」與「value objects」。 現行的 JEP 401 由 Dan Smith 起草、Brian Goetz 審閱,講得很簡單。一個新東西:以 value 修飾符宣告的 value class。它的實例就是 value objects:沒有 identity 的物件。並且(這是重點)value class 仍然是參考型別。非空型別(null 限制)整個被拆到另一個獨立、可選的 JEP(Null-Restricted Value Class Types),後面會談。所以現在我們有兩個簡單、正交的概念取代一個複雜概念:「它有 identity 嗎?」與(之後另議)「它允許 null 嗎?」。值得記住,因為如果你翻到舊文章(或 Baeldung 把「primitive classes」描述成獨立機制),你讀的是過時模型。在 OpenJDK 正典裡,那個意義的「primitive classes」已經不存在。還有更多東西被丟掉了。最初的「Value Objects」JEP 草案被撤回、由 JEP 401 取代。最初的「Universal Generics」草案也打回去重做。JEP 401 伴隨 JEP 402:Enhanced Primitive Boxing(也是 preview),以及一整串早期存取版本(LW1、LW2、LW3…)和 JVM Language Summit 的演講,包括 Frédéric Parain 講 heap flattening、Daniel Smith 講新的物件初始化模型。這段的寓意是:十二年不是十二年的「寫程式」。是十二年不斷淘汰構想,直到留下真正能維護的那一個。來看具體細節。我們究竟會得到什麼。

宣告。 用 value 修飾符建立 value class:


value class USDCurrency implements Comparable<USDCurrency> {

private int cents; // implicitly final

public USDCurrency(int dollars, int cents) {

this.cents = dollars * 100 + cents;

}

public USDCurrency plus(USDCurrency that) {

return new USDCurrency(0, this.cents + that.cents);

---

## JEP 401 細節:宣告語法與核心特徵

}

// dollars(), cents(), compareTo(), toString()...

}

它也可以是 value record。規則是:所有實例欄位隱含 final、方法不能 synchronized、class 預設 final(或者由 value class 和 abstract value class 組成階層)、不能繼承自有 identity 的 class,但能開心實作介面。除了這些限制,就是個普通 class。

核心特徵:沒有 identity。 這是關鍵。一般物件有 identity:兩個分別 new Point(1,2) 是兩個不同物件,即使內容相同。Value object 沒有 identity,就像不存在兩個「不同的」int 4 一樣。所有後果從這裡展開:

== 改變含義。 以前 == 比較 identity(是否同一個位址)。對 value object 來說,== 檢查 substitutability:兩者是否同型別且欄位相同(遞迴比較,原生欄位逐 bit、物件欄位再用 ==)。所以 new USDCurrency(3,95) == new USDCurrency(3,95) 會回傳 true。這是好消息:終結了 Integer 上 == 的著名混淆。但小心:== 看的是內部狀態,不總是物件所代表的語義,所以「這兩筆資料是否相同」這類比較還是該用 equals。

synchronized 會丟出例外。 沒有東西可以鎖。嘗試的話會得到 IdentityException。當你需要強制 identity,可以用新的輔助方法 Objects.requireIdentity 與 Objects.hasIdentity。

現在是最重要的觀念陷阱:value object 仍然可以是 null。 這讓所有以為「value = 像原生型別 = 絕不 null」的人大吃一驚。在 JDK 28 模型中,value class 是參考型別,所以 USDCurrency d = null; 完全合法。不可空型別(帶 null 限制)是另一個獨立的、未來的 JEP。它們不在 JDK 28。後面會回頭談,因為這不是小事:它是完整效能的鑰匙。

JEP 401 給 JVM 自由,主要以兩種方式優化 value object。

Scalarization 是 JIT 編譯器技巧。對 value object 的參考被「拆解成基本元素」,化約成它的本質——欄位集合,不加封裝。JIT 不再傳遞指向 Color 的指標,而是直接傳三個 byte r、g、b(加一個 flag bit 表示參考是否非 null)。這樣的物件實際上免費:沒有配置、GC 沒事做。有點像 escape analysis,但更可預測、覆蓋更廣:即使跨越未被 inlining 的方法呼叫邊界也有效。限制是:當變數型別是 value class 的父型別時(例如 Object,或重要的、被擦除的泛型參數),scalarization 通常失效,物件就必須在 heap 上具體化。

Heap flattening 是第二種機制。物件本質被編碼成緊密的 bit vector,直接寫入欄位或陣列元素,不再有指向其他記憶體位置的指標。密度與局部性正是從這裡誕生。不過有個值得知道的限制:扁平化資料必須能原子地讀寫(否則並發存取時會「撕裂」)。在典型平台上,「夠小」今天指的是 64 bits 以內,含 null flag。這就是為什麼很多小型 value class 能漂亮地扁平化,但例如有兩個 int 欄位或一個 double 的 class,可能放不進原子寫入,最終還是落到 heap 上當普通物件。未來 128-bit 編碼會到來,前面提到的 null 限制 JEP 會讓更大的 class 也能扁平化,代價是放棄原子性保證。這正是不可空從修飾變成效能槓桿的轉捩點。還記得把 int 包成 Integer 的 boxing 老成本嗎?在新模型中,wrapper class 本身就變成 value class(在 preview 開啟時,Integer、Long、Double 等失去 identity)。既然 box 不再有 identity,JVM 就能 scalarize 和 flatten 它。效果:Integer[] 開始逼近 int[] 的效率,而 boxing 開銷,用 JEP 401 的話說,大幅縮小。配套的 JEP 402(Enhanced Primitive Boxing)更進一步,把原生型別與其 box 之間的轉換理順,鋪平了寫 List 這類程式的道路。但那仍是另一塊在成熟的部件,別假設它會和 401 一起完整到位。效果最明顯的場景在這裡:與其拿 100 萬個指標指向 100 萬個散落物件,一個 Color[] 陣列能直接以扁平的 32-bit 編碼依序存色彩(再次強調:加 null flag)。從記憶體角度看,這樣的陣列開始像 plain int[] 一樣運作:連續的資料區塊,處理器一條 cache line 接一條 cache line 掃過。要讓這一切運作,若干深層基礎被搬動了:新的 value 修飾符;嚴格的建構規則(所有欄位必須在任何東西看到新物件之前設定,實務上在 super() 呼叫前,避免 final 欄位的「變動」被觀察到);把 == 重新定義為 substitutability 測試;為參考比較 bytecode(acmp)加上 value-object 檢查;scalarization 與 flattening 機制;IdentityException;以及既有「value-based」class 的遷移。簡言之,這不是語法糖。這是對 Java 自 1995 年以來一條根本假設的重建:每個物件都有 identity。讓我們用最簡單的例子追一遍,即使不懂 JVM 內部也能看清楚。


final class Point { // an ordinary class with identity

final int x;

final int y;

Point(int x, int y) { this.x = x; this.y = y; }

}

---

## 官方 EA 試玩講解

在進入技術細節之前,先看 Nicolai Parlov(Inside Java 主持人)親自試玩 JEP 401 EA 版本的 26 分鐘講解,把 value class 的實際效果直接演給你看:

<iframe width="560" height="315" src="https://www.youtube.com/embed/Eua3nTkye2Y" title="Inside Java Newscast #100: Try the New Valhalla EA Build" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

---

## Scalarization 與 Heap Flattening:效能如何實現

}

Point[] points = new Point[1_000_000];

程式碼的差別剛好一個字:value。但記憶體的差別是根本性的。JVM 現在能把數值本身存進陣列,緊密排列:一個接一個、每個 8 bytes(加可能的 null flag),連續區塊。沒有逐元素的 header。沒有指標。沒有在 heap 跳來跳去。現在走訪陣列時,處理器循序讀資料。每個 64-byte cache line 一次帶進好幾個完整 point。100 萬個座標加總跑在記憶體頻寬上限,而不是被 miss 拖垮。在資料密集的程式碼上,這可以是倍數級的差距,不是幾個百分點。最重要的是對維護性而言:你沒有為此犧牲抽象。Point 仍然是 class:有名字、有建構式、可以有驗證(if (x < 0) throw …)、可以有方法。你不必像以前那樣把 point 拆成兩個裸 int[] xs、int[] ys 然後祈禱別把索引搞混。你同時拿到原生型別的密度和 class 的可讀性。整個 Project Valhalla 濃縮成這一個範例。這是 Valhalla 的下半場,老實說更難的部分。讓我們從問題根源開始。

Java 用型別擦除(type erasure)實作泛型。實務上:List 與 List 在執行期是同一個普通 List,型別參數 T 被擦除成 Object。這常被嘲弄,但值得知道這是當年深思後的決定,不是偷懶。Erasure 給了 Java 漸進式遷移相容性:你可以把一個既有的非泛型 class 變成泛型,不會破壞任何既有原始檔或編譯後 class,客戶端可以立刻、稍後、或永不遷移。2004 年時 Java 已經有龐大程式碼基底,替代方案(「泛型來了,但把整個函式庫丟掉」)會是糟糕的交易。今天更糟。問題是 erasure 正好和 Valhalla 在最在意效能的地方衝突。因為 T 擦除成 Object,一個 value object 放進 List 必須在 heap 上具體化為普通物件。換句話說:你漂亮、可扁平的 Point 一進泛型容器就失去扁平化:容器裝的是參考,不是扁平數值。在 Point[] 拿到的密度,到 ArrayList 全蒸發了。修復計畫像所有 Valhalla 一樣分兩期:


實際差異:100 萬個 Point[] 的記憶體佈局

第 1 期:Universal Generics。 語言層級的改動:讓型別變數也能涵蓋 value type,也就是能表達 ArrayList 或 List。目前仍透過 erasure。程式設計師主要會感受到新的編譯器警告,關於「null pollution」,因為 T 型別的欄位預設是 null,即使 T 是 value type。處理這些警告會讓 API「ready for specialization」。

第 2 期:Specialized Generics。 未來的 JVM 擴充,會為具體型別引數生成異質、專門化的 class 佈局(專案術語:species 與 type restrictions)。要到那時,ArrayList 才真正由扁平記憶體支撐。這部分仍大致是研究階段。對函式庫和框架的影響是巨大的,這正是漸進推出的原因。最終,集合、stream、整個 API 都能在 value type 上變得平坦、零配置。但函式庫作者必須處理新警告、從一開始就以 specialization 為前提來設計。坦白說:最初的 Universal Generics 草案經過了重做,specialization 帶來的完整紅利要等未來版本。JDK 28 沒帶來。把以上整理到一處,因為「已經有了!」和「還沒到!」很容易讓人迷路。

已通過的部分: JEP 401(Value Classes and Objects)作為 preview,鎖定 JDK 28(2027 年 3 月釋出),預計 2026 年 7 月左右整合進 mainline。19.7 萬行、1,816 個檔案,Lois Foltan 主導整合,請其他 committers 暫緩大型變更。預設關閉:要玩這個語法必須打開 —enable-preview。

真正會到使用者手上的: 宣告 value class 與 value record 的能力;JDK 中既有「value-based」class(包含 Integer 等原生型別 wrapper)在 preview 下遷移為 value class;對合資格 class 套用 scalarization 與 flattening;更便宜的 boxing。

還會演進、JDK 28 沒有的: null 限制型別(不可空);完整的 specialized generics;128-bit 編碼;成熟的 JEP 402。還有語法本身,因為是 preview,這正是 preview 該有的樣子:會根據回饋逐版本調整。這也是 Goetz 為什麼說「只是第一部分」。

對生態系的潛在影響: 對高效能 Java(資料、向量運算、ML、遊戲開發、金融、codec)來說,這是一條「不犧牲抽象就拿到密集資料」的路徑,正是這些領域期待多年的東西。框架與函式庫會開始遷移它們的 value-based class。你也得留意 == 與 synchronized 周圍一長串行為驚喜——那些程式碼可能(自願或不自願地)依賴了 identity。還有一點規劃時值得記住:JDK 28 不是 LTS;下一個 LTS 預計是 2027 年 9 月的 JDK 29。所以大多數企業要到 LTS 才會見到穩定版 Valhalla,但正是 28 的 preview 啟動了真實程式碼的真實回饋迴圈。如果你手上的東西能受惠於這項改動,現在就是開始實驗、提交回饋的時機。為何這被視為平台史上最重大的改動之一?因為 Valhalla 不是再往語言上多掛一個特性;它動到了最深層的假設。「每個物件都有 identity」自 1995 年以來在 Java 中一直為真;那是其他一切建立的基礎。讓程式設計師選擇退出這個假設(哪些物件需要 identity、哪些不需要)不是重構,是基礎的轉移。這也正是它為什麼會解鎖未來十年的工作:統一原生型別與物件、specialized generics、更密集的集合、更快的數值運算。與此同時,這也是「Valhalla 進入 JDK 28」這個標題的誠實版本:這是個半真。它是多階段推出的第一個 preview 步驟。但正是這個團隊的紀律(為人簡化模型、把困難的效能優化做成可選)說明了為什麼花了十二年,也說明了為什麼現在終於能出貨。對程式設計師而言,有一件事比語法更重要:把 identity 與 value 的區別內化。==、flattening、generics 都是這個區別的延伸後果。早期存取版本已經就位:你能在競爭對手之前親手摸到這些東西。


常見問答:

1. value class 就是 record 嗎? 不是,兩者正交。record 代表「放棄獨立的內部狀態」(內容 = 各 component)。value 代表「放棄 identity」。可以自由組合:普通 class、record、value class、value record。

2. value object 能用 == 比較嗎? 可以,但 == 現在含義不同:substitutability——遞迴比較所有欄位,而不是記憶體位址。「它們是否代表相同資料」這類問題通常還是用 equals 較好,因為 == 看的是內部狀態,不總等於所代表的狀態。

3. value class 可以是 null 嗎? 在 JDK 28 模型中,可以。value class 仍是參考型別。不可空型別(帶 null 限制)是另一個獨立的、未來的 JEP,它們才是解鎖更大 value class 扁平化的關鍵。它們不在 JDK 28。

4. Integer 變成 value class 不會壞掉嗎? 多數情況不會。二進位仍能 link,唯一新的編譯錯誤是嘗試 synchronized 這類型別。可能會注意到的改變涉及依賴 identity 的程式碼:Integer 上的 == 開始比較數值,synchronized(someInteger) 會失效。如果你依賴了其中之一,本來就是脆弱程式碼。

5. 會有快速扁平的 ArrayList 嗎? 還沒。因為型別擦除,泛型容器內的物件會在 heap 上具體化。扁平的泛型容器需要 universal 與 specialized generics:那在未來。JDK 28 中,flattening 直接作用於 value 型別的欄位與陣列,例如 Point[]。

6. 這跟 C# 的 struct 差在哪? C# 的 struct 有 identity、允許 mutate,所以賦值與傳遞的複製語義必須精確定義,對程式設計師模型較重、給執行階段的自由較少。Valhalla 的 value object 沒有 identity,記憶體中的佈局由 JVM 自行裁量。對人更簡單、對機器更自由。

7. escape analysis 不就在做這件事嗎? 部分是。escape analysis 能在證明物件不依賴 identity 時避免配置,但它不可預測,且當物件落入欄位、陣列、或「逃出」優化範圍時幫不上忙。Value object 的 scalarization 更可預測、覆蓋更廣,包括跨越方法呼叫邊界。

8. 必須改寫程式碼才能受益嗎? 對自己的 class,通常只要在那些代表「簡單領域數值」、不依賴 identity 的 class 上加上 value 修飾符;遷移大致相容。有些紅利甚至白拿,因為 JDK 自己在遷移(如原生型別 wrapper)。

10. 什麼時候能看到完整 Valhalla,包含 generics、不可空型別、以及其餘部分? 在未來版本。團隊採漸進方式出貨:JDK 28 是 value class 的第一個 preview。完整故事(specialized generics、null 限制型別、128-bit 編碼)會橫跨多個版本,可能要到下個 LTS 才穩定。

PS:早期存取版本在 jdk.java.net/valhalla,這大概是你比寫另一期專題更快形成自己看法的方式。


Generics 的兩期修復計畫


JDK 28 的範圍:已通過 / 真正會到 / 還沒到


常見問答


延伸資源

網友熱門留言 (1)

#1 Hacker News 用戶 torginus ▲ 15
撇開 Java 社群不願承認 .NET 存在的事不談,這跟 .NET struct 差在哪?Value type、generic specialization、boxing——快速掃一眼好像做了同樣的選擇。