JDK JDK 学习手册8 → 27 · JDK 专用学习站
QUICKREF · LANGUAGE · PATTERN MATCHING

语言与类型系统

JDK 21 · 速查主题 01 / 12 · 预计 30 分钟
核心30 分钟JEP 440JEP 441JEP 443
一句话JDK 21 的语言主线是「让数据形状与类型分支都变成一等公民」:record 定义数据,sealed 封闭类型集合,模式匹配负责安全解构,三者组合后可以写出接近代数数据类型的 Java。

学完你要能自己回答

  1. record、sealed、switch 模式匹配分别解决什么问题?为什么它们经常一起出现?
  2. switch 的穷尽性检查在什么条件下生效?为什么加了 default 反而可能丢掉编译期保护?
  3. record pattern 能解构到什么程度?嵌套 record 与 null 值分别怎么处理?

record:数据载体,而不是万能类01

record 在 JDK 16 转正。它用一行声明换来构造器、访问器、equals / hashCode / toString,并默认不可变(字段是 private final)。它适合透明地承载一组值,不适合需要继承或复杂生命周期的对象。

public record Point(int x, int y) {
    // 紧凑构造器:参数校验,不重复写字段赋值
    public Point {
        if (x < 0 || y < 0) {
            throw new IllegalArgumentException("坐标必须非负");
        }
    }

    // 额外方法可以有,但不能添加实例字段
    public Point translate(int dx, int dy) {
        return new Point(x + dx, y + dy);
    }
}

紧凑构造器里写 x = ... 是在给隐式字段赋值;不要写 this.x = x。record 的访问器是 x() 而不是 getX(),这是刻意的命名选择。

  • 适合:DTO、返回值、模式匹配的载体、Map 的复合 key。
  • 不适合:需要 setter、需要继承、需要惰性计算大量派生状态的对象。
  • 注意:record 不可变不等于深不可变——如果组件是 List,列表内容仍可能被修改。需要防御性拷贝时在紧凑构造器里做 List.copyOf。
  • record 可以显式重写访问器来做派生值,但不要在访问器里做昂贵计算;每次调用都会重新执行。
public record Range(int from, int to) {
    public Range {
        if (from > to) throw new IllegalArgumentException("from > to");
    }
    public int length() { return to - from; }
}

var r = new Range(3, 8);
System.out.println(r);          // Range[from=3, to=8]
System.out.println(r.length()); // 5
record 不能声明额外的实例字段,但可以有静态字段和静态方法。需要「带缓存的不可变值」时,record 不是合适工具,考虑普通类或 JDK 25 的 Stable Values。

sealed:封闭类型集合02

sealed 在 JDK 17 转正,用来限制「谁可以继承 / 实现我」。它本身不提供多态,价值在于让编译器知道所有子类型,从而让 switch 的穷尽性检查成为可能。

public sealed interface Shape permits Circle, Square, Triangle {}

public record Circle(double radius) implements Shape {}
public record Square(double side) implements Shape {}
public non-sealed class Triangle implements Shape {
    public double base, height; // 允许继续被继承
}
  • 子类型必须显式声明为 final、sealed 或 non-sealed 之一。
  • permits 可以省略:当所有子类型都在同一个源文件中时,编译器可以自动推断。
  • 子类型通常要与父类型在同一个模块;未命名模块中则要同一个包。
  • non-sealed 打开继承,意味着这一支不再受穷尽性保护。
sealed 的常见误解:它不是访问控制,也不是「禁止实例化」。它只约束继承关系;实例化仍由构造器可见性决定。

instanceof 模式:判断与绑定合并03

JDK 16 转正后,if (o instanceof String s) 在判断成功的同时完成绑定,不再需要强转。模式变量的作用域是「确定匹配成功」之后的代码路径。

static String normalize(Object o) {
    if (o instanceof String s && !s.isBlank()) {
        return s.strip().toLowerCase();
    }
    return "";
}

// 否定分支也能用,但作用域受流分析限制
if (!(o instanceof String s)) {
    return "";
}
return s.toUpperCase(); // 这里 s 已确定存在

&& 的右侧可以安全使用模式变量;|| 的右侧不行,因为左侧失败并不代表变量已绑定。这个规则由编译器做流分析,不需要你手动判断。

switch 模式匹配:类型分支的终局写法04

JDK 21 转正后,switch 的选择器可以是任意引用类型,case 支持类型模式、when 守卫、null 分支。配合 sealed 层级,可以做到不写 default 也穷尽。

static String describe(Object o) {
    return switch (o) {
        case null -> "null";
        case Integer i when i > 0 -> "正整数 " + i;
        case Integer i -> "非正整数 " + i;
        case String s when s.isBlank() -> "空白字符串";
        case String s -> "字符串 " + s;
        case int[] arr -> "数组长度 " + arr.length;
        default -> "其他 " + o.getClass().getSimpleName();
    };
}

System.out.println(describe(42));      // 正整数 42
System.out.println(describe(-1));      // 非正整数 -1
System.out.println(describe("  "));    // 空白字符串
System.out.println(describe(null));    // null
  • case null 必须显式处理,否则选择器为 null 时抛 NPE。
  • when 是守卫条件,不满足会继续尝试后续 case。
  • case 有支配顺序:更具体的类型必须写在更宽泛的类型前面,否则编译报错(例如 case Object o 不能放在 case String s 前面)。
  • 同一个 switch 中不能出现两个「类型擦除后相同」的模式,例如 case List<String> l 与 case List<Integer> l。
sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double side) implements Shape {}

static double area(Shape s) {
    return switch (s) {           // 无 default,编译器检查穷尽性
        case Circle c -> Math.PI * c.r() * c.r();
        case Square q -> q.side() * q.side();
    };
}
如果你给 sealed switch 加了 default,新增子类型时编译器不会提醒你补分支——穷尽性保护就丢了。只有在确实要兜底未来类型时才加 default。

Record Patterns:在模式里解构05

record pattern 在 JDK 21 转正,允许在 instanceof 和 switch 中直接拆开 record 的组件,并支持多层嵌套。它把「判断类型 → 强转 → 调访问器」三步压缩成一个模式。

record Point(int x, int y) {}
record Line(Point start, Point end) {}

static String quadrant(Object o) {
    if (o instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) {
        return "线段 (" + x1 + "," + y1 + ") → (" + x2 + "," + y2 + ")";
    }
    return "不是线段";
}

System.out.println(quadrant(new Line(new Point(0, 0), new Point(3, 4))));
// 线段 (0,0) → (3,4)

组件也可以是类型模式,而不只是 var。例如 case Line(Point(int x1, int y1), Point(int x2, int y2)) 会进一步要求组件确实是 int 坐标。使用 var 最简洁,但类型模式能表达更强的约束。

static int sum(Object o) {
    return switch (o) {
        case Point(int x, int y) -> x + y;
        case Line(Point(var x1, var y1), Point(var x2, var y2)) -> x1 + y1 + x2 + y2;
        default -> 0;
    };
}
record pattern 只适用于 record。普通类即使有访问器也不能这样解构。null 不会匹配任何 record pattern,会继续走后续 case 或 default。

从 Java 8 写法迁移06

把旧代码改成 JDK 21 风格,不需要一次性重写。按下面的优先级逐步替换,风险最低、收益最明显。

旧写法JDK 21 写法注意
多个 if (x instanceof ...) + 强转switch 模式匹配 + record pattern先确认类型层级是否 sealed,否则保留 default
数据类 + getter + equals/hashCoderecord需要可变或继承时不要硬套
switch 里每个 case 都 break + 赋值临时变量switch 表达式 + yield注意穷尽性,enum 或 sealed 最合适
多行字符串用 \n 拼接文本块 """注意 incidental whitespace 与行尾空格
instanceof 后强转变量模式变量直接绑定变量作用域由编译器流分析决定
接口 + 注释约定「只允许这几个实现」sealed interface需要第三方扩展时不要 sealed
// JDK 8
if (obj instanceof String) {
    String s = (String) obj;
    if (s.length() > 3) {
        return s.toUpperCase();
    }
}

// JDK 21
if (obj instanceof String s && s.length() > 3) {
    return s.toUpperCase();
}

版本边界BOUNDARY

这张表比 API 本身更值得记。写代码时先确认你的运行时版本。

特性 / API状态说明
record16 正式record pattern 则是 21 才正式。
sealed17 正式JDK 15 预览,17 转正。
instanceof 模式16 正式JDK 14 预览。
switch 模式匹配21 正式JDK 17–20 多次预览,API 与语义有调整。
record patterns21 正式JDK 19–20 预览。
unnamed patterns `_`21 预览 / 22 正式预览期运行时需要 --enable-preview。
String Templates21 预览,23 撤回不要在生产代码中依赖。

费曼自检SELF CHECK

先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。

01. 为什么说 record + sealed + 模式匹配是一组,而不是三个孤立特性?
record 定义不可变数据形状,sealed 封闭可能的子类型集合,模式匹配负责安全解构;sealed 让编译器能做穷尽性检查,因此 switch 可以在不写 default 的情况下保证覆盖所有情况,三者组合才形成「代数数据类型」式表达。
02. switch 模式匹配中,什么时候必须写 case null?不写会怎样?
当选择器可能为 null 时必须显式写 case null;否则运行到 null 会抛 NullPointerException。编译器不会因为你写了 sealed 穷尽分支就自动处理 null。
03. record pattern 里写 var 和写具体类型有什么区别?
var 表示对该组件不做额外类型约束,直接绑定;写具体类型模式(如 int x)会要求组件同时匹配该类型,否则整个模式失败。后者表达更强的约束,但可读性通常不如 var。
04. 为什么给 sealed switch 加 default 可能是不好的做法?
因为 default 会吞掉未来新增的子类型,编译器不再提醒你补分支,穷尽性保护失效。只有确实想兜底未知类型时才加 default。
学习进度 记录保存在本机浏览器 · 不上传

JDK 学习手册 · JDK / Java 专用学习站 · 纯静态 · 双击 index.html 即可使用

本站专注 JDK 版本演进与 Java 教学,非 Oracle / OpenJDK 官方站点。版本数据来自 OpenJDK 官方 Features 表;课程示例已在对应本机 JDK 实测。构建日期 2026-11-12。