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 在运行时抛出 anull
。空选择器表达式不是编译时问题。以下带有全部匹配大小写标签的简单应用程序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 代码。
null
Java在新的模式匹配功能中添加了对大小写的支持。以下应用程序中的 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 可以与
default
case 组合,如下所示: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; }; } }
为了详尽起见,可能需要使用case
default
标签,但如果选择器表达式的可能值很少,有时可以避免使用 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 语句和传统的失败语义来支持模式匹配。
更多Java相关精彩内容,B站搜索“千锋教育”
- switch 的选择器表达式仅支持特定类型,即整型原始数据类型
- 点赞
- 收藏
- 关注作者
评论(0)