天天看點

Java 新特性綜合指南:Switch 的模式比對

作者:千鋒IT教育
Java 新特性綜合指南:Switch 的模式比對

要點

  • 控制流語句的模式比對switch是 Java 17 中引入的新功能,并在後續版本中進行了完善。
  • 模式可用于案例标簽,如case p。評估選擇器表達式,并根據可能包含模式的 case 标簽測試結果值。第一個比對的 case 标簽的執行路徑适用于 switch 語句/表達式。
  • 除了現有的舊類型之外,模式比對還添加了對任何引用類型的選擇器表達式的支援。
  • when保護模式可以與case 标簽模式中的new 子句一起使用。
  • 模式比對可以與傳統的 switch 語句以及 switch 語句的傳統失敗語義一起使用。
  • 語句switch是一種控制流語句,最初設計為if-else if-else控制流語句的簡短形式替代方案,适用于某些用例,這些用例涉及基于給定表達式計算結果的多個可能的執行路徑。
  • switch 語句由選擇器表達式和由case 标簽組成的switch 塊組成;對選擇器表達式進行求值,并根據哪個 case 标簽與求值結果比對來切換執行路徑。
  • 最初 switch 隻能用作帶有case ...:fall-through 語義的傳統标簽文法的語句。Java 14 添加了對新case ...->标簽文法的支援,沒有失敗語義。
  • Java 14 還添加了對switch 表達式的支援。switch 表達式的計算結果為單個值。引入了一個yield語句來顯式地産生一個值。
  • 對 switch 表達式的支援(在另一篇文章中詳細讨論)意味着 switch 可以用在需要表達式(例如指派語句)的執行個體中。
  • 問題
  • 然而,即使 Java 14 中進行了增強,該開關仍然存在一些限制:
  • switch 的選擇器表達式僅支援特定類型,即整型原始資料類型byte、short、char、 和int;相應的盒裝形式Byte、Short、Character和Integer;班級String;和枚舉類型。
  • 隻能測試 switch 選擇器表達式的結果是否與常量完全相等。将案例标簽與僅針對一個值的常量測試相比對。
  • 該null值的處理方式與任何其他值不同。
  • 錯誤處理不統一。
  • 枚舉的使用範圍并不明确。
  • 解決方案
  • 已經提出并實作了一種友善的解決方案來克服這些限制: switch 語句和表達式的模式比對。該解決方案解決了上述所有問題。
  • 開關的模式比對在 JDK 17 中引入,在 JDK 18、19 和 20 中完善,并将在 JDK 21 中最終确定。
  • 模式比對從幾個方面克服了傳統交換機的局限性:
  • 選擇器表達式的類型可以是除整型原始類型(不包括long)之外的任何引用類型。
  • 除了常量之外,案例标簽還可以包含模式。模式大小寫标簽可以應用于多個值,這與僅應用于一個值的常量大小寫标簽不同。引入了一個新的案例标簽 ,case p其中p是一個圖案。
  • 案例标簽可以包括null.
  • 可選when子句可以跟在 case 标簽後面,以進行條件或受保護的模式比對。帶有“when”的 case 标簽稱為受保護的 case 标簽。
  • 枚舉常量大小寫标簽可以被限定。使用枚舉常量時,選擇器表達式不必是枚舉類型。
  • 引入它MatchException是為了在模式比對中進行更統一的錯誤處理。
  • 傳統的switch語句和傳統的fall-through語義也支援模式比對。
  • 模式比對的一個好處是促進面向資料的程式設計,例如提高複雜的面向資料的查詢的性能。
  • 什麼是模式比對?
  • 模式比對是一項強大的功能,它擴充了程式設計中控制流結構的功能。除了針對傳統支援的常量進行測試之外,此功能還允許針對多種模式測試選擇器表達式。switch 的語義保持不變;根據可能包含模式的 case 标簽測試 switch 選擇器表達式值,如果選擇器表達式值與 case 标簽模式比對,則該 case 标簽适用于 switch 控制流的執行路徑。唯一的增強是選擇器表達式可以是除原始整型類型(不包括 long)之外的任何引用類型。除了常量之外,案例标簽還可以包含模式。此外,在 case 标簽中支援 null 和限定枚舉常量是一項附加功能。
  • switch 塊中 switch 标簽的文法如下:
  • SwitchLabel: case CaseConstant { , CaseConstant } case null [, default] case Pattern default
  • 模式比對可以與具有fall-through語義的傳統case …:标簽文法一起使用,也可以與case … ->不具有fall-through語義的标簽文法一起使用。盡管如此,必須注意的是,switch 塊不能混合兩種類型的 case 标簽。
  • 通過這些修改,模式比對為更複雜的控制流結構鋪平了道路,改變了處理代碼中邏輯的更豐富的方式。
  • 設定環境
  • 運作本文中的代碼示例的唯一先決條件是安裝 Java 20 或 Java 21(如果可用)。Java 21 僅比 Java 20 進行了一項增強,即支援 case 标簽中的限定枚舉常量。Java版本可以通過以下指令找到:
  • java --version java version "20.0.1" 2023-04-18 Java(TM) SE Runtime Environment (build 20.0.1+9-29) Java HotSpot(TM) 64-Bit Server VM (build 20.0.1+9-29, mixed mode, sharing)
  • 因為開關模式比對是 Java 20 中的預覽功能,javac是以java指令必須使用以下文法運作:
  • javac --enable-preview --release 20 SampleClass.java java --enable-preview SampleClass
  • 但是,可以使用源代碼啟動器直接運作它。在這種情況下,指令行将是:
  • java --source 20 --enable-preview Main.java
  • jshell選項也可用,但也需要啟用預覽功能:
  • jshell --enable-preview 模式比對的簡單示例
  • 我們從一個簡單的模式比對示例開始,其中 switch 表達式的選擇器表達式類型是引用類型;Collection;并且案例标簽包括表格的圖案case p。
  • import java.util.Collection; import java.util.LinkedList; import java.util.Stack; import java.util.Vector; public class SampleClass { static Object get(Collection c) { return switch (c) { case Stack s -> s.pop(); case LinkedList l -> l.getFirst(); case Vector v -> v.lastElement(); default -> c; }; } public static void main(String[] argv) { var stack = new Stack<String>(); stack.push("firstStackItemAdded"); stack.push("secondStackItemAdded"); stack.push("thirdStackItemAdded"); var linkedList = new LinkedList<String>(); linkedList.add("firstLinkedListElementAdded"); linkedList.add("secondLinkedListElementAdded"); linkedList.add("thirdLinkedListElementAdded"); var vector = new Vector<String>(); vector.add("firstVectorElementAdded"); vector.add("secondVectorElementAdded"); vector.add("thirdVectorElementAdded"); System.out.println(get(stack)); System.out.println(get(linkedList)); System.out.println(get(vector)); } }
  • 編譯并運作 Java 應用程式,輸出:
  • thirdStackItemAdded firstLinkedListElementAdded thirdVectorElementAdded 模式比對支援所有引用類型
  • 在前面給出的示例中,Collection類類型用作選擇器表達式類型。但是,任何引用類型都可以用作選擇器表達式類型。是以,case 标簽模式可以是與選擇器表達式值相容的任何引用類型。例如,以下修改後的 SampleClass 使用對象類型選擇器表達式,除了先前使用的 、 和引用類型的大小寫标簽模式之外,還包括記錄模式和數組引用類型模式的大小寫Stack标簽LinkedList模式Vector。
  • import java.util.LinkedList; import java.util.Stack; import java.util.Vector; record CollectionType(Stack s, Vector v, LinkedList l) { } public class SampleClass { static Object get(Object c) { return switch (c) { case CollectionType r -> r.toString(); case String[] arr -> arr.length; case Stack s -> s.pop(); case LinkedList l -> l.getFirst(); case Vector v -> v.lastElement(); default -> c; }; } public static void main(String[] argv) { var stack = new Stack<String>(); stack.push("firstStackItemAdded"); stack.push("secondStackItemAdded"); stack.push("thirdStackItemAdded"); var linkedList = new LinkedList<String>(); linkedList.add("firstLinkedListElementAdded"); linkedList.add("secondLinkedListElementAdded"); linkedList.add("thirdLinkedListElementAdded"); var vector = new Vector<String>(); vector.add("firstVectorElementAdded"); vector.add("secondVectorElementAdded"); vector.add("thirdVectorElementAdded"); var r = new CollectionType(stack, vector, linkedList); System.out.println(get(r)); String[] stringArray = {"a", "b", "c"}; System.out.println(get(stringArray)); System.out.println(get(stack)); System.out.println(get(linkedList)); System.out.println(get(vector)); } }
  • 這次的輸出如下:
  • CollectionType[s=[firstStackItemAdded, secondStackItemAdded, thirdStackItemAdded ], v=[firstVectorElementAdded, secondVectorElementAdded, thirdVectorElementAdded ], l=[firstLinkedListElementAdded, secondLinkedListElementAdded, thirdLinkedList ElementAdded]] 3 thirdStackItemAdded firstLinkedListElementAdded thirdVectorElementAdded 空案例标簽
  • NullPointerException傳統上,如果選擇器表達式的計算結果為 ,則switch 在運作時抛出 a null。空選擇器表達式不是編譯時問題。以下帶有全部比對大小寫标簽的簡單應用程式default示範了空選擇器表達式如何NullPointerException在運作時抛出 a 。
  • import java.util.Collection; public class SampleClass { static Object get(Collection c) { return switch (c) { default -> c; }; } public static void main(String[] argv) { get(null); } }
  • 可以在 switch 塊外部顯式測試 null 值,并僅在非 null 時調用 switch,但這涉及添加 if-else 代碼。nullJava在新的模式比對功能中添加了對大小寫的支援。以下應用程式中的 switch 語句使用case null來測試選擇器表達式是否為空。
  • import java.util.Collection; public class SampleClass { static void get(Collection c) { switch (c) { case null -> System.out.println("Did you call the get with a null?"); default -> System.out.println("default"); } } public static void main(String[] argv) { get(null); } }
  • 在運作時,應用程式輸出:
  • Did you call the get with a null?
  • case null 可以與defaultcase 組合,如下所示:
  • import java.util.Collection; public class SampleClass { static void get(Collection c) { switch (c) { case null, default -> System.out.println("Did you call the get with a null?"); } } public static void main(String[] argv) { get(null); } }
  • 但是,case null 不能與任何其他 case 标簽組合。例如,以下類将 case null 與帶有模式 Stack 的 case 标簽組合在一起s:
  • import java.util.Collection; import java.util.Stack; public class SampleClass { static void get(Collection c) { switch (c) { case null, Stack s -> System.out.println("Did you call the get with a null?"); default -> System.out.println("default"); } } public static void main(String[] args) { get(null); } }
  • 該類生成編譯時錯誤:
  • SampleClass.java:11: error: invalid case label combination case null, Stack s -> System.out.println("Did you call the get with a null?"); 使用when子句的保護模式
  • 有時,開發人員可能會使用根據布爾表達式的結果進行比對的條件案例标簽模式。這就是該when條款派上用場的地方。該子句計算布爾表達式,形成所謂的“受保護模式”。例如,when以下代碼片段中第一個 case 标簽中的子句确定 a 是否Stack為空。
  • import java.util.Stack; import java.util.Collection; public class SampleClass { static Object get(Collection c) { return switch (c) { case Stack s when s.empty() -> s.push("first"); case Stack s2 -> s2.push("second"); default -> c; }; } }
  • 位于“ ”右側的相應代碼->僅在堆棧确實為空時才執行。
  • 帶圖案的案例标簽的順序很重要
  • 将案例标簽與模式一起使用時,開發人員必須確定順序不會産生與類型或子類型層次結構相關的任何問題。這是因為,與常量 case 标簽不同,case 标簽中的模式允許選擇器表達式與包含模式的多個 case 标簽相容。switch 模式比對功能比對第一個 case 标簽,其中模式與選擇器表達式的值比對。
  • 如果一個 case 标簽模式的類型是出現在它之前的另一個 case 标簽模式的類型的子類型,則會發生編譯時錯誤,因為後一個 case 标簽将被識别為無法通路的代碼。
  • 為了示範此場景,開發人員可以編譯并運作以下示例類,其中類型的案例标簽模式Object主導類型的後續代碼标簽模式Stack。
  • import java.util.Stack; public class SampleClass { static Object get(Object c) { return switch (c) { case Object o -> c; case Stack s -> s.pop(); }; } }
  • 編譯該類時,會産生一條錯誤消息:
  • SampleClass.java:12: error: this case label is dominated by a preceding case lab el case Stack s -> s.pop(); ^
  • 隻需颠倒兩個 case 标簽的順序即可修複編譯時錯誤,如下所示:
  • public class SampleClass { static Object get(Object c) { return switch (c) { case Stack s -> s.pop(); case Object o -> c; }; } }
  • 類似地,如果 case 标簽包含的模式與前面具有無條件/無保護模式(前面部分讨論的保護模式)的 case 标簽具有相同的引用類型,則出于同樣的原因,它将導緻編譯類型錯誤,如課堂上所示:
  • import java.util.Stack; import java.util.Collection; public class SampleClass { static Object get(Collection c) { return switch (c) { case Stack s -> s.push("first"); case Stack s2 -> s2.push("second"); }; } }
  • 編譯時,會生成以下錯誤消息:
  • SampleClass.java:13: error: this case label is dominated by a preceding case lab el case Stack s2 -> s2.push("second"); ^
  • 為了避免此類錯誤,開發人員應該保持案例标簽的簡單易讀的順序。應首先列出常量标簽,然後是标簽case null、保護模式标簽和非保護類型模式标簽。箱default标簽可以與箱null标簽組合在一起,也可以單獨放置作為最後一個箱标簽。下面的類示範了正确的排序:
  • import java.util.Collection; import java.util.Stack; import java.util.Vector; public class SampleClass { static Object get(Collection c) { return switch (c) { case null -> c; //case label null case Stack s when s.empty() -> s.push("first"); // case label with guarded pattern case Vector v when v.size() > 2 -> v.lastElement(); // case label with guarded pattern case Stack s -> s.push("first"); // case label with unguarded pattern case Vector v -> v.firstElement(); // case label with unguarded pattern default -> c; }; } } 模式比對可以與傳統的 switch 語句和失敗語義一起使用
  • 模式比對功能與 switch 語句還是 switch 表達式無關。模式比對也與是否使用帶标簽的貫穿語義case …:或帶标簽的非貫穿語義無關。case …->在以下示例中,模式比對與 switch 語句一起使用,而不是與 switch 表達式一起使用。case 标簽使用case …:标簽的fall-through 語義。第一個 case 标簽中的子句when使用受保護的模式。
  • import java.util.Stack; import java.util.Collection; public class SampleClass { static void get(Collection c) { switch (c) { case Stack s when s.empty(): s.push("first"); break; case Stack s : s.push("second"); break; default : break; } } } 模式變量的範圍
  • 模式變量是出現在案例标簽模式中的變量。模式變量的範圍僅限于出現在箭頭右側的塊、表達式或 throw 語句->。為了進行示範,請考慮以下代碼片段,其中在預設 case 标簽中使用了前面 case 标簽中的模式變量。
  • import java.util.Stack; public class SampleClass { static Object get(Object c) { return switch (c) { case Stack s -> s.push("first"); default -> s.push("first"); }; } }
  • 編譯時錯誤結果:
  • import java.util.Collection; SampleClass.java:13: error: cannot find symbol default -> s.push("first"); ^ symbol: variable s location: class SampleClass
  • 出現在受保護的 case 标簽的模式中的模式變量的範圍包括 when 子句,如示例中所示:
  • import java.util.Stack; import java.util.Collection; public class SampleClass { static Object get(Collection c) { return switch (c) { case Stack s when s.empty() -> s.push("first"); case Stack s -> s.push("second"); default -> c; }; } }
  • 鑒于模式變量的範圍有限,可以在多個 case 标簽中使用相同的模式變量名稱。前面的示例對此進行了說明,其中模式變量s用于兩個不同的 case 标簽。
  • 當處理具有fall-through語義的case标簽時,模式變量的範圍擴充到位于“ ”右側的語句組:。這就是為什麼通過使用與傳統 switch 語句的模式比對,可以對上一節中的兩個 case 标簽使用相同的模式變量名稱。然而,聲明模式變量的 case 标簽失敗是一個編譯時錯誤。這可以在早期課程的以下變體中得到證明:
  • import java.util.Stack; import java.util.Vector; import java.util.Collection; public class SampleClass { static void get(Collection c) { switch (c) { case Stack s : s.push("second"); case Vector v : v.lastElement(); default : System.out.println("default"); } } }
  • 如果第一個語句組中沒有break;語句,則 switch 可能會跳過第二個語句組,而不會初始化v第二個語句組中的模式變量。前面的類會生成編譯時錯誤:
  • SampleClass.java:12: error: illegal fall-through to a pattern case Vector v : v.lastElement(); ^
  • 隻需在第一個語句組中添加一條break;語句即可修複錯誤:
  • import java.util.Stack; import java.util.Vector; import java.util.Collection; public class SampleClass { static void get(Collection c) { switch (c) { case Stack s : s.push("second"); break; case Vector v : v.lastElement(); default : System.out.println("default"); } } } 每個箱子标簽隻有一種圖案
  • 在單個 case 标簽内組合多個模式,無論是類型的 case 标簽,還是 不允許的case …:類型,都是編譯時錯誤。case …->這可能并不明顯,但在單個 case 标簽中組合模式會導緻模式失敗,如以下課程所示。
  • import java.util.Stack; import java.util.Vector; import java.util.Collection; public class SampleClass { static Object get(Collection c) { return switch (c) { case Stack s, Vector v -> c; default -> c; }; } }
  • 生成編譯時錯誤:
  • SampleClass.java:11: error: illegal fall-through from a pattern case Stack s, Vector v -> c; ^ 開關塊中隻有一個全比對大小寫标簽
  • 在 switch 塊中包含多個全比對 case 标簽是一種編譯時錯誤,無論是 switch 語句還是 switch 表達式。比對所有大小寫标簽是:
  • 帶有無條件比對選擇器表達式的模式的 case 标簽
  • 預設案例标簽
  • 為了進行示範,請考慮以下類:
  • import java.util.Collection; public class SampleClass { static Object get(Collection c) { return switch (c) { case Collection coll -> c; default -> c; }; } }
  • 編譯該類,卻得到一條錯誤消息:
  • SampleClass.java:13: error: switch has both an unconditional pattern and a default label default -> c; ^ 類型覆寫的詳盡性
  • 詳盡性意味着 switch 塊必須處理選擇器表達式的所有可能值。僅當滿足以下一項或多項條件時,才實施詳盡性要求:
    • a) 使用模式開關表達式/語句,
    • b)case null使用的是,
    • c) 選擇器表達式不是舊類型之一(char、byte、short、int、Character、Byte、Short、Integer、String或枚舉類型)。
  • 為了實作詳盡性,如果子類型很少,則為選擇器表達式類型的每個子類型添加 case 标簽可能就足夠了。然而,如果亞型很多,這種方法可能會很乏味。例如,為 type 的選擇器表達式的每個引用類型添加 case 标簽Object,甚至為 type 的選擇器表達式的每個子類型添加 case 标簽Collection,都是不可行的。
  • 為了證明詳盡性要求,請考慮以下類:
  • import java.util.Collection; import java.util.Stack; import java.util.LinkedList; import java.util.Vector; public class SampleClass { static Object get(Collection c) { return switch (c) { case Stack s -> s.push("first"); case null -> throw new NullPointerException("null"); case LinkedList l -> l.getFirst(); case Vector v -> v.lastElement(); }; } }
  • 該類生成編譯時錯誤消息:
  • SampleClass.java:10: error: the switch expression does not cover all possible in put values return switch (c) { ^
  • 隻需添加預設情況即可解決該問題,如下所示:
  • import java.util.Collection; import java.util.Stack; import java.util.LinkedList; import java.util.Vector; public class SampleClass { static Object get(Collection c) { return switch (c) { case Stack s -> s.push("first"); case null -> throw new NullPointerException("null"); case LinkedList l -> l.getFirst(); case Vector v -> v.lastElement(); default -> c; }; } }
  • 具有無條件比對選擇器表達式的模式的全比對大小寫标簽(例如以下類中的模式)将是詳盡的,但它不會明确地處理或處理任何子類型。
  • import java.util.Collection; public class SampleClass { static Object get(Collection c) { return switch (c) { case Collection coll -> c; }; } }
  • 為了詳盡起見,可能需要使用casedefault标簽,但如果選擇器表達式的可能值很少,有時可以避免使用 case 标簽。例如,如果選擇器表達式的類型為java.util.Vector,則單個子類隻java.util.Stack需要一種 case 标簽模式即可避免出現這種default情況。類似地,如果選擇器表達式是密封類類型,則隻有在密封類類型的 Permits 子句中聲明的類才需要由 switch 塊處理。
  • 泛型在 switch case 标簽中記錄模式
  • Java 20 添加了對 switch 語句/表達式中通用記錄模式的類型參數推斷的支援。例如,考慮通用記錄:
  • record Triangle<S,T,V>(S firstCoordinate, T secondCoordinate,V thirdCoordinate){};
  • 在下面的開關塊中,推斷的記錄模式是
  • Triangle<Coordinate,Coordinate,Coordinate>(var f, var s, var t): static void getPt(Triangle<Coordinate, Coordinate, Coordinate> tr){ switch (tr) { case Triangle(var f, var s, var t) -> …; case default -> …; } } 使用 MatchException 進行錯誤處理
  • Java 19 引入了該類的新子類java.lang.Runtime,以便在模式比對期間進行更統一的異常處理。調用的新類java.lang.MatchException是預覽 API。MatchException 不是專門為 switch 中的模式比對而設計的,而是為任何模式比對語言構造而設計的。當詳盡的模式比對與任何提供的模式都不比對時,可能會在運作時抛出 MatchException。為了示範這一點,請考慮以下應用程式,該應用程式在記錄的 case 标簽中包含記錄模式,該記錄聲明除以 0 的通路器方法。
  • record DivisionByZero(int i) { public int i() { return i / 0; } } public class SampleClass { static DivisionByZero get(DivisionByZero r) { return switch(r) { case DivisionByZero(var i) -> r; }; } public static void main(String[] argv) { get(new DivisionByZero(42)); } }
  • 示例應用程式編譯時沒有錯誤,但在運作時抛出MatchException:
  • Exception in thread "main" java.lang.MatchException: java.lang.ArithmeticException: / by zero at SampleClass.get(SampleClass.java:7) at SampleClass.main(SampleClass.java:14) Caused by: java.lang.ArithmeticException: / by zero at DivisionByZero.i(SampleClass.java:1) at SampleClass.get(SampleClass.java:1) ... 1 more 結論
  • 本文介紹了對交換機控制流構造的新模式比對支援。主要改進是 switch 的選擇器表達式可以是任何引用類型,并且 switch 的 case 标簽可以包含模式,包括條件模式比對。而且,如果您不想更新完整的代碼庫,則可以使用傳統的 switch 語句和傳統的失敗語義來支援模式比對。

感謝大家的關注和點贊,回複1領取學習Java資料大禮包!

繼續閱讀