【注】本文譯自: What’s New Between Java 11 and Java 17?
9 月 14 日 Java 17 釋出。是時候仔細看看自上一個 LTS 版本(即 Java 11)以來的變化。我們先簡要介紹許可模型,然後重點介紹 Java 11 和 Java 17 之間的一些變化,主要是通過 例子。享受吧!
1. 介紹
首先,讓我們仔細看看 Java 許可和支援模型。Java 17 是一個 LTS(長期支援)版本,就像 Java 11 一樣。Java 11 開始了一個新的釋出節奏。Java 11 支援到 2023 年 9 月,擴充套件支援到 2026 年 9 月。此外,在 Java 11 中,Oracle JDK 不再免費用於生產和商業用途。每 6 個月釋出一個新的 Java 版本,即所謂的非 LTS 釋出,從 Java 12 直至幷包括 Java 16。但是,這些都是生產就緒版本。與 LTS 版本的唯一區別是支援在下一個版本釋出時結束。例如。 Java 12 的支援在 Java 13 釋出時結束。當您想要保持支援時,您或多或少必須升級到 Java 13。當您的某些依賴項尚未為 Java 13 做好準備時,這可能會導致一些問題。大多數情況下,對於生產用途,公司將等待 LTS 版本。但即便如此,一些公司也不願意升級。最近 Snyk 的一項調查顯示,只有 60% 的人在生產中使用 Java 11,而這距離 Java 11 釋出已經過去了 3 年!60% 的公司仍在使用 Java 8。另一個值得注意的有趣事情是,下一個 LTS 版本將是 Java 21,它將在 2 年內釋出。關於庫在 Java 17 是否存在問題的一個很好的概述,可以在此處找到。
隨著 Java 17 的推出,Oracle 許可模式發生了變化。Java 17 是根據新的 NFTC(Oracle 免費條款和條件)許可釋出的。因此,再次允許免費將 Oracle JDK 版本用於生產和商業用途。在同一個 Snyk 調查中,有人指出 Oracle JDK 版本在生產環境中僅被 23% 的使用者使用。請注意,對 LTS 版本的支援將在下一個 LTS 版本釋出一年後結束。看看這將如何影響升級到下一個 LTS 版本將會很有趣。
Java 11 和 Java 17 之間發生了什麼變化?可以在 OpenJDK 網站上找到 JEP(Java 增強提案)的完整列表。在這裡,您可以閱讀每個 JEP 的詳細資訊。 有關自 Java 11 以來每個版本更改的完整列表,Oracle 發行說明提供了一個很好的概述。
在接下來的部分中,將通過示例解釋一些更改,但主要取決於您對這些新功能進行試驗以熟悉它們。這篇文章中使用的所有資源都可以在 GitHub 上找到。
最後一件事是 Oracle 釋出了 dev.java,所以不要忘記看一下。
2. Text Blocks(本文塊)
為了使 Java 更具可讀性和更簡潔,已經進行了許多改進。文字塊無疑使程式碼更具可讀性。首先,我們來看看問題。假設您需要一些 JSON 字串到您的程式碼中並且您需要列印它。這段程式碼有幾個問題:
- 雙引號的轉義;
- 字串連線,使其具有或多或少的可讀性;
JSON 的複製貼上是一項勞動密集型的工作(您的 IDE 可能會幫助您解決該問題)。
private static void oldStyle() { System.out.println(""" ************* * Old Style * *************"""); String text = "{\n" + " \"name\": \"John Doe\",\n" + " \"age\": 45,\n" + " \"address\": \"Doe Street, 23, Java Town\"\n" + "}"; System.out.println(text); }
上面程式碼的輸出是格式良好的 JSON。
{
"name": "John Doe",
"age": 45,
"address": "Doe Street, 23, Java Town"
}
文字塊用三個雙引號定義,其中結尾的三個雙引號不能與起始的在同一行。首先,只需列印一個空塊。為了視覺化發生了什麼,文字被列印在兩個雙管之間。
private static void emptyBlock() {
System.out.println("""
***************
* Empty Block *
***************""");
String text = """
""";
System.out.println("|" + text + "|");
}
輸出是:
||||
有問題的 JSON 部分現在可以寫成如下,這樣可讀性更好。不需要轉義雙引號,它看起來就像會被列印。
private static void jsonBlock() {
System.out.println("""
**************
* Json Block *
**************""");
String text = """
{
"name": "John Doe",
"age": 45,
"address": "Doe Street, 23, Java Town"
}
""";
System.out.println(text);
}
輸出當然是相同的。
{
"name": "John Doe",
"age": 45,
"address": "Doe Street, 23, Java Town"
}
在前面的輸出中,沒有前面的空格。但是,在程式碼中,前面有空格。如何確定剝離前面的空格? 首先,將結尾的三個雙引號向左移動更多。
private static void jsonMovedBracketsBlock() {
System.out.println("""
*****************************
* Json Moved Brackets Block *
*****************************""");
String text = """
{
"name": "John Doe",
"age": 45,
"address": "Doe Street, 23, Java Town"
}
""";
System.out.println(text);
}
輸出現在在每行之前列印兩個空格。這意味著結尾的三個雙引號表示文字塊的開始。
{
"name": "John Doe",
"age": 45,
"address": "Doe Street, 23, Java Town"
}
123
當你將結尾的三個雙引號向右移動時會發生什麼?
private static void jsonMovedEndQuoteBlock() {
System.out.println("""
******************************
* Json Moved End Quote Block *
******************************""");
String text = """
{
"name": "John Doe",
"age": 45,
"address": "Doe Street, 23, Java Town"
}
""";
System.out.println(text);
}
前面的間距現在由文字塊中的第一個非空格字元決定。
{
"name": "John Doe",
"age": 45,
"address": "Doe Street, 23, Java Town"
}
3. Switch 表示式
Switch 表示式將允許您從 switch 返回值並在賦值等中使用這些返回值。此處顯示了一個經典的 switch,其中,根據給定的 Fruit 列舉值,需要執行一些操作。故意忽略了 break。
private static void oldStyleWithoutBreak(FruitType fruit) {
System.out.println("""
***************************
* Old style without break *
***************************""");
switch (fruit) {
case APPLE, PEAR:
System.out.println("Common fruit");
case ORANGE, AVOCADO:
System.out.println("Exotic fruit");
default:
System.out.println("Undefined fruit");
}
}
使用 APPLE 呼叫該方法。
oldStyleWithoutBreak(Fruit.APPLE);
這將列印每個 case,因為沒有 break 語句,case 就失效了。
Common fruit
Exotic fruit
Undefined fruit
因此,有必要在每個 case 中新增一個 break 語句,以防止這種失效。
private static void oldStyleWithBreak(FruitType fruit) {
System.out.println("""
************************
* Old style with break *
************************""");
switch (fruit) {
case APPLE, PEAR:
System.out.println("Common fruit");
break;
case ORANGE, AVOCADO:
System.out.println("Exotic fruit");
break;
default:
System.out.println("Undefined fruit");
}
}
執行此方法會為您提供所需的結果,但現在程式碼的可讀性稍差。
Common fruit
這可以通過使用 Switch 表示式來解決。用箭頭 (->) 替換冒號 (:) 並確保在大小寫中使用表示式。Switch 表示式的預設行為是沒有失敗,因此不需要 break。
private static void withSwitchExpression(FruitType fruit) {
System.out.println("""
**************************
* With switch expression *
**************************""");
switch (fruit) {
case APPLE, PEAR -> System.out.println("Common fruit");
case ORANGE, AVOCADO -> System.out.println("Exotic fruit");
default -> System.out.println("Undefined fruit");
}
}
這已經不那麼囉嗦了,結果是相同的。
Switch 表示式也可以返回一個值。在上面的示例中,您可以返回 String 值並將它們分配給變數 text。在此之後,可以列印 text 本變數。不要忘記在最後一個案例括號後新增一個分號。
private static void withReturnValue(FruitType fruit) {
System.out.println("""
*********************
* With return value *
*********************""");
String text = switch (fruit) {
case APPLE, PEAR -> "Common fruit";
case ORANGE, AVOCADO -> "Exotic fruit";
default -> "Undefined fruit";
};
System.out.println(text);
}
而且,更短的是,上面的內容可以用一個語句重寫。這是否比上面的更具可讀性取決於您。
private static void withReturnValueEvenShorter(FruitType fruit) {
System.out.println("""
**********************************
* With return value even shorter *
**********************************""");
System.out.println(
switch (fruit) {
case APPLE, PEAR -> "Common fruit";
case ORANGE, AVOCADO -> "Exotic fruit";
default -> "Undefined fruit";
});
}
當您需要在 case 中做不止一件事情時,您會怎麼做? 在這種情況下,您可以使用方括號來表示 case 塊,並在返回值時使用關鍵字 yield。
private static void withYield(FruitType fruit) {
System.out.println("""
**************
* With yield *
**************""");
String text = switch (fruit) {
case APPLE, PEAR -> {
System.out.println("the given fruit was: " + fruit);
yield "Common fruit";
}
case ORANGE, AVOCADO -> "Exotic fruit";
default -> "Undefined fruit";
};
System.out.println(text);
}
輸出現在有點不同,執行了兩個列印語句。
the given fruit was: APPLE
Common fruit
您可以在“舊” switch 語法中使用 yield 關鍵字也很酷。這裡不需要 break。
private static void oldStyleWithYield(FruitType fruit) {
System.out.println("""
************************
* Old style with yield *
************************""");
System.out.println(switch (fruit) {
case APPLE, PEAR:
yield "Common fruit";
case ORANGE, AVOCADO:
yield "Exotic fruit";
default:
yield "Undefined fruit";
});
}
4. Records(記錄)
Records 將允許您建立不可變的資料類。目前,您需要例如 使用 IDE 的自動生成函式建立 GrapeClass 以生成建構函式、getter、hashCode、equals 和 toString,或者您可以使用 Lombok 達到同樣的目的。最後,您會得到一些樣板程式碼,或者您的專案最終會依賴 Lombok。
public class GrapeClass {
private final Color color;
private final int nbrOfPits;
public GrapeClass(Color color, int nbrOfPits) {
this.color = color;
this.nbrOfPits = nbrOfPits;
}
public Color getColor() {
return color;
}
public int getNbrOfPits() {
return nbrOfPits;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GrapeClass that = (GrapeClass) o;
return nbrOfPits == that.nbrOfPits && color.equals(that.color);
}
@Override
public int hashCode() {
return Objects.hash(color, nbrOfPits);
}
@Override
public String toString() {
return "GrapeClass{" +
"color=" + color +
", nbrOfPits=" + nbrOfPits +
'}';
}
}
使用上述 GrapeClass 類執行一些測試。建立兩個例項,列印它們,比較它們,建立一個副本並也比較這個。
private static void oldStyle() {
System.out.println("""
*************
* Old style *
*************""");
GrapeClass grape1 = new GrapeClass(Color.BLUE, 1);
GrapeClass grape2 = new GrapeClass(Color.WHITE, 2);
System.out.println("Grape 1 is " + grape1);
System.out.println("Grape 2 is " + grape2);
System.out.println("Grape 1 equals grape 2? " + grape1.equals(grape2));
GrapeClass grape1Copy = new GrapeClass(grape1.getColor(), grape1.getNbrOfPits());
System.out.println("Grape 1 equals its copy? " + grape1.equals(grape1Copy));
}
測試的輸出是:
Grape 1 is GrapeClass{color=java.awt.Color[r=0,g=0,b=255], nbrOfPits=1}
Grape 2 is GrapeClass{color=java.awt.Color[r=255,g=255,b=255], nbrOfPits=2}
Grape 1 equals grape 2? false
Grape 1 equals its copy? true
GrapeRecord 具有與 GrapeClass 相同的功能,但要簡單得多。您建立一個記錄並指出欄位應該是什麼,然後您就完成了。
record GrapeRecord(Color color, int nbrOfPits) {
}
一個記錄可以在它自己的檔案中定義,但是因為它非常緊湊,所以在需要的地方定義它也是可以的。上面用記錄重寫的測試變成如下:
private static void basicRecord() {
System.out.println("""
****************
* Basic record *
****************""");
record GrapeRecord(Color color, int nbrOfPits) {}
GrapeRecord grape1 = new GrapeRecord(Color.BLUE, 1);
GrapeRecord grape2 = new GrapeRecord(Color.WHITE, 2);
System.out.println("Grape 1 is " + grape1);
System.out.println("Grape 2 is " + grape2);
System.out.println("Grape 1 equals grape 2? " + grape1.equals(grape2));
GrapeRecord grape1Copy = new GrapeRecord(grape1.color(), grape1.nbrOfPits());
System.out.println("Grape 1 equals its copy? " + grape1.equals(grape1Copy));
}
輸出與上面相同。重要的是要注意記錄的副本應該以相同的副本結束。新增額外的功能,例如 grape1.nbrOfPits() 為了做一些處理並返回與初始 nbrOfPits 不同的值是一種不好的做法。雖然這是允許的,但您不應該這樣做。
建構函式可以通過一些欄位驗證進行擴充套件。請注意,將引數分配給記錄欄位發生在建構函式的末尾。
private static void basicRecordWithValidation() {
System.out.println("""
********************************
* Basic record with validation *
********************************""");
record GrapeRecord(Color color, int nbrOfPits) {
GrapeRecord {
System.out.println("Parameter color=" + color + ", Field color=" + this.color());
System.out.println("Parameter nbrOfPits=" + nbrOfPits + ", Field nbrOfPits=" + this.nbrOfPits());
if (color == null) {
throw new IllegalArgumentException("Color may not be null");
}
}
}
GrapeRecord grape1 = new GrapeRecord(Color.BLUE, 1);
System.out.println("Grape 1 is " + grape1);
GrapeRecord grapeNull = new GrapeRecord(null, 2);
}
上述測試的輸出向您展示了此功能。 在建構函式內部,欄位值仍然為 null,但在列印記錄時,它們被分配了一個值。驗證也做它應該做的事情,並在顏色為 null 時丟擲 IllegalArgumentException。
Parameter color=java.awt.Color[r=0,g=0,b=255], Field color=null
Parameter nbrOfPits=1, Field nbrOfPits=0
Grape 1 is GrapeRecord[color=java.awt.Color[r=0,g=0,b=255], nbrOfPits=1]
Parameter color=null, Field color=null
Parameter nbrOfPits=2, Field nbrOfPits=0
Exception in thread "main" java.lang.IllegalArgumentException: Color may not be null
at com.mydeveloperplanet.myjava17planet.Records$2GrapeRecord.<init>(Records.java:40)
at com.mydeveloperplanet.myjava17planet.Records.basicRecordWithValidation(Records.java:46)
at com.mydeveloperplanet.myjava17planet.Records.main(Records.java:10)
5. Sealed Classes(密封類)
密封類將讓您更好地控制哪些類可以擴充套件您的類。密封類可能更像是一個對庫所有者有用的功能。一個類在 Java 11 final 中或者可以擴充套件。如果您想控制哪些類可以擴充套件您的超類,您可以將所有類放在同一個包中,並賦予超類包可見性。現在一切都在您的控制之下,但是,不再可能從包外部訪問超類。讓我們通過一個例子來看看這是如何工作的。
在包
com.mydeveloperplanet.myjava17planet.nonsealed 中建立一個具有公共可見性的抽象類 Fruit。在同一個包中,建立了最終的類 Apple 和 Pear,它們都擴充套件了 Fruit。
public abstract class Fruit {
}
public final class Apple extends Fruit {
}
public final class Pear extends Fruit {
}
在包
com.mydeveloperplanet.myjava17planet 中建立一個帶有 problemSpace 方法的 SealedClasses.java 檔案。如您所見,可以為 Apple、 Pear 和 Apple 建立例項,可以將 Apple 分配給 Fruit。除此之外,還可以建立一個擴充套件 Fruit 的 Avocado 類。
public abstract sealed class FruitSealed permits AppleSealed, PearSealed {
}
public non-sealed class AppleSealed extends FruitSealed {
}
public final class PearSealed extends FruitSealed {
}
假設您不希望有人擴充套件 Fruit。 在這種情況下,您可以將 Fruit 的可見性更改為預設可見性(刪除 public 關鍵字)。在將 Apple分配給 Fruit 和建立 Avocado 類時,上述程式碼將不再編譯。後者是需要的,但我們確實希望能夠將一個 Apple 分配給一個 Fruit。這可以在帶有密封類的 Java 17 中解決。
在包
com.mydeveloperplanet.myjava17planet.sealed 中,建立了 Fruit、Apple 和 Pear 的密封版本。唯一要做的就是將 sealed 關鍵字新增到 Fruit 類中,並使用 permits 關鍵字指示哪些類可以擴充套件此 Sealed 類。子類需要指明它們是 final、 sealed 還是 non-sealed。超類無法控制子類是否可以擴充套件以及如何擴充套件。
public abstract sealed class FruitSealed permits AppleSealed, PearSealed {
}
public non-sealed class AppleSealed extends FruitSealed {
}
public final class PearSealed extends FruitSealed {
}
在 sealedClasses 方法中,仍然可以將 AppleSealed 分配給 FruitSealed,但 Avocado 不允許擴充套件 FruitSealed。 然而,允許擴充套件 AppleSealed 因為這個子類被指示為非密封的。
private static void sealedClasses() {
AppleSealed apple = new AppleSealed();
PearSealed pear = new PearSealed();
FruitSealed fruit = apple;
class Avocado extends AppleSealed {};
}
6. instanceof 的模式匹配
通常需要檢查物件是否屬於某種型別,如果是,首先要做的是將物件強制轉換為該特定型別的新變數。可以在以下程式碼中看到一個示例:
private static void oldStyle() {
System.out.println("""
*************
* Old Style *
*************""");
Object o = new GrapeClass(Color.BLUE, 2);
if (o instanceof GrapeClass) {
GrapeClass grape = (GrapeClass) o;
System.out.println("This grape has " + grape.getNbrOfPits() + " pits.");
}
}
輸出是:
This grape has 2 pits.
使用 instanceof 的模式匹配,上面的可以改寫如下。如您所見,可以在 instanceof 檢查中建立變數,並且不再需要用於建立新變數和轉換物件的額外行。
private static void patternMatching() {
System.out.println("""
********************
* Pattern matching *
********************""");
Object o = new GrapeClass(Color.BLUE, 2);
if (o instanceof GrapeClass grape) {
System.out.println("This grape has " + grape.getNbrOfPits() + " pits.");
}
}
輸出當然與上面相同。
仔細檢視變數的範圍很重要。它不應該是模稜兩可的。在下面的程式碼中,&& 之後的條件只會在 instanceof 檢查結果為 true 時進行評估。 所以這是允許的。將 && 更改為 || 不會編譯。
private static void patternMatchingScope() {
System.out.println("""
*******************************
* Pattern matching scope test *
*******************************""");
Object o = new GrapeClass(Color.BLUE, 2);
if (o instanceof GrapeClass grape && grape.getNbrOfPits() == 2) {
System.out.println("This grape has " + grape.getNbrOfPits() + " pits.");
}
}
下面的程式碼顯示了另一個有關範圍的示例。如果物件不是 GrapeClass 型別,則丟擲 RuntimeException。在這種情況下,永遠不會到達列印語句。在這種情況下,也可以使用 grape 變數,因為編譯器肯定知道 grape 存在。
private static void patternMatchingScopeException() {
System.out.println("""
**********************************************
* Pattern matching scope test with exception *
**********************************************""");
Object o = new GrapeClass(Color.BLUE, 2);
if (!(o instanceof GrapeClass grape)) {
throw new RuntimeException();
}
System.out.println("This grape has " + grape.getNbrOfPits() + " pits.");
}
7.有用的空指標異常
有用的 NullPointerException 將為您節省一些寶貴的分析時間。以下程式碼導致 NullPointerException。
public class HelpfulNullPointerExceptions {
public static void main(String[] args) {
HashMap<String, GrapeClass> grapes = new HashMap<>();
grapes.put("grape1", new GrapeClass(Color.BLUE, 2));
grapes.put("grape2", new GrapeClass(Color.white, 4));
grapes.put("grape3", null);
var color = ((GrapeClass) grapes.get("grape3")).getColor();
}
}
對於 Java 11,輸出將顯示 NullPointerException 發生的行號,但您不知道哪個鏈式方法解析為 null。你必須通過除錯的方式找到自己。
Exception in thread "main" java.lang.NullPointerException
at com.mydeveloperplanet.myjava17planet.HelpfulNullPointerExceptions.main(HelpfulNullPointerExceptions.java:13)
在 Java 17 中,相同的程式碼會產生以下輸出,其中準確顯示了 NullPointerException 發生的位置。
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "com.mydeveloperplanet.myjava17planet.GrapeClass.getColor()" because the return value of "java.util.HashMap.get(Object)" is null
at com.mydeveloperplanet.myjava17planet.HelpfulNullPointerExceptions.main(HelpfulNullPointerExceptions.java:13)
8. 精簡數字格式支援
NumberFormat 中新增了一個工廠方法,以便根據 Unicode 標準以緊湊的、人類可讀的形式格式化數字。 SHORT 格式樣式如下面的程式碼所示:
NumberFormat fmt = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.SHORT);
System.out.println(fmt.format(1000));
System.out.println(fmt.format(100000));
System.out.println(fmt.format(1000000));
輸出是:
1K
100K
1M
LONG 格式樣式:
fmt = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.LONG);
System.out.println(fmt.format(1000));
System.out.println(fmt.format(100000));
System.out.println(fmt.format(1000000));
輸出是:
1 thousand
100 thousand
1 million
荷蘭語替換英語的 LONG 格式:
fmt = NumberFormat.getCompactNumberInstance(Locale.forLanguageTag("NL"), NumberFormat.Style.LONG);
System.out.println(fmt.format(1000));
System.out.println(fmt.format(100000));
System.out.println(fmt.format(1000000));
輸出是:
1 duizend
100 duizend
1 miljoen
9. 新增了日週期支援
新增了一個新模式 B 用於格式化 DateTime,該模式根據 Unicode 標準指示日期時間段。
使用預設的中文語言環境,列印一天中的幾個時刻:
System.out.println("""
**********************
* Chinese formatting *
**********************""");
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("B");
System.out.println(dtf.format(LocalTime.of(8, 0)));
System.out.println(dtf.format(LocalTime.of(13, 0)));
System.out.println(dtf.format(LocalTime.of(20, 0)));
System.out.println(dtf.format(LocalTime.of(23, 0)));
System.out.println(dtf.format(LocalTime.of(0, 0)));
輸出是:
上午
下午
晚上
晚上
午夜
現在使用荷蘭語本地環境:
System.out.println("""
********************
* Dutch formatting *
********************""");
dtf = DateTimeFormatter.ofPattern("B").withLocale(Locale.forLanguageTag("NL"));
System.out.println(dtf.format(LocalTime.of(8, 0)));
System.out.println(dtf.format(LocalTime.of(13, 0)));
System.out.println(dtf.format(LocalTime.of(20, 0)));
System.out.println(dtf.format(LocalTime.of(0, 0)));
System.out.println(dtf.format(LocalTime.of(1, 0)));
輸出如下。請注意,英國之夜從 23 點開始,荷蘭之夜從 01 點開始。可能是文化差異;-)。
’s ochtends
’s middags
’s avonds
middernacht
’s nachts
10. Stream.toList()
為了將 Stream 轉換為 List,您需要使用 collect 的 Collectors.toList() 方法。這非常冗長,如下面的示例所示。
private static void oldStyle() {
System.out.println("""
*************
* Old style *
*************""");
Stream<String> stringStream = Stream.of("a", "b", "c");
List<String> stringList = stringStream.collect(Collectors.toList());
for(String s : stringList) {
System.out.println(s);
}
}
在 Java 17 中,新增了一個 toList 方法來替換舊的行為。
private static void streamToList() {
System.out.println("""
*****************
* stream toList *
*****************""");
Stream<String> stringStream = Stream.of("a", "b", "c");
List<String> stringList = stringStream.toList();
for(String s : stringList) {
System.out.println(s);
}
}
11. 結論
在本文中,您快速瀏覽了自上一個 LTS 版本 Java 11 以來新增的一些功能。現在由您開始考慮遷移到 Java 17 的計劃,以及瞭解有關這些新功能的更多資訊以及您如何 可以將它們應用到您的日常編碼習慣中。提示:IntelliJ 會幫你解決這個問題!