语言与类型系统
学完你要能自己回答
- record、sealed、switch 模式匹配分别解决什么问题?为什么它们经常一起出现?
- switch 的穷尽性检查在什么条件下生效?为什么加了 default 反而可能丢掉编译期保护?
- 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
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打开继承,意味着这一支不再受穷尽性保护。
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();
};
}
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;
};
}
null 不会匹配任何 record pattern,会继续走后续 case 或 default。从 Java 8 写法迁移06
把旧代码改成 JDK 21 风格,不需要一次性重写。按下面的优先级逐步替换,风险最低、收益最明显。
| 旧写法 | JDK 21 写法 | 注意 |
|---|---|---|
多个 if (x instanceof ...) + 强转 | switch 模式匹配 + record pattern | 先确认类型层级是否 sealed,否则保留 default |
| 数据类 + getter + equals/hashCode | record | 需要可变或继承时不要硬套 |
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 | 状态 | 说明 |
|---|---|---|
| record | 16 正式 | record pattern 则是 21 才正式。 |
| sealed | 17 正式 | JDK 15 预览,17 转正。 |
| instanceof 模式 | 16 正式 | JDK 14 预览。 |
| switch 模式匹配 | 21 正式 | JDK 17–20 多次预览,API 与语义有调整。 |
| record patterns | 21 正式 | JDK 19–20 预览。 |
| unnamed patterns `_` | 21 预览 / 22 正式 | 预览期运行时需要 --enable-preview。 |
| String Templates | 21 预览,23 撤回 | 不要在生产代码中依赖。 |
费曼自检SELF CHECK
先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。