更多學習資料,可以關注下方微信公衆號,回覆關鍵字:Java高級資料 。 即可免費獲取完整的Java高級開發工程師視頻課程。
注意
- IDEA 2020.1以上版本才支持JDK14
從哪幾個方面學習新特性
- 關注 語法層面(學習其他語言的語法)
- 關注 API層面(新增 / 過時 / 廢棄 )
- 底層優化(JVM優化) 面試 / 瞭解底層原理
JDK版本使用者佔比
根據國外網站2019年統計,使用 Java 8 的開發者比重最大,其次就是Java 11。其他版本的比重不大。這主要是由於 Java 8 和 Java 11 是 LTS 版本。其他的都是非 LTS 版本,意思就是其他版本Oracle不會一直維護。
版本更新時間
JDK14概述
2020年3月17日,Java 14 正式GA(General Available)。這是自從Java宣佈採用六個月一次的發佈週期後的第五次發佈。
此次發佈的版本包含 16 個JEP(Java Enhancement Proposals,JDK增強提案)。包括兩個孵化器模塊丶三個預覽特性丶兩個廢棄功能丶兩個刪除功能。
- 孵化器模塊:將尚未定稿的API和工具提前給開發者使用,以便獲得反饋,並對反饋的信息整理分析後進一步改進Java的特性。
- 預覽特性:功能已經成型丶實現已經確定,但是還沒有最終定稿的功能。爲了收集用戶使用後真實的反饋信息,將功能提供給用戶使用。促進這些功能的完善以便最終定稿。
總特性概述
中文版本
JEP(Java 增強提案) | 功能概述 |
---|---|
305: | instanceof的模式匹配(預覽) |
343: | 包裝工具(孵化器) |
345: | G1的NUMA感知內存分配 |
349: | JFR事件流 |
352: | 非易失性映射字節緩衝區 |
358: | 有用的NullPointerExceptions |
359: | 記錄(預覽) |
361: | 開關表達式(標準) |
362: | 棄用Solaris和SPARC端口 |
363: | 刪除併發標記掃描(CMS)垃圾收集器 |
364: | Mac OS上的ZGC |
365: | Windows上的ZGC |
366: | 棄用ParallelScavenge + SerialOld GC組合 |
367: | 刪除Pack200工具和API |
368: | 文本塊(第二預覽) |
370: | 外部存儲器訪問API(孵化器) |
英文版本
實用特性
JDK 14 的新特性中,對於語法層面的只有5個。所以下面將對這5個新特性進行詳細講解。
JEP(Java 增強提案) | 功能概述 |
---|---|
305: | Pattern Matching for instanceof (Preview) |
358: | Helpful NullPointerExceptions |
359: | Records (Preview) |
361: | Switch Expressions (Standard) |
368: | Text Blocks (Second Preview) |
JEP 305:instanceof的模式匹配(預覽)
這個特性目前在 JDK 14 中還是預覽版本。通過爲開發人員提供 模式匹配 來增強Java編程語言instanceof。模式匹配使程序中的通用邏輯(如從對象中有條件的提取組件)更加簡潔安全。
幾乎所有程序都會包含一種特殊邏輯,就是先用表達式判斷是否滿足某種條件。然後有條件的做進一步處理。如:instanceof表達式判斷。
Object obj = "string";
if (obj instanceof String) {
String s = (String) obj;
// 使用 s 對象
System.out.println(s);
}
這一段代碼需要處理三件事情:
- 判斷(str 是否爲String類型)
- 轉換(將obj對象轉換爲String)
- 定義一個新的局部變量 s ,然後我們就可以使用這個字符串。
JEP 305寫法
Object obj = "String";
if (obj instanceof String s) {
// 使用 s 對象
System.out.println(s);
} else {
// 不可以使用 s 對象
}
如果obj是一個String實例,那麼它被轉換爲String並分配給String類型變量 s 。綁定變量在if語句的true代碼塊中,而不是在if語句的false代碼塊中。
與局部變量的作用範圍不同,綁定變量的作用域由包含的表達式和語句的語義決定。例如下面的代碼中
Object obj = "String";
if (!(obj instanceof String s)) {
// 報錯,不可以使用 s
System.out.println(s.trim());
} else {
// 可以使用 s
System.out.println(s.trim());
}
// 複雜的判斷條件寫法
Object obj = "String";
// 由於obj instanceof String s條件成立,那麼這個if域就可以使用s變量,所以&&條件可以正常
if (obj instanceof String s && s.length() == 5) {
System.out.println("obj對象是String類型,並且長度爲:5");
}
// 錯誤的判斷條件寫法
Object obj = "String";
// 如果obj instanceof String s條件不成立,那麼if域就不可以使用s變量,s.length() == 5中不可以繼續使用s變量。變量s不在||右側的範圍內。
if (obj instanceof String s || s.length() == 5) {
System.out.println("obj對象是String類型,並且長度爲:5");
}
// 類型轉換和條件判斷,簡化代碼
public boolean strNotBlank(Object obj) {
return obj instanceof String s && s.length() > 0;
}
JEP 358:有用的NullPointerExceptions
官方描述
- 通過精確地描述哪個變量是null來提高由JVM生成的NullPointerException信息的可用性。
目標
- 向開發人員和支持人員提供有關程序提前終止的有用信息。
- 通過更清晰地將動態異常與靜態程序代碼關聯起來,提高對程序的理解。
- 減少新開發人員對NullPointerException的混淆和關注。
任何一個 Java 開發人員都會遇到NullPointerException(NPE)。由於NPE幾乎會出現在程序中的任何位置,因此嘗試捕捉和恢復它們通常都不切實際。
即使開發人員依賴JVM查明NPE的實際來源。但是對於複雜的代碼,不適用調試器就無法確定那個變量爲空。假設下面的代碼出現一個NPE:
Object obj = null;
System.out.println(obj.toString().trim().contains(""));
JVM將打印出導致NPE的方法丶文件名和行號:
java.lang.NullPointerException
at com.zhichunqiu.features.java14.Features02.test(Features02.java:16)
文件名稱和行號無法精準的定位到那個變量丶方法爲空。obj
丶obj.toString()
還是obj.toString().trim()
。
如果JVM可以提供所需的信息以查明NPE的來源,然後確定其根本原因,而無需使用額外的工具或改組代碼,則整個Java生態系統都將受益。自2006年以來,SAP的商業JVM就已經做到了這一點,獲得了開發人員和支持工程師的一致好評。
JDK 14 優化JVM處理NPE
JVM NullPointerException
在程序試圖取消引用引用的位置處拋出(NPE)null
。通過分析程序的字節碼指令,JVM將精確地確定哪個變量是null
,並在NPE 中用空細節消息描述該變量(根據源代碼)。然後,null-detail消息將顯示在JVM的消息中,以及方法,文件名和行號。
注意
在 JDK 14 中並沒有默認開啓Helpful NullPointerException特性。需要自己配置JVM參數,可能以後的版本會默認開啓。開啓參數:-XX:+ShowCodeDetailsInExceptionMessages
還是用上面的代碼執行,看報錯效果:
Object obj = null;
System.out.println(obj.toString().trim().contains(""));
JVM 消息將剖析該語句並通過顯示導致以下內容的完整訪問路徑來查明原因null
:
java.lang.NullPointerException: Cannot invoke "Object.toString()" because "obj" is null
at com.zhichunqiu.features.java14.Features02.test(Features02.java:16)
JVM打印的消息除了說明NPE產生的類丶方法還說明了是obj爲空導致產生了NPE。
JEP 359:Records(預覽)
通過Records
增強 Java編程語言。Records提供了一種緊湊的語法來聲明類,這些類都是淺拷貝不可變數據。
動機和目標
早在2019年2月份,Java語言架構師Brian Goetz曾經寫過一篇文章,詳細的說明並吐槽了Java語言,他和許多程序員一樣抱怨"Java太羅嗦太繁瑣"。他提到開發人員想要創建純數據載體類,通常都必須編寫大量低價值丶重複丶容易出錯的代碼。比如:構造函數丶getter / setter丶equals()丶hashCode()丶toString()等方法。
官方說明如下
描述
Records是 Java 語言中一種新的類型聲明。就像enum
一樣,record
是一種受限制的Class
形式。record
的引入獲得了極大程度的語言簡潔性,但是也沒有了類的自由度:將API與表示分離的能力。
record
是包含名稱和狀態描述的,這個狀態描述是聲明record
組件的。record
的主體是可選的。record
的定義如下:
public record Features03(String name, int age) { }
隱藏特徵
由於records
的聲明非常簡單,所以records
將會默認的獲得很多標準的成員變量或方法。
-
每一個組成部分(變量 )都有一個隱藏的
final
字段聲明; -
每個成員變量都有一個與變量名稱相同的獲取值的方法;例如:
name
屬性的獲取值方法不是instance.getName()
而是instance.name()
方法。這些都是編譯後默認生成的。 -
一個
public
的全參構造方法,構造方法的參數和順序與類名稱後面的()
中聲明的一致。 -
實現了
equals
和hashCode
方法。如果兩個records
類型的類具有相同的類型和相同的狀態,那麼他們一定是相等。(其實就是說明實現的equals
和hashCode
方法是標準的) -
實現了
toString
方法,會打印類名稱和相關的屬性說明。(其實就是說明實現的toString
方法是標準的)
反編譯後的Features03解讀
// 特點1:record定義的類,默認都繼承了Record,由於Java是單繼承,所以record定義的類不允許在繼承其他類。
// 錯誤示範:public record Features03 extends Object(String name, int age) { }
// 特點2:record都是final類,不允許被繼承。
public final class Features03 extends java.lang.Record {
// 特點3:所有屬性都是final類型的
private final java.lang.String name;
private final int age;
// 特點4:默認生成全參數構造函數
public Features03(java.lang.String name, int age) { /* compiled code */ }
// 特點5:默認生成toString hashCode equals方法
public java.lang.String toString() { /* compiled code */ }
public final int hashCode() { /* compiled code */ }
public final boolean equals(java.lang.Object o) { /* compiled code */ }
// 特點6:所有字段都提供getter方法,但是方法名稱和屬性名稱一致
public java.lang.String name() { /* compiled code */ }
public int age() { /* compiled code */ }
}
Records的限制
records
不能繼承任何的其他類,除了頭部定義的屬性(字段)外,聲明的任何其他屬性都必須是static
的。records
是隱式final
類,不可以是abstract
抽象類。records
的組件是隱式final
。因此可以將records
作爲一個數據的集合,因爲不用擔心數據會被修改。records
只提供了屬性的getter
方法,屬性都是final的賦值後無法修改。- 除了以上的限制外,
records
的其他行爲和正常的類是一樣的。具體如下:- 可以嵌套類丶聲明泛型丶實現接口丶通過new關鍵字實例化;
- 可以聲明靜態方法丶靜態字段丶靜態初始化容器丶構造函數丶實例方法和嵌套類型;
// 特徵1:定義泛型 / 實現接口
public record Features03<T>(String name, int age) implements Serializable {
// 特徵2:定義的成員變量,必須是靜態的,可以初始化值
private static String address;
// 定義公共靜態常量
private static final String desc = null;
//特徵3:實例方法
public String getDesc() {
return null;
}
//特徵4:靜態方法
public static String getAddress() {
return null;
}
// 特徵5:可以自定義構造函數
public Features03 {
if (age > 10) {
throw new IllegalArgumentException("年齡大於10歲");
}
}
// 嵌套的records
record MyFeatures(String dept, String desc) {
}
}
Features03反編譯後的文件
public final class Features03 <T> extends java.lang.Record implements java.io.Serializable {
private final java.lang.String name;
private final int age;
private static java.lang.String address;
private static final java.lang.String desc;
public Features03(java.lang.String name, int age) { /* compiled code */ }
public java.lang.String getDesc() { /* compiled code */ }
public static java.lang.String getAddress() { /* 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 int age() { /* compiled code */ }
static final class MyFeatures extends java.lang.Record {
private final java.lang.String dept;
private final java.lang.String desc;
public MyFeatures(java.lang.String dept, java.lang.String desc) { /* 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 dept() { /* compiled code */ }
public java.lang.String desc() { /* compiled code */ }
}
}
JEP 361:Switch 表達式(標準)
switch表達式並不是在 JDK 14纔出現的,早在 JDK 12(JEP 325) 和 13(JEP 354) 中switch就作爲預覽語言特性出現。JDK 12 中出現了 ->
操作符,JDK 13 中出現了yield
關鍵字。而 JDK 14 是將12 / 13的預覽特性最終確定下來作爲標準特性發布。
JDK 12 之前的寫法
public void test() {
Week day = Week.FRIDAY;
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
System.out.println(6);
break;
case TUESDAY:
System.out.println(7);
break;
case THURSDAY:
case SATURDAY:
System.out.println(8);
break;
case WEDNESDAY:
System.out.println(9);
break;
}
}
// 將匹配條件後的值賦值給一個變量
public void test2() {
int numLetters;
Week day = Week.FRIDAY;
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
numLetters = 6;
break;
case TUESDAY:
numLetters = 7;
break;
case THURSDAY:
case SATURDAY:
numLetters = 8;
break;
case WEDNESDAY:
numLetters = 9;
break;
}
}
enum Week {
MONDAY,
FRIDAY,
SUNDAY,
TUESDAY,
THURSDAY,
SATURDAY,
WEDNESDAY
}
JDK 12寫法
在 JDK 12 中引入一種新形式的開關標籤 ”case L ->“,以表示如果標籤匹配則僅執行標籤右邊的代碼。而且還允許使用逗號分隔多個常量。
-
”case L ->“開關標籤右側的代碼不僅限於表達式,還可以是代碼塊{}或者throw語句。
-
switch既然在 JDK 12 中的描述是表達式,那麼就說明可以將結果賦值給一個變量。
public void test1() {
Week day = Week.FRIDAY;
switch (day) {
case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
case TUESDAY -> {
// case 後面是一個代碼塊,包含多行代碼
System.out.println("輸入的日期爲:" + TUESDAY);
System.out.println(7);
}
case THURSDAY, SATURDAY -> System.out.println(8);
case WEDNESDAY -> System.out.println(9);
// case 後面拋出異常
default -> throw new IllegalArgumentException("輸入的星期有誤");
}
}
// 將匹配條件後的值賦值給一個變量,然後將結果輸出
// 即將結果賦值給一個變量,作爲表達式使用,switch需要以分號結尾
// 方式一:賦值變量後輸出結果
@Test
public void test3() {
Week day = Week.FRIDAY;
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
// case 後面拋出異常
default -> throw new IllegalArgumentException("輸入的星期有誤");
};// 末尾需要以分號結束,因爲這個時候switch作爲表達式使用
System.out.println(numLetters);
}
// 方式二:將switch作爲表達式直接輸出
public void test4() {
Week day = Week.FRIDAY;
System.out.println(
// 將表達式結果輸出
switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
// case 後面拋出異常
default -> throw new IllegalArgumentException("輸入的星期有誤");
}
);
}
enum Week {
MONDAY,
FRIDAY,
SUNDAY,
TUESDAY,
THURSDAY,
SATURDAY,
WEDNESDAY
}
JDK 13 寫法
在 JDK 13 中爲了解決 JDK 12 中switch作爲表達式如果返回結果時候,case -> 右邊如果是一個代碼塊,那麼就不知道何時返回結果。因此引入了一個關鍵字:yield
- 如果使用case -> 的寫法,如果右邊只有一個表達式,就不需要使用
yield
關鍵字。只有代碼塊{}的時候才需要使用。 - 如果使用case : 的寫法,那麼無論右邊如何,需要返回結果都必須使用
yield
。
// yield關鍵字的用法,返回結果
// 方法一: case -> 寫法
public void test5() {
Week day = Week.FRIDAY;
// 將表達式結果輸出,
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
// case後面如果是代碼塊,返回結果需要使用關鍵字:yield,不可以使用return返回結果
case WEDNESDAY -> {
System.out.println("case -> 後面是代碼塊時候返回結果");
yield 9;
}
// case 後面拋出異常
default -> throw new IllegalArgumentException("輸入的星期有誤");
};
}
// 方法二:case: 寫法
public void test6() {
Week day = Week.FRIDAY;
// 將表達式結果輸出
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY: yield 6;
case TUESDAY: yield 7;
case THURSDAY, SATURDAY: yield 8;
// case後面如果是代碼塊,返回結果需要使用關鍵字:yield,不可以使用return返回結果
case WEDNESDAY: {
System.out.println("case -> 後面是代碼塊時候返回結果");
yield 9;
}
// case 後面拋出異常
default: throw new IllegalArgumentException("輸入的星期有誤");
};
}
enum Week {
MONDAY,
FRIDAY,
SUNDAY,
TUESDAY,
THURSDAY,
SATURDAY,
WEDNESDAY
}
警告
對於控制語句丶break丶yield丶return和continue,不能跳過switch表達式。
for (int i = 0; i < Integer.MAX_VALUE; ++i) {
int k = switch (e) {
case 0:
yield 1;
case 1:
yield 2;
default:
// 錯誤的寫法,不能跳過switch表達式
continue z;
};
}
JEP 368:文本塊(第二次預覽)
一句話含義
文本塊是多行字符串文字,它可以有效避免字符串特殊字符需要轉義序列。
JEP 目標
- 通過簡化跨行源代碼的字符串工作,從而簡化了編寫Java程序的任務,同時避免了常見情況下的轉義序列。
- 增強Java程序中表示用非Java語言編寫的代碼字符串的可讀性。
- 通過規定相同的轉義序列並且以字符串文字的形式操作,從而支持字符串文字的遷移。
病症所在
在 Java 中,在字符串文字中嵌入HTML丶XML丶SQL和JSON等片段,通常需要先進行轉義和大量的串聯,然後才能編譯包含該片段的代碼。通常這些代碼都是難以維護的。
因此,通過具有一種語言學機制,可以將多行文字更直觀的表示字符串,從而跨越多行也不會出現轉義,從而提高了Java類程序的可讀性和可寫性。這就是文本塊誕生的原因。
語法規則
文本塊的定義由開始定界符(三個雙引號字符)和結束定界符(三個雙引號字符)確定文本塊的邊界String str = """ 文本內容 """
。同時引入了\
表示取消換行和\s
表示一個空格。
文本塊示例
// 注意文本塊每一行末尾都有一個換行符,所以普通的字符串長度要多1個單位。
public void testBlock() {
// 每一行字符串後面都有\n轉義,可讀性太差
String html = "<html>\n" +
"\t<body>\n" +
"\t\t<p>Hello, world</p>\n" +
"\t</body>\n" +
"</html>";
// 文本塊寫法,簡單清晰,可讀性好
String htmlBlock = """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";
System.out.println(html.length()); // 結果:53
System.out.println(htmlBlock.length()); // 結果:54
}
語法示例
// 正確的定義文本塊
String sql = """ // 開始分隔符一行
文本塊內容
"""; // 結束分隔符一行
// 文本塊中三個"字符的序列,至少需要轉義一個字符,以避免和結束定界符衝突
String code =
"""
String text = \"""
A text block inside a text block
\""";
""";
// 錯誤語法示範
String a = """"""; // 錯誤,在開始分隔符後面沒有換行符
String b = """ """; // 錯誤,在開始分隔符後面沒有換行符
String c = """
"; // 錯誤,並沒有結束分隔符
String d = """
abc \ def
"""; // 錯誤,斜杆沒有轉義
新的轉義序列
爲了更好的處理換行符和空格的處理,JDK 14 引入了兩個新的轉義序列。
\
轉義序列明確禁止插入換行符 , 由於字符文字和傳統字符串文字不允許嵌入換行符的原因,\
轉義序列僅適用於文本塊- 新的
\s
轉義序列僅轉換爲一個空格(\u0020
),該\s
轉義序列可以在這兩個文本塊和傳統的字符串文字中。
// 對於一個很長的字符串內容,爲了更加方便查看,通常會拆分爲較小的子字符串進行拼接,然後將結果的字符串表達式分散到幾行中:
String literal = "Lorem ipsum dolor sit amet, consectetur adipiscing " +
"elit, sed do eiusmod tempor incididunt ut labore " +
"et dolore magna aliqua.";
// 使用\<line-terminator>轉義序列,可以將其表示爲:
// \ 表示後面是沒有換行符的,所以字符串會是一行輸出
String text = """
Lorem ipsum dolor sit amet, consectetur adipiscing \
elit, sed do eiusmod tempor incididunt ut labore \
et dolore magna aliqua.\
""";
// \s在這個示例中,在每行的末尾使用可以確保每行正好是六個字符長度
String colors = """
red \s
green\s
blue \s
""";
// 可以在任何使用字符串文字的地方使用文本塊。例如,文本塊和字符串文字可以互換使用:
String code = "public void print(Object o) {" +
"""
System.out.println(Objects.toString(o));
}
""";
// 不建議的做法:使用文本塊拼接,這樣會顯得很笨拙
String code = """
public void print(""" + type + """
o) {
System.out.println(Objects.toString(o));
}
""";
API對文本塊的支持:附加方法
String::stripIndent()
:用於從文本塊內容中去除附帶的空白String::translateEscapes()
:用於翻譯轉義序列String::formatted(Object... args)
:簡化文本塊中的值替換