七、Java 14 新特性
七、Java 14 新特性
Java 14 已如期于 2020 年 3 月 17 日正式發布,此次更新是繼半年前 Java 13 這大版本發布之后的又一次常規版本更新,即便在全球疫情如此嚴峻形勢下,依然保持每六個月的版本更新頻率,為大家及時帶來改進和增強,這一點值得點贊。在這一版中,主要帶來了 ZGC 增強、instanceof 增強、Switch 表達式更新為標準版等方面的改動、增強和新功能。本文主要介紹 Java 14 中的主要新特性,帶您快速了解 Java 14 帶來了哪些不一樣的體驗和便利。
1、知識體系
2、語言特性增強
1、JEP 359: Switch 表達式(正式版)
switch 表達式在之前的 Java 12 和 Java 13 中都是處于預覽階段,而在這次更新的 Java 14 中,終于成為穩定版本,能夠正式可用。
switch 表達式帶來的不僅僅是編碼上的簡潔、流暢,也精簡了 switch 語句的使用方式,同時也兼容之前的 switch 語句的使用;之前使用 switch 語句時,在每個分支結束之前,往往都需要加上 break 關鍵字進行分支跳出,以防 switch 語句一直往后執行到整個 switch 語句結束,由此造成一些意想不到的問題。switch 語句一般使用冒號 :來作為語句分支代碼的開始,而 switch 表達式則提供了新的分支切換方式,即 -> 符號右則表達式方法體在執行完分支方法之后,自動結束 switch 分支,同時 -> 右則方法塊中可以是表達式、代碼塊或者是手動拋出的異常。以往的 switch 語句寫法如下:
清單 9. Switch 語句
int dayOfWeek; switch (day) {case MONDAY:case FRIDAY:case SUNDAY:dayOfWeek = 6;break;case TUESDAY:dayOfWeek = 7;break;case THURSDAY:case SATURDAY:dayOfWeek = 8;break;case WEDNESDAY:dayOfWeek = 9;break;default:dayOfWeek = 0;break; }而現在 Java 14 可以使用 switch 表達式正式版之后,上面語句可以轉換為下列寫法:
清單 10. Switch 表達式
int dayOfWeek = switch (day) {case MONDAY, FRIDAY, SUNDAY -> 6;case TUESDAY -> 7;case THURSDAY, SATURDAY -> 8; case WEDNESDAY -> 9;default -> 0;};很明顯,switch 表達式將之前 switch 語句從編碼方式上簡化了不少,但是還是需要注意下面幾點:
- 需要保持與之前 switch 語句同樣的 case 分支情況。
- 之前需要用變量來接收返回值,而現在直接使用 yield 關鍵字來返回 case 分支需要返回的結果。
- 現在的 switch 表達式中不再需要顯式地使用 return、break 或者 continue 來跳出當前分支。
- 現在不需要像之前一樣,在每個分支結束之前加上 break 關鍵字來結束當前分支,如果不加,則會默認往后執行,直到遇到 break 關鍵字或者整個 switch 語句結束,在 Java 14 表達式中,表達式默認執行完之后自動跳出,不會繼續往后執行。
- 對于多個相同的 case 方法塊,可以將 case 條件并列,而不需要像之前一樣,通過每個 case 后面故意不加 break 關鍵字來使用相同方法塊。
使用 switch 表達式來替換之前的 switch 語句,確實精簡了不少代碼,提高了編碼效率,同時也可以規避一些可能由于不太經意而出現的意想不到的情況,可見 Java 在提高使用者編碼效率、編碼體驗和簡化使用方面一直在不停的努力中,同時也期待未來有更多的類似 lambda、switch 表達式這樣的新特性出來。
3、新功能和庫的更新
1、JEP 358: 改進 NullPointerExceptions 提示信息
Java 14 改進 NullPointerException 的可查性、可讀性,能更準確地定位 null 變量的信息。該特性能夠幫助開發者和技術支持人員提高生產力,以及改進各種開發工具和調試工具的質量,能夠更加準確、清楚地根據動態異常與程序代碼相結合來理解程序。
相信每位開發者在實際編碼過程中都遇到過 NullPointerException,每當遇到這種異常的時候,都需要根據打印出來的詳細信息來分析、定位出現問題的原因,以在程序代碼中規避或解決。例如,假設下面代碼出現了一個 NullPointerException:
book.id = 99;打印出來的 NullPointerException 信息如下:
清單 4. NullPointerException 信息
Exception in thread "main" java.lang.NullPointerExceptionat Book.main(Book.java:5)像上面這種異常,因為代碼比較簡單,并且異常信息中也打印出來了行號信息,開發者可以很快速定位到出現異常位置:book 為空而導致的 NullPointerException,而對于一些復雜或者嵌套的情況下出現 NullPointerException 時,僅根據打印出來的信息,很難判斷實際出現問題的位置,具體見下面示例:
shoopingcart.buy.book.id = 99;對于這種比較復雜的情況下,僅僅單根據異常信息中打印的行號,則比較難判斷出現 NullPointerException 的原因。
而 Java 14 中,則做了對 NullPointerException 打印異常信息的改進增強,通過分析程序的字節碼信息,能夠做到準確的定位到出現 NullPointerException 的變量,并且根據實際源代碼打印出詳細異常信息,對于上述示例,打印信息如下:
清單 5. NullPointerException 詳細信息
Exception in thread "main" java.lang.NullPointerException: Cannot assign field "book" because "shoopingcart.buy" is nullat Book.main(Book.java:5)對比可以看出,改進之后的 NullPointerException 信息,能夠準確打印出具體哪個變量導致的 NullPointerException,減少了由于僅帶行號的異常提示信息帶來的困惑。該改進功能可以通過如下參數開啟:
-XX:+ShowCodeDetailsInExceptionMessages該增強改進特性,不僅適用于屬性訪問,還適用于方法調用、數組訪問和賦值等有可能會導致 NullPointerException 的地方。
4、舊功能的刪除和棄用
1、JEP 367: 刪除 pack200 和 unpack200 工具
刪除 pack200 和 unpack200 工具,以及 java.util.jar 包中的 Pack200 API。這些工具和 API 在 Java SE 11 中已被棄用,以便在未來的版本中刪除它們。
5、JVM 相關
1、JEP 345: G1 的 NUMA 可識別內存分配
Java 14 改進非一致性內存訪問(NUMA)系統上的 G1 垃圾收集器的整體性能,主要是對年輕代的內存分配進行優化,從而提高 CPU 計算過程中內存訪問速度。
NUMA 是 non-unified memory access 的縮寫,主要是指在當前的多插槽物理計算機體系中,比較普遍是多核的處理器,并且越來越多的具有 NUMA 內存訪問體系結構,即內存與每個插槽或內核之間的距離并不相等。同時套接字之間的內存訪問具有不同的性能特征,對更遠的套接字的訪問通常具有更多的時間消耗。這樣每個核對于每一塊或者某一區域的內存訪問速度會隨著核和物理內存所在的位置的遠近而有不同的時延差異。
Java 中,堆內存分配一般發生在線程運行的時候,當創建了一個新對象時,該線程會觸發 G1 去分配一塊內存出來,用來存放新創建的對象,在 G1 內存體系中,其實就是一塊 region(大對象除外,大對象需要多個 region),在這個分配新內存的過程中,如果支持了 NUMA 感知內存分配,將會優先在與當前線程所綁定的 NUMA 節點空閑內存區域來執行 allocate 操作,同一線程創建的對象,盡可能的保留在年輕代的同一 NUMA 內存節點上,因為是基于同一個線程創建的對象大部分是短存活并且高概率互相調用的。
具體啟用方式可以在 JVM 參數后面加上如下參數:
-XX:+UseNUMA通過這種方式來啟用可識別的內存分配方式,能夠提高一些大型計算機的 G1 內存分配回收性能。
2、JEP 363: 刪除 CMS 垃圾回收器
CMS 是老年代垃圾回收算法,通過標記-清除的方式進行內存回收,在內存回收過程中能夠與用戶線程并行執行。CMS 回收器可以與 Serial 回收器和 Parallel New 回收器搭配使用,CMS 主要通過并發的方式,適當減少系統的吞吐量以達到追求響應速度的目的,比較適合在追求 GC 速度的服務器上使用。
因為 CMS 回收算法在進行 GC 回收內存過程中是使用并行方式進行的,如果服務器 CPU 核數不多的情況下,進行 CMS 垃圾回收有可能造成比較高的負載。同時在 CMS 并行標記和并行清理時,應用線程還在繼續運行,程序在運行過程中自然會創建新對象、釋放不用對象,所以在這個過程中,會有新的不可達內存地址產生,而這部分的不可達內存是出現在標記過程結束之后,本輪 CMS 回收無法在周期內將它們回收掉,只能留在下次垃圾回收周期再清理掉。這樣的垃圾就叫做浮動垃圾。由于垃圾收集和用戶線程是并發執行的,因此 CMS 回收器不能像其他回收器那樣進行內存回收,需要預留一些空間用來保存用戶新創建的對象。由于 CMS 回收器在老年代中使用標記-清除的內存回收策略,勢必會產生內存碎片,內存當碎片過多時,將會給大對象分配帶來麻煩,往往會出現老年代還有空間但不能再保存對象的情況。
所以,早在幾年前的 Java 9 中,就已經決定放棄使用 CMS 回收器了,而這次在 Java 14 中,是繼之前 Java 9 中放棄使用 CMS 之后,徹底將其禁用,并刪除與 CMS 有關的選項,同時清除與 CMS 有關的文檔內容,至此曾經輝煌一度的 CMS 回收器,也將成為歷史。
當在 Java 14 版本中,通過使用參數: -XX:+UseConcMarkSweepGC,嘗試使用 CMS 時,將會收到下面信息:
Java HotSpot(TM) 64-Bit Server VM warning: Ignoring option UseConcMarkSweepGC; \ support was removed in <version>3、JEP 364&365: ZGC 支持 MacOS 和 Windows 系統(實驗階段)
ZGC 是最初在 Java 11 中引入,同時在后續幾個版本中,不斷進行改進的一款基于內存 Region,同時使用了內存讀屏障、染色指針和內存多重映射等技,并且以可伸縮、低延遲為目標的內存垃圾回收器器,不過在 Java 14 之前版本中,僅僅只支持在 Linux/x64 位平臺。
此次 Java 14,同時支持 MacOS 和 Windows 系統,解決了開發人員需要在桌面操作系統中使用 ZGC 的問題。
在 MacOS 和 Windows 下面開啟 ZGC 的方式,需要添加如下 JVM 參數:
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC4、JEP 366: 棄用 ParallelScavenge 和 SerialOld GC 的組合使用
由于 Parallel Scavenge 和 Serial Old 垃圾收集算法組合起來使用的情況比較少,并且在年輕代中使用并行算法,而在老年代中使用串行算法,這種并行、串行混搭使用的情況,本身已屬罕見同時也很冒險。由于這兩 GC 算法組合很少使用,卻要花費巨大工作量來進行維護,所以在 Java 14 版本中,考慮將這兩 GC 的組合棄用。
具體棄用情況如下,通過棄用組合參數:-XX:+UseParallelGC -XX:-UseParallelOldGC,來棄用年輕代、老年期中并行、串行混搭使用的情況;同時,對于單獨使用參數:-XX:-UseParallelOldGC 的地方,也將顯示該參數已被棄用的警告信息。
6、新功能的預覽和實驗
1、JEP 305: instanceof 模式匹配(預覽階段)
Java 14 中對 instanceof 的改進,主要目的是為了讓創建對象更簡單、簡潔和高效,并且可讀性更強、提高安全性。
在以往實際使用中,instanceof 主要用來檢查對象的類型,然后根據類型對目標對象進行類型轉換,之后進行不同的處理、實現不同的邏輯,具體可以參考清單 1:
清單 1. instanceof 傳統使用方式
if (person instanceof Student) {Student student = (Student) person;student.say();// other student operations } else if (person instanceof Teacher) {Teacher teacher = (Teacher) person;teacher.say();// other teacher operations }上述代碼中,我們首先需要對 person 對象進行類型判斷,判斷 person 具體是 Student 還是 Teacher,因為這兩種角色對應不同操作,亦即對應到的實際邏輯實現,判斷完 person 類型之后,然后強制對 person 進行類型轉換為局部變量,以方便后續執行屬于該角色的特定操作。
上面這種寫法,有下面兩個問題:
- 每次在檢查類型之后,都需要強制進行類型轉換。
- 類型轉換后,需要提前創建一個局部變量來接收轉換后的結果,代碼顯得多余且繁瑣。
Java 14 中,對 instanceof 進行模式匹配改進之后,上面示例代碼可以改寫成:
清單 2. instanceof 模式匹配使用方式
if (person instanceof Student student) {student.say();// other student operations } else if (person instanceof Teacher teacher) {teacher.say();// other teacher operations }清單 2 中,首先在 if 代碼塊中,對 person 對象進行類型匹配,校驗 person 對象是否為 Student 類型,如果類型匹配成功,則會轉換為 Student 類型,并賦值給模式局部變量 student,并且只有當模式匹配表達式匹配成功是才會生效和復制,同時這里的 student 變量只能在 if 塊中使用,而不能在 else if/else 中使用,否則會報編譯錯誤。
注意,如果 if 條件中有 && 運算符時,當 instanceof 類型匹配成功,模式局部變量的作用范圍也可以相應延長,如下面代碼:
清單 3. Instanceof 模式匹配 && 方式
if (obj instanceof String s && s.length() > 5) {.. s.contains(..) ..}另外,需要注意,這種作用范圍延長,并不適用于或 || 運算符,因為即便 || 運算符左邊的 instanceof 類型匹配沒有成功也不會造成短路,依舊會執行到||運算符右邊的表達式,但是此時,因為 instanceof 類型匹配沒有成功,局部變量并未定義賦值,此時使用會產生問題。
與傳統寫法對比,可以發現模式匹配不但提高了程序的安全性、健壯性,另一方面,不需要顯式的去進行二次類型轉換,減少了大量不必要的強制類型轉換。模式匹配變量在模式匹配成功之后,可以直接使用,同時它還被限制了作用范圍,大大提高了程序的簡潔性、可讀性和安全性。instanceof 的模式匹配,為 Java 帶來的有一次便捷的提升,能夠剔除一些冗余的代碼,寫出更加簡潔安全的代碼,提高碼代碼效率。
2、JEP 359: Record 類型(預覽功能)
Java 14 富有建設性地將 Record 類型作為預覽特性而引入。Record 類型允許在代碼中使用緊湊的語法形式來聲明類,而這些類能夠作為不可變數據類型的封裝持有者。Record 這一特性主要用在特定領域的類上;與枚舉類型一樣,Record 類型是一種受限形式的類型,主要用于存儲、保存數據,并且沒有其它額外自定義行為的場景下。
在以往開發過程中,被當作數據載體的類對象,在正確聲明定義過程中,通常需要編寫大量的無實際業務、重復性質的代碼,其中包括:構造函數、屬性調用、訪問以及 equals() 、hashCode()、toString() 等方法,因此在 Java 14 中引入了 Record 類型,其效果有些類似 Lombok 的 @Data 注解、Kotlin 中的 data class,但是又不盡完全相同,它們的共同點都是類的部分或者全部可以直接在類頭中定義、描述,并且這個類只用于存儲數據而已。對于 Record 類型,具體可以用下面代碼來說明:
清單 6. Record 類型定義
public record Person(String name, int age) {public static String address;public String getName() {return name;} }對上述代碼進行編譯,然后反編譯之后可以看到如下結果:
清單 7. Record 類型反編譯結果
public final class Person extends java.lang.Record {private final java.lang.String name;private final java.lang.String age;public Person(java.lang.String name, java.lang.String age) { /* compiled code */ }public java.lang.String getName() { /* compiled code */ }public java.lang.String toString() { /* compiled code */ }public final int hashCode() { /* compiled code */ }public final boolean equals(java.lang.Object o) { /* compiled code */ }public java.lang.String name() { /* compiled code */ }public java.lang.String age() { /* compiled code */ } }根據反編譯結果,可以得出,當用 Record 來聲明一個類時,該類將自動擁有下面特征:
- 擁有一個構造方法
- 獲取成員屬性值的方法:name()、age()
- hashCode() 方法和 euqals() 方法
- toString() 方法
- 類對象和屬性被 final 關鍵字修飾,不能被繼承,類的示例屬性也都被 final 修飾,不能再被賦值使用。
- 還可以在 Record 聲明的類中定義靜態屬性、方法和示例方法。注意,不能在 Record 聲明的類中定義示例字段,類也不能聲明為抽象類等。
可以看到,該預覽特性提供了一種更為緊湊的語法來聲明類,并且可以大幅減少定義類似數據類型時所需的重復性代碼。
另外 Java 14 中為了引入 Record 這種新的類型,在 java.lang.Class 中引入了下面兩個新方法:
清單 8. Record 新引入至 Class 中的方法
RecordComponent[] getRecordComponents() boolean isRecord()其中 getRecordComponents() 方法返回一組 java.lang.reflect.RecordComponent 對象組成的數組,java.lang.reflect.RecordComponent也是一個新引入類,該數組的元素與 Record 類中的組件相對應,其順序與在記錄聲明中出現的順序相同,可以從該數組中的每個 RecordComponent 中提取到組件信息,包括其名稱、類型、泛型類型、注釋及其訪問方法。
而 isRecord() 方法,則返回所在類是否是 Record 類型,如果是,則返回 true。
3、JEP 368: 文本塊(第二預覽版本)
Java 13 引入了文本塊來解決多行文本的問題,文本塊主要以三重雙引號開頭,并以同樣的以三重雙引號結尾終止,它們之間的任何內容都被解釋為文本塊字符串的一部分,包括換行符,避免了對大多數轉義序列的需要,并且它仍然是普通的 java.lang.String 對象,文本塊可以在 Java 中能夠使用字符串的任何地方進行使用,而與編譯后的代碼沒有區別,還增強了 Java 程序中的字符串可讀性。并且通過這種方式,可以更直觀地表示字符串,可以支持跨越多行,而且不會出現轉義的視覺混亂,將可以廣泛提高 Java 類程序的可讀性和可寫性。
Java 14 在 Java 13 引入的文本塊的基礎之上,新加入了兩個轉義符,分別是:\ 和 \s,這兩個轉義符分別表達涵義如下:
- \:行終止符,主要用于阻止插入換行符;
- \s:表示一個空格。可以用來避免末尾的白字符被去掉。
在 Java 13 之前,多行字符串寫法為:
清單 11. 多行字符串寫法
String literal = "Lorem ipsum dolor sit amet, consectetur adipiscing " +"elit, sed do eiusmod tempor incididunt ut labore " +"et dolore magna aliqua.";在 Java 14 新引入兩個轉義符之后,上述內容可以寫為:
清單 12. 多行文本塊加上轉義符的寫法
String text = """Lorem ipsum dolor sit amet, consectetur adipiscing \elit, sed do eiusmod tempor incididunt ut labore \et dolore magna aliqua.\""";上述兩種寫法,text 實際還是只有一行內容。
對于轉義符:\s,用法如下,能夠保證下列文本每行正好都是六個字符長度:
清單 13. 多行文本塊加上轉義符的寫法
String colors = """red \sgreen\sblue \s""";Java 14 帶來的這兩個轉義符,能夠簡化跨多行字符串編碼問題,通過轉義符,能夠避免對換行等特殊字符串進行轉移,從而簡化代碼編寫,同時也增強了使用 String 來表達 HTML、XML、SQL 或 JSON 等格式字符串的編碼可讀性,且易于維護。
同時 Java 14 還對 String 進行了方法擴展:
- stripIndent() :用于從文本塊中去除空白字符
- translateEscapes():用于翻譯轉義字符
- formatted(Object... args):用于格式化
4、JEP 343: 打包工具(孵化器版本)
創建用于打包自包含 Java 應用程序的工具。
它基于 JavaFX javapackager 工具創建一個簡單的打包工具,主要目標是:
- 支持原生打包格式,為最終用戶提供自然的安裝體驗。這些格式包括 Windows 上的 msi 和 exe,macOS 上的 pkg 和 dmg,以及 Linux 上的 deb 和 rpm。
- 允許在打包時指定啟動時間參數。
- 可以從命令行直接調用,也可以通過 ToolProvider API 以編程方式調用。
5、JEP 370: 外部存儲器訪問 API(孵化器版)
外存訪問 API(二次孵化),可以允許 Java 應用程序安全有效地訪問 Java 堆之外的外部內存。目的是引入一個 API,以允許 Java 程序安全、有效地訪問 Java 堆之外的外部存儲器。如本機、持久和托管堆。如下內容來源于https://xie.infoq.cn/article/8304c894c4e38318d38ceb116
在實際的開發過程中,絕大多數的開發人員基本都不會直接與堆外內存打交道,但這并不代表你從未接觸過堆外內存,像大家經常使用的諸如:RocketMQ、MapDB 等中間件產品底層實現都是基于堆外存儲的,換句話說,我們幾乎每天都在間接與堆外內存打交道。那么究竟為什么需要使用到堆外內存呢?簡單來說,主要是出于以下 3 個方面的考慮:
- 減少 GC 次數和降低 Stop-the-world 時間;
- 可以擴展和使用更大的內存空間;
- 可以省去物理內存和堆內存之間的數據復制步驟。
在 Java14 之前,如果開發人員想要操作堆外內存,通常的做法就是使用 ByteBuffer 或者 Unsafe,甚至是 JNI 等方式,但無論使用哪一種方式,均無法同時有效解決安全性和高效性等 2 個問題,并且,堆外內存的釋放也是一個令人頭痛的問題。以 DirectByteBuffer 為例,該對象僅僅只是一個引用,其背后還關聯著一大段堆外內存,由于 DirectByteBuffer 對象實例仍然是存儲在堆空間內,只有當 DirectByteBuffer 對象被 GC 回收時,其背后的堆外內存才會被進一步釋放。
在此大家需要注意,程序中通過 ByteBuffer.allocateDirect()方法來申請物理內存資源所耗費的成本遠遠高于直接在 on-heap 中的操作,而且實際開發過程中還需要考慮數據結構如何設計、序列化/反序列化如何支撐等諸多難題,所以與其使用語法層面的 API 倒不如直接使用 MapDB 等開源產品來得更實惠。
如今,在堆外內存領域,我們似乎又多了一個選擇,從 Java14 開始,Java 的設計者們在語法層面為大家帶來了嶄新的 Memory Access API,極大程度上簡化了開發難度,并得以有效的解決了安全性和高效性等 2 個核心問題。示例:
// 獲取內存訪問var句柄 var handle = MemoryHandles.varHandle(char.class,ByteOrder.nativeOrder()); // 申請200字節的堆外內存 try (MemorySegment segment = MemorySegment.allocateNative(200)) {for (int i = 0; i < 25; i++) {handle.set(segment, i << 2, (char) (i + 1 + 64));System.out.println(handle.get(segment, i << 2));} }關于堆外內存段的釋放,Memory Access API 提供有顯式和隱式 2 種方式,開發人員除了可以在程序中通過 MemorySegment 的 close()方法來顯式釋放所申請的內存資源外,還可以注冊 Cleaner 清理器來實現資源的隱式釋放,后者會在 GC 確定目標內存段不再可訪問時,釋放與之關聯的堆外內存資源。
7、結束語
Java 在更新版本周期為每半年發布一次之后,目前來看,確實是嚴格保持每半年更新的節奏。Java 14 版本的發布帶來了不少新特性、功能實用性的增強、性能提升和 GC 方面的改進嘗試。本文僅針對其中對使用人員影響較大的以及其中主要的特性做了介紹,如有興趣,您還可以自行下載相關代碼,繼續深入研究。
更多內容:
更多內容大家可以關注一下個人博客網,https://blog.xueqimiao.com/,內容更豐富喔。
總結
以上是生活随笔為你收集整理的七、Java 14 新特性的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《2013-I want to talk
- 下一篇: 爱奇艺的Java缓存之路,你应该知道的缓