1. 概述

在模式匹配出现之前,switch 语句只能对选择器表达式进行简单的精确常量值匹配。Java SE 21 正式引入了 switch 表达式和语句的模式匹配功能(JEP 441)。模式匹配为定义 switch 选择器和条件提供了更大的灵活性

我们可以在 case 标签中使用模式(如 Integer iString s)。此外,选择器表达式现在支持任意引用类型(如 ArrayListStack),而不仅限于原有类型。我们还能在 case 标签中匹配 null,并通过 when 子句实现条件匹配——这种带 when 的模式称为带守卫的模式匹配

Java SE 24 进一步将基本类型模式(如 int ilong l)作为预览特性加入 case 标签支持。

本文将深入探讨如何在 switch 中使用模式匹配,并分析几个关键特性:值覆盖、子类顺序处理和 null 值处理。

2. Switch 语句基础

在 Java 中,switch 是一种控制流语句,用于将控制权转移到多个预定义的 case 分支之一。switch 语句根据选择器表达式的值匹配对应的 case 标签。

在早期 Java 版本中,选择器表达式只能是数字、字符串或枚举常量,且 case 标签只能包含常量:

final String b = "B";
switch (args[0]) {
    case "A" -> System.out.println("Parameter is A");
    case b -> System.out.println("Parameter is b");
    default -> System.out.println("Parameter is unknown");
};

踩坑提醒:如果变量 b 不是 final,编译器会抛出"需要常量表达式"错误。

3. 模式匹配基础

模式匹配最初作为预览特性出现在 Java SE 14。类型模式由类型名和用于绑定结果的变量组成。将类型模式应用于 instanceof 操作符可简化类型检查和转换,让我们能将两者合并为单一表达式:

if (o instanceof String s) {
    System.out.printf("Object is a string %s", s);
} else if (o instanceof Number n) {
    System.out.printf("Object is a number %n", n);
}

这种语言增强特性帮助我们用更少的代码写出更易读的程序。instanceof 的模式匹配 在 Java SE 16 转为正式特性。

4. Switch 中的模式匹配

Java SE 17 引入了 switch 表达式和语句的模式匹配,后续版本持续优化,最终在 Java SE 21 成为正式特性。

4.1. 类型模式

看如何在 switch 中应用类型模式。假设我们要用 if-else 实现一个方法,将不同类型转换为 double

static double getDoubleUsingIf(Object o) {
    double result;
    if (o instanceof Integer) {
        result = ((Integer) o).doubleValue();
    } else if (o instanceof Float) {
        result = ((Float) o).doubleValue();
    } else if (o instanceof String) {
        result = Double.parseDouble(((String) o));
    } else {
        result = 0d;
    }
    return result;
}

用类型模式重写后,代码更简洁:

static double getDoubleUsingSwitch(Object o) {
    return switch (o) {
        case Integer i -> i.doubleValue();
        case Float f -> f.doubleValue();
        case String s -> Double.parseDouble(s);
        default -> 0d;
    };
}

关键改进:早期 Java 中选择器表达式仅支持整数基本类型(除 long 外)、其包装类、枚举和 String。现在类型模式让选择器表达式支持任意引用类型

4.2. 带守卫的模式

类型模式能基于特定类型转移控制,但有时我们还需要对传入值进行额外检查

例如,用 if 检查字符串长度:

static double getDoubleValueUsingIf(Object o) {
    return switch (o) {
        case String s -> {
            if (s.length() > 0) {
                yield Double.parseDouble(s);
            } else {
                yield 0d;
            }
        }
        default -> 0d;
    };
}

用带守卫的模式可以更优雅地解决:

static double getDoubleValueUsingGuardedPatterns(Object o) {
    return switch (o) {
        case String s when s.length() > 0 -> Double.parseDouble(s);
        default -> 0d;
    };
}

核心优势:带守卫的模式让我们避免在 switch 块内写额外 if,直接将条件逻辑移到 case 标签

4.3. 基本类型模式

Java SE 24 作为预览特性,支持在 switch case 标签中使用基本类型模式。例如结合带守卫的模式:

void primitiveTypePatternExample() {
    Random r=new Random();
    switch (r.nextInt()) {
        case 1 -> System.out.println("int is 1");
        case int i when i > 1 && i < 100 -> System.out.println("int is greater than 1 and less than 100");
        default -> System.out.println("int is greater or equal to 100");
    }
}

支持范围:选择器表达式可以是任意基本类型(包括 longfloatdoubleboolean)及其包装类。

5. Switch 特殊场景

使用模式匹配时需注意几个关键场景。

5.1. 值覆盖检查

使用模式匹配时,Java 编译器会检查类型覆盖情况

考虑一个只覆盖 String 类型的 switch

static double getDoubleUsingSwitch(Object o) {
    return switch (o) {
        case String s -> Double.parseDouble(s);
    };
}

编译器会报错:

[ERROR] Failed to execute goal ... Compilation failure
[ERROR] /D:/Projects/.../HandlingNullValuesUnitTest.java:[10,16] the switch expression does not cover all possible input values

原因:**case 标签必须覆盖所有可能的输入值(本例中是所有 Object 类型)**。可用 default 替代具体类型。

5.2. 子类顺序问题

使用子类模式时,**case 顺序至关重要**。

看一个 StringCharSequence 之后的错误示例:

static double getDoubleUsingSwitch(Object o) {
    return switch (o) {
        case CharSequence c -> Double.parseDouble(c.toString());
        case String s -> Double.parseDouble(s);
        default -> 0d;
    };
}

编译错误:

[ERROR] Failed to execute goal ... Compilation failure
[ERROR] /D:/Projects/.../HandlingNullValuesUnitTest.java:[12,18] this case label is dominated by a preceding case label

问题本质:由于 StringCharSequence 的子类,第二个 case 永远不会执行——所有字符串对象会被第一个 case 捕获。

5.3. 处理 null 值

早期 Java 中,向 switchnull 会抛 NullPointerException。现在类型模式支持null 作为独立 case 处理

static double getDoubleUsingSwitch(Object o) {
    return switch (o) {
        case String s -> Double.parseDouble(s);
        case null -> 0d;
        default -> 0d;
    };
}

约束:switch 表达式不能同时包含 default 和总类型模式(覆盖所有输入值的类型)。否则编译报错:

[ERROR] Failed to execute goal ... Compilation failure
[ERROR] /D:/Projects/.../HandlingNullValuesUnitTest.java:[14,13] switch has both a total pattern and a default label

最后提醒:即使使用了模式匹配,switch 仍可能抛 NullPointerException——但仅当缺少 null 匹配的 case 时。

6. 总结

本文深入探讨了 Java SE 21 引入的 switch 模式匹配特性。我们了解到:

  • 通过在 case 标签中使用模式,选择逻辑从简单相等判断升级为模式匹配
  • Java SE 24 进一步增加了基本类型模式支持
  • 实际应用中需特别注意值覆盖、子类顺序和 null 处理三大场景

模式匹配让 switch 语句更灵活强大,是 Java 语言现代化的重要一步。


原始标题:Pattern Matching for Switch | Baeldung