Java 基础体系 · 第 3/100 篇。示例统一以 Java 25 LTS 为语言和 JVM 基线;框架示例使用与其兼容的现代稳定版本。
Java 25 类型与控制流:基本类型、引用、Record、sealed 和模式匹配
Java 的类型系统决定“一个值是什么、能否赋给某个变量、能调用哪些操作”;控制流决定“程序按什么顺序执行,以及在什么条件下改变路径”。record、sealed 和模式匹配并不是孤立语法,它们分别把数据建模、继承边界和类型判断连接起来。
本文以 Java 25 LTS 为范围,区分 Java 语言规范保证的行为、JDK 的常见实现方式,以及 Java 25 中仍处于预览阶段的能力。
1. 先建立类型系统的基本模型
Java 中的类型分为两大类:
Type
├── Primitive type 基本类型
└── Reference type 引用类型
1.1 基本类型表示值本身
Java 有 8 种基本类型:
| 类型 | 位宽 | 典型用途 |
|---|---|---|
boolean |
语言层面只有 true、false |
逻辑值 |
byte |
8 位有符号整数 | 小整数、二进制数据 |
short |
16 位有符号整数 | 较小整数 |
int |
32 位有符号整数 | 默认整数类型 |
long |
64 位有符号整数 | 大整数、时间戳 |
char |
16 位无符号 UTF-16 代码单元 | UTF-16 单元 |
float |
32 位 IEEE 754 浮点数 | 较低精度浮点 |
double |
64 位 IEEE 754 浮点数 | 默认浮点类型 |
void 常被称为一种类型,但它不能声明变量,也没有值。它表示某个方法不返回结果。
基本类型变量直接存储一个值:
int a = 42;
int b = a;
b = 100;
System.out.println(a); // 42
System.out.println(b); // 100
赋值时复制的是整数值,因此修改 b 不会影响 a。
1.2 char 不是完整 Unicode 字符
char 的范围是 \u0000 到 \uFFFF,表示一个 UTF-16 代码单元,而不一定表示一个完整 Unicode 码点。
char a = 'A';
String text = "😀";
System.out.println(text.length()); // 2
😀 的 Unicode 码点超出 BMP,需要两个 UTF-16 代码单元。若要按 Unicode 码点处理字符串,应使用:
int codePointCount =
text.codePointCount(0, text.length());
System.out.println(codePointCount); // 1
因此,char 适合表达 UTF-16 单元,不应未经判断就当作“一个用户可见字符”。
1.3 整数溢出是规范定义的结果
对于整数基本类型,算术结果超出类型范围时不会自动抛出异常,而是按该类型的位宽进行补码截断。
int x = Integer.MAX_VALUE;
int y = x + 1;
System.out.println(y); // -2147483648
Integer.MAX_VALUE 是 2^31 - 1。加一后需要第 32 位之外的结果,转换回 int 后得到最小值 -2^31。
这和 Math.addExact 不同:
int result = Math.addExact(Integer.MAX_VALUE, 1);
// 抛出 ArithmeticException
两者的差别是:
+:按基本类型算术规则溢出;Math.addExact:检测溢出并抛出异常。
在金额、计数器、容量和安全边界计算中,不能假设整数溢出会自动失败。
1.4 浮点数遵循 IEEE 754 的特殊值规则
浮点类型可以表示:
- 正无穷和负无穷;
NaN,即 Not-a-Number;- 正零和负零。
double a = 0.0 / 0.0;
double b = 1.0 / 0.0;
System.out.println(a); // NaN
System.out.println(b); // Infinity
System.out.println(a == a); // false
NaN 不等于任何值,包括它自己。判断应使用:
Double.isNaN(a)
Double.isInfinite(b)
浮点数也不适合直接用 == 判断十进制金额是否相等,因为很多十进制小数无法用二进制浮点数精确表示。
2. 引用类型:变量保存的是对象引用
引用类型包括:
- 类类型;
- 接口类型;
- 数组类型;
- 枚举类型;
record类型;- 注解类型;
- 类型变量和参数化类型等。
引用变量通常保存一个指向对象的引用,而不是对象本身:
String first = new String("java");
String second = first;
System.out.println(first == second); // true
first 和 second 引用同一个对象。
2.1 == 与 equals
对于基本类型,== 比较值:
int a = 10;
int b = 10;
System.out.println(a == b); // true
对于引用类型,== 比较引用是否指向同一个对象:
String a = new String("java");
String b = new String("java");
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
因此:
==:引用身份比较;equals:由类型定义的逻辑相等比较。
Object.equals 默认仍然按身份比较。String 覆盖了它,改为比较字符内容。
2.2 null 是没有对象的引用值
任何引用类型的变量都可以为 null:
String value = null;
但 null 不是一个对象,也不能调用方法:
value.length(); // NullPointerException
null 也不能赋给基本类型:
int x = null; // 编译错误
更隐蔽的情况来自自动拆箱:
Integer value = null;
int x = value; // 运行时 NullPointerException
这里发生了隐式的:
int x = value.intValue();
所以包装类型不仅改变存储方式,也会把 null 风险带入基本类型运算。
2.3 数组是对象,数组类型是引用类型
即使元素是基本类型,数组本身仍然是对象:
int[] numbers = {1, 2, 3};
System.out.println(numbers.getClass().getName());
// [I
数组有固定长度,并且运行时知道自己的元素类型:
Object value = new String[] {"a", "b"};
Object[] objects = (Object[]) value; // 成功
objects[0] = "c"; // 成功
objects[1] = new StringBuilder(); // ArrayStoreException
String[] 是 Object[] 的子类型,所以赋值转换合法;但数组运行时仍检查实际元素类型,写入 StringBuilder 时失败。
这体现了数组协变的代价。泛型集合通常使用不变类型,避免类似的运行时数组存储错误。
3. 类型转换:赋值是否成立取决于转换上下文
3.1 扩大基本类型转换通常是安全的
例如:
int count = 10;
long total = count;
double value = total;
这些转换不需要显式强制转换,因为目标类型可以表示源类型的整数范围,或者语言允许进行扩大转换。
但“扩大”不等于“数学上永远精确”。例如从 long 到 float 可能丢失低位精度,因为 float 的有效精度有限。
3.2 缩小基本类型转换必须显式进行
int value = 130;
byte result = (byte) value;
System.out.println(result); // -126
byte 的范围是 -128 到 127。转换过程保留低 8 位,130 的结果解释为 -126。
显式强制转换只表示“开发者确认接受这个转换”,并不表示结果一定符合业务预期。
3.3 常量表达式有特殊的窄化规则
下面的代码可以编译:
byte x = 10;
因为整数常量 10 在编译期可确定,并且能表示在 byte 范围内。
但变量不适用同样规则:
int n = 10;
byte x = n; // 编译错误
即使运行时 n 的值恰好是 10,编译器也必须按照 int 类型处理它,不能假设所有执行路径都在 byte 范围内。
3.4 装箱与拆箱
包装类型把基本值包装成对象:
Integer boxed = 42; // 装箱
int primitive = boxed; // 拆箱
等价的显式形式大致是:
Integer boxed = Integer.valueOf(42);
int primitive = boxed.intValue();
规范定义了装箱、拆箱转换;具体对象缓存范围属于实现和库行为,不应依赖对象身份:
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // 常见实现中为 true,但不应作为业务判断
System.out.println(a.equals(b)); // true
判断包装数值时应使用 equals,或者先安全拆箱。
3.5 二元数值提升会改变表达式类型
对数值运算,Java 会根据操作数进行二元数值提升。简化来看:
- 只要有
double,结果通常提升为double; - 否则有
float,提升为float; - 否则有
long,提升为long; - 否则提升为
int。
因此:
byte a = 1;
byte b = 2;
var sum = a + b;
System.out.println(((Object) sum).getClass()); // Integer
a + b 的结果是 int,不是 byte。下面代码不能编译:
byte sum = a + b; // 编译错误
如果确实接受截断,可以显式转换:
byte sum = (byte) (a + b);
但当运算可能溢出时,显式转换只是隐藏风险,不会消除风险。
4. 控制流的基础:表达式、语句和完成方式
Java 程序由表达式和语句组成。
- 表达式产生值,或者产生副作用;
- 语句控制执行路径;
- 某些语句以“正常完成”结束;
- 某些语句以“突然完成”结束,例如
return、break、continue和抛出异常。
4.1 表达式通常按从左到右求值
int result = first() + second();
Java 先求值 first(),再求值 second(),最后执行加法。方法调用参数也遵循确定的求值顺序:
call(makeA(), makeB(), makeC());
执行顺序是:
makeA()
→ makeB()
→ makeC()
→ call(...)
如果前面的表达式抛出异常,后面的表达式不会执行。
4.2 短路逻辑运算符改变控制流
&& 和 || 不一定求值右侧表达式:
if (value != null && !value.isEmpty()) {
System.out.println(value);
}
当 value == null 时,右侧 !value.isEmpty() 不会执行,因此不会发生空指针异常。
对比:
if (value != null & !value.isEmpty()) {
// 仍可能抛出 NullPointerException
}
& 对布尔操作数是非短路逻辑与,左右两侧都会求值。
4.3 if 的条件必须是 boolean
Java 不把整数自动转换为布尔值:
int count = 1;
if (count) { // 编译错误
}
必须明确写出条件:
if (count != 0) {
}
这与某些允许“零为假、非零为真”的语言不同。
4.4 循环中的 break 和 continue
for (int i = 0; i < 5; i++) {
if (i == 2) {
continue;
}
if (i == 4) {
break;
}
System.out.println(i);
}
输出:
0
1
3
执行路径是:
i = 0,打印;i = 1,打印;i = 2,执行continue,跳过打印;i = 3,打印;i = 4,执行break,结束循环。
嵌套循环可以使用带标签的 break,但标签改变的是控制流目标,不改变变量作用域:
outer:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (i == 1 && j == 1) {
break outer;
}
}
}
4.5 switch 表达式必须产生结果
Java 支持语句形式和表达式形式的 switch。
int score = 85;
String level = switch (score / 10) {
case 10, 9 -> "A";
case 8 -> "B";
case 7 -> "C";
default -> "D";
};
箭头规则不会发生传统 case 的贯穿执行。若使用代码块,需要通过 yield 返回结果:
String level = switch (score / 10) {
case 10, 9 -> "A";
case 8 -> {
System.out.println("good");
yield "B";
}
default -> "D";
};
switch 表达式必须覆盖所有可能结果,否则编译器拒绝代码。通常使用 default,或者使用枚举、sealed 类型的穷尽性分析。
5. 类、接口和引用转换:运行时类型与编译时类型
考虑以下层次:
interface Named {
String name();
}
class User implements Named {
public String name() {
return "user";
}
void login() {
}
}
Named value = new User();
这里:
- 编译时类型是
Named; - 运行时对象类型是
User; - 通过
value只能直接调用Named声明的成员; - 覆盖方法的实际实现由运行时类型决定。
System.out.println(value.name()); // 调用 User.name()
value.login(); // 编译错误
如果明确确认对象类型,可以强制转换:
User user = (User) value;
user.login();
但错误转换会在运行时抛出 ClassCastException:
Named value = new OtherNamed();
User user = (User) value; // 运行时失败
类型模式把“判断类型”和“转换变量”合并起来:
if (value instanceof User user) {
user.login();
}
只有当 value 非 null 且运行时类型可赋值给 User 时,模式匹配成功。
User user = null;
if (user instanceof User u) {
// 不会进入,因为 null 不匹配类型模式
}
6. Record:以数据状态为中心的类
record 是一种特殊的类声明,适合表示主要由固定组件构成的数据值。
public record Point(int x, int y) {
}
这相当于声明了两个组件:
组件 x: int
组件 y: int
编译器会生成:
- 私有且不可重新赋值的字段;
- 同名访问器
x()、y(); - 规范构造器;
equals;hashCode;toString。
访问器不是 JavaBean 风格的 getX(),而是组件名本身:
Point point = new Point(3, 4);
System.out.println(point.x()); // 3
System.out.println(point.y()); // 4
System.out.println(point); // Point[x=3, y=4]
6.1 Record 的规范构造器
可以使用紧凑构造器验证输入:
public record UserId(String value) {
public UserId {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("value must not be blank");
}
}
}
紧凑构造器中可以直接使用组件名。编译器负责完成最终字段赋值,因此不能再次写:
this.value = value; // 紧凑构造器中不应这样写
创建对象时,验证流程是:
new UserId(input)
→ 进入规范构造器
→ 检查 null 和空白
→ 检查失败则抛出异常
→ 检查成功后由编译器完成字段初始化
也可以显式声明规范构造器:
public record UserId(String value) {
public UserId(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException();
}
this.value = value;
}
}
6.2 Record 不是深度不可变对象
Record 的组件字段不能被重新赋值,但组件引用指向的对象仍可能可变:
import java.util.List;
record Group(List<String> members) {
}
var members = new java.util.ArrayList<>(List.of("A"));
var group = new Group(members);
members.add("B");
System.out.println(group.members()); // [A, B]
这里 Group 的字段引用没有改变,但它引用的 ArrayList 改变了。
如果需要更强的不可变边界,必须在构造时复制:
import java.util.List;
record ImmutableGroup(List<String> members) {
public ImmutableGroup {
members = List.copyOf(members);
}
}
List.copyOf 还会拒绝 null 元素,并返回不可修改的列表视图或副本。这个设计取舍属于组件类型的语义,不是 record 自动提供的能力。
6.3 Record 可以自定义行为,但不能改变其核心身份
Record 可以声明方法、静态成员和实现接口:
record Celsius(double value) {
double toFahrenheit() {
return value * 9 / 5 + 32;
}
}
但它不能:
- 显式继承另一个类;
- 声明实例字段来增加独立状态;
- 把 record 组件字段改成可变字段;
- 通过普通继承扩展为子类。
Record 隐式继承 java.lang.Record,本身是 final 的。它适合表达值对象、消息、解析结果和模式匹配中的数据分支,不适合需要复杂可变生命周期的实体对象。
6.4 Record 的相等性是组件相等性
record Point(int x, int y) {
}
System.out.println(new Point(1, 2).equals(new Point(1, 2))); // true
对于引用组件,生成的 equals 使用组件对象的 equals 语义;对于数组组件,数组的 equals 默认仍是身份比较:
record Bytes(byte[] value) {
}
var a = new Bytes(new byte[] {1, 2});
var b = new Bytes(new byte[] {1, 2});
System.out.println(a.equals(b)); // false,数组引用不同
如果业务需要按数组内容相等,就需要重新设计组件类型,例如使用不可变字节值对象,或明确覆盖 equals 与 hashCode。
7. Sealed:显式限制继承边界
sealed 用来限制某个类或接口允许哪些直接子类型:
sealed interface Shape
permits Circle, Rectangle {
}
record Circle(double radius) implements Shape {
}
record Rectangle(double width, double height) implements Shape {
}
这里 Shape 的直接实现者只有 Circle 和 Rectangle。
每个直接子类型必须明确选择一种继承策略:
final:不允许继续扩展;sealed:继续限制下一层;non-sealed:重新开放继承。
sealed interface Message
permits TextMessage, BinaryMessage, ExtensionMessage {
}
record TextMessage(String text) implements Message {
}
final class BinaryMessage implements Message {
}
non-sealed class ExtensionMessage implements Message {
}
继承关系是:
Message
├── TextMessage final record
├── BinaryMessage final class
└── ExtensionMessage non-sealed,重新开放
7.1 permits 不是任意白名单
直接子类型必须满足语言规范要求,例如:
- 必须直接扩展或实现 sealed 类型;
- 必须出现在
permits中,或满足允许省略permits的声明规则; - 相关类型必须满足包、模块和可访问性约束;
- 直接子类型必须声明
final、sealed或non-sealed。
因此,sealed 是编译器和运行时都能识别的类型层次约束,不只是文档约定。
7.2 Sealed 类型支持穷尽性分析
如果 Shape 的直接子类型只有两个,并且它们都是不可继续扩展的,编译器可以推导出所有可能分支:
static double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
};
}
这里不需要 default,因为:
Shape
→ Circle 或 Rectangle
→ 两个分支均已覆盖
如果未来给 Shape 增加新的 permitted subtype,这段代码可能无法再编译,迫使调用方重新审查处理逻辑。这是 sealed 类型的重要价值:让类型层次的变化暴露在编译阶段。
但是,若使用:
case Shape s -> ...
这个总类型模式已经覆盖所有非 null 的 Shape,后续更具体的分支会因为被支配而无法编译。
8. 模式匹配:让类型测试、绑定和数据分解协同工作
模式匹配可以拆成三个动作:
- 判断输入是否满足某种形状;
- 在匹配成功时绑定变量;
- 直接取得对象中的数据。
8.1 instanceof 类型模式
传统写法:
if (value instanceof String) {
String text = (String) value;
System.out.println(text.length());
}
类型模式写法:
if (value instanceof String text) {
System.out.println(text.length());
}
模式变量 text 只在匹配成功且变量确定非空的路径上可用。
if (value instanceof String text && !text.isBlank()) {
System.out.println(text);
}
这里 && 的右侧成立的前提是左侧匹配成功,所以 text 在右侧可用。
下面的写法不成立:
if (value instanceof String text || text.isBlank()) {
// 编译错误
}
因为当左侧为假时,右侧可能仍然执行,而 text 没有确定赋值。
这就是模式变量的流作用域:变量的可用范围由控制流证明决定,而不是简单地从声明位置延伸到代码块末尾。
8.2 否定条件可以缩短失败路径
if (!(value instanceof String text)) {
return;
}
System.out.println(text.length());
执行到 return 之后的代码时,唯一剩余路径就是匹配成功,因此 text 在后续代码中可用。
8.3 Record pattern:同时检查类型和分解组件
对于:
record Point(int x, int y) {
}
可以使用 record pattern:
static int manhattanDistance(Object value) {
if (value instanceof Point(int x, int y)) {
return Math.abs(x) + Math.abs(y);
}
return -1;
}
匹配步骤是:
value == null → 失败
value 不是 Point → 失败
value 是 Point → 读取 x 和 y
x、y 绑定为局部变量 → 执行计算
Record pattern 依赖 record 已声明的组件结构,因此它不是对任意 JavaBean 自动解构。
8.4 嵌套 Record pattern
record Address(String city) {
}
record User(String name, Address address) {
}
static String cityOf(Object value) {
if (value instanceof User(String name, Address(String city))) {
return name + "@" + city;
}
return "unknown";
}
这个模式要求:
- 外层对象是
User; User.name()和User.address()可用于分解;address是Address;Address.city()提供字符串值。
如果任意嵌套层为 null,嵌套 record pattern 不匹配,而不是自动跳过空值。
9. Pattern switch:按类型和结构选择分支
switch 的选择器可以是对象,并使用类型模式:
static String describe(Object value) {
return switch (value) {
case Integer i -> "integer: " + i;
case String s -> "string: " + s;
default -> "other";
};
}
与 instanceof 相比,switch 更适合表达互斥的多分支分类。
9.1 null 必须显式考虑
传统对象 switch 遇到 null 通常会抛出 NullPointerException。Pattern switch 可以显式处理:
static String describe(Object value) {
return switch (value) {
case null -> "null";
case String s -> "string: " + s;
default -> "other";
};
}
case null 不是类型模式,它专门匹配空引用。
9.2 when 添加守卫条件
可以在模式之后加入 when 条件:
static String classify(Object value) {
return switch (value) {
case null -> "null";
case Integer i when i < 0 -> "negative integer";
case Integer i -> "non-negative integer";
default -> "other";
};
}
对于 -3,执行过程是:
匹配 null?否
匹配 Integer 且 i < 0?是
返回 "negative integer"
对于 5:
匹配 null?否
匹配 Integer 且 i < 0?否
匹配 Integer?是
返回 "non-negative integer"
守卫条件不会改变模式的类型覆盖范围,因此分支顺序仍然重要。
9.3 分支支配规则
更宽的模式不能放在更窄模式之前:
return switch (value) {
case Object o -> "object";
case String s -> "string"; // 编译错误:永远到达不了
};
Object 已经匹配所有非 null 对象,后面的 String 不可能被选择。
正确顺序是:
return switch (value) {
case String s -> "string";
case Object o -> "object";
};
带守卫的模式通常不能替代无守卫的覆盖关系,因此仍应按照从具体到一般的顺序组织分支。
10. Record、sealed 和模式匹配的完整组合
下面是一个可以保存为 ExpressionDemo.java 并运行的完整示例:
public class ExpressionDemo {
sealed interface Expr permits NumberLiteral, Add {
}
record NumberLiteral(int value) implements Expr {
}
record Add(Expr left, Expr right) implements Expr {
}
static int evaluate(Expr expr) {
return switch (expr) {
case NumberLiteral(int value) -> value;
case Add(Expr left, Expr right) ->
evaluate(left) + evaluate(right);
};
}
static String describe(Expr expr) {
return switch (expr) {
case NumberLiteral(int value) ->
"number(" + value + ")";
case Add(NumberLiteral(int left),
NumberLiteral(int right)) ->
"simple-add(" + left + ", " + right + ")";
case Add(Expr left, Expr right) ->
"compound-add(" + describe(left)
+ ", " + describe(right) + ")";
};
}
public static void main(String[] args) {
Expr expression = new Add(
new NumberLiteral(1),
new Add(
new NumberLiteral(2),
new NumberLiteral(3)
)
);
System.out.println(evaluate(expression));
System.out.println(describe(expression));
}
}
使用 Java 25 编译运行:
javac --release 25 ExpressionDemo.java
java ExpressionDemo
预期输出:
6
compound-add(number(1), simple-add(2, 3))
10.1 evaluate 的穷尽性推导
Expr 的 permitted direct subtypes 是:
Expr
├── NumberLiteral
└── Add
两个 record 都是隐式 final,因此不存在未声明的 Expr 子类型。evaluate 的两个分支覆盖:
NumberLiteral → 返回 value
Add → 递归计算 left 和 right
对 Add 分支,递归调用满足:
evaluate(Add(left, right))
= evaluate(left) + evaluate(right)
当表达式树为:
Add(
NumberLiteral(1),
Add(NumberLiteral(2), NumberLiteral(3))
)
计算过程为:
evaluate(NumberLiteral(1)) = 1
evaluate(NumberLiteral(2)) = 2
evaluate(NumberLiteral(3)) = 3
evaluate(Add(2, 3)) = 2 + 3 = 5
evaluate(Add(1, 5)) = 1 + 5 = 6
10.2 describe 展示了模式的优先级
Add(NumberLiteral(...), NumberLiteral(...)) 比一般的 Add(Expr, Expr) 更具体,因此必须排在后者之前。
如果交换两个分支:
case Add(Expr left, Expr right) -> ...;
case Add(NumberLiteral(int left), NumberLiteral(int right)) -> ...;
第二个分支会被编译器判定为不可达,因为第一个模式已经匹配所有 Add。
11. 基本类型模式:Java 25 中需要区分预览能力
截至 Java 25,基本类型参与模式匹配和 switch 的能力仍属于预览特性,应明确使用预览参数编译运行:
javac --enable-preview --release 25 Example.java
java --enable-preview Example
预览特性不是 Java SE 25 永久稳定语言能力,后续版本可能修改或移除语法和语义。生产代码使用时必须经过版本、编译参数和部署运行时的统一验证。
基本类型模式的目标是让模式匹配不再只围绕引用类型,例如对 int、long 等进行模式判断和 switch 分派。需要特别注意:
- 基本类型没有
null; - 包装类型存在
null; - 包装类型参与匹配时可能涉及拆箱;
- 拆箱遇到
null可能抛出NullPointerException; - 不同基本类型之间的可匹配关系受数值转换和模式规则限制;
- 不能把预览语法当作 Java 25 正式永久语法使用。
例如,下面的非预览代码已经可以稳定表达“先判断包装类型,再读取值”:
static String classify(Integer value) {
return switch (value) {
case null -> "null";
case Integer i when i < 0 -> "negative";
case Integer i -> "non-negative";
};
}
而涉及基本类型 selector、基本类型模式或相关原始类型扩展时,应使用 --enable-preview --release 25,并在构建流水线中固定编译器和运行时版本。
12. 泛型与模式匹配的边界
泛型类型在运行时受到类型擦除影响:
List<String> names = new ArrayList<>();
运行时通常只知道它是某种 List,不能直接这样判断:
if (value instanceof List<String> names) {
// 编译错误:不能对不可具体化参数化类型做 instanceof
}
可以判断原始或通配符类型:
if (value instanceof List<?> names) {
System.out.println(names.size());
}
但这只能证明它是一个列表,不能证明其中每个元素都是 String。元素类型检查仍需逐个进行:
if (value instanceof List<?> list
&& list.stream().allMatch(String.class::isInstance)) {
// 此时仍需谨慎处理类型转换和空元素策略
}
类型擦除还解释了为什么不能重载:
void process(List<String> value) {}
void process(List<Integer> value) {} // 编译错误
两者擦除后都近似为 process(List),签名冲突。
模式匹配能够利用编译期可判断的类型关系,但不能凭空恢复运行时已经擦除的泛型实参。
13. 常见失败表现与诊断方法
13.1 把 record 当成深度不可变对象
失败表现通常不是编译错误,而是状态从外部被改变:
record Config(Map<String, String> values) {
}
如果直接保存外部传入的可变 Map,调用者仍可修改 record 观察到的内容。
诊断方法:
- 查组件是否为集合、数组或其他可变对象;
- 检查构造器是否进行防御性复制;
- 检查访问器是否直接暴露可变内部对象;
- 检查
equals和hashCode使用期间组件是否会改变。
13.2 误用 == 比较包装对象或字符串
失败表现可能随缓存、输入来源或运行环境变化:
Integer a = Integer.valueOf(1000);
Integer b = Integer.valueOf(1000);
System.out.println(a == b); // 不应依赖结果
应改为:
a.equals(b)
并先处理可能的 null:
java.util.Objects.equals(a, b)
13.3 忽略整数溢出
失败表现包括:
- 分页计算得到负数;
- 时间间隔变成负值;
- 容量检查绕过边界;
- 数组长度或缓冲区大小计算异常。
诊断时检查:
Math.addExact(...)
Math.subtractExact(...)
Math.multiplyExact(...)
是否适用于该计算;如果值本身可能超过 long 范围,应进一步考虑 BigInteger。
13.4 在 switch 中遗漏 null
对象类型 selector 为 null 时,未处理的 switch 可能抛出 NullPointerException。若 null 是合法输入状态,应显式写:
case null -> ...
若 null 代表违反前置条件,也应在 API 边界明确拒绝,而不是依靠深层 switch 偶然失败。
13.5 通过 non-sealed 破坏穷尽性假设
如果 sealed 层次中存在:
non-sealed class ExtensionMessage implements Message {
}
那么 Message 的子类集合不再完全封闭。对 Message 做穷尽性 switch 时,通常需要 default 或覆盖更一般的模式。
这不是编译器缺陷,而是继承边界已经被 non-sealed 有意重新开放。
14. 设计边界:何时使用哪种机制
14.1 使用基本类型表达没有对象身份的值
计数、标志、索引和简单数值通常适合基本类型。但要注意:
- 包装类型允许
null,基本类型不允许; - 泛型参数不能直接使用基本类型,会发生装箱;
- 需要高精度金额时不要用二进制浮点数;
- 需要溢出检测时使用显式检查 API。
14.2 使用引用类型表达身份、生命周期或可组合行为
实体对象通常需要:
- 唯一身份;
- 可变生命周期;
- 与外部资源交互;
- 多态行为;
- 复杂不变量。
这些场景不应为了语法简洁强行使用 record。
14.3 使用 Record 表达稳定的数据边界
Record 适合:
- 请求和响应数据;
- 值对象;
- 解析后的不可变结果;
- 事件载荷;
- 模式匹配中的数据分支。
但组件的可变性、复制策略、空值策略和相等性仍然由开发者负责。
14.4 使用 sealed 表达受控的变体集合
当业务概念有明确且有限的变体时,sealed 类型能够把设计意图传递给编译器:
支付结果
├── 成功
├── 余额不足
└── 风控拒绝
如果业务允许第三方扩展或插件注册未知类型,则不应把整个层次错误地声明为封闭集合;可以只封闭核心内部模型,并在边界处使用开放接口或扩展分支。
14.5 使用模式匹配消除不安全的类型转换
模式匹配的价值不是减少几行代码,而是让类型检查、变量绑定和控制流保持一致:
if (value instanceof User user) {
user.login();
}
编译器能够验证 user 的作用域和分支关系,减少“判断的是一个对象、转换的却是另一个对象”这类错误。
15. 版本边界与规范依据
Java 25 中应区分以下能力:
- 基本类型、引用类型、类、接口、数组、泛型和常规控制流:Java 语言基础能力;
record:正式语言特性;sealed类和接口:正式语言特性;instanceof类型模式:正式语言特性;switch模式匹配:正式语言特性;- record patterns:正式语言特性;
- 基本类型参与模式和
switch的相关扩展:Java 25 预览能力,编译运行需启用 preview。
具体语法、类型转换、模式变量作用域、switch 支配规则、sealed 类型穷尽性和 record 构造器语义,应以 Java SE 25 的 Java Language Specification 为准;类库行为和命令说明可参考 Java SE 25 Documentation。
掌握这些机制后,可以把一段 Java 代码按以下顺序分析:
1. 值是基本类型还是引用类型?
2. 发生了哪些隐式转换、装箱或拆箱?
3. 引用的编译时类型和运行时类型分别是什么?
4. 控制流是否保证变量已赋值、对象非 null、模式已匹配?
5. record 的组件是否真的满足所需的不变性?
6. sealed 层次是否封闭,switch 是否穷尽?
7. 使用的语法是否属于 Java 25 预览特性?
这套分析路径把类型、数据建模、继承边界和控制流连接起来,也是理解 Java 对象模型、泛型边界以及不可变设计的基础。
系列导航与关联阅读
- 系列入口:Java 完整学习路线:从 Java 25 语言与 JVM 到 Spring、微服务和生产交付
- 上一篇:Java 25 工具链:JDK、javac、jar、Maven、Gradle 与可重复构建
- 下一篇:Java 对象模型:类、接口、继承、组合、不可变性和设计边界
- 延伸:Java 泛型完整基础:类型擦除、通配符、PECS、边界和反射
官方资料
本文依据 Java、Spring 与相关项目官方文档重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论