Java 25 於 2025/9/16 正式發佈。
Java 25 / JDK 25: 一般可用性
Oracle 發佈 Java 25
Java 25 的到來
作為 LTS 版本,Java 25 將有許多來自 Java 21 的變更,因此我認為這將是一個長期使用的版本。這次的主要變化包括 main 方法的簡化 和 模組層級的 import 的正式化。除此之外,看來沒有其他重大影響。
支援 IO.println 因此在補完功能無法使用的環境中編寫程式碼會更方便。
另外, SoundClip 在開發簡單遊戲時非常實用。
如果希望在不安裝 JDK 的情況下嘗試語言或函式庫的新功能,Java Playground 是個不錯的選擇。
https://dev.java/playground/
Samples 中有新功能的範例。
將在福岡舉辦新功能解說的活動。
10/17(星期五) 在福岡工程師咖啡館將於19點開始進行。
Java 25 發佈活動@福岡 - connpass
詳細資訊請參考
JDK 25 發佈說明
Java SE 25 平台 JSR 400
OpenJDK JDK 25 GA 發佈
API 文件請見
概述 (Java SE 25 & JDK 25)
新增的 API 概覽請見
https://docs.oracle.com/en/java/javase/25/docs/api/new-list.html
API 差異請參見
https://cr.openjdk.org/~iris/se/25/build/latest/java-se--jdk-24-ga--jdk-25%2B36/
對於 Mac 和 Linux 的安裝,建議使用 SDKMAN!。
除了 Oracle OpenJDK 之外,還有幾個可以無償商業使用的發行版,如下所示:
※ Temurin、Liberica、Microsoft 尚未發佈
如果不知該使用哪個發行版,Eclipse Temurin 是個安全的選擇。
更新的時間表為 10 月將發佈 25.0.1,1 月將發佈 25.0.2。
Oracle JDK 的免費授權 NFTC 將在下一個 LTS 版本之前有效,之後的更新將轉為付費的 OTN,因此需要特別注意。
參與開發的組織比例並不高,但日本企業如富士通及 NTT 數據持續貢獻。
較大的變更已在 JEP 中匯總。
https://openjdk.org/projects/jdk/25/
本次共納入 18 個 JEP,其中 12 個正式採用。從預覽版本正式化的有 Scoped Values、Module Import Declarations、Compact Source Files 和 Flexible Constructor Bodies 四個,後三者使得程式碼變得更容易撰寫。預覽版本有 6 個,其中 3 個是新納入的。
這次的 JEP 被分為以下 5 個類別:
470: PEM 格式的加密物件編碼 (預覽)
502: 穩定值 (預覽)
503: 移除 32 位 x86 端口
505: 結構化併發 (第五次預覽)
506: Scoped Values
507: Patterns、instanceof 和 switch 中的基本類型 (第三次預覽)
508: 向量 API (第十次孵化)
509: JFR CPU 時間分析 (實驗性)
510: 雜湊功能 API
511: 模組導入聲明
512: 簡潔源文件與實例 main 方法
513: 靈活的建構子主體
514: 提前執行命令行舒適性
515: 提前執行方法分析
518: JFR 協作採樣
519: 簡潔物件標頭
520: JFR 方法計時與追蹤
521: 世代性 Shenandoah
這 18 個 JEP 是自實施半年以來的次多數量,僅次於上次的 24 個。
考量到上次 LTS 的 Java 21 與更早的 Java 17 之間的 JEP 數量為 37 個,這次吸納了從 Java 21 開始的 66 個 JEP,顯示功能有了相當大的改善。不過,同樣內容的 JEP 可能已多次作為預覽引入,需要相應地減少其計數。
在語言功能的變更中,沒有新功能,但之前是預覽的功能已經正式化。
507: Patterns、instanceof 和 switch 中的基本類型 (第三次預覽)
511: 模組導入聲明
512: 簡潔源文件與實例 main 方法
513: 靈活的建構子主體
此功能於 Java 23 中作為預覽引入,並且沒有變更地進入第三次預覽狀態。
當使用 instanceof 檢查原始類型時,判斷標準是檢查值是否缺失。
jshell> int i = 128
i ==> 128
jshell> i instanceof byte
$2 ==> false
jshell> i = 127
i ==> 127
jshell> i instanceof byte
$4 ==> true
當用整數類型檢查實數類型時,則會判斷精度是否降低。
jshell> double d = 1
d ==> 1.0
jshell> d instanceof int
$2 ==> true
jshell> d = 1.2
d ==> 1.2
jshell> d instanceof int
$4 ==> false
不過,對於包裝類的物件,當僅是其基本類型時才會返回 true。
jshell> Long num = 123L
num ==> 123
jshell> num instanceof int
| 錯誤:
| 不符合的類型: java.lang.Long無法轉換為int:
| num instanceof int
| ^-^
jshell> num instanceof long
$7 ==> true
然後可以用作模式匹配。
jshell> long num = 123
num ==> 123
jshell> var out = new ByteArrayOutputStream()
out ==>
jshell> if (num instanceof byte b) out.write(b)
jshell> out.toByteArray()
$21 ==> byte[1] { 123 }
jshell> num = 1234
num ==> 1234
jshell> if (num instanceof byte b) out.write(b)
jshell> out.toByteArray()
$24 ==> byte[1] { 123 }
這個模式匹配也可以用於 switch,且由於有常數模式的存在,所以所有類型都可以用於 switch。
jshell> long num = 1234
num ==> 1234
jshell> switch (num) { case int n -> "整數"; default -> "其他";}
$25 ==> "整數"
jshell> num = 30_0000_0000L
num ==> 3000000000
jshell> switch (num) { case int n -> "整數"; default -> "其他";}
$27 ==> "其他"
類似於 if 表達式也可以這麼寫。
boolean flag = isFoo();
var s = switch (flag) {
case true -> "是的";
case false -> "不是";
}
此功能在 Java 23 中作為預覽引入,並在 Java 25 中正式化。
支援模組層級的 import,進行 java.base 的 import 通常應該會沒問題。此外,外部函式庫的模組支援也應該會跟進。
與 Java 23 時的變更相比,現在依賴的模組會隨之 import。因此導入 java.se 時,會自動導入 Java SE 的所有 API。
像下面這樣會導入屬於 java.base 模組的 java.util 及 java.io。
import module java.base;
在 JShell 中,java.base 會自動被導入。
例如,因為 java.time 並未自動導入,直接使用 LocalDate 會報錯。
>jshell
| 歡迎來到 JShell -- 版本 24
| 如需介紹,請輸入: /help intro
jshell> LocalDate.now()
| 錯誤:
| 找不到符號
| 符號: 變數 LocalDate
| 位置: 類別
| LocalDate.now()
| ^-------^
在 Java 25 中,無需導入即可直接使用 LocalDate。
>jshell
| 歡迎來到 JShell -- 版本 25
| 如需介紹,請輸入: /help intro
jshell> LocalDate.now()
$1 ==> 2025-09-16
進行 GUI 程式設計時,需要導入 java.desktop
模組。
jshell> import module java.desktop;
jshell> var f = new JFrame("你好")
f ==> javax.swing.JFrame[frame0,0,0,0x0,invalid,hidden, ... tPaneCheckingEnabled=true]
jshell> var b = new JButton("確定")
b ==> javax.swing.JButton[,0,0,0x0,invalid,alignmentX=0 ... text=確定,defaultCapable=true]
但這樣會導致 List
變成 java.util.List
和 java.awt.List
的重名,因此使用時會發生歧義。
jshell> List a = null;
| 錯誤:
| List 的引用存在歧義
| java.awt 的類別 java.awt.List 與 java.util 的介面 java.util.List兩者都符合
| List a = null;
| ^--^
在這種情況下,需要導入實際使用的類別,例如 java.util.List
。
jshell> import java.util.List
jshell> List a = null
a ==> null
Java 24 中的 Simple Source Files and Instance Main Methods 改名為 Compact Source Files...並正式化。這樣的命名更為精確。
這讓我們擺脫了 public static void main 的麻煩。
Java 脫離 public static void main 的束縛 - きしだのHatena
在 Java 中,撰寫簡單的 Hello World 程式碼需要以下代碼:
public class Hello {
public static void main(String[] args) {
System.out.println("Hello Java!");
}
}
現在可以這樣編寫:
void main() {
IO.println("Hello Java!");
}
關鍵字 public、class、static 等都消失了,[]
這種怪異的符號也消失了。
在學習程式中,最初想要做的事是編寫程式碼,而類別是組織這些程式的手段,因此在理解程式之前學習類別並沒有太大意義。
public 等訪問修飾符是為了防止在大型程式中使用不當元素,在入門範例中並不需要。
static 的解釋需要理解類別或實例,但在學習之前來說過早,因此留著這個功能。
陣列等知識也無法在未知變數的情況下提前學習,因此很少在入門時的範例中傳遞參數 args
。
這導致「難以理解的習慣」被放置未處理,或只能在無法理解的狀態下進行「System.out.pritln 的解釋」,但因類別名與文件名不同,導致運行的過程中無法完成。
因此,為了集中於初期應學的內容,放寬了以下限制。
可能會有人問「方法是否也可以不需要?」不過,當前沒有同一層級寫陳述和方法的機制,因此仍需保留,因為推出新的一級函數式樣會大大影響「專注於初期應學的內容」。
相關設計筆記已在以下文檔中概述。
https://openjdk.org/projects/amber/design-notes/on-ramp
main 方法無需使用 static 限定符。因此,因不必將 static 附加於 main 方法,從主方法中調用的其他方法也不需要 static,使撰寫稍大範圍的範例變得容易。
public class Hello {
public void main() {
foo();
}
void foo() {
IO.println("Hello");
}
}
不必撰寫無用代碼使得代碼簡潔。
main 方法無法設為 private,protected 則是允許的。
在未看參考的情況下能夠流暢地撰寫「public static void main(String[] args)」既是一種幸福感,失去這部分的確有些遺憾,但這只是一種懷舊想法。
除了不需要瞭解類別,還可不必考慮班名,並且縮排減少了,更少出錯的可能,因為只需要一組中括號,做有齊全需找一個合適的中括號。
void main() {
IO.println("Hello Java");
}
省略類別並使用實例方法的 main 時,將會被包裹在 new Object(){}
中。
new Object() {
void main() {
IO.println("Hello Java");
}
}.main();
不過,在省略類別定義的情況下,如果文件名帶有 static.java 這樣的關鍵字,就會報錯。在有類別定義的情況下,如果直接使用 java
命令執行則不會有問題。
>java static.java
static.java:3: 錯誤: 無效的文件名: static
void main() {
^
錯誤數量 1
錯誤: 編譯失敗
在使用隱式類別時,java.base 模組也會被導入。
該代碼可以無需顯式導入。
void main() {
var data = List.of("test", "data");
IO.println(data.stream().collect(Collectors.joining(" ")));
}
在 Java 22 中「在 super 之前的陳述」改名後於 Java 23 中變更,並在 Java 25 中正式化。
現在可以在呼叫父類別構造函數前使用 this
的語句。
例如,如果有一個接受兩個 List 的類別構造函數:
class Foo {
Foo(List l1, List l2) {
}
}
在繼承這個構造函數時,必須給定兩個 List。
class Bar extends Foo {
Bar() {
super(List.of("abc", "def"), List.of("abc", "def"));
}
}
由於都是相同的,想要合併時,由於無法在 super
之前執行語句,需考慮使用迂迴構造。
class Bar extends Foo {
private Bar(List l) {
super(l, l);
}
Bar() {
this(List.of("abc", "def"));
}
}
現在可以這麼寫:
class Bar extends Foo {
Bar() {
var param = List.of("abc", "def");
super(param, param);
}
}
如此一來,當需要參數檢查、構建或共享的時候,就能以自然的方式進行程式碼的編寫。
若在使用 super
的前面進行字段的訪問,則會導致錯誤。
class Bar extends Foo {
List<String> param;
Bar() {
param = List.of("abc", "def");
super(param, param);
}
}
test.java:11: 錯誤: 無法在呼叫超類構造函數之前引用 param
param = List.of("abc", "def");
^
API 的變更除安全性外共有 4 個 JEP,其中新增的為 Stable Value。Scoped Value 已正式化。此外,還有許多未成為 JEP 的變更。
502: 穩定值 (預覽)
505: 結構化併發 (第五次預覽)
506: Scoped Values
508: 向量 API (第十次孵化)
未成為 JEP 的 API 變更中,有些動作是更容易理解的。
引入了 java.lang.IO
類別。此類別定義了 print
、println
和 readln
三個靜態方法。
利用它可以使用 IO.println
而非 System.out.println
來編寫 Hello World,如下所示。
void main() {
IO.println("Hello");
}
若進行靜態導入,僅透過 println
就可進行字符串輸出。
import static java.lang.IO.*;
void main() {
println("Hello");
}
在 JShell 或記事本等無法使用補全功能的編輯環境中輸入代碼特別方便。
可以這樣播放 WAV 文件。
var file = new File("sound.wav");
var clip = SoundClip.createSoundClip(file);
clip.play();
過去,播放音頻文件最便捷的手段是 Applet API 的 AudioClip,但在 Java 17 中已被標記為過時,預定於 Java 26 中被刪除
因此,引入 SoundClip 類作為便捷播放音頻的替代方案。
[JDK-8356049] 需要一種簡單方法播放聲音片段 - Java 錯誤系統
標準情況下僅支持無壓縮 PCM,因此播放 MP3 時需要引入 SPI。
Java 25 播放 MP3 的方法 - きしだのHatena
在將其用於遊戲音效時的注意事項如下。
Java 25 的 SoundClip 為遊戲添加音效 - きしだのHatena
在 Reader 類中新增了返回 List<String>
的 [readAllLines
](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/io/Reader.html#readAllLines()) 和返回 String
的 [readAllAsString
](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/io/Reader.html#readAllAsString()) 這兩個方法。
public List<String> readAllLines() throws IOException
public String readAllAsString() throws IOException
在對字符串進行字符級處理時,新增了按範圍將字符批量讀入 char 陣列的方法。
這似乎旨在簡化 Reader.of
的實現,並且在對字符級字符串進行操作時也會提高效率。
[JDK-8343110] 為 CharSequence 和 CharBuffer 新增 getChars(int, int, char[], int) - Java 錯誤系統
在使用 HttpClient 處理傳輸數據的 BodyHandler / BodySubscriber 時,現在可以更輕鬆地設置最大字節數。
如下面這樣執行時,僅獲取 250 字節的字符串。
client.send(request, BodyHandlers.limiting(BodyHandlers.ofString(), 250))
在 ForkJoinPool
中新增了與 ScheduledFuture
相關的方法。
每次都有少量方法增加至 StrictMath,本次也新增了 7 個方法。
在 Java 中,持有不可變值的機制是 final
。
final Logger logger = Logger.create();
不過,當在類別或物件初始化時立即初始化所有不可變值,會導致啟動變慢,或集中存取網路和檔案。
因此,通常希望在需要值時進行初始化。
這是透過訪問器獲取值的方式來實現延遲初始化的。
private Logger logger;
Logger getLogger() {
if (logger == null) {
logger = Logger.create();
}
return logger;
}
但如此一來,從字段移除 final
會使得 JVM 在優化時的效果減弱。此外考慮到多線程情況,可能需要一些複雜的邏輯。或者,還需要評估在多線程場景下是否允許重複初始化,在沒有實質問題的情況下仍使用不安全的代碼。
此次引入的 StableValue
給了我們一種新的選擇。
private final StableValue<Logger> logger = StableValue.of();
Logger getLogger() {
return logger.orElseSet(() -> Logger.create());
}
在此時,傳遞給 orElseSet
的初始化代碼僅會被調用一次,由 StableValue
保證,並且是線程安全的。此外,JVM 也會進行優化。
如果覺得定義訪問器太繁瑣,可以使用 supplier
。
final Supplier<Logger> logger = StableValue.supplier(() -> Logger.create());
要操作不變值的集合,可以使用 list
。
final List<Connection> pool = StableValue.list(10, idx -> new Connection(idx + "th connection"));
如此一來,透過 pool.get(3)
訪問時,對應索引處的值會延遲初始化。
StableValue
為預覽版,使用時需要添加 --enable-preview
。
在 Java 19 中作為孵化功能,引入的結構化任務範圍於 Java 24 中從類別變更為介面,並使用 open
工廠方法獲取實例並進入第五個預覽版本。
在並行處理時,執行多個任務時,可能會要求同時完成兩個任務之後正常結束,或其中一個任務完成後退出,也可在其中一個任務出現例外情況時也要結束。
不過,使用現有的 join
和 wait
等方式控制時,實際上容易產生難以追蹤的代碼。
因此,導入結構化並行性。
如下示範,詳細情況稍後會進一步說明!(自 Java 19 起即宣告...)
Response handle() throws InterruptedException {
try (var scope = StructuredTaskScope.open()) {
Subtask<String> user = scope.fork(() -> findUser());
Subtask<Integer> order = scope.fork(() -> fetchOrder());
scope.join(); // 同時關聯檢測結果是否發生例外
// 兩個子任務都成功完成后,從而組合結果
return new Response(user.get(), order.get());
}
}
Subtask
繼承自 Supplier
,因此將其作為 Supplier
使用可能更加合適。
在 Java 20 中作為實驗性引入,Java 21 提供預覽,Java 24 開始正式化,則使用 ScopedValue.orElse
不再接受 null。
共享同一線程內的值時,可以使用 ThreadLocal
,並以輕鬆的方式為每個線程指定值。