剛剛 Java 25 炸裂發布!讓 Java 再次偉大
大家好,我是程序員魚皮。
剛剛,Java 25 正式發布!這是繼 Java 21 之后,又一個 LTS 長期支持版本,也是 Java 開發者們最期待的版本之一。其中有個特性可以說是顛覆了我對 Java 的認知,讓 Java 再次偉大!
那么 Java 25 都發布了哪些新特性?有沒有必要升級?
一篇文章,帶你速通 Java 新特性,學會后又能愉快地和面試官吹牛皮了~
推薦觀看視頻版:

?? 正式特性
這些特性在 Java 25 中正式穩定,可以在生產環境中放心使用。
【實用】Scoped Values 作用域值
如果我問你:怎么在同一個線程內共享數據?
估計你的答案是 ThreadLocal。
但是你有沒有想過,ThreadLocal 存在什么問題?
舉一個典型的 ThreadLocal 使用場景,在同一個請求內獲取用戶信息:
public class UserService { private static final ThreadLocal<String> USER_ID = new ThreadLocal<>(); public void processRequest(String userId) { USER_ID.set(userId); // 寫入 doWork(); USER_ID.remove(); // 問題:必須手動清理,容易忘記 } public void doWork() { String userId = USER_ID.get(); // 讀取 System.out.println("處理用戶: " + userId); // 問題:其他代碼可以隨意修改 USER_ID.set("被篡改的值"); } }
這段簡單的代碼其實暗藏玄雞,可以看出 ThreadLocal 的痛點:
-
容易內存泄漏:必須手動調用
remove() -
可以隨意修改數據,可能導致不可預期的結果
此外,如果想讓子線程也共享數據,每個子線程都要復制一份數據。如果你使用的是 Java 21 的虛擬線程,1000 個虛擬線程就要復制1000 次,性能很差。
InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>(); threadLocal.set("用戶數據"); for (int i = 0; i < 1000; i++) { Thread.ofVirtual().start(() -> { String data = threadLocal.get(); // 每個線程都有自己的副本 }); }
現在 Java 25 的 Scoped Values 特性轉正了,能解決 ThreadLocal 的這些問題。
什么是 Scoped Values?
Scoped Values 允許方法 在線程內以及子線程間安全高效地共享不可變數據。
和傳統的 ThreadLocal 相比,它不僅更安全,而且在虛擬線程環境下的內存開銷要小很多。
Scoped Values 和 ThreadLocal 的寫法很像:
import static java.lang.ScopedValue.where; ? public class UserService { private static final ScopedValue<String> USER_ID = ScopedValue.newInstance(); public void processRequest(String userId) { where(USER_ID, userId) // 寫入并綁定作用域 .run(this::doWork); // 自動清理,無需 remove() } public void doWork() { String userId = USER_ID.get(); // 讀取 System.out.println("處理用戶: " + userId); } }
這段代碼中,我們使用 where().run() 自動管理作用域,出了作用域就自動清理,更安全。
而且作用域一旦綁定,值就不能被修改,避免意外的狀態變更。
和虛擬線程配合使用時,所有虛擬線程共享同一份數據,內存占用更小:
ScopedValue<String> scopedValue = ScopedValue.newInstance(); where(scopedValue, "用戶數據").run(() -> { for (int i = 0; i < 1000; i++) { Thread.ofVirtual().start(() -> { String data = scopedValue.get(); // 所有線程共享同一份數據 }); } });
使用方法
1)支持返回值
除了 run() 方法,還可以使用 call() 方法來處理有返回值的場景:
public String processWithResult(String input) { return where(CONTEXT, input) .call(() -> { String processed = doSomeWork(); return "結果: " + processed; }); }
2)嵌套作用域
支持在已有作用域內建立新的嵌套作用域:
void outerMethod() { where(X, "hello").run(() -> { System.out.println(X.get()); // 輸出 "hello" where(X, "goodbye").run(() -> { System.out.println(X.get()); // 輸出 "goodbye" }); System.out.println(X.get()); // 輸出 "hello" }); }
3)多值綁定
可以在一個調用中綁定多個 Scoped Values,或者直接用類封裝多個值:
where(USER_ID, userId) .where(REQUEST_ID, requestId) .where(TENANT_ID, tenantId) .run(() -> { processRequest(); });
4)和結構化并發配合
Scoped Values 和 Java 結構化并發 API 可以打個配合,子線程自動繼承父線程的作用域值:
void handleRequest() { where(USER_ID, getCurrentUserId()) .run(() -> { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { var userTask = scope.fork(() -> loadUser()); // 子線程可以訪問 USER_ID var ordersTask = scope.fork(() -> loadOrders()); // 子線程可以訪問 USER_ID scope.join().throwIfFailed(); return new Response(userTask.get(), ordersTask.get()); } }); }
不過結構化并發這哥們也挺慘的,過了這么多個版本還沒轉正。等它轉正了,感覺 Java 并發編程的模式也要改變了。
使用場景
雖然 Scoped Values 聽起來比 ThreadLocal 更高級,但它不能 100% 替代 ThreadLocal。
如果你要在線程中共享不可變數據、尤其是使用了虛擬線程的場景,建議使用 Scoped Values;但如果線程中共享的數據可能需要更新,那么還是使用 ThreadLocal,要根據實際場景選擇。
【實用】模塊導入聲明
模塊導入聲明特性(Module Import Declarations)雖然是首次亮相,但它的設計理念可以追溯到 Java 9 的模塊系統。
模塊系統允許我們將代碼組織成模塊,每個模塊都有明確的依賴關系和導出接口,讓大型應用的架構變得更加清晰和可維護。

模塊導入聲明是在這個基礎上進一步簡化開發體驗。
以前我們使用多個 Java 標準庫的包需要大量的導入語句:
import java.util.Map; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.function.Function; import java.nio.file.Path; import java.nio.file.Files; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; // ... 還有更多
現在可以一行導入整個模塊:
import module java.base;
對于聚合模塊(比如 java.se),一次導入可以使用大量包:
import module java.se; // 導入 100 多個包 ? public class FullFeatureApp { // 可以使用整個 Java SE 平臺的所有公開 API }
不過我感覺這個特性會比較有爭議,我記得大廠的 Java 規約中是禁止使用通配符 * 方式導入所有類的,可讀性差、有命名沖突風險、依賴不明確。
而模塊導入的范圍更大,類名沖突可能更嚴重。如果導入多個模塊時遇到了同名類,還要再通過具體導入來解決:
import module java.base; // 包括 java.util.List 接口 import module java.desktop; // 包括 java.awt.List 類 ? import java.util.List; // 明確指定使用 util 包的 List
所以我可能不會用這個特性,純個人偏好。
【必備】緊湊源文件和實例主方法
這是我最喜歡的特性,直接打破了外界對于 Java 的刻板印象!
之前不是都說 Java 入門比 Python 難么?一個簡單的 Hello World 程序就要包含類、public、static、方法參數等概念。
傳統的 Hello World 程序:
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } }
用 Python 或者 JavaScript 直接寫一行代碼就完成了:
print("Hello World")
在 Java 25 中,Hello World 程序可以直接簡寫為 3 行代碼!
void main() { IO.println("Hello, World!"); }
這么一看和好多語言都有點像啊。。。安能辨我是 Java?
但是你知道,這 3 含代碼的含金量么?你知道 Java 為了支持簡寫成這 3 行代碼付出了多少努力么?

新的 IO 類
首先是 Java 25 在 java.lang 包中新增了 IO 類,提供更簡單的控制臺 I/O 操作:
void main() { String name = IO.readln("請輸入您的姓名: "); IO.print("很高興認識您,"); IO.println(name); }
IO 類的主要方法包括:
public static void print(Object obj); public static void println(Object obj); public static void println(); public static String readln(String prompt); public static String readln();
自動導入 java.base 模塊
在緊湊源文件中,所有 java.base 模塊導出的包都會自動導入,就像有這樣一行代碼:
import module java.base;
也就是說,你可以直接使用 List、Map、Stream 等常用類:
void main() { var fruits = List.of("蘋果", "香蕉", "橙子"); var lengths = fruits.stream() .collect(Collectors.toMap( fruit -> fruit, String::length )); IO.println("水果及其長度: " + lengths); }
這樣一來,Java 對初學者更友好,同時也讓有經驗的開發者能快速編寫腳本和小工具。
我覺得這個特性是讓 Java 再次偉大的關鍵,為什么呢?
Java 的核心競爭優勢在于它成熟完善的生態系統,但語法不夠簡潔;現在 Java 只要持續借鑒其他新興編程語言的優秀設計和語法特性、給開發者提供平滑的技術升級路徑,就還是會有很多開發者繼續使用 Java,就不會被別的語言取代。
【實用】靈活的構造函數體
這個特性解決的是 Java 自誕生以來就存在的一個限制:構造函數中的 super() 或 this() 調用必須是第一條語句。
這個限制雖然保證了對象初始化的安全性,但可能也會影響我們的編碼。
舉個例子:
class Employee extends Person { Employee(String name, int age) { super(name, age); // 必須是第一條語句 if (age < 18 || age > 67) { throw new IllegalArgumentException("員工年齡不符要求"); } } }
這種寫法的問題是,即使參數不符合要求,也會先調用父類構造函數,做一些可能不必要的工作。
Java 25 打破了這個限制,引入了新的構造函數執行模型,分為兩個階段:
-
序言階段:構造函數調用之前的代碼
-
尾聲階段:構造函數調用之后的代碼
簡單來說,允許在構造函數調用之前添加語句!
class Employee extends Person { String department; Employee(String name, int age, String department) { // 序言階段,可以校驗參數 if (age < 18 || age > 67) { throw new IllegalArgumentException("員工年齡必須在 18-67 之間"); } // 可以初始化字段 this.department = department; // 驗證通過后,再調用父類構造函數 super(name, age); // 尾聲階段,可以干點兒別的事 IO.println("新員工 " + name + " 已加入 " + department + " 部門"); } }
怎么樣,感覺是不是一直以來背的八股文、做的選擇題崩塌了!

此外,這個特性還能防止父類構造函數調用子類未初始化的方法:
class Base { Base() { this.show(); // 調用可能被重寫的方法 } void show() { IO.println("Base.show()"); } } ? class Yupi extends Base { private String message; Yupi(String message) { this.message = message; // 在 super() 之前初始化 super(); } @Override void show() { IO.println("消息: " + message); // message 已經正確初始化了 } }
現在,當創建 Yupi 對象時,message 字段會在父類構造函數運行之前就被正確初始化,避免了打印 null 值的問題。
限制條件
但是要注意,在序言階段有一些限制:
-
不能使用 this 引用(除了字段賦值)
-
不能調用實例方法
-
只能對未初始化的字段進行賦值
class Example { int value; String name = "default"; // 有初始化器 Example(int val, String nm) { // ? 允許:驗證參數 if (val < 0) throw new IllegalArgumentException(); // ? 允許:初始化未初始化的字段 this.value = val; // ? 不允許:字段已有初始化器 // this.name = nm; // ? 不允許:調用實例方法 // this.helper(); super(); // ? 現在可以正常使用 this 了 this.name = nm; this.helper(); } void helper() { /* ... */ } }
總之,這個特性讓構造函數變得更加靈活和安全,特別適合需要復雜初始化邏輯的場景。
【了解】密鑰派生函數 API
隨著量子計算技術的發展,傳統的密碼學算法面臨威脅,后量子密碼學成為必然趨勢。
因此 Java 也順應時代,推出了密鑰派生函數(KDF),這是一種從初始密鑰材料、鹽值等輸入生成新密鑰的加密算法。
簡單來說,你理解為 Java 出了一個新的加密工具類就好了,適用于對密碼進行加強、從主密鑰派生多個子密鑰的場景。
核心是 javax.crypto.KDF 類,提供了兩個主要方法:
-
deriveKey() 生成 SecretKey 對象
-
deriveData() 生成字節數組
比如使用 HKDF(HMAC-based Key Derivation Function)算法:
// 創建 HKDF 實例 KDF hkdf = KDF.getInstance("HKDF-SHA256"); ? // 準備初始密鑰材料和鹽值 byte[] initialKeyMaterial = "my-secret-key".getBytes(); byte[] salt = "random-salt".getBytes(); byte[] info = "application-context".getBytes(); ? // 創建 HKDF 參數 AlgorithmParameterSpec params = HKDFParameterSpec.ofExtract() .addIKM(initialKeyMaterial) // 添加初始密鑰材料 .addSalt(salt) // 添加鹽值 .thenExpand(info, 32); // 擴展為 32 字節 ? // 派生 AES 密鑰 SecretKey aesKey = hkdf.deriveKey("AES", params); ? // 或者直接獲取字節數據 byte[] derivedData = hkdf.deriveData(params);
【了解】緊湊對象頭
了解過 Java 對象結構的同學應該知道,Java 對象除了存儲數據外,還要通過 對象頭 存儲很多額外的信息,比如類型信息、GC 標記、鎖狀態等元數據。

如果程序中要創建大量小對象,可能對象頭本身占用的空間都比實際要存儲的數據多了!
比如下面這段代碼:
class Point { int x, y; // 實際數據只有 8 字節 } ? // 創建一堆 Point 對象 List<Point> points = new ArrayList<>(); for (int i = 0; i < 10000; i++) { points.add(new Point(i, i)); // 每個對象實際占用 24 字節! }
用來存儲傳統的對象頭在 64 位系統上占 16 字節,對于只有 8 字節數據的 Point 來說,開銷確實有點大。
Java 25 將緊湊對象頭特性轉正,把對象頭從 16 字節壓縮到 8 字節,減少了小對象的內存開銷。

但是,緊湊對象頭并不是默認開啟的,需要手動指定。畢竟少存了一些信息,必須考慮到兼容性,比如下面這幾種情況可能會有問題:
-
使用了 JNI 直接操作對象頭的代碼
-
依賴特定對象頭布局的調試工具
-
某些第三方性能分析工具
【了解】Shenandoah 分代收集
對于 Java 這種自動垃圾收集的語言,我們經常遇到這種問題:GC 一運行,應用就卡頓。特別是占用大堆內存的應用,傳統的垃圾收集器一跑起來,停頓時間可能多達幾百毫秒甚至幾秒。
Shenandoah 作為 Java 中延遲最低的垃圾收集器,在 Java 25 中 Shenandoah 的分代模式轉為正式特性。
什么是分代垃圾回收呢?
大部分對象都是 “朝生夕死” 的。將堆內存劃分為年輕代和老年代兩個區域,年輕代的垃圾收集可以更加頻繁和高效,因為大部分年輕對象很快就會死亡,收集器可以快速清理掉這些垃圾;而老年代的收集頻率相對較低,減少了對長期存活對象的不必要掃描。

經過大量測試,分代 Shenandoah 在保持超低延遲的同時,還能獲得更好的吞吐量。對于延遲敏感的應用來說,這個改進還是很實用的。
但是 Shenandoah 并不是默認的垃圾收集器,需要手動指定。而且分代模式也不是 Shenandoah 的默認模式,默認情況下 Shenandoah 還是使用單代模式。
預覽特性
這些特性仍在預覽階段,可以體驗 但不建議在生產環境使用!
【了解】結構化并發
目前我們經常使用 ExecutorService 實現并發,可能會寫下面這種代碼:
// 傳統寫法 Response handle() throws ExecutionException, InterruptedException { Future<String> user = executor.submit(() -> findUser()); Future<Integer> order = executor.submit(() -> fetchOrder()); String theUser = user.get(); // 如果這里異常了 int theOrder = order.get(); // 這個任務還在跑,造成線程泄漏! return new Response(theUser, theOrder); }
上述代碼存在一個問題,如果 findUser() 方法失敗了,fetchOrder() 還在后臺運行,浪費資源。而且如果當前線程被中斷,子任務不會自動取消。
什么是結構化并發?
結構化并發就是來解決并發編程中 線程泄露和資源管理 問題的。它在 Java 25 中第 5 次預覽,API 已經成熟,盲猜下個版本就要轉正了。
結構化并發的基本使用方法:
Response handle() throws InterruptedException { // 打開新作用域,打開作用域的線程是作用域的所有者。 try (var scope = StructuredTaskScope.open()) { // 使用 fork 方法在作用域中開啟子任務 var userTask = scope.fork(() -> findUser()); var orderTask = scope.fork(() -> fetchOrder()); // 等待所有子任務完成或失敗 scope.join(); return new Response(userTask.get(), orderTask.get()); } // 作用域結束時,所有子任務自動清理,不會泄漏 }
這樣就解決了幾個問題:
-
自動清理:任一任務失敗,其他任務自動取消
-
異常傳播:主線程被中斷,子任務也會取消
-
資源管理:可以配合 try-with-resources 保證資源釋放
更多用法
Java 25 提供了多種執行策略:
// 1. 默認策略:所有任務都要成功 try (var scope = StructuredTaskScope.open()) { // 任一失敗就全部取消 } ? // 2. 競速策略:任一成功即可 try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulResultOrThrow())) { var task1 = scope.fork(() -> callService1()); var task2 = scope.fork(() -> callService2()); var task3 = scope.fork(() -> callService3()); // 誰先成功就用誰的結果,其他任務取消 return scope.join(); } ? // 3. 收集所有結果(忽略失敗) try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) { // 等待所有任務完成,不管成功失敗 }
結構化并發搭配虛擬線程,可以輕松處理大量并發:
void processLotsOfRequests() throws InterruptedException { try (var scope = StructuredTaskScope.open()) { // 創建上萬個虛擬線程都沒問題 for (int i = 0; i < 10000; i++) { int requestId = i; scope.fork(() -> processRequest(requestId)); } scope.join(); // 等待所有請求處理完成 } // 自動清理,不用擔心線程泄漏 }
【了解】基本類型模式匹配
之前 Java 優化過很多次模式匹配,可以利用 switch 和 instanceof 快速進行類型檢查和轉換。
比如:
public String processMessage(Object message) { return switch (message) { case String text -> "文本消息:" + text; case Integer number -> "數字消息:" + number; case List<?> list -> "列表消息,包含 " + list.size() + " 個元素"; case -> "空消息"; default -> "未知消息類型"; }; }
但是目前模式匹配只能用于引用類型,如果你想在 switch 中匹配基本類型,只能用常量,不能綁定變量。
支持基本類型模式匹配后,switch 中可以使用基本類型:
switch (value) { case int i when i > 100 -> "大整數: " + i; case int i -> "小整數: " + i; case float f -> "浮點數: " + f; case double d -> "雙精度: " + d; }
注意這里的 int i,變量 i 綁定了匹配的值,可以直接使用。
而且基本類型模式會檢查轉換是否安全,這比手動寫范圍檢查方便多了!
int largeInt = 1000000; ? // 檢查能否安全轉換為 byte if (largeInt instanceof byte b) { IO.println("可以安全轉換為 byte: " + b); } else { IO.println("轉換為 byte 會丟失精度"); // 這個會執行 } ? // 檢查 int 轉 float 是否會丟失精度 int preciseInt = 16777217; // 2^24 + 1 if (preciseInt instanceof float f) { IO.println("轉換為 float 不會丟失精度"); } else { IO.println("轉換為 float 會丟失精度"); // 這個會執行 }
【了解】Stable Values 穩定值
這個特性對大多數開發者來說應該是沒什么用的。
不信我先考考大家:final 字段有什么問題?
答案是必須在構造時初始化。
舉個例子:
class OrderController { private final Logger logger = Logger.create(OrderController.class); }
這段代碼中,logger 必須立刻初始化,如果創建 logger 很耗時,所有實例都要等待,可能影響啟動性能。
特別是在對象很多、但不是每個都會用到某個字段的場景下,這種強制初始化就很浪費。
但我又想保證不可變性,怎么辦呢?
Stable Values 可以解決上述問題,提供 延遲初始化的不可變性:
class OrderController { private final StableValue<Logger> logger = StableValue.of(); Logger getLogger() { // 只在首次使用時初始化,之后直接返回同一個實例 return logger.orElseSet(() -> Logger.create(OrderController.class)); } }
說白了其實就是包一層。。。
還有更簡潔的寫法,可以在聲明時指定初始化邏輯:
class OrderController { private final Supplier<Logger> logger = StableValue.supplier(() -> Logger.create(OrderController.class)); void logOrder() { logger.get().info("處理訂單"); // 自動延遲初始化 } }
還支持集合的延遲初始化:
class ConnectionPool { // 延遲創建連接池,每個連接按需創建 private static final List<Connection> connections = StableValue.list(POOL_SIZE, index -> createConnection(index)); public static Connection getConnection(int index) { return connections.get(index); // 第一次訪問才創建這個連接 } }
重點是,Stable Values 底層使用 JVM 的 @Stable 注解,享受和 final 字段一樣的優化(比如常量折疊),所以不用擔心性能。
這個特性特別適合:
-
創建成本高的對象
-
不是每個實例都會用到的字段
-
需要延遲初始化但又要保證不可變的場景
沒記錯的話這個特性應該是首次亮相,對于追求性能的開發者來說還是挺實用的。
其他特性
【了解】移除 32 位 x86 支持
Java 25 正式移除了對 32 位 x86 架構的支持。
簡單來說就是:32 位系統現在用不了 Java 25 了。
不過對絕大多數朋友來說,這個變化應該沒啥影響。
【了解】JFR 性能分析增強
JFR(Java Flight Recorder)飛行記錄器是 JDK 內置的低開銷性能監控工具,可以記錄程序運行時的詳細數據。
它在這個版本獲得了幾個重要增強,包括更精確地測量 CPU 使用情況、通過協作式采樣保證了 JVM 安全性、可以更安全地在生產環境開啟分析等。
【了解】Vector API(第 10 次孵化)
Vector API 繼續孵化,主要改進:
-
更好的數學函數支持:現在通過 FFM API 調用本地數學庫,提高可維護性
-
Float16 支持增強:在支持的 CPU 上可以自動向量化 16 位浮點運算
-
VectorShuffle 增強:支持與 MemorySegment 交互
這個 API 對高性能計算、機器學習等領域很重要,尤其是現在 AI 的發展帶火了向量運算。但是我想說真的別再孵化了,等你轉正了,黃花菜都涼了。
以上就是 Java 25 的新特性了,不知道大家有什么想法,可以在評論區留言分享。
我的評價是:雖然 Java 25 在開發體驗、性能優化和并發編程方面都有進步,雨露均沾,但是目前對于新項目選擇 Java 21 就夠了,老項目還是可以繼續用 Java 8,真沒必要升級到 Java 25。
但是建議大家還是把 Java 8 之后的 Java 新特性都了解一下,拓寬下知識面,面試的時候也能跟面試官多聊幾句。我也把免費 Java 教程和 Java 新特性大全都整理到編程導航上了:
免費 Java 教程 + 新特性大全:

如果本期內容有收獲,記得點贊關注三連支持,我們下期見。


剛剛,Java 25 正式發布!
Java 25 都發布了哪些新特性?有沒有必要升級?一篇文章,帶你速通 Java 新特性。