Java 16 新特性介紹


本文要點

  • Java 16 和即將發布的 Java 17 引入了大量特性和語言增強,有助於提高開發人員的生產力和應用程序性能
  • Java 16 Stream API 為常用的終端操作提供了很多新方法,有助於減少樣板代碼的混亂現象
  • Record 是 Java 16 中的一項語言新特性,可簡潔地定義純數據類。編譯器提供了構造器、訪問器和一些常見 Object 方法的實現
  • 模式匹配是 Java 16 中的另一個新特性,它簡化了使用 instanceof 代碼塊完成的顯式和冗長的轉換,此外還有很多好處

Java 16 於 2021 年 3 月發布,版本類型是可用於生產的 GA 構建,我在這段深度視頻演示中介紹了該版本的新特性。下一個 LTS 版本 Java 17 計划於今年 9 月發布。Java 17 將包含許多改進和語言增強,其中大部分是自 Java 11 以來交付的所有新特性和更改的成果結晶。

就 Java 16 中的新特性而言,我將分享 Stream API 中一項討喜的更新,然后主要關注語言更改部分。

從 Stream 到 List

List<String> features = Stream.of("Records", "Pattern Matching", "Sealed Classes") .map(String::toLowerCase) .filter(s -> s.contains(" ")) .collect(Collectors.toList());

復制代碼

如果你習慣使用 Java Stream API,那么應該會很熟悉上面這個代碼段。

這段代碼里有一個包含一些字符串的流。我們在它上面映射一個函數,然后過濾這個流。

最后,我們將流物化為一個列表。

如你所見,我們通常會調用終端操作 collect 並給它傳遞一個收集器。

這里是很常見的實踐——使用 collect 並將 Collectors.toList()傳遞給它,感覺就像是樣板代碼一樣。

好消息是,在 Java 16 中 Stream API 中添加了一個新方法,使我們能夠立即將 toList()作為一個流的一個終端操作來調用。

List<String> features = Stream.of("Records", "Pattern Matching", "Sealed Classes") .map(String::toLowerCase) .filter(s -> s.contains(" ")) .toList();

復制代碼

在上面的代碼中使用這個新方法會生成一個來自這個流,且包含一個空格的字符串的列表。請注意,我們返回的這個列表是一個不可修改的列表。這意味着你不能再從這個終端操作返回的列表中添加或刪除任何元素。如果要將流收集到一個可變列表中,則必須繼續使用一個帶有 collect()函數的收集器。所以這個 Java 16 中新引入的 toList()方法真的很討喜。這個更新應該能讓流管道代碼塊讀起來更容易一些。

Stream API 的另一個更新是 mapMulti()方法。它的用途有點像 flatMap()方法。如果你平常用的是 flatMap(),並且映射到 lambda 中的內部流並傳遞給它,那么 mapMulti()為你提供了一種替代方法,你可以將元素推送給一個消費者。我不會在本文中具體介紹這個方法,因為我想討論的是 Java 16 中的語言新特性。如果你有興趣進一步了解 mapMulti(),我強烈建議你查看Java文檔中關於這種方法的介紹。

Records

Java 16 中引入的第一個重大語言特性稱為記錄(records)。記錄用來將數據表示為 Java 代碼中的數據,而不是任意類。在 Java 16 之前,有時我們只是需要表示一些數據,最終卻得到了一個任意類,如下面的代碼段所示。

public class Product { private String name; private String vendor; Private int price; private boolean inStock; }

復制代碼

這里我們有一個 Product 類,它有四個成員。定義這個類所需的所有信息應該就是這些了。當然,我們需要更多的代碼來完成這項工作。例如,我們需要有一個構造器。我們需要有相應的 getter 方法來獲取成員的值。為了補充完整,我們還需要有與我們定義的成員一致的 equals()、hashCode()和 toString()實現。一些樣板代碼可以由 IDE 生成,但這樣做會有一些缺陷。你也可以使用 Lombok 等框架,但它們也有一些缺陷。

我們真正需要的是由 Java 語言提供一種機制,可以更准確地描述擁有純數據類這個概念。所以在 Java 16 中我們有了記錄的概念。在以下代碼段中,我們將 Product 類重新定義為一個記錄。

public record Product( String name, String vendor, int price, boolean inStock) { }

復制代碼

請注意這里引入了新關鍵字 record。我們需要在關鍵字 record 之后指定記錄類型的名稱。在我們的示例中,這個名稱是 Product。然后我們只需要提供組成這些記錄的組件。在這里,我們給出了四個組件的類型和名稱以提供它們。然后我們就完成了。Java 中的記錄是類的一個特殊形式,其中只包含數據。

記錄能給我們帶來什么呢?一旦我們有了一個記錄聲明,我們就會得到一個類,它有一個隱式構造器,接受這個記錄的組件的所有值。我們會根據所有記錄組件自動獲取 equals()、hashCode()和 toString()方法的實現。此外,我們還為記錄中的每個組件獲取訪問器方法。在上面的例子中,我們得到了一個 name 方法、一個 vendor 方法、一個 price 方法和一個 inStock 方法,它們分別返回這個記錄的組件的實際值。

記錄永遠是不可變的。這里沒有 setter 方法。一旦使用某些值實例化一個記錄,那么你就無法再更改它了。此外,記錄類就是最終形式。你可以使用一個記錄實現一個接口,但在定義記錄時不能擴展其他任何類。總而言之,這里有一些限制。但是記錄為我們提供了一種非常強大的方式來在我們的應用程序中簡潔地定義純數據類。

怎樣看待記錄

你應該如何看待和處理這些新的語言元素呢?記錄是一種新的、受限形式的類,用於將數據建模為數據。我們不可能向記錄添加任何附加狀態;除了記錄的組件之外,你不能定義(非靜態)字段。記錄實際上是建模不可變數據的。你也可以將記錄視為元組,但它並不只是其他一些語言所有的那種一般意義上的元組,在那種元組里有一些可以由索引引用的任意組件。在 Java 中,元組元素有實際名稱,並且元組類型本身——即記錄,也有一個名稱,因為名稱在 Java 中很重要。

記錄不適合哪些場景

有些場景中我們可能會覺得記錄用起來並不是很合適。首先,它們並不是任何現有代碼的一個樣板縮減機制。雖然我們現在有一種非常簡潔的方式來定義這些記錄,但這並不意味着你的應用程序中的任何數據(如類)都可以輕松地被記錄替換,這主要是因為記錄存在的一些限制所致。這也不是它真正的設計目標。

記錄的設計目標是提供一種將數據建模為數據的好方法。它也不是 JavaBeans 的直接替代品,因為正如我之前提到的,訪問器這樣的方法不符合 JavaBeans 的 get 標准。另外 JavaBeans 通常是可變的,而記錄是不可變的。盡管它們的用途有點像,但記錄並不會以某種方式取代 JavaBean。你也不應該將記錄視為值類型。

值類型可能會在未來的 Java 版本中作為語言增強引入,其主要關注內存布局和類中數據的有效表示。當然,這兩條世界線在未來某一時刻可能會合並在一起,但就目前而言,記錄只是表達純數據類的一種更簡潔的方式。

進一步了解記錄

考慮以下代碼,我們創建了 Product 類型的記錄 p1 和 p2,具有完全相同的值。

Product p1 = new Product("peanut butter", "my-vendor", 20, true); Product p2 = new Product("peanut butter", "my-vendor", 20, true);

復制代碼

我們可以通過引用相等來比較這些記錄,也可以使用 equals()方法比較它們,該方法已由記錄實現自動提供。

System.out.println(p1 == p2); // Prints false System.out.println(p1.equals(p2)); // Prints true

復制代碼

可以看到,這兩條記錄是兩個不同的實例,因此引用對比將給出 false。但是當我們使用 equals()時,它只查看這兩個記錄的值,所以它會評估為 true。因為它只考慮記錄內部的數據。重申一下,相等性和哈希碼的實現完全基於我們為記錄的構造器提供的值。

需要注意的一件事是,你仍然可以覆蓋記錄定義中的任何訪問器方法,或者相等性和哈希碼實現。但是,你有責任在記錄的上下文中保留這些方法的語義。並且你可以向記錄定義添加其他方法。你還可以訪問這些新方法中的記錄值。

另一個你可能想在記錄中執行的重要特性是驗證。例如,你只想在提供給記錄構造器的輸入有效時才創建記錄。傳統的驗證方法是定義一個帶有輸入參數的構造器,這些參數在將參數分配給成員變量之前進行驗證。但是對於記錄而言,我們可以使用一種新格式,即所謂的緊湊構造器。在這種格式中,我們可以省略正式的構造器參數。構造器將隱式地訪問組件值。在我們的 Product 示例中,我們可以說如果 price 小於零,則拋出一個新的 IllegalArgumentException。

public record Product( String name, String vendor, int price, boolean inStock) { public Product { if (price < 0) { throw new IllegalArgumentException(); } } }

復制代碼

從上面的代碼段中可以看出,如果價格高於零,我們就不必手動做任何賦值。在編譯此記錄時,編譯器會自動添加從(隱式)構造器參數到記錄字段的賦值。

如果我們願意,甚至可以進行正則化。例如,我們可以將隱式可用的價格參數設置為一個默認值,而不是在價格小於零時拋出異常。

public Product { if (price < 0) { price = 100; } }

復制代碼

同樣,對記錄的實際成員的賦值——即作為這個記錄定義一部分的最終字段,是由編譯器在這個緊湊構造器的末尾自動插入的。總而言之,這是在 Java 中定義純數據類的一種非常通用且非常棒的方法。

你還可以在方法中本地聲明和定義記錄。如果你想在方法中使用一些中間狀態,這會非常方便。例如,假設我們要定義一個打折產品。我們可以定義一個記錄,包含 Product 和一個指示產品是否打折的 boolean 值。

public static void main(String... args) { Product p1 = new Product("peanut butter", "my-vendor", 100, true); record DiscountedProduct(Product product, boolean discounted) {} System.out.println(new DiscountedProduct(p1, true)); }

復制代碼

從上面的代碼段中可以看出,我們不必為新記錄定義提供正文。我們可以使用 p1 和 true 作為參數來實例化 DiscountedProduct。運行代碼時,你會看到它的行為方式與源文件中的頂級記錄完全相同。如果你希望在流管道的中間階段分組某些數據,作為本地構造的記錄會非常有用。

你會在哪里使用記錄

記錄有一些顯而易見的使用場景。比如說當我們想要使用數據傳輸對象(Data Transfer Objects,DTO)時就可以使用記錄。根據定義,DTO 是不需要任何身份或行為的對象。它們只是用來傳輸數據的。例如,從 2.12 版本開始,Jackson 庫支持將記錄序列化和反序列化為 JSON 和其他支持的格式。

如果你希望一個映射中的鍵由充當復合鍵的多個值組成,記錄也會很好用,因為你會自動獲得 equals 和 hashcode 實現的正確行為。由於記錄也可以被認為是名義元組(其中每個組件都有一個名稱),使用記錄將多個值從方法返回給調用者也是很方便的。

另一方面,我認為記錄在 Java Persistence API 中用的不會很多。如果你想使用記錄來表示實體,那實際上是不可能的,因為實體在很大程度上是基於 JavaBeans 約定。並且實體通常傾向於是可變的。當然,當你在查詢中實例化只讀視圖對象時,有些情況下你可以使用記錄代替常規類。

總而言之,我認為 Java 中引入記錄是一項激動人心的改進。我認為它們會得到廣泛使用。

instanceof 的模式匹配

Java 16 中的第二大語言更改是 instanceof 的模式匹配。這是將模式匹配引入 Java 的漫長旅程的第一步。就目前而言,我認為 Java 16 中提供的初期支持已經很不錯了。看看下面的代碼段。

if (o instanceOf String) { String s = (String) o; return s.length(); }

復制代碼

你可能會認出這種模式,其中一些代碼負責檢查對象是否是一個類型的實例,在本例中是 String 類。如果檢查通過,我們需要聲明一個新的作用域變量,轉換並賦值,然后我們才能開始使用這個類型化的變量。在這個示例中,我們需要聲明變量 s,cast o 為一個 String,然后調用 length()方法。雖然這種辦法也能用,但太啰嗦了,而且並沒有反映出代碼的真實意圖。我們有更好的辦法。

從 Java 16 開始,我們可以使用新的模式匹配特性了。使用模式匹配時,我們可以將 o 匹配一個類型模式,而不是說 o 是一個特定類型的實例。類型模式由一個類型和一個綁定變量組成。我們來看一個例子。

if (o instanceOf String s) { return s.length(); }

復制代碼

在上面的代碼段中,如果 o 確實是 String 的實例,那么 String s 將立即綁定到 o 的值。這意味着我們可以立即開始使用 s 作為一個字符串,而無需在 if 主體內進行顯式轉換。這里的另一個好處是 s 的作用域僅限於 if 的主體。這里需要注意的一點是,源代碼中 o 的類型不應該是 String 的子類型,因為如果是這種情況,條件將始終為真。因此一般而言,如果編譯器檢測到正在測試的對象的類型是模式類型的子類型,則會拋出編譯時錯誤。

另一個需要指出的有趣的事情是,編譯器很聰明,可以根據條件的計算結果為 true 還是 false 來推斷 s 的作用域,正如以下代碼段中所示。

if (!(o instanceOf String s)) { return 0; } else { return s.length(); }

復制代碼

編譯器看到,如果模式匹配不成功,那么在 else 分支中,我們的 s 將在 String 類型的作用域內。並且在 if 分支 s 不在作用域內時,我們在作用域內就只有 o。這種機制稱為流作用域,其中類型模式變量僅在模式實際匹配時才在作用域內。這真的很方便,能夠有效簡化這段代碼。你需要注意這個變化,可能需要一點時間來適應。

另一個例子里你也可以清楚地看到這個流的作用。當你重寫 equals()方法的以下代碼實現時,常規的實現是首先檢查 o 是否是 MyClass 的一個實例。如果是,我們將 o 轉換為 MyClass,然后將 o 的 name 字段與 MyClass 的當前實例進行匹配。

@Override public boolean equals(Object o) { return (o instanceOf MyClass) && ((MyClass) o).name.equals(name); }

復制代碼

我們可以使用新的模式匹配機制來簡化這個實現,如下面的代碼段所示。

@Override public boolean equals(Object o) { return (o instanceOf MyClass m) && m.name.equals(name); }

復制代碼

這里又一次對代碼中顯式、冗長的轉換做了很好的簡化。只要用在合適的用例里,模式匹配會抽象出許多樣板代碼。

模式匹配:未來發展

Java 團隊已經勾勒出了模式匹配的一些未來發展方向。當然,團隊並沒有承諾這些設想何時或如何引入官方語言。在下面的代碼段中可以看到,在新的 switch 表達式中,我們可以像之前討論的那樣使用 instanceOf 來做類型模式。

static String format(Object o) { return switch(o) { case Integer i -> String.format("int %d", i); case Double d -> String.format("int %f", d); default -> o.toString(); }; }

復制代碼

在 o 是整數的情況下,流作用域開始起作用,我們可以立即將變量 i 用作一個整數。其他情況和默認分支也是如此。

另一個令人興奮的新方向是記錄模式,我們可以模式匹配我們的記錄並立即將組件值綁定到新變量。看看下面的代碼段。

if (o instanceOf Point(int x, int y)) { System.out.println(x + y); }

復制代碼

我們有一個包含 x 和 y 的 Point 記錄。如果對象 o 確實是一個點,我們將立即將 x 和 y 分量綁定到 x 和 y 變量並立即開始使用它們。

數組模式是可能在 Java 的未來版本中引入的另一種模式匹配。看看下面的代碼段。

if (o instanceOf String[] {String s1, String s2, ...}) { System.out.println(s1 + s2); }

復制代碼

如果 o 是字符串數組,則可以立即將這個字符串數組的第一部分和第二部分提取到 s1 和 s2。當然,這只適用於字符串數組中有兩個或更多元素的情況。我們可以使用三點表示法忽略數組元素的其余部分。

總而言之,使用 instanceOf 進行模式匹配是一個不錯的小特性,但它是邁向新未來的一步。我們可能會引入其他類型的模式來幫助編寫干凈、簡單和可讀的代碼。

特性預覽:密封類

下面來談談密封類(sealed class)這個特性。請注意,這是 Java 16 中的預覽特性,將在 Java 17 中成為最終版本。你需要將--enable-preview 標志傳遞給編譯器調用和 JVM 調用才能在 Java 16 中使用這個特性。該特性允許你控制繼承層次結構。

假設你想對一個超類型 Option 建模,其中你只想有 Some 和 Empty 兩個子類型。並且你想預防 Option 類型獲得任何擴展。例如,你不想在層次結構中允許 Maybe 類型。

Java 16 新特性介紹

 

因此,你已經詳細描述了 Option 類型的所有子類型。如你所知,目前在 Java 中控制繼承的唯一工具是通過 final 關鍵字。這意味着根本不能有任何子類,但這不是我們想要的。有一些解決方法可以在沒有密封類的情況下建模這個特性,但有了密封類后就容易多了。

密封類特性帶有新的關鍵字 sealed 和 permits。看看下面的代碼段。

public sealed class Option<T> permits Some, Empty { ... } public final class Some extends Option<String> { ... } public final class Empty extends Option<Void> { ... }

復制代碼

我們可以定義要 sealed 的 Option 類。然后,在類聲明之后,我們使用 permit 關鍵字來規定只允許 Some 和 Empty 類擴展 Option 類。然后,我們可以像往常一樣將 Some 和 Empty 定義為類。我們希望將這些子類設為 final,以防止進一步繼承。現在系統就不能編譯其他類來擴展 Option 類了,這是由編譯器通過密封類機制強制執行的。

關於此特性還有很多要說的內容,本文不能一一盡述。如果你有興趣了解更多信息,我建議你瀏覽密封類Java增強提案頁面JEP360。

小結

Java 16 中還有很多我們無法在本文中介紹的內容,例如Vector API、Foreign Linker API和Foreign-Memory Access API等孵化器 API 都非常有前途。並且新版在 JVM 層面也做了很多改進。例如ZGC有一些性能改進;在 JVM 中做了一些Elastic Metaspace改進;還有一個新的 Java 應用程序打包工具,允許你為 Windows、Mac 和 Linux 創建原生安裝程序。最后,當你從 classpath 運行應用程序時,JDK 中的封裝類型將受到嚴格保護,我認為這也會有很大影響。

我強烈建議你研究所有這些新特性和語言增強,因為其中一些改進會對你的應用程序產生重大影響。

作者介紹

Sander Mak是一名 Java Champion,在 Java 社區活躍了十多年。目前他是 Picnic 的技術總監。同時,Mal 也經常做知識分享,通過各種會議和在線電子學習平台傳授經驗。

萬方


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM