本篇内容聚焦Java在语句逻辑和流程控制方面的优化,会详细介绍Switch Expressions和Pattern Matching的相关知识。文章中代码较多,适合在PC上查看,源码可查看原文链接。
4.1 Switch表达式(Switch Expressions)
现有的switch语句语法上有很多地方不够符合直觉,使用上也有很多需要额外注意的点,比如switch case之间的默认控制流行为是穿透的、switch块中的默认作用域是整个switch代码块。另外switch只能执行语句,但实际如果能作为多条件选择的表达式,可以有返回值,在大部分情况下使用起来会更加自然。针对这些问题,Java在后续版本中对switch语法做出了很多重大改进。
- switch 匹配语法
Java switch语句过去的语法设计和C和C++等语言一致,支持默认情况下的穿透语义。虽然这种传统的控制流通常对于编写低级代码(如二进制编码的解析器)很有用,但Java毕竟是一门以业务开发为主要场景的面向对象语言,随着switch在更高抽象层次的上下文中使用,其容易出错的负面影响已经超过了其灵活性。例如,在以下代码中,许多break语句既会让代码变得冗长,又影响理解,且难以调试:
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
System.out.println(6);
break;
case TUESDAY:
System.out.println(7);
break;
case THURSDAY:
case SATURDAY:
System.out.println(8);
break;
case WEDNESDAY:
System.out.println(9);
break;
}
新的 switch 匹配语法的形式为 "case L ->",表示仅当匹配该标签时才执行标签右侧的代码。新语法还提议允许在一个 case 中使用多个常量,用逗号分隔。前面的代码现在可以这样写:
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
System.out.println(6);
break;
case TUESDAY:
System.out.println(7);
break;
case THURSDAY:
case SATURDAY:
System.out.println(8);
break;
case WEDNESDAY:
System.out.println(9);
break;
}
"case L ->" 右侧的代码被限制为表达式、代码块或 throw 语句,如果某个分支引入了一个局部变量,它必须包含在这个分支代码块内,不会对 switch 块中的任何其他分支的作用域产生影响。这消除了传统 switch 块的另一个问题,即局部变量的作用域是整个switch代码块,例如:
switch (day) {
case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
case TUESDAY -> System.out.println(7);
case THURSDAY, SATURDAY -> System.out.println(8);
case WEDNESDAY -> System.out.println(9);
}
而在新语法下,这样声明局部变量不会有问题:
switch (day) {
case MONDAY:
case TUESDAY:
int temp = ... // 'temp' 的作用域一直到 }
break;
case WEDNESDAY:
case THURSDAY:
int temp2 = ... // 不能声明此变量为 'temp'
break;
default:
int temp3 = ... // 不能声明此变量为 'temp'
}
-
switch 表达式语法
许多现有的 switch 语句本质上是 switch 表达式的模拟,其中每个case 分支要么将值分配给共同的目标变量,要么抛异常,例如:
int numLetters;
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
numLetters = 6;
break;
case TUESDAY:
numLetters = 7;
break;
case THURSDAY:
case SATURDAY:
numLetters = 8;
break;
case WEDNESDAY:
numLetters = 9;
break;
default:
throw new IllegalStateException("Wat: " + day);
}
这种switch语句很繁琐、很重复且容易出错的,switch 表达式正是用来处理这类场景,通过 -> 直接接返回值,来让整个switch 代码块具备返回值的能力。上面这段代码的意图是给传入的每天计算一个 numLetters 值,使用 switch 表达式直接表示这一点,这既更清晰也更安全:
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
default -> throw new IllegalStateException("Wat: " + day);
};
-
yield 返回值
对于switch表达式,如果 -> 不能直接返回结果,而是需要一个代码块来计算返回值,则需要使用新的关键字yield,例如:
int j = switch (day) {
case MONDAY -> 0;
case TUESDAY -> 1;
default -> {
int k = day.toString().length();
int result = f(k);
yield result;
}
};
一个 switch 表达式可以像 switch 语句一样,也使用传统的“case L:” 标签,在这种情况下,使用新的 yield 语句产生值,这样也可以带来分支的写法的统一性,例如:
int result = switch (s) {
case "Foo":
yield 1;
case "Bar":
yield 2;
default:
System.out.println("Neither Foo nor Bar, hmmm...");
yield 0;
};
break 和 yield 两个语句,有助于区分 switch 语句和 switch 表达式:break 作用于 switch 语句,目标是执行语句;yield 作用于 switch 表达式,目标是返回值 。switch表达式要求输入值必须可以匹配到某个条件,一般情况下是需要有default条件的,除非所有的case已经穷举了,比如枚举、sealed class授权的子类等情况。在一个switch代码块中,要么所有条件都是执行语句、要么所有条件都是返回值,即switch 语句和switch 表达式是不能混用的。
-
switch 语法小结
switch表达式语法是Java流程控制语法的重要优化,也可以说是Java新版本的一大核心改进,语句的选择器类型也做了大量的扩展。通过switch新的匹配语法和表达式语法,我们可以在很多场景下对复杂的 if-else 控制流进行优化,进而改进我们的代码质量和可维护性。switch语句的case 条件,除了我们熟悉的值匹配外,还可以进行类型匹配,以及使用 case - when 语法,在匹配类型后再做值匹配,接下来的模式匹配小节中马上就会介绍。
4.2 模式匹配(Pattern Matching)
你一定写过很多这样的代码:
if (obj instanceof String) {
String s = (String) obj;
...
}
这段代码里做了三件事情:测试(obj 是否为 String?)、转换(将 obj 强制转换为 String)和声明一个新的局部变量(s),以便我们可以使用字符串值。虽然这种模式是简单明了的,但你会觉得这段代码很繁琐。在 instanceof 测试之后,再去强制转换应该是不必要的,因为这几乎必然是接下来的语句。另外 String 在这段代码里甚至出现了三次,使得更重要的逻辑难以理解,重复的代码也增加了出错的风险。
- 语法解释
为了优化这些问题,instanceof 改进了现有语法,增加了模式匹配,上面的代码可以改写为:
if (obj instanceof String s) {
// 直接使用s
...
}
关于什么模式匹配,官方有如下解释:一个模式是由两部分组成:(1)可以应用于目标的谓词或测试,和(2)从目标中提取的一组局部变量,称为模式变量,仅在谓词成功应用于它时才提取。 类型模式,就是由指定类型(String)的谓词(instanceof)和一个模式变量(s)组成。instanceof运算符被扩展为接受类型模式,而不仅仅是类型。在上面的代码中,短语String s是类型模式,instanceof运算符将目标obj与类型模式匹配,如果obj是String的一个实例,则它被强制转换为String并将该值赋给变量s。模式变量的作用域是流程敏感的,只有编译器能够推断出模式已经被匹配并且变量一定已经被赋值时,模式变量才处于作用域内。下列代码是编译通过的:
if (a instanceof Point p) {
// p 在作用域内
...
}
// p 在这里不在作用域内
if (b instanceof Point p) { // 可以定义p
...
}
判断模式变量作用域的原则是:模式变量在确定匹配时才处于作用域内,用这种方式写出的代码符合Java开发者的逻辑,既安全又直观。当 if 语句的条件表达式比单个instanceof更复杂时,模式变量的作用域也相应增长。例如,在以下代码中:
if (obj instanceof String s && s.length() > 5) {
flag = s.contains("jdk");
}
模式变量s的作用域包含了&&运算符右侧的语句,因为只有当模式匹配成功并将值分配给s时,才会执行&&运算符的右侧。反之,以下代码不会编译通过:
if (obj instanceof String s || s.length() > 5) { // 错误!
...
}
原因是执行||操作符右侧的语句,模式变量s可能尚未被分配。再看一个稍微复杂一些的可以正常编译的例子:
public void onlyForStrings(Object o) throws RuntimeException {
if (!(o instanceof String s))
throw new RuntimeException();
// s 在作用域内
System.out.println(s);
...
}
总之,模式变量的作用域,由流程逻辑来确定,你可以多利用这个特性构建更好的代码。
-
使用场景举例
模式匹配可以在很多场景中显著地提高代码可读性和安全性,例如在《Effective Java》中有一个实现equals方法的例子:
public final boolean equals(Object o) {
return (o instanceof CaseInsensitiveString) &&
((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
使用模式匹配可以改写为:
public final boolean equals(Object o) {
return (o instanceof CaseInsensitiveString cis) &&
cis.s.equalsIgnoreCase(s);
}
再比如重写一个Point类的equals方法,效果更加明显:
public final boolean equals(Object o) {
if (!(o instanceof Point))
return false;
Point other = (Point) o;
return x == other.x
&& y == other.y;
}
可以改写为:
public final boolean equals(Object o) {
return (o instanceof Point other)
&& x == other.x
&& y == other.y;
}
-
Records 模式匹配
除了一般Class类型支持模式匹配,新的Records类型也可以很方便地进行模式匹配,如果我们要使用record Point中的属性,常规写法大概如下:
record Point(int x, int y) {}
static void printSum(Object obj) {
if (obj instanceof Point p) {
int x = p.x();
int y = p.y();
System.out.println(x+y);
}
}
Record模式匹配允许我们用以下语法来实现:
tatic void printSum(Object obj) {
if (obj instanceof Point(int x, int y)) {
System.out.println(x+y);
}
}
再举一个复杂一些的例子,定义record嵌套类型如下:
record Point(int x, int y) {}
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}
我们可以用同样的语法获取record的嵌套属性:
static void printColorOfUpperLeftPoint(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint(Point p, Color c),
ColoredPoint lr)) {
System.out.println(c);
}
}
// 或者使用var
static void printXCoordOfUpperLeftPointWithPatterns(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint(Point(var x, var y), var c), var lr)) {
System.out.println("Upper-left corner: " + x);
}
}
-
匿名匹配和匿名变量
在上面这个例子里的printXCoordOfUpperLeftPointWithPatterns这个函数中,我们使用模式匹配获取到了Rectangle类型的ColoredPoint类型属性中的Point类型属性中的x属性,并进行了打印,通过四层嵌套获取到了我们需要的值。虽然Rectangle其他所有属性和嵌套属性都不是我们需要的,但对于每个属性我们都定义了新的属性名来完成匹配,显然是非常复杂的。Java21引入了匿名匹配和变量的语法,对于不需要使用的属性和变量,我们可以用下划线“_”代替,跟Go语言是一样的,比如上面的方法我们可以改为:
static void printXCoordOfUpperLeftPointWithPatterns(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint(Point(var x, _), _), _)) {
System.out.println("Upper-left corner: " + x);
}
}
类似的,匿名变量可以在变量声明和参数定义的场景下使用,例如:
int _ = q.remove();
... } catch (NumberFormatException _) { ...
(int x, int _) -> x + x
-
模式匹配小结
类型判断是Java编程中一项重要又繁琐的工作,模式匹配机制极大地简化了这部分工作,并且可以独立地在其他场景中发挥作用。在下一小节介绍的 switch 模式匹配中,你会继续看到这套机制能给开发带来多么大的帮助。
4.3 Switch 模式匹配
-
基本语法
在switch语法中同样可以使用模式匹配,比如当我们用switch判断一个Object的类型时,可能有如下代码:
static String formatter(Object obj) {
String formatted = "unknown";
if (obj instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (obj instanceof Long l) {
formatted = String.format("long %d", l);
} else if (obj instanceof Double d) {
formatted = String.format("double %f", d);
} else if (obj instanceof String s) {
formatted = String.format("String %s", s);
}
return formatted;
}
这段代码虽然使用了模式匹配,但没有更进一步直接使用switch表达式,原因是case语句没有办法跟模式匹配相结合。而使用switch模式匹配语法,这段代码可以写成:
static String formatterPatternSwitch(Object obj) {
return switch (obj) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> obj.toString();
};
}
Switch模式匹配除了代码更清晰外,执行的时间复杂度也从原来用if-else的O(N),降低到了O(1),代码性能也得到了提升。当switch的入参如果是null时,代码会报空指针异常,因此一般我们需要在switch之前做一次非空判断:
static void testFooBarOld(String s) {
if (s == null) {
System.out.println("Oops!");
return;
}
switch (s) {
case "Foo", "Bar" -> System.out.println("Great");
default -> System.out.println("Ok");
}
}
随着switch模式匹配的引入,null值也可以作为匹配条件,上述代码可以重构为:
static void testFooBarNew(String s) {
switch (s) {
case null -> System.out.println("Oops");
case "Foo", "Bar" -> System.out.println("Great");
default -> System.out.println("Ok");
}
}
值得注意的是,当null作为case条件时,表达式可以正常匹配,但当null没有显示地写为case条件,switch依然会抛空指针异常。传统上switch支持的类型必须是整数原始类型(除了 long)、相应的包装形式(即 Character、Byte、Short 或 Integer)、String 或枚举类型,switch模式匹配扩展到了可以支持任意引用类型,下面是一段代码示例:
record Point(int x, int y) {}
enum Color { RED, GREEN, BLUE; }
static void typeTester(Object obj) {
switch (obj) {
case null -> System.out.println("null");
case String s -> System.out.println("String");
case Color c -> System.out.println("Color: " + c.toString());
case Point p -> System.out.println("Record class: " + p.toString());
case int[] ia -> System.out.println("Array of ints of length" + ia.length);
default -> System.out.println("Something else");
}
}
case - when语法:模式匹配 + 值匹配
假如我们有这样一段代码:
static void testStringOld(String response) {
switch (response) {
case null -> { }
case String s -> {
if (s.equalsIgnoreCase("YES"))
System.out.println("You got it");
else if (s.equalsIgnoreCase("NO"))
System.out.println("Shame");
else
System.out.println("Sorry?");
}
}
}
使用case - when可以简化case嵌套的if - else,改写后如下:
static void testStringNew(String response) {
switch (response) {
case null -> { }
case String s
when s.equalsIgnoreCase("YES") -> {
System.out.println("You got it");
}
case String s
when s.equalsIgnoreCase("NO") -> {
System.out.println("Shame");
}
case String s -> {
System.out.println("Sorry?");
}
}
}
假如我们要加入 'Y/y' 'N/n' 的条件,判断条件会更复杂,使用switch模式匹配和值条件相结合,可以让代码更清晰,可读性更好:
static void testStringEnhanced(String response) {
switch (response) {
case null -> { }
case "y", "Y" -> {
System.out.println("You got it");
}
case "n", "N" -> {
System.out.println("Shame");
}
case String s
when s.equalsIgnoreCase("YES") -> {
System.out.println("You got it");
}
case String s
when s.equalsIgnoreCase("NO") -> {
System.out.println("Shame");
}
case String s -> {
System.out.println("Sorry?");
}
}
}
这个例子里额外要注意的一点是,switch入参如果可以匹配多个条件,例如当response为“y”,实际会匹配两个条件(case "y", "Y" 以及最后一个case String s),switch只会执行第一条被匹配的语句。 通过这一篇的介绍,相信你已经对Java代码流程控制的改进有了深刻的印象。新的语法特性其实都在Java后续版本中经历了几个预览版的迭代,可以说这些都是Java为了提升开发效率和优化代码质量而做出的重大优化。也许你暂时还找不到合适的场景来使用这些特性,但就像stream和lambda一样,新的特性需要开发者在实践中沉淀优秀的案例,希望你未来能够探索出更多更好的实践。