JDK17 與 JDK11 特性差異淺談
1.1 switch 表達式語法變化
- 在 JDK12 之前如果 switch 忘記寫 break 將導致貫穿,在 JDK12 中對 switch 的這一貫穿性做了改進。你只要將 case 后面的冒號改成箭頭,那么你即使不寫break也不會貫穿了。
- switch 可作為表達式,不再是單獨的語句。
- 當你把 switch 中的 case 后的冒號改為箭頭之后,此時 switch 就不會貫穿了,但在某些情況下,程序本來就希望貫穿比如我就希望兩個 case 共用一個執(zhí)行體。JDK12 的 switch 中的 case 也支持多值匹配,這樣程序就變得更加簡潔了。
- JDK13 引入了一個新的 yield 語句來產(chǎn)生一個值,該值成為封閉的 switch 表達式的值。yield 和 return 的主要區(qū)別在于它們?nèi)绾慰刂瞥绦虻牧鞒獭eturn 會結(jié)束當前的方法或函數(shù),并將控制權(quán)返回給調(diào)用者。而 yield 則會暫時離開當前的 switch 表達式,將一個值返回給調(diào)用者,然后再回到 switch 表達式的地方繼續(xù)執(zhí)行。
public class Demo{
public static void main(String[] args){
var score = 'C';
// 執(zhí)行switch分支語句
String s = switch (score){
case 'A', 'B' -> "上等";
case 'C' -> "中等";
case 'D', 'E' -> "下等";
default -> {
if (score > 100) {
yield "數(shù)據(jù)不能超過100";
} else {
yield score + "此分數(shù)低于0分";
}
}
}
}
}
1.2 微基準測試套件
JMH ,即 Java Microbenchmark Harness ,是專門用于代碼微基準測試的工具套件。
JMH 典型的應(yīng)用場景
- 想定量地知道某個方法需要執(zhí)行多長時間,以及執(zhí)行時間和輸入?yún)?shù)的相關(guān)性。
- 一個接口有兩種不同實現(xiàn),希望比較哪種實現(xiàn)性能更好。
JMH 使用案例
增加 JMH 的依賴
<properties>
<jmh.version>1.14.1</jmh.version>
</properties>
<dependencies>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
代碼編寫
import org.openjdk.jmh.annotations.*;
@State(Scope.Thread)
public class MyBenchmark {
@Benchmark
@BenchmarkMode(Mode.All)
public void testMethod() {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Benchmark
@BenchmarkMode(Mode.All)
public void testMethod2() {
try {
Thread.sleep(600);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
public class BenchmarkRunner {
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.forks(1)
.warmupIterations(5)
.measurementIterations(5)
.build();
new Runner(opt).run();
}
}
//以下這些方法都是JMH的一部分,可以在任何版本的JMH中使用。
//include(SimpleBenchmark.class.getSimpleName()) :這個方法表示你想要運行哪個類的基準測試。
//exclude("xxx") :這個方法表示你想要在基準測試中排除哪個方法。
//forks(1) :這個方法表示你想要進行多少輪的基準測試。每一輪測試都會在一個新的 JVM 進程中進行,以確保每輪測試的環(huán)境是獨立的。
//warmupIterations(5) :這個方法表示你想要進行多少次預熱迭代。預熱迭代是為了讓 JVM 達到穩(wěn)定狀態(tài),預熱迭代的結(jié)果不會被計入最終的基準測試結(jié)果。
//measurementIterations(5) :這個方法表示你想要進行多少次正式的基準測試迭代,這些迭代的結(jié)果會被用來計算基準測試的最終結(jié)果。
結(jié)果輸出(只截取了一部分)
圖片
圖片
相關(guān)注解
@BenchmarkMode
對應(yīng) Mode 選項,可用于類或者方法上,需要注意的是,這個注解的 value 是一個數(shù)組,可以把幾種 Mode 集合在一起執(zhí)行,還可以設(shè)置為 Mode.All ,即全部執(zhí)行一遍。
圖片
- 吞吐量 (thrpt) :單位時間內(nèi)完成的操作次數(shù),也可以理解為每秒能處理的事務(wù)數(shù)。
- 平均時間 (avgt) :每次操作所需的平均時間。
- 樣本時間 (sample) :隨機取樣,最后輸出取樣結(jié)果的分布,例如“99%的調(diào)用在xxx毫秒以內(nèi),99.99%的調(diào)用在xxx毫秒以內(nèi)”。
- 單次啟動時間 (ss) :SingleShotTime ,度量一次操作的運行時間,即每次迭代前都會重新初始化狀態(tài)。
@State
類注解,JMH 測試類必須使用 @State 注解,State 定義了一個類實例的生命周期,可以類比 SpringBean 的 Scope 。由于 JMH 允許多線程同時執(zhí)行測試,不同的選項含義如下:
Scope.Thread :默認的 State ,每個測試線程分配一個實例。
Scope.Benchmark :所有測試線程共享一個實例,用于測試有狀態(tài)實例在多線程共享下的性能。
Scope.Group :每個線程組共享一個實例。
如果你想測試一個對象在多線程環(huán)境下的行為,你可以選擇 Scope.Benchmark 。如果你想要每個線程都有自己的狀態(tài),你可以選擇 Scope.Thread 。如果你想要在同一線程組內(nèi)的所有線程共享狀態(tài),你可以選擇 Scope.Group 。
@OutputTimeUnit
benchmark 結(jié)果所使用的時間單位,可用于類或者方法注解,使用 java.util.concurrent.TimeUnit 中的標準時間單位。
@Benchmark
方法注解,表示該方法是需要進行 benchmark 的對象。
1.3 生成類數(shù)據(jù)共享特性優(yōu)化
背景:在同一個物理機上啟動多個 JVM 時,如果每個虛擬機都單獨裝載自己需要的所有類,啟動成本和內(nèi)存占用是比較高的。所以引入了類數(shù)據(jù)共享機制 ( Class Data Sharing ,簡稱 CDS ) 的概念,通過把一些核心類在每個 JVM 間共享,每個 JVM 只需要裝載自己的應(yīng)用類即可。
JDK12 之前想要利用 CDS 的用戶,必須 -Xshare:dump 作為額外的步驟來運行。
JDK12 針對 64 位平臺使其默認生成類數(shù)據(jù)共享 ( CDS ) 歸檔。
好處:JVM啟動時間減少了。因為核心類是共享的,所以 JVM 的內(nèi)存占用也減少了。
JDK13 則支持在應(yīng)用運行之后進行動態(tài)歸檔。需要使用 -XX:ArchiveClassesAtExit=filename.jsa 選項來指定一個文件,JVM 會在退出時將應(yīng)用程序類和標準庫類的元數(shù)據(jù)寫入這個文件。然后,在下一次啟動 JVM 時,你可以使用 -XX:SharedArchiveFile=filename.jsa 選項來指定剛才創(chuàng)建的文件。
好處:這個特性允許 JVM 在運行時捕獲類的元數(shù)據(jù),然后在下一次 JVM 啟動時重用這些元數(shù)據(jù),從而提高啟動速度和減少內(nèi)存占用。
1.4 G1 垃圾收集器和 ZGC 功能增強
- 在 JDK12 之前,一旦開始執(zhí)行垃圾收集,即使可能會超過 -XX:MaxGCPauseMillis 參數(shù)設(shè)定的值,也不會停止。JDK12 中,如果 G1 垃圾收集器有可能超過預期的暫停時間,則可以終止。G1 垃圾收集器發(fā)現(xiàn)反復標記過多的區(qū)域后,G1 就會切換到更增量的一種 Mix GC 。它將待回收的集合分為兩個部分:強制回收和可選回收。強制回收的部分是 G1 無法增量回收的部分(比如年輕代)也可以包含能增量回收的部分(比如老年代)來提升性能,它占待回收集合的 80% 。當 G1 回收完強制回收部分后,如果還有多余的時間,將會更細粒度的處理可選回收部分,每次只會處理一個區(qū)域,避免超過用戶指定的時間。在處理完一個區(qū)域后,G1 會根據(jù)剩余的時間來決定是否繼續(xù)處理剩余的可選部分。
- JDK12 中,如果應(yīng)用程序活動非常低,G1 可以使其能夠在空閑時自動將 Java 堆內(nèi)存返還給操作系統(tǒng)。
- JDK13 中,ZGC 也能夠主動釋放未使用內(nèi)存給操作系統(tǒng),但可以通過 -XX : -ZUncommit 參數(shù)來顯示關(guān)閉此功能。ZGC 支持最大堆大小為 16TB ,可以滿足大多數(shù)大型服務(wù)器的需求。
- ZGC 是在 JDK11 中引入的垃圾回收器,但一直都是實驗版本,在 JDK15 中正式上線,如果你的應(yīng)用程序需要處理非常大的堆或者更低的暫停時間,那么 ZGC 可能是一個更好的選擇。如果你對兼容性和穩(wěn)定性有更高的要求,因為 G1 經(jīng)過長時間的驗證和優(yōu)化,可能 G1 更適合。
1.5 ShenandoahGC
添加一個名為 Shenandoah 的新垃圾收集算法,通過與正在運行的 Java 線程同時進行疏散工作來減少 GC 暫停時間,最終目標旨在針對 JVM 上的內(nèi)存收回實現(xiàn)低停頓的需求。
Shenandoah 是以實驗特性在 JDK12 中引入的。在 JDK15 中正式上線。
使用 Shenandoah 的暫停時間與堆大小無關(guān),這意味著無論堆是 200MB 還是 200GB ,都將具有相同的一致暫停時間。與 ZGC 類似,Shenandoah GC 主要目標是 99.9% 的暫停小于 10ms ,暫停與堆大小無關(guān)等。
ZGC 和 ShenandoahGC 的一些主要區(qū)別:
- 設(shè)計目標:ZGC 和 ShenandoahGC 都是為了實現(xiàn)低延遲的垃圾收集而設(shè)計的。
- 并發(fā)回收:ShenandoahGC 實現(xiàn)了并發(fā)回收,這意味著它可以在應(yīng)用線程運行的同時進行垃圾收集,從而減少了垃圾收集對應(yīng)用性能的影響。
- 內(nèi)存管理:ShenandoahGC 使用名為“連接矩陣”的全局數(shù)據(jù)結(jié)構(gòu)來記錄跨 Region 的引用關(guān)系,降低了處理跨代指針時的記憶集維護消耗。而 ZGC 的 Region 可以動態(tài)創(chuàng)建和銷毀,容量也可以動態(tài)調(diào)整。
- 開發(fā)者:Shenandoah 由 RedHat 開發(fā),而 ZGC 由 Oracle 開發(fā)。
使用方法:要啟用/使用 Shenandoah GC,需要以下 JVM 選項: -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC。作為實驗性功能,Shenandoah 構(gòu)建系統(tǒng)會自動禁用不受支持的配置。
1.6 String 新增方法
- transform(Function):對字符串進行處理后返回。
var rs = "test".transform(s -> s + "Java").transform(s -> s.toUpperCase());
// TESTJAVA
- indent:該方法允許我們調(diào)整String實例的縮進。
String result = "Java\njava\ntest".indent(3);
/*結(jié)果會縮進三格
Java
java
test
*/
1.7 Files 新增 mismatch 方法
返回內(nèi)容第一次不匹配的字符位置索引。
System.out.println(Files.mismatch(Path.of("a.txt"),Path.of("b.txt")));
1.8 核心庫 java.text 支持壓縮數(shù)字格式
NumberFormat 添加了對 ”緊湊形式格式化數(shù)字“ 的支持。
”緊湊數(shù)字格式“是指以簡短或人類可讀形式表示的數(shù)字。
例如,在 en_US 語言環(huán)境中,1000 可以格式化為 “ 1K ”,1000000 可以格式化為 “ 1M ”,具體取決于指定的樣式 NumberFormat.Style 。緊湊數(shù)字格式由 LDML 的 Compact Number 格式規(guī)范定義。要獲取實例,請使用 NumberFormat 緊湊數(shù)字格式所給出的工廠方法之一。
NumberFormat fmt = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);
String result = fmt.format(1000);
//1K
var cnf = NumberFormat.getCompactNumberInstance(Locale.CHINA,NumberFormat.Style.SHORT);
System.out.println(cnf.format(5_0000));
//"5萬"
System.out.println(cnf.format(7_9200));
//"7.9萬"
System.out.println(cnf.format(8_000_000));
//"800萬"
System.out.println(cnf.format(9L << 30));
//"96億"
System.out.println(cnf.format(6L << 50));
//"5637142兆"
System.out.println(cnf.format(6L << 60));
//"6917529京"
1.9 JDK17 支持到 Unicode13
JDK12 支持 Unicode11.0
JDK13 支持 Unicode12.1
從 JDK14 到 JDK17 均是支持 Unicode13.0
1.10 NullPointerExceptions 升級
JDK14 之前,從報錯中我們只能得到錯誤出現(xiàn)的行數(shù),但在 JDK14 之后,會清晰的告訴你哪個對象空指針了。
Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "String.charAt(int)" because "str" is null
at com.qf.jdk14.Test.main(Test.java:11)
1.11 文本塊特性
背景:在 Java 中,在字符串文字中嵌入 HTML ,XML ,SQL 或 JSON 片段通常需要先進行轉(zhuǎn)義和串聯(lián)的大量編輯,然后才能編譯包含該片段的代碼。該代碼段通常難以閱讀且難以維護。
Java 的文本塊特性是在 JDK15 中正式實現(xiàn)的。這個特性首先在 JDK13 中以預覽版的形式發(fā)布,然后在 JDK14 中改進并再次以預覽版的形式發(fā)布。這一特性提高了 Java 程序書寫大段字符串文本的可讀性和方便性。
文本塊的開頭定界符是由三個雙引號 """ 開始,從新的一行開始字符串的內(nèi)容,以 """ 結(jié)束。如果結(jié)束的 """ 另起一行時,字符串內(nèi)容最后會留有一新行。
使用案例
String query = "SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`\n" +
"WHERE `CITY` = 'INDIANAPOLIS'\n" +
"ORDER BY `ID`, `LAST_NAME`;";
//使用文本塊語法
String query = """
SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`
WHERE `CITY` = 'INDIANAPOLIS'
ORDER BY `EMP_ID`, `LAST_NAME`;""";
String html = "<html>\n" +
" <body>\n" +
" <p>Hello, world</p>\n" +
" </body>\n" +
"</html>\n";
//使用文本塊語法
String html = """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";
縮進示例
Java 編譯器會自動刪除不需要的縮進:
- 每行結(jié)尾的空格都會刪除。
- 每行開始的共有的空格會自動刪除。
- 只保留相對縮進。
- 新行 """ 結(jié)束時,將 """ 向左調(diào)整,則可以給所有行前加相應(yīng)數(shù)量的空格。將 """ 向右調(diào)整,沒有作用。
System.out.println("""
Hello,
multiline
text blocks!
""");
// 結(jié)果
// > Hello,
// > multiline
// > text blocks!
1.12 重新實現(xiàn)舊版 Socket API
背景:現(xiàn)在已有的 java.net.Socket 和 java.net.ServerSocket 以及它們的實現(xiàn)類,都可以回溯到 JDK1.0 時代了。原始 socket 的維護和調(diào)試都很痛苦。實現(xiàn)類還使用了線程棧作為 I/O 的緩沖,導致在某些情況下還需要增加線程棧的大小。該實現(xiàn)還存在幾個并發(fā)問題,需要徹底解決。在未來的網(wǎng)絡(luò)世界,要快速響應(yīng),不能阻塞本地方法線程,當前的實現(xiàn)不適合使用了。
JDK13 全新實現(xiàn)的 NioSocketImpl 來替換 JDK1 的 SocketImpl 和 PlainSocketImpl。
- 它便于維護和調(diào)試,與 NewI/O (NIO) 使用相同的 JDK 內(nèi)部結(jié)構(gòu),因此不需要使用系統(tǒng)本地代碼。
- 它與現(xiàn)有的緩沖區(qū)緩存機制集成在一起,這樣就不需要為 I/O 使用線程棧。
- 它使用 java.util.concurrent 鎖,而不是 synchronized 同步方法,增強了并發(fā)能力。
- 新的實現(xiàn)是 JDK13 中的默認實現(xiàn),但是舊的實現(xiàn)還沒有刪除,可以通過設(shè)置參數(shù) -Djdk.net.usePlainSocketImpl=true 來切換到舊版本。
1.13 Hidden Classes
通常我們在使用大型的框架或者 lambda 表達式的時候,會動態(tài)生成很多類。但是不幸的是標準的定義類的API:ClassLoader::defineClass 和 Lookup::defineClass 不能夠區(qū)分出這些類是動態(tài)生成(運行時生成)的還是靜態(tài)生成(編譯生成)的。
一般來說動態(tài)生成的類生命周期更短,并且其可?性要更低。但是現(xiàn)有的 JDK 并沒有這個功能。
所有有了 HiddenClasses 的提案,通過 HiddenClasses ,不管是 JDK 還是 JDK 外部的框架,在生成動態(tài)類的時候都可以定義為 HiddenClasses,這樣可以更加有效的控制這些動態(tài)生成類的生命周期和可?性。
1.14 instanceof 關(guān)鍵詞
instanceof關(guān)鍵詞主要用來判斷某個對象是不是某個類的實例。
比如,有時候我們要處理一個類似這樣的數(shù)據(jù)集:
Map<String, Object> data = new HashMap<>();
data.put("test", "111");
data.put("test2", 222);
JDK16 之前需要先判斷獲取的 value 是否是 String ,再做強制類型轉(zhuǎn)換:
Object value = data.get("test");
if (value instanceof String)
{
String s = (String) value;
System.out.println(s.substring(1));
}
在 JDK16 的增強之后,對于 instanceof 的判斷以及類型轉(zhuǎn)換可以合二為一了:
Object value = data.get("test");
if (value instanceof String s)
{
System.out.println(s.substring(1));
}
1.15 檔案類
Records 的目標是擴展 Java 語言語法,Records 為聲明類提供了一種緊湊的語法,通過對類做這樣的聲明,編譯器可以通過自動創(chuàng)建所有方法并讓所有字段參與 hashCode() 等方法。其目的是為了充當不可變數(shù)據(jù)的透明載體的類。
舊方法定義實體類,代碼如下:
public final class User {
final String name;
final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
通過 Record 類方式,一句話就可以實現(xiàn)以上功能,代碼如下:
public record User(String username, String password) {}
在 JDK16 之前的版本中,我們不能在類名后面直接寫參數(shù)來定義類的狀態(tài)。這是 JDK16 引入 record 類的一個新特性。
調(diào)用 Record 類方式,如下:
public class App {
public static void main(String[] args) {
User user = new User("user", "123456");
System.out.println(user.username());
}
}
注意事項:
- record 類不允許使用 abstract 關(guān)鍵字定義為抽象
- 所有成員變量均為 final 修飾,不允許再次賦值
- 允許出現(xiàn)靜態(tài)變量/實例方法/靜態(tài)方法
- 允許出現(xiàn)其他構(gòu)造方法,但必須調(diào)用 record 構(gòu)造方法
- Record 不允許 extends 繼承其他類
1.16 密封類
在 JDK15 中,Java 提出了密封類( Sealed Classes )的概念,在 JDK17 中被正式確認。密封類允許類和接口定義其允許的子類型。因此,如果一個類沒有顯式地使用 sealed 、non-sealed 或 final 關(guān)鍵字,那么它的默認權(quán)限就是 non-sealed 。
以下是一個密封類的代碼示例:
sealed class Human permits Kyrie, LeBron, George {
public void printName() {
System.out.println("Default");
}
}
non-sealed class Kyrie extends Human {
public void printName() {
System.out.println("Bob");
}
}
sealed class LeBron extends Human {
public void printName() {
System.out.println("Mike");
}
}
final class George extends Human {
public void printName() {
System.out.println("Yannick");
}
}
在這個例子中,Human 是一個密封類,它只允許 Kyrie,LeBron 和 George 這三個類繼承。這樣,我們就可以更精確地控制哪些類可以繼承 Human 類,從而提高代碼的安全性和可讀性。
1.17 統(tǒng)一日志異步刷新
在 JDK17 中,引入了一項新特性:統(tǒng)一日志異步刷新。
先將日志寫入緩存,然后再異步地刷新到日志文件,這樣寫日志的操作就不會阻塞執(zhí)行業(yè)務(wù)邏輯的線程,從而提高了程序的運行效率。這個特性對于需要大量日志輸出,并且對性能有較高要求的應(yīng)用來說,是一個非常實用的改進。可以通過傳遞命令行選項 -Xlog:async 來開啟此功能。
總結(jié)
從 JDK11 到 JDK17 ,Java 的發(fā)展經(jīng)歷了一系列重要的里程碑。其中最重要的是 JDK17 的發(fā)布,這是一個長期支持(LTS)版本,它將獲得長期的更新和支持,有助于保持程序的穩(wěn)定性和可靠性。此外,Java 的性能也有了顯著的提升。這些進步都反映了 Java 在持續(xù)改進和適應(yīng)現(xiàn)代編程需求方面的承諾。
參考文檔
- https://openjdk.org/projects/jdk/
- JDK12: JDK12新功能深度解析_jdk12新特性-CSDN博客
- switch statement - What does the new keyword "yield" mean in Java 13? - Stack Overflow
- New Features in Java 13 | Baeldung
- Java 11 and 12 - New Features (packtpub.com)
- Java 16 and IntelliJ IDEA | The IntelliJ IDEA Blog (jetbrains.com)
- Sealed Class in Java - GeeksforGeeks