從 Java 12 到 Java 17 那些激動(dòng)人心的新特性
2021 年 9 月,Oracle 發(fā)布了 Java 17,Java 的下一個(gè)長期支持版本。如果你在使用 Java 8 或 Java 11,可能不會(huì)注意到 Java 12 之后新增的一些很酷的新特性。
因?yàn)檫@是一個(gè)很重要的版本,我會(huì)突出介紹一些我個(gè)人很感興趣的新特性!
需要注意的是,Java 中的大多數(shù)變更首先需要經(jīng)過“預(yù)覽”階段,也就是說它們被添加到一個(gè)版本中,但還沒有完成。人們可以嘗試使用它們,但不建議將其用在生產(chǎn)環(huán)境中。
這里所列舉的所有特性都已正式添加到 Java 中,并且已經(jīng)過了預(yù)覽階段。
?1:封印類
在 Java 15 中處于預(yù)覽階段并在 Java 17 中成為正式特性的 封印類,提供了一種新的繼承規(guī)則限定方法。當(dāng)你在類或接口前面添加 sealed 關(guān)鍵字的同時(shí),也添加了一個(gè)允許擴(kuò)展這個(gè)類或?qū)崿F(xiàn)這個(gè)接口的類的清單。例如,如果你定義了一個(gè)類:
public?abstract?sealed class?Color?permits?Red, Blue, Yellow也就是說,只有 Red、Blue 和 Yellow 可以繼承這個(gè)類,其他類想要繼承它都無法通過編譯。
你也可以不使用 permits 關(guān)鍵字,然后將類定義與類放在相同的文件中,如下所示:
public?abstract?sealed class?Color?{...}
... class?Red?????extends?Color?{...}
... class?Blue????extends?Color?{...}
... class?Yellow??extends?Color?{...}注意,這些子類并不是嵌套在封印類中,而是放在類定義之后。這與使用關(guān)鍵字 permit 是一樣的效果,可以擴(kuò)展 Color 的類只有 Red、Blue 和 Yellow。
那么,封印類通常用在哪里?通過限定繼承規(guī)則,同時(shí)也限定了封裝規(guī)則。假設(shè)你正在開發(fā)一個(gè)庫,并且需要將抽象類 Color 包含在其中。你知道 Color 這個(gè)類以及哪些類需要擴(kuò)展它,但如果它被聲明為 public 的,那么你有什么辦法可以阻止外部代碼擴(kuò)展它?
如果有人誤解了它的用途并用 Square 對它進(jìn)行了擴(kuò)展,該怎么辦?這符合你的意圖嗎?或者你其實(shí)是想讓 Color 保持私有?但即使是這樣,包級別的可見性也不能避免所有問題。如果后來有人對這個(gè)庫進(jìn)行了擴(kuò)展了該怎么辦?他們?nèi)绾文軌蛑滥阒淮蛩阕屢恍〔糠诸惣?Color?
封印類不僅可以保護(hù)你的代碼不受外部代碼的影響,還是一種向你可能從未見過的人傳達(dá)意圖的方式。如果一個(gè)類是封印的,你是在傳達(dá)只有某些類可以擴(kuò)展它。這種健壯性可以確保在多年以后任何閱讀你代碼的人都會(huì)理解代碼的嚴(yán)謹(jǐn)。
2:增強(qiáng)的空指針異常
增強(qiáng)的 空指針異常 是一個(gè)有趣的更新——不會(huì)太復(fù)雜,但仍然很受歡迎。這個(gè)增強(qiáng)在 Java 14 中正式發(fā)布,提高了空指針異常 (NullPointerException,簡稱 NPE) 的可讀性,可以打印出在拋出異常位置所調(diào)用的方法的名稱和空變量的名稱。例如,如果你調(diào)用 a.b.getName(),而 b 為空,那么異常的堆棧跟蹤信息會(huì)告訴你調(diào)用 getName() 失敗,因?yàn)?b 是空的。
我們都知道,NPE 是一種非常常見的異常,雖然在大多數(shù)情況下找出導(dǎo)致拋出異常的根源并不難,但你會(huì)時(shí)不時(shí)地遇到同時(shí)有兩三個(gè)可疑變量的情況。你進(jìn)入調(diào)試模式,開始查看代碼,但問題很難重現(xiàn)。你只能試著回憶最初做了什么導(dǎo)致拋出 NPE 的。
如果你能提前獲得這些信息,就不用這些麻煩地調(diào)試了。這就是這個(gè)特性的閃光點(diǎn):不用再猜測 NPE 是從哪里拋出來的。在關(guān)鍵時(shí)刻,當(dāng)你遇到難以重現(xiàn)的異常場景時(shí),你就有了解決問題所需的一切。
這絕對是個(gè)救星!
3:switch 表達(dá)式
希望你耐心聽我說幾句——switch 表達(dá)式(在 Java 12 中預(yù)覽,并正式添加到 Java 14 中) 是 switch 語句和 lambda 之間的某種結(jié)合。真的,當(dāng)我第一次向別人描述 switch 表達(dá)式時(shí),我的說法是他們把 switch 語句 lambda 化了。請看下面這個(gè)語法:
String?adjacentColor = switch?(color) {
????case?Blue, Green -> "yellow";
????case?Red, Purple -> "blue";
????case?Yellow, Orange -> "red";
????default?????????????-> "Unknown Color";
};現(xiàn)在明白我的意思了嗎?
一個(gè)明顯的區(qū)別是沒有了 break 語句。switch 表達(dá)式延續(xù)了 Oracle 讓 Java 語法更簡潔的趨勢。Oracle 非常討厭大多數(shù) switch 語句包含很多的 CASE BREAK、CASE BREAK、CASE BREAK……。
老實(shí)說,他們討厭這個(gè)是對的,因?yàn)槿藗兒苋菀自谶@個(gè)地方犯錯(cuò)。我們當(dāng)中是否有人敢說他們從來沒有遇到過這種情況:忘記在 switch 里添加 break 語句,只有當(dāng)代碼在運(yùn)行時(shí)發(fā)生崩潰才知道?switch 表達(dá)式通過一種有趣的方式修復(fù)了這個(gè)問題,你只需要用逗號隔開同一個(gè)代碼塊里所有的值。沒錯(cuò),不需要使用 break 了!它會(huì)替你處理好!
switch 表達(dá)式還新增了 yield 關(guān)鍵字。如果一個(gè) case 進(jìn)入了一個(gè)代碼塊,yield 將被作為 switch 表達(dá)式的返回語句。例如,如果我們將上面的代碼稍作修改:
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";
??}
};在默認(rèn) case 里,System.out.println() 方法將被執(zhí)行,adjacentColor 變量最終的值是“Unknown Color”,因?yàn)檫@是 yield 返回的結(jié)果。
總的來說,switch 表達(dá)式是一種更簡潔的 switch 語句,但它不會(huì)取代 switch 語句,這兩種語句都可用。
4:文本塊
文本塊 特性在 Java 13 中預(yù)覽,并正式添加到 Java 15 中,它可以簡化多行字符串的寫法,支持換行,并在不需要轉(zhuǎn)義字符的情況下保持縮進(jìn)。要?jiǎng)?chuàng)建一個(gè)文本塊,只需要這樣:
String text = """
Hello
World""";注意,這個(gè)變量仍然是一個(gè)字符串,只是它隱含了換行和制表符。同樣,如果我們想要使用引號,也不需要轉(zhuǎn)義字符:
String text = """
You can "quote" without complaints!"""; // You can "quote"?without complaints!唯一需要使用反斜杠轉(zhuǎn)義字符的地方是當(dāng)你想要在文本塊里包含""":
String text = """
The only necessary escape is \""",
everything else?is?maintained.""";除此之外,你可以調(diào)用 String 的 format() 方法,用動(dòng)態(tài)內(nèi)容替換文本塊中的占位符:
String name = "Chris";
String text = """
My name is %s.""".format(name); // My name is?Chris.每行后面的空格都會(huì)被剪切掉,除非你指定了'\s',這是文本塊的一個(gè)轉(zhuǎn)義字符:
String text1 = """
No trailing spaces.
Trailing spaces. \s""";那么,在什么情況下會(huì)使用文本塊呢?除了能夠?qū)Υ髩K的文本進(jìn)行格式化外,將代碼片段粘貼到字符串中也變得非常容易。因?yàn)榭s進(jìn)被保留了,如果你要寫一個(gè) HTML 或 Python 代碼塊,或使用其他任何語言,你都可以按照正常的方式寫好它們,然后用"""把它們括起來,就可以保留代碼的格式。你甚至可以用文本塊來編寫 JSON,并使用 format() 方法輕松地插入值。
總的來說,這是個(gè)一個(gè)很方便的特性。雖然文本塊看起來只是一個(gè)小功能,但從長遠(yuǎn)來看,類似這種可以提升開發(fā)效率的小功能會(huì)逐漸增加。
5:record 類
record 類 在 Java 14 中預(yù)覽,并正式添加到 Java 16 中,是一種數(shù)據(jù)類,處理所有與 POJO 相關(guān)的樣板代碼。也就是說,如果你聲明了一個(gè) record 類:
public?record Coord(int?x, int?y)?{
}equals() 和 hashcode() 方法會(huì)自動(dòng)實(shí)現(xiàn),toString() 將返回這個(gè)類實(shí)例包含的所有字段的值,最重要的是,x() 和 y() 將分別返回 x 和 y 的值。想想你之前寫過的 POJO 類,并想象一下用 record 類來代替它們會(huì)怎樣。是不是好看多了?省了多少事了?
除此之外,record 類是 final 和不可變的——不能被繼承,并且類實(shí)例一旦被創(chuàng)建,它的字段就不能被修改。你可以在 record 類中聲明方法,包括非靜態(tài)方法和靜態(tài)方法:
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 類可以有多個(gè)構(gòu)造器:
public?record Coord(int?x, int?y)?{
??public?Coord()?{
????this(0,0); // The default constructor is still implemented.
??}
}需要注意的是,當(dāng)你在 record 類中聲明自定義構(gòu)造函數(shù)時(shí),必須調(diào)用默認(rèn)構(gòu)造函數(shù)。否則,record 類將不知道如何處理它的值。如果你聲明了一個(gè)與默認(rèn)構(gòu)造函數(shù)一樣的構(gòu)造函數(shù),你要初始化所有的字段:
public?record Coord(int?x, int?y)?{
??public?Coord(int?x, int?y)?{
????this.x = x;
????this.y = y;
??} // Will replace the default constructor.
}關(guān)于 record 類,有很多可討論的話題。這是一個(gè)大的變更,在合適的地方使用它們,它們會(huì)非常有用。我在這里沒有涵蓋所有內(nèi)容,但希望這能讓你了解它們所提供的能力。
6:模式匹配
模式匹配 是 Oracle 在與 Java 冗長語法的斗爭中做出的另一個(gè)舉措。模式匹配在 Java 14 和 Java 15 中預(yù)覽過,并正式添加到 Java 16 中,它可以在 instanceof 條件得到滿足后消除不必要的類型轉(zhuǎn)換。例如,我們都很熟悉這樣的代碼:
if?(o instanceof Car) {
??System.out.println(((Car) o).getModel());
}如果你想要訪問 Car 的方法,必要要這么做。在第二行,o 是 Car 的實(shí)例,這是毫無疑問的,instanceof 已經(jīng)確認(rèn)了這一點(diǎn)。如果我們使用模式匹配,只要做一個(gè)小小的改變:
if?(o instanceof Car c) {
??System.out.println(c.getModel());
}現(xiàn)在,所有的對象類型轉(zhuǎn)換都由編譯器完成。看起來改變很小,但它避免了很多樣板代碼。這也適用于條件分支,當(dāng)你進(jìn)入一個(gè)已經(jīng)明確了對象類型的分支:
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");
}雖然模式匹配不像其他一些變更那么大,但還是簡化了常用的代碼。
Java 17 將繼續(xù)演進(jìn)
當(dāng)然,Java 12 到 Java 17 并不是只推出了這些更新,這些只是我認(rèn)為比較有趣的部分。用最新的 Java 版本來運(yùn)行大型項(xiàng)目需要很大的勇氣,如果是從 Java 8 遷移過來,則更需要勇氣。
如果有人猶豫不決,是可以理解的。但是,即使你沒有遷移計(jì)劃,或者某個(gè)升級計(jì)劃可能持續(xù)數(shù)年之久,跟上語言新特性的變化總歸是件好事。我希望我分享的這些內(nèi)容能夠讓它們更加深入人心,讓閱讀過這些內(nèi)容的人都可以開始考慮如何使用它們!
Java 17 很特別——它是下一個(gè)長期支持版本,接過了 Java 11 的接力棒,并且很可能在未來幾年內(nèi)成為遷移最多的 Java 版本。即使你現(xiàn)在還沒有做好準(zhǔn)備,可以開始學(xué)習(xí)起來了,當(dāng)你身處基于 Java 17 的項(xiàng)目當(dāng)中,你已經(jīng)是一名經(jīng)驗(yàn)豐富的開發(fā)者!
來源:infoq.com/articles/six-features-jdk12-to-jdk17/
往期推薦
