2021 年 9 月,Oracle 釋出了 Java 17,Java 的下一個長期支援版本。如果你在使用 Java 8 或 Java 11,可能不會注意到 Java 12 之後新增的一些很酷的新特性。
因為這是一個很重要的版本,我會突出介紹一些我個人很感興趣的新特性!
需要注意的是,Java 中的大多數變更首先需要經過“預覽”階段,也就是說它們被新增到一個版本中,但還沒有完成。人們可以嘗試使用它們,但不建議將其用在生產環境中。
這裡所列舉的所有特性都已正式新增到 Java 中,並且已經過了預覽階段。
1:封印類
在 Java 15 中處於預覽階段並在 Java 17 中成為正式特性的 封印類,提供了一種新的繼承規則限定方法。當你在類或介面前面新增 sealed 關鍵字的同時,也添加了一個允許擴充套件這個類或實現這個介面的類的清單。例如,如果你定義了一個類:
public abstract sealed class Color permits Red, Blue, Yellow
也就是說,只有 Red、Blue 和 Yellow 可以繼承這個類,其他類想要繼承它都無法透過編譯。
你也可以不使用 permits 關鍵字,然後將類定義與類放在相同的檔案中,如下所示:
public abstract sealed class Color {...}
... class Red extends Color {...}
... class Blue extends Color {...}
... class Yellow extends Color {...}
注意,這些子類並不是巢狀在封印類中,而是放在類定義之後。這與使用關鍵字 permit 是一樣的效果,可以擴充套件 Color 的類只有 Red、Blue 和 Yellow。
那麼,封印類通常用在哪裡?透過限定繼承規則,同時也限定了封裝規則。假設你正在開發一個庫,並且需要將抽象類 Color 包含在其中。你知道 Color 這個類以及哪些類需要擴充套件它,但如果它被宣告為 public 的,那麼你有什麼辦法可以阻止外部程式碼擴充套件它?
如果有人誤解了它的用途並用 Square 對它進行了擴充套件,該怎麼辦?這符合你的意圖嗎?或者你其實是想讓 Color 保持私有?但即使是這樣,包級別的可見性也不能避免所有問題。如果後來有人對這個庫進行了擴充套件了該怎麼辦?他們如何能夠知道你只打算讓一小部分類整合 Color?
封印類不僅可以保護你的程式碼不受外部程式碼的影響,還是一種向你可能從未見過的人傳達意圖的方式。如果一個類是封印的,你是在傳達只有某些類可以擴充套件它。這種健壯性可以確保在多年以後任何閱讀你程式碼的人都會理解程式碼的嚴謹。
2:增強的空指標異常
增強的 空指標異常 是一個有趣的更新——不會太複雜,但仍然很受歡迎。這個增強在 Java 14 中正式釋出,提高了空指標異常 (NullPointerException,簡稱 NPE) 的可讀性,可以打印出在丟擲異常位置所呼叫的方法的名稱和空變數的名稱。例如,如果你呼叫 a.b.getName(),而 b 為空,那麼異常的堆疊跟蹤資訊會告訴你呼叫 getName() 失敗,因為 b 是空的。
我們都知道,NPE 是一種非常常見的異常,雖然在大多數情況下找出導致丟擲異常的根源並不難,但你會時不時地遇到同時有兩三個可疑變數的情況。你進入除錯模式,開始檢視程式碼,但問題很難重現。你只能試著回憶最初做了什麼導致丟擲 NPE 的。
如果你能提前獲得這些資訊,就不用這些麻煩地除錯了。這就是這個特性的閃光點:不用再猜測 NPE 是從哪裡丟擲來的。在關鍵時刻,當你遇到難以重現的異常場景時,你就有了解決問題所需的一切。
這絕對是個救星!
3:switch 表示式
希望你耐心聽我說幾句——switch 表示式(在 Java 12 中預覽,並正式新增到 Java 14 中) 是 switch 語句和 lambda 之間的某種結合。真的,當我第一次向別人描述 switch 表示式時,我的說法是他們把 switch 語句 lambda 化了。請看下面這個語法:
String adjacentColor = switch (color) {
case Blue, Green -> "yellow";
case Red, Purple -> "blue";
case Yellow, Orange -> "red";
default -> "Unknown Color";
};
現在明白我的意思了嗎?
一個明顯的區別是沒有了 break 語句。switch 表示式延續了 Oracle 讓 Java 語法更簡潔的趨勢。Oracle 非常討厭大多數 switch 語句包含很多的 CASE BREAK、CASE BREAK、CASE BREAK……。
老實說,他們討厭這個是對的,因為人們很容易在這個地方犯錯。我們當中是否有人敢說他們從來沒有遇到過這種情況:忘記在 switch 裡新增 break 語句,只有當代碼在執行時發生崩潰才知道?switch 表示式透過一種有趣的方式修復了這個問題,你只需要用逗號隔開同一個程式碼塊裡所有的值。沒錯,不需要使用 break 了!它會替你處理好!
switch 表示式還新增了 yield 關鍵字。如果一個 case 進入了一個程式碼塊,yield 將被作為 switch 表示式的返回語句。例如,如果我們將上面的程式碼稍作修改:
String adjacentColor = switch (color) {
case Blue, Green -> "yellow";
case Red, Purple -> "blue";
case Yellow, Orange -> "red";
default -> {
System.out.println("The color could not be found.");
yield "Unknown Color";
}
};
在預設 case 裡,System.out.println() 方法將被執行,adjacentColor 變數最終的值是“Unknown Color”,因為這是 yield 返回的結果。
總的來說,switch 表示式是一種更簡潔的 switch 語句,但它不會取代 switch 語句,這兩種語句都可用。
4:文字塊
文字塊 特性在 Java 13 中預覽,並正式新增到 Java 15 中,它可以簡化多行字串的寫法,支援換行,並在不需要跳脫字元的情況下保持縮排。要建立一個文字塊,只需要這樣:
String text = """
Hello
World""";
注意,這個變數仍然是一個字串,只是它隱含了換行和製表符。同樣,如果我們想要使用引號,也不需要跳脫字元:
String text = """
You can "quote" without complaints!"""; // You can "quote" without complaints!
唯一需要使用反斜槓跳脫字元的地方是當你想要在文字塊裡包含""":
String text = """
The only necessary escape is \""",
everything else is maintained.""";
除此之外,你可以呼叫 String 的 format() 方法,用動態內容替換文字塊中的佔位符:
String name = "Chris";
String text = """
My name is %s.""".format(name); // My name is Chris.
每行後面的空格都會被剪下掉,除非你指定了'\s',這是文字塊的一個跳脫字元:
String text1 = """
No trailing spaces.
Trailing spaces. \s""";
那麼,在什麼情況下會使用文字塊呢?除了能夠對大塊的文字進行格式化外,將程式碼片段貼上到字串中也變得非常容易。因為縮排被保留了,如果你要寫一個 HTML 或 Python 程式碼塊,或使用其他任何語言,你都可以按照正常的方式寫好它們,然後用"""把它們括起來,就可以保留程式碼的格式。你甚至可以用文字塊來編寫 JSON,並使用 format() 方法輕鬆地插入值。
總的來說,這是個一個很方便的特性。雖然文字塊看起來只是一個小功能,但從長遠來看,類似這種可以提升開發效率的小功能會逐漸增加。
5:record 類
record 類 在 Java 14 中預覽,並正式新增到 Java 16 中,是一種資料類,處理所有與 POJO 相關的樣板程式碼。也就是說,如果你聲明瞭一個 record 類:
public record Coord(int x, int y) {
}
equals() 和 hashcode() 方法會自動實現,toString() 將返回這個類例項包含的所有欄位的值,最重要的是,x() 和 y() 將分別返回 x 和 y 的值。想想你之前寫過的 POJO 類,並想象一下用 record 類來代替它們會怎樣。是不是好看多了?省了多少事了?
除此之外,record 類是 final 和不可變的——不能被繼承,並且類例項一旦被建立,它的欄位就不能被修改。你可以在 record 類中宣告方法,包括非靜態方法和靜態方法:
public record Coord(int x, int y) {
public boolean isCenter() {
return x() == 0 && y() == 0;
}
public static boolean isCenter(Coord coord) {
return coord.x() == 0 && coord.y() == 0;
}
}
record 類可以有多個構造器:
public record Coord(int x, int y) {
public Coord() {
this(0,0); // The default constructor is still implemented.
}
}
需要注意的是,當你在 record 類中宣告自定義建構函式時,必須呼叫預設建構函式。否則,record 類將不知道如何處理它的值。如果你聲明瞭一個與預設建構函式一樣的建構函式,你要初始化所有的欄位:
public record Coord(int x, int y) {
public Coord(int x, int y) {
this.x = x;
this.y = y;
} // Will replace the default constructor.
}
關於 record 類,有很多可討論的話題。這是一個大的變更,在合適的地方使用它們,它們會非常有用。我在這裡沒有涵蓋所有內容,但希望這能讓你瞭解它們所提供的能力。
6:模式匹配
模式匹配 是 Oracle 在與 Java 冗長語法的鬥爭中做出的另一個舉措。模式匹配在 Java 14 和 Java 15 中預覽過,並正式新增到 Java 16 中,它可以在 instanceof 條件得到滿足後消除不必要的型別轉換。例如,我們都很熟悉這樣的程式碼:
if (o instanceof Car) {
System.out.println(((Car) o).getModel());
}
如果你想要訪問 Car 的方法,必要要這麼做。在第二行,o 是 Car 的例項,這是毫無疑問的,instanceof 已經確認了這一點。如果我們使用模式匹配,只要做一個小小的改變:
if (o instanceof Car c) {
System.out.println(c.getModel());
}
現在,所有的物件型別轉換都由編譯器完成。看起來改變很小,但它避免了很多樣板程式碼。這也適用於條件分支,當你進入一個已經明確了物件型別的分支:
if (!(o instance of Car c)) {
System.out.println("This isn't a car at all!");
} else {
System.out.println(c.getModel());
}
你甚至可以在 instanceof 那一行使用模式匹配:
public boolean isHonda(Object o) {
return o instanceof Car c && c.getModel().equals("Honda");
}
雖然模式匹配不像其他一些變更那麼大,但還是簡化了常用的程式碼。