Java 新特性-switch 表达式、switch 模式匹配

switch 表达式

switch 表达式这个特性在 JDK14 版本中正式发布。在以往 switch 只有代码块,如今通过 switch 计算的值可以作为表达式,我们通过计算一个月份有多少天这个经典案例来了解它。

传统的代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public int getCountByOldWay(LocalDateTime dateTime) {
int dayCount;

switch (dateTime.getMonth()) {
case JANUARY:
case MARCH:
case MAY:
case JULY:
case AUGUST:
case OCTOBER:
case DECEMBER:
dayCount = 31;
break;
case APRIL:
case JUNE:
case SEPTEMBER:
case NOVEMBER:
dayCount = 30;
break;
case FEBRUARY:
dayCount = Year.isLeap(dateTime.getYear()) ? 29 : 28;
break;
default:
throw new IllegalArgumentException("Invalid month");
}

return dayCount;
}

使用 switch 表达式,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int getCountByNewExpression(LocalDateTime dateTime) {
return switch (dateTime.getMonth()) {
// 一三五七八十腊 月
case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER -> 31;
// 四六九十二 月
case APRIL, JUNE, SEPTEMBER, NOVEMBER -> 30;
// 二 月
case FEBRUARY -> {
// 使用 yield 关键字
if (Year.isLeap(dateTime.getYear())) {
yield 29;
} else {
yield 28;
}
}
};
}

上面的代码中,使用了新的关键字 yield。可以把 yield 语句产生的值看成是 switch 表达式的返回值,因此 yield 只能用在 switch 表达式里,而不能用在 switch 语句里。

1
2
3
4
5
6
7
8
9
10
public int getCountByNewExpression2(LocalDateTime dateTime) {
return switch (dateTime.getMonth()) {
// 一三五七八十腊 月
case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER -> 31;
// 四六九十二 月
case APRIL, JUNE, SEPTEMBER, NOVEMBER -> 30;
// 二 月
case FEBRUARY -> Year.isLeap(dateTime.getYear()) ? 29 : 28;
};
}

使用 switch 语句,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int getCountByNewStatement(LocalDateTime dateTime) {
int dayCount = 0;

switch (dateTime.getMonth()) {
// 一三五七八十腊 月
case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER -> dayCount = 31;
// 四六九十二 月
case APRIL, JUNE, SEPTEMBER, NOVEMBER -> dayCount = 30;
// 二 月
case FEBRUARY -> dayCount = (Year.isLeap(dateTime.getYear()) ? 29 : 28);
}

return dayCount;
}

switch 模式匹配

switch 模式匹配这个特性在 JDK21 版本中正式发布。之前 switch 匹配的数据可以是数字、枚举、字符串,现在增加了对引用类型的支持。

借助这个特性,我们可以优雅地解决子类扩充出现的兼容性问题,降低代码的维护难度,提高多情景处理的性能。

假设有这样一个场景:有一个定义形状的接口 Shape,在 1.0 版本的代码中有接口 Shape 的 2 个实现类(长方形类 Rectangle、正方形类 Square),以及一个工具类 ShapeUtils(主要提供计算面积的方法);2.0 版本的代码中增加了 1 个新的实现类(圆形类 Circle),但实际情况中我们可能会忘记去对工具类 ShapeUtils 进行扩展,那能否借助某种机制在第一时间帮我们发现这类问题呢?答案是 switch 的模式匹配。

接口及其实现类的代码如下(注意:Circle 类在 2.0 版本才引入):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 用于定义形状
*/
public sealed interface Shape permits Shape.Rectangle, Shape.Square, Shape.Circle {

/**
* 长方形类
*
* @since 1.0
*/
record Rectangle(double length, double width) implements Shape {
}

/**
* 正方形类
*
* @since 1.0
*/
record Square(double length) implements Shape {
}

/**
* 圆形类
*
* @since 2.0
*/
record Circle(double radius) implements Shape {
}

}

1.0 版本的 ShapeUtils 工具类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.Objects;

/**
* 形状工具类
*/
public class ShapeUtils {

/**
* 计算面积:if else 形式
*
* @since 1.0
*/
public static double calcArea(Shape shape) {
if (Objects.isNull(shape)) {
throw new IllegalArgumentException("shape is null");
}

double area = 0;
if (shape instanceof Shape.Rectangle rectangle) {
// 长方形
area = rectangle.length() * rectangle.width();
} else if (shape instanceof Shape.Square square) {
// 正方形
area = square.length() * square.length();
}

// TODO 如果再新增 Shape 的实现类,需要增加新的 else if 语句

return area;
}

}

如果未对 ShapeUtils 中的 calcArea() 方法进行扩展,那么计算 Circle 类的面积会返回数值 0,实际上忘记对其实现也很正常,往往出现了线上问题我们才可能发现。通过 switch 的模式匹配可以很好地解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import java.util.Objects;

/**
* 形状工具类
*/
public class ShapeUtils {

/**
* 计算面积:switch 模式匹配
*
* @since 2.0
*/
public static double calcArea(Shape shape) {
/*
借助 switch 模式匹配,在没有使用 default 的情况下需要穷举出所有的情景。
下面的代码在编辑阶段就会报错:'switch' expression does not cover all possible input values
*/
/*return switch (shape) {
case null -> throw new RuntimeException("shape is null");
case Rectangle rectangle -> rectangle.getLength() * rectangle.getWidth();
case Square square -> square.getLength() * square.getLength();
};*/

/*
下面的代码使用了 default,可以正常编译,但在当前业务是不合适的。
一般来说,只有我们能够确信,待匹配类型的升级,不会影响 switch 表达式的逻辑的时候,我们才能考虑使用缺省选择情景。
*/
/*return switch (shape) {
case null -> throw new RuntimeException("shape is null");
case Rectangle rectangle -> rectangle.getLength() * rectangle.getWidth();
case Square square -> square.getLength() * square.getLength();
default -> throw new RuntimeException("shape is not supported");
};*/

/*
下面的代码中,我们没有使用 default,而是穷举出所有的情景
*/
return switch (shape) {
case null -> throw new RuntimeException("shape is null");
case Shape.Rectangle rectangle -> rectangle.length() * rectangle.width();
case Shape.Square square -> square.length() * square.length();
case Shape.Circle circle -> Math.PI * (circle.radius() * circle.radius());
};
}

}

Java 新特性-switch 表达式、switch 模式匹配
https://blog.yohlj.cn/posts/8ba936b1/
作者
Enoch
发布于
2023年3月6日
许可协议