一、知识点总览
Java 8 与 Java 17 是 Java 语言发展史上两个最重要的长期支持版本(LTS,Long-Term Support)。
- Java 8 于 2014 年发布,彻底改变了 Java 的编程范式,引入了函数式编程能力;
- Java 17 于 2021 年发布,在 Java 8 的基础上进一步演进,强化了类型安全、代码简洁性与模式匹配能力。
官方定义:
- Java 8(JDK 1.8):Oracle 于 2014 年 3 月发布的 Java 平台标准版,核心特性包括 Lambda 表达式、Stream API、Optional、新日期时间 API、接口默认方法等,是 Java 从纯面向对象向"面向对象 + 函数式"混合范式转型的里程碑版本。
- Java 17(JDK 17):Oracle 于 2021 年 9 月发布的 LTS 版本,累计了 Java 9 至 Java 17 共 9 个版本的核心特性,标志性特性包括 Sealed Classes(密封类)、Records(记录类)、Pattern Matching for instanceof(模式匹配)、Text Blocks(文本块)、Switch Expressions(Switch 表达式)等,是继 Java 8 之后企业级应用升级的首选目标版本。
核心定位: 本手册围绕 Java 8 与 Java 17 两大 LTS 版本的核心新特性展开,覆盖语法特性、API 变更、底层原理、实战规范与面试高频考点,适用于零基础入门、日常技术提升与求职面试备考。
二、底层原理深度拆解
1. 完整执行流程
Java 8 核心特性执行流程
(1)Lambda 表达式执行流程
源码编写 Lambda 表达式
↓
javac 编译器将 Lambda 转换为 invokedynamic 指令(而非内部类,旧版会额外生成Outer$1.class)
↓
JVM 运行时通过 invokedynamic 调用 Bootstrap Method(LambdaMetafactory)
↓
Bootstrap Method 根据上下文生成目标函数式接口的实现类实例
↓
Lambda 体被编译为宿主类的静态方法(通过方法句柄 MethodHandle 绑定)
↓
运行时调用时,直接通过方法句柄分派到静态方法,零额外对象创建开销
(2)Stream API 执行流程
数据源(Collection、Array、I/O Channel)
↓
调用 stream() / parallelStream() 创建 Stream 实例
↓
中间操作(filter、map、sorted 等)惰性注册,构建操作管道(Pipeline)
↓
终端操作(collect、forEach、reduce 等)触发执行
↓
Stream 框架将管道拆解为 Spliterator 迭代器
↓
若为并行流,通过 ForkJoinPool 将数据分片,多线程并行处理
↓
各分片结果通过合并器(Combiner)汇聚,输出最终结果
(3)Optional 执行流程
方法返回值包装为 Optional<T> 对象
↓
调用方通过 isPresent() / isEmpty() 判断值是否存在
↓
通过 get() 获取值(需先判断存在性,否则抛 NoSuchElementException)
↓
或通过 orElse() / orElseGet() / orElseThrow() 提供默认值或异常
↓
通过 map() / flatMap() / filter() 进行链式函数式处理
↓
避免显式 null 检查,从类型层面约束空值处理
Java 17 核心特性执行流程
(1)Record 类执行流程
开发者声明 record 类(如 record Point(int x, int y) {})
↓
javac 编译器自动生成:
- private final 字段(与组件一一对应)
- 全参构造器
- accessor 方法(方法名与组件名相同,非 getXxx 形式)
- equals():基于所有组件的值比较
- hashCode():基于所有组件的哈希计算
- toString():输出 record 类名及所有组件值
↓
Record 类隐式继承 java.lang.Record,不可再继承其他类
↓
Record 类天然不可变(字段 final),可作为 Map 的 Key、Set 的元素
↓
支持在 record 内部声明静态方法、实例方法、静态工厂,实现紧凑构造器(compact constructor)进行参数校验
(2)Sealed Classes 执行流程
开发者使用 sealed 关键字声明密封类/接口
↓
通过 permits 子句显式指定允许的子类列表
↓
所有 permitted 子类必须使用 final、sealed 或 non-sealed 之一修饰
↓
编译器在编译期验证:所有子类必须在同一模块或同一包内
↓
运行时,JVM 通过 PermittedSubclasses 属性强制约束反射创建子类
↓
配合 switch 模式匹配(Pattern Matching),编译器可实现穷举检查(exhaustiveness checking)
(3)Pattern Matching for instanceof 执行流程
传统写法:if (obj instanceof String) { String s = (String) obj; ... }
↓
模式匹配写法:if (obj instanceof String s) { ... 直接使用 s ... }
↓
编译器将模式匹配拆解为:
1. 类型检查(invokevirtual instanceof 或 checkcast)
2. 若类型匹配,自动将变量绑定到指定名称(s)
3. 在 if 块的作用域内,s 的类型被窄化为 String
↓
支持逻辑表达式中的模式变量作用域扩展(&& 可用,|| 受限)
2. 核心组件拆解
Java 8 核心组件
| 组件 | 核心作用 | 关键特性 | 与其他组件的协作关系 |
|---|---|---|---|
| Lambda 表达式 | 提供匿名函数的简洁语法,实现行为参数化 | 语法简洁 (params) -> body;依赖函数式接口 |
是 Stream API、CompletableFuture 等所有函数式特性的基础语法支撑 |
| 函数式接口(Functional Interface) | 定义 Lambda 的目标类型约束 | 有且仅有一个抽象方法;@FunctionalInterface 注解校验 |
为 Lambda 提供类型系统支撑;Java 8 预置 Function、Predicate、Consumer、Supplier 四大核心接口 |
| Stream API | 对集合数据进行声明式、链式、可并行的批量操作 | 惰性求值、短路操作、并行流自动分片 | 依赖 Lambda 定义操作逻辑;依赖 Spliterator 实现迭代与分片 |
| Optional | 以类型安全的方式表达"值可能存在也可能不存在" | 避免 NullPointerException;支持链式调用 | 常作为 Stream 终端操作(如 findFirst())的返回类型;与 Lambda 的 map/flatMap 协作 |
| 新日期时间 API(java.time) | 替代线程不安全的 Date/Calendar |
不可变对象、线程安全、纳秒精度、时区支持 | LocalDate/LocalTime/LocalDateTime/ZonedDateTime 形成完整时间体系 |
| 接口默认方法(default method) | 允许接口中定义带实现的方法 | 使用 default 关键字;支持 static 方法 |
解决接口演进问题,使 Stream API 可在不破坏实现类的情况下向 Collection 添加 stream() 方法 |
| CompletableFuture | 提供异步编程的编排能力 | 支持链式回调、组合、异常处理 | 依赖 Lambda 定义异步任务与回调逻辑 |
| 方法引用(Method Reference) | Lambda 的进一步简化写法 | ClassName::methodName 四种形式 |
本质是 Lambda 的语法糖,编译后与 Lambda 等价 |
Java 17 核心组件
| 组件 | 核心作用 | 关键特性 | 与其他组件的协作关系 |
|---|---|---|---|
| Record(记录类) | 创建不可变数据载体类,消除样板代码 | 自动生成构造器、accessor、equals、hashCode、toString | 常作为 Stream 操作的数据单元;配合 Sealed Classes 构建代数数据类型 |
| Sealed Classes(密封类) | 精确控制类的继承层次,限定哪些类可以继承 | sealed + permits;子类必须为 final/sealed/non-sealed |
与 Pattern Matching 配合实现穷举检查;与 Record 配合构建领域模型 |
| Pattern Matching for instanceof | 消除类型检查后的强制类型转换样板代码 | 类型检查与变量绑定一步完成 | 为 Switch Pattern Matching(Java 21 完善)奠定基础;与 Sealed Classes 协作实现类型安全的分支处理 |
| Text Blocks(文本块) | 多行字符串字面量,保留格式 | """...""" 语法;自动处理缩进与转义 |
替代传统字符串拼接,适用于 SQL、JSON、HTML 等多行文本场景 |
| Switch Expressions(Switch 表达式) | Switch 既可作为语句也可作为表达式返回值 | 箭头语法 ->;支持 yield 返回值;无 fall-through |
配合 Pattern Matching 可实现类型分派;替代冗长的 if-else 链 |
| Records + Pattern Matching 联动 | 实现解构式数据访问 | Record 组件可直接在模式匹配中解构(预览/完善中) | 构建完整的代数数据类型(ADT)处理链路 |
| 增强的 NullPointerException 信息 | NPE 消息精确指出哪个变量为 null | JVM 参数 -XX:+ShowCodeDetailsInExceptionMessages |
调试辅助,不影响业务逻辑 |
3. 技术诞生背景
Java 8 的诞生背景
研发初衷: 在 Java 8 之前,Java 是纯粹的面向对象语言,缺乏函数式编程的一等支持。面对以下痛点,Oracle 启动了 Java 8 的重大革新:
| 传统痛点 | 具体表现 | Java 8 解决方案 |
|---|---|---|
| 集合操作冗长 | 过滤、转换集合需要 for 循环 + 临时变量,代码量大且意图不清晰 | Stream API 提供声明式链式操作 |
| 匿名内部类臃肿 | 实现回调、策略模式需编写大量样板代码(如 new Runnable() { ... }) |
Lambda 表达式一行搞定 |
| 并发编程困难 | 手写 Thread + Callback 嵌套,异常处理复杂 | CompletableFuture 提供函数式异步编排 |
| 日期时间 API 缺陷 | java.util.Date 线程不安全、Calendar 设计混乱、月份从 0 开始 |
java.time 包全面重构 |
| 空指针泛滥 | 方法返回值可能为 null,调用方必须显式检查,遗漏即 NPE | Optional 从类型层面约束空值语义 |
| 接口无法演进 | 接口新增方法会破坏所有实现类 | 默认方法(default method)允许接口向后兼容地演进 |
Java 17 的诞生背景
Java 17 并非单一版本的功能,而是 Java 9 至 Java 17 共 9 个版本核心特性的集大成者。其诞生背景围绕以下核心诉求:
| 传统痛点 | 具体表现 | Java 17 解决方案 |
|---|---|---|
| 数据载体类样板代码过多 | 一个简单 DTO 需要手写构造器、getter、equals、hashCode、toString(或使用 Lombok) | Record 类一行声明,编译器自动生成全部 |
| 继承层次不可控 | 框架设计者无法约束用户如何继承,导致类型安全性下降 | Sealed Classes 精确限定继承树 |
| 类型检查 + 转换冗余 | instanceof 后必须手动强转,代码重复且易出错 |
Pattern Matching 一步完成检查与绑定 |
| 多行字符串处理痛苦 | SQL、JSON 等需要大量 + 拼接和 \n 转义 |
Text Blocks 原生支持多行字符串 |
| Switch 语句功能弱 | 不支持返回值、存在 fall-through 陷阱、无法做类型分派 | Switch Expressions + Pattern Matching |
| 企业升级动力不足 | Java 8 过于稳定,企业缺乏升级动力 | Java 17 作为 LTS 版本,累积足够多的高价值特性形成升级拉力 |
4. 优劣势与适用边界
Java 8
| 维度 | 详细说明 |
|---|---|
| 核心优势 | ① Lambda + Stream 彻底改变 Java 编程范式,代码量减少 30%-50%;② Optional 从类型层面减少 NPE;③ 新日期 API 线程安全且设计优雅;④ 生态成熟度最高,几乎所有第三方库完美兼容;⑤ 企业存量代码最多,学习资料最丰富 |
| 固有局限 | ① 无 Record 类,DTO 仍需大量样板代码;② 无密封类,继承层次不可控;③ 无模式匹配,类型分派依赖 if-else 或 Visitor 模式;④ 无文本块,多行字符串处理痛苦;⑤ Switch 不支持表达式语义,无 fall-through 保护 |
| 适用场景 | ① 存量企业级项目维护(Spring Boot 2.x 生态);② 对稳定性要求极高、不愿承担升级风险的金融/电信系统;③ 学习 Java 函数式编程的入门版本 |
| 不适用场景 | ① 新项目首选(建议直接 Java 17+);② 需要现代语言特性提升开发效率的场景;③ 需要与最新框架(Spring Boot 3.x 最低要求 Java 17)配合的场景 |
Java 17
| 维度 | 详细说明 |
|---|---|
| 核心优势 | ① Record 类消除 DTO 样板代码,配合不可变性天然线程安全;② Sealed Classes 实现代数数据类型(ADT),类型安全性大幅提升;③ Pattern Matching 消除类型转换冗余;④ Text Blocks 极大简化多行字符串处理;⑤ Switch Expressions 更安全、更简洁;⑥ Spring Boot 3.x 的最低要求版本,代表未来主流方向;⑦ ZGC/Shenandoah 低延迟 GC 成熟可用 |
| 固有局限 | ① 部分特性(如 Record 的模式匹配解构)在 Java 17 中仍为预览特性;② 第三方库兼容性略逊于 Java 8(但主流库已全面支持);③ 从 Java 8 升级需要处理模块化(JPMS)相关的包访问限制;④ 学习曲线略高,需理解密封类、模式匹配等新范式 |
| 适用场景 | ① 新项目首选版本;② Spring Boot 3.x / Spring Framework 6.x 项目;③ 需要强类型安全与领域建模的复杂业务系统;④ 追求低延迟的高性能系统(配合 ZGC);⑤ 从 Java 8 升级的目标版本 |
| 不适用场景 | ① 依赖大量仅兼容 Java 8 的遗留第三方库的项目;② 运行在无法升级 JDK 的受限环境(如某些嵌入式/旧版中间件);③ 团队对 Java 新特性完全陌生且无时间学习(短期过渡可暂用 Java 8/11) |
三、实战最佳实践
1. 标准落地步骤
Java 8 核心特性标准落地步骤
步骤一:Lambda 表达式落地
java
// 1. 替换匿名内部类为 Lambda
// ❌ 旧写法
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
// ✅ 新写法
list.sort((a, b) -> a.compareTo(b));
// 进一步简化为方法引用
list.sort(String::compareTo);
步骤二:Stream API 落地
java
// 1. 集合过滤 + 转换 + 收集的标准范式
List<String> result = list.stream()
.filter(item -> item != null && !item.isEmpty()) // 过滤
.map(String::trim) // 转换
.distinct() // 去重
.sorted() // 排序
.collect(Collectors.toList()); // 收集
// 2. 分组统计的标准范式
Map<String, Long> countByName = users.stream()
.collect(Collectors.groupingBy(User::getName, Collectors.counting()));
// 3. 并行流的标准用法(数据量 > 10000 时考虑)
long count = largeList.parallelStream()
.filter(item -> item.isValid())
.count();
步骤三:Optional 落地
java
// 1. 方法返回值使用 Optional 替代 null
public Optional<User> findById(Long id) {
return Optional.ofNullable(userRepository.find(id));
}
// 2. 调用方链式处理
String city = findById(id)
.map(User::getAddress)
.map(Address::getCity)
.orElse("Unknown");
// 3. Optional 组合
Optional<User> user = findByName(name);
Optional<User> userById = findById(id);
User result = user.or(() -> userById).orElseThrow(
() -> new UserNotFoundException("User not found")
);
步骤四:新日期时间 API 落地
java
// 1. 获取当前时间
LocalDate today = LocalDate.now();
LocalDateTime now = LocalDateTime.now();
ZonedDateTime zonedNow = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
// 2. 日期计算
LocalDate nextWeek = today.plusWeeks(1);
LocalDate endOfMonth = today.with(TemporalAdjusters.lastDayOfMonth());
// 3. 格式化与解析
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = now.format(formatter);
LocalDateTime parsed = LocalDateTime.parse("2024-01-01 12:00:00", formatter);
// 4. 时间间隔
Duration duration = Duration.between(startTime, endTime);
Period period = Period.between(startDate, endDate);
Java 17 核心特性标准落地步骤
步骤一:Record 类落地
java
// 1. 定义数据载体
public record UserDTO(Long id, String name, String email) {
// 紧凑构造器:参数校验
public UserDTO {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be blank");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
}
}
// 2. 使用 Record
UserDTO user = new UserDTO(1L, "Alice", "alice@example.com");
System.out.println(user.name()); // "Alice"(注意:不是 getName())
System.out.println(user); // UserDTO[id=1, name=Alice, email=alice@example.com]
// 3. Record 在 Stream 中使用
List<UserDTO> dtos = users.stream()
.map(u -> new UserDTO(u.getId(), u.getName(), u.getEmail()))
.toList(); // Java 16+ 新增的不可变集合收集方式
步骤二:Sealed Classes 落地
java
// 1. 定义密封接口
public sealed interface PaymentResult
permits PaymentSuccess, PaymentFailure, PaymentPending {
}
// 2. 实现子类(必须声明 final/sealed/non-sealed)
public record PaymentSuccess(String transactionId, BigDecimal amount)
implements PaymentResult {}
public record PaymentFailure(String errorCode, String message)
implements PaymentResult {}
public final class PaymentPending implements PaymentResult {
private final String pendingId;
// ...
}
// 3. 配合 switch 进行类型安全的分支处理
public String handlePayment(PaymentResult result) {
return switch (result) {
case PaymentSuccess s -> "Success: " + s.transactionId();
case PaymentFailure f -> "Failed: " + f.message();
case PaymentPending p -> "Pending processing";
// 编译器知道所有子类,无需 default 分支(穷举检查)
};
}
步骤三:Pattern Matching 落地
java
// 1. 基础 instanceof 模式匹配
// ❌ 旧写法
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
// ✅ 新写法
if (obj instanceof String s) {
System.out.println(s.length());
}
// 2. 结合逻辑表达式
if (obj instanceof String s && s.length() > 5) {
System.out.println(s.toUpperCase());
}
// 3. 在 equals 方法中使用
@Override
public boolean equals(Object obj) {
return obj instanceof User other
&& this.id.equals(other.id)
&& this.name.equals(other.name);
}
步骤四:Text Blocks 落地
java
// 1. SQL 查询
String sql = """
SELECT u.id, u.name, u.email
FROM users u
WHERE u.status = 'ACTIVE'
AND u.created_at > ?
ORDER BY u.name ASC
""";
// 2. JSON 模板
String json = """
{
"id": %d,
"name": "%s",
"email": "%s"
}
""".formatted(user.id(), user.name(), user.email());
// 3. HTML 模板
String html = """
<!DOCTYPE html>
<html>
<body>
<h1>Hello, %s!</h1>
</body>
</html>
""".formatted(userName);
步骤五:Switch Expressions 落地
java
// 1. 基础 Switch 表达式
String label = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> "Weekend/Start";
case TUESDAY -> "Second day";
case WEDNESDAY -> "Midweek";
case THURSDAY -> "Almost there";
case SATURDAY -> "Weekend!";
};
// 2. 配合 yield 处理复杂逻辑
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
default -> {
int remaining = calculateRemaining(day);
yield remaining; // yield 用于多行代码块返回值
}
};
2. 行业通用规范
Lambda 与 Stream 规范
| 规范项 | 标准写法 | 说明 |
|---|---|---|
| Lambda 参数命名 | 单参数可省略括号,多参数必须加括号 | (x) -> x * 2 或 x -> x * 2 |
| Lambda 体简洁性 | 单行表达式省略 {} 和 return |
x -> x.getName() 而非 x -> { return x.getName(); } |
| 方法引用优先 | 能用方法引用就不用 Lambda | String::toUpperCase 优于 s -> s.toUpperCase() |
| Stream 不修改源数据 | Stream 操作应为纯函数,不产生副作用 | 禁止在 map/filter 中修改外部变量 |
| 并行流谨慎使用 | 仅在数据量大(>10000)且操作无状态时使用 | 小数据量并行流因线程切换开销反而更慢 |
| Optional 使用边界 | 仅用于方法返回值,不用于字段、参数、集合元素 | Optional 作为字段会增加内存开销且无意义 |
| 避免 Optional 嵌套 | 使用 flatMap 而非返回 Optional<Optional<T>> |
flatMap 自动展平嵌套 |
Record 与 Sealed Classes 规范
| 规范项 | 标准写法 | 说明 |
|---|---|---|
| Record 命名 | 使用名词,语义上表示数据载体 | UserDTO、Point、OrderSummary |
| Record 不可变性 | 所有字段天然 final,引用类型应使用不可变集合 | 不要在 Record 中存放 List 的可变引用 |
| Record 校验 | 在紧凑构造器中完成参数校验 | 保证 Record 实例一旦创建即合法 |
| Sealed 类层次设计 | 同一模块/包内定义所有子类 | 编译器要求 permits 子类可访问 |
| 配合 switch 穷举 | 利用密封类的穷举检查,省略 default | 新增子类时编译器自动提示未处理的分支 |
Text Blocks 规范
| 规范项 | 标准写法 | 说明 |
|---|---|---|
| 缩进对齐 | 结束引号 """ 的位置决定公共缩进基准 |
利用缩进保持代码可读性 |
| 转义控制 | 使用 \ 行尾续行消除换行符 |
"Hello \" 续行后无换行 |
| 格式化 | 使用 .formatted() 替代 String.format() |
更简洁的链式调用 |
3. 高频踩坑案例
踩坑案例 1:Stream 并行流导致数据竞争
错误现象: 使用 parallelStream() 后,结果偶发不正确或抛出 ConcurrentModificationException。
核心原因: 在 Stream 操作中修改了外部共享可变状态(如向外部 List 添加元素),并行流多线程同时操作导致数据竞争。
java
// ❌ 错误写法
List<String> result = new ArrayList<>();
list.parallelStream()
.filter(s -> s.length() > 3)
.forEach(result::add); // ArrayList 非线程安全!
修复方案:
java
// ✅ 方案一:使用 collect 收集结果(推荐)
List<String> result = list.parallelStream()
.filter(s -> s.length() > 3)
.collect(Collectors.toList());
// ✅ 方案二:如必须使用 forEach,改用线程安全容器
List<String> result = Collections.synchronizedList(new ArrayList<>());
规避技巧: 永远不要在 Stream 操作中修改外部可变状态;使用 collect 作为终端操作收集结果。
踩坑案例 2:Optional.get() 未判断存在性导致 NoSuchElementException
错误现象: 调用 optional.get() 时抛出 NoSuchElementException。
核心原因: 未使用 isPresent() 判断或使用 orElse 系列方法,直接调用 get()。
java
// ❌ 错误写法
Optional<User> opt = findUser(id);
User user = opt.get(); // 若为空直接异常!
// ✅ 正确写法
User user = opt.orElseThrow(() -> new BusinessException("User not found"));
// 或
User user = opt.orElse(defaultUser);
// 或
opt.ifPresent(u -> processUser(u));
规避技巧: 将 Optional.get() 视为废弃方法,团队规范中禁止直接使用 get(),统一使用 orElse/orElseThrow/ifPresent。
踩坑案例 3:Record 中引用可变对象破坏不可变性
错误现象: Record 的字段声明为 final,但通过 getter 获取的 List/Map 仍可被外部修改。
核心原因: final 只保证引用不可变,不保证引用对象的内容不可变。
java
// ❌ 错误写法
public record UserRoles(String name, List<String> roles) {}
UserRoles user = new UserRoles("Alice", new ArrayList<>(List.of("ADMIN")));
user.roles().add("SUPER_ADMIN"); // 编译通过!不可变性被破坏!
// ✅ 正确写法:在紧凑构造器中做防御性拷贝
public record UserRoles(String name, List<String> roles) {
public UserRoles {
roles = List.copyOf(roles); // 不可变副本
}
}
规避技巧: Record 中所有引用类型字段,若为集合/数组,必须在紧凑构造器中使用 List.copyOf()、Map.copyOf() 等创建不可变副本。
踩坑案例 4:Sealed Classes 的 permits 子类不在同一包/模块
错误现象: 编译报错 class is not allowed to extend sealed class。
核心原因: Java 要求 sealed 类的所有 permitted 子类必须与密封类在同一模块(module)中;若未使用模块系统,则必须在同一包(package)中。
修复方案:
java
// 确保所有子类与密封类在同一包内
// com.example.payment 包下:
package com.example.payment;
public sealed interface PaymentResult
permits PaymentSuccess, PaymentFailure {} // 两个子类也必须在 com.example.payment 包下
// ✅ 若需跨包继承,使用模块系统(module-info.java)
module com.example.payment {
exports com.example.payment;
// permitted subclasses 在同一模块中
}
规避技巧: 设计密封类时,提前规划好包结构,将所有相关子类放在同一包内。
踩坑案例 5:Text Blocks 缩进导致输出格式异常
错误现象: Text Block 输出的字符串包含意外的前导空格或换行。
核心原因: Text Block 的缩进由结束引号 """ 的位置决定,编译器会去除所有行与结束引号共同的缩进前缀。
java
// ❌ 意外缩进
String json = """
{
"name": "Alice"
}
""";
// 实际输出无缩进(结束引号在最左侧时,公共缩进被去除)
// ✅ 正确控制缩进:结束引号位置决定基准
String json = """
{
"name": "Alice"
}
"""; // 结束引号在4空格处,每行去除4个前导空格
规避技巧: 始终将结束引号 """ 放在期望的缩进基准位置;使用 \s 保留尾部空格(Text Block 默认去除尾部空白)。
四、分层面试题库
1. 基础入门题
问:什么是 Lambda 表达式?它解决了什么问题?
答:
Lambda 表达式是 Java 8 引入的一种匿名函数语法,允许将函数作为方法参数传递。其基本语法为 (参数列表) -> { 方法体 }。它主要解决了两个问题:第一,替代匿名内部类的冗长写法,将多行代码简化为一行;第二,为 Java 引入函数式编程能力,使行为参数化成为可能。Lambda 表达式的使用前提是目标类型必须是函数式接口(即有且仅有一个抽象方法的接口)。
问:什么是函数式接口?Java 8 有哪些内置函数式接口?
答:
函数式接口是指有且仅有一个抽象方法的接口,可以使用 @FunctionalInterface 注解标注(非强制,但推荐用于编译期校验)。Java 8 在 java.util.function 包中预置了四大核心函数式接口:① Function<T, R>:接收一个 T 类型参数,返回 R 类型结果,用于类型转换;② Predicate<T>:接收一个 T 类型参数,返回 boolean,用于条件判断;③ Consumer<T>:接收一个 T 类型参数,无返回值,用于消费数据;④ Supplier<T>:无参数,返回 T 类型结果,用于生产数据。此外还有 BiFunction、UnaryOperator、BinaryOperator 等扩展接口。
问:Stream API 是什么?它和 Collection 有什么区别?
答:
Stream API 是 Java 8 引入的用于对集合数据进行声明式、链式操作的抽象层。与 Collection 的核心区别有四点:① Stream 不存储数据,只是数据的计算管道;② Stream 不修改源数据,每次操作返回新的 Stream;③ Stream 操作是惰性求值的,中间操作不会立即执行,只有终端操作才触发实际计算;④ Stream 元素只能消费一次,使用后即关闭,而 Collection 可反复遍历。Stream 支持 filter、map、reduce、collect 等操作,并可通过 parallelStream() 实现自动并行处理。
问:Optional 类的作用是什么?如何正确使用?
答:
Optional 是 Java 8 引入的容器类,用于以类型安全的方式表达"值可能存在也可能不存在",核心目的是减少 NullPointerException。正确使用方式:① 方法返回值使用 Optional<T> 替代 null,明确告知调用方"结果可能为空";② 使用 orElse() 提供默认值,使用 orElseThrow() 在无值时抛异常;③ 使用 map()/flatMap() 进行链式处理,避免嵌套的 null 检查;④ 使用 ifPresent() 在值存在时执行操作。注意事项:Optional 不应用于字段、方法参数或集合元素,仅适用于方法返回值。
问:Java 8 的新日期时间 API 相比旧 API 有什么优势?
答:
Java 8 引入 java.time 包替代旧的 java.util.Date 和 java.util.Calendar,核心优势有五点:① 不可变性:LocalDate、LocalTime 等类均为不可变对象,天然线程安全;② 设计清晰:日期(LocalDate)、时间(LocalTime)、日期时间(LocalDateTime)、带时区时间(ZonedDateTime)职责分明;③ 精度更高:支持纳秒级精度;④ API 友好:月份从 1 开始(不再从 0),提供丰富的计算方法如 plusDays()、with(TemporalAdjusters);⑤ 格式化与解析:DateTimeFormatter 线程安全且支持自定义模式。
问:什么是接口默认方法?为什么需要它?
答:
接口默认方法是 Java 8 引入的特性,允许在接口中使用 default 关键字定义带实现体的方法。它的核心目的是解决接口演进问题:在 Java 8 之前,接口新增方法会破坏所有实现类(实现类必须实现新方法)。通过默认方法,接口可以在不破坏现有实现类的情况下新增方法。最典型的应用是 Java 8 向 Collection 接口添加 stream() 默认方法,使得所有已有集合实现类无需修改即可获得 Stream 能力。当类实现了多个接口且接口中有同名默认方法时,类必须重写该方法以消除歧义。
问:什么是 Record 类?它和普通类有什么区别?
答:
Record 是 Java 17(预览自 Java 14)引入的一种特殊类,用于创建不可变数据载体。与普通类的核心区别:① 语法简洁:record Point(int x, int y) {} 一行声明,编译器自动生成构造器、getter、equals、hashCode、toString;② 字段天然 final,保证不可变性;③ 隐式继承 java.lang.Record,不可再继承其他类;④ 不能声明实例字段(只有 record 组件对应的 final 字段);⑤ accessor 方法名与组件名相同(如 x() 而非 getX())。Record 适用于纯数据载体场景,如 DTO、VO、配置对象等,可大幅减少样板代码。
问:什么是 Sealed Classes(密封类)?它的作用是什么?
答:
Sealed Classes 是 Java 17 引入的特性,允许类或接口精确控制哪些类可以继承它。通过 sealed 关键字声明密封类,并使用 permits 子句显式列出允许的子类。其核心作用有三点:① 类型安全:编译器知道所有可能的子类,可在 switch 中进行穷举检查,遗漏分支会编译报错;② 领域建模:构建代数数据类型(ADT),明确表达"一个结果只能是这几种情况之一";③ 框架设计:库作者可以限定用户如何扩展接口,防止不当继承。所有 permitted 子类必须使用 final(不可再继承)、sealed(继续密封)或 non-sealed(开放继承)之一修饰。
问:什么是 Pattern Matching for instanceof?它简化了什么?
答:
Pattern Matching for instanceof 是 Java 17 引入的特性,将类型检查与变量绑定合并为一步操作。传统写法需要先 instanceof 判断类型,再手动强制类型转换:if (obj instanceof String) { String s = (String) obj; ... }。新模式匹配写法:if (obj instanceof String s) { ... 直接使用 s ... },编译器自动完成类型检查并将变量 s 绑定为 String 类型。它简化了三个方面:① 消除强制类型转换的样板代码;② 减少类型转换错误的可能性;③ 在逻辑表达式中支持模式变量的作用域扩展,如 if (obj instanceof String s && s.length() > 5)。
问:什么是 Text Blocks?它解决了什么问题?
答:
Text Blocks 是 Java 17 引入的多行字符串字面量特性,使用 """...""" 语法。它主要解决了三个问题:① 多行字符串拼接痛苦:传统写法需要大量 + 连接和 \n 转义,可读性极差;② 嵌入结构化文本困难:SQL、JSON、HTML、XML 等需要保留格式的文本处理繁琐;③ 转义字符过多:双引号需要 \" 转义,代码难以阅读。Text Blocks 自动处理缩进(由结束引号位置决定基准),保留内部格式,支持 .formatted() 方法进行参数化,极大提升了多行字符串的可读性和可维护性。
2. 原理深挖题
问:Lambda 表达式的底层实现原理是什么?它是如何工作的?
答:
Lambda 表达式的底层实现基于 invokedynamic 指令和 Lambda Metafactory 机制,而非传统的匿名内部类。具体流程:① 编译阶段:javac 将 Lambda 表达式转换为 invokedynamic 指令,并将 Lambda 体编译为宿主类的私有静态方法;② 链接阶段:JVM 首次执行 invokedynamic 时,调用 Bootstrap Method(LambdaMetafactory.metafactory),该方法根据上下文生成目标函数式接口的实现类;③ 实例生成:Bootstrap Method 通过 ASM 字节码框架动态生成实现类,或使用 MethodHandle 直接绑定到静态方法;④ 运行时调用:后续调用直接通过 MethodHandle 分派到静态方法,无额外对象创建开销。这种设计的优势:① 性能优于匿名内部类(无额外类加载);② 灵活性高(可在运行时选择实现策略);③ 内存占用更低。
问:Stream API 的惰性求值(Lazy Evaluation)是如何实现的?为什么需要它?
答:
Stream API 的惰性求值通过 Pipeline 模式实现。具体机制:① 中间操作(如 filter、map)不立即执行,而是将操作封装为 Sink 对象,构建操作链(Pipeline);② 终端操作(如 collect、forEach)触发时,才从数据源拉取元素,依次通过所有 Sink 处理;③ 每个元素"流经"整个管道,而非所有元素先经过一个操作再进入下一个操作。这种设计的核心优势:① 性能优化:短路操作(如 findFirst、limit)可在满足条件时提前终止,无需处理全部数据;② 内存效率:无需为中间结果创建临时集合;③ 并行化友好:惰性求值使框架可将数据分片并行处理。例如 stream().filter(...).map(...).findFirst() 只需处理到第一个匹配元素即可返回,而非处理全部数据。
问:并行流(parallelStream)的底层实现原理是什么?什么场景下使用并行流反而更慢?
答:
并行流的底层基于 Fork/Join 框架实现。具体流程:① 调用 parallelStream() 时,Stream 使用 Spliterator 将数据源递归拆分为多个子任务;② 子任务提交到全局共享的 ForkJoinPool.commonPool()(默认线程数为 CPU 核心数 - 1);③ 各线程独立处理子任务,通过 Combiner 合并结果;④ 使用工作窃取(Work-Stealing)算法平衡负载:空闲线程从繁忙线程的任务队列中"窃取"任务。并行流反而更慢的场景:① 数据量小(< 10000):线程创建与任务分片的开销超过并行收益;② 操作有状态或副作用:如修改外部变量,导致线程竞争;③ 数据源不适合拆分:如 LinkedList、Stream.iterate() 无法高效分片;④ 操作本身很快:如简单的字段访问,拆分开销占比过高;⑤ 共享 ForkJoinPool 繁忙:其他并行任务占用线程资源。建议:仅在数据量大、操作无状态、数据源为 ArrayList/HashMap 等可高效拆分的集合时使用并行流。
问:Optional 的底层实现原理是什么?为什么不能用于字段或参数?
答:
Optional 的底层是一个泛型容器类,内部持有一个 value 字段(可为 null)和一个静态常量 EMPTY(表示空值)。核心方法实现:① of(value):若 value 为 null 抛 NPE,否则创建 Optional 实例;② ofNullable(value):value 为 null 返回 EMPTY,否则创建实例;③ map(function):若有值则应用函数并包装结果,否则返回 EMPTY;④ flatMap(function):与 map 类似,但函数返回值本身是 Optional,自动展平避免嵌套。Optional 不能用于字段或参数的原因:① 内存开销:每个 Optional 实例额外占用 16 字节对象头 + 引用空间,作为字段会显著增加内存消耗;② 序列化问题:Optional 未实现 Serializable,作为字段会导致序列化失败;③ 语义不清:字段和参数的空值语义应通过业务逻辑或注解(如 @Nullable)表达,Optional 的设计初衷是方法返回值的空值约束;④ 控制流滥用:若用于参数,调用方需创建 Optional 包装,增加代码复杂度且无实际收益。
问:Record 类的底层实现原理是什么?它与普通类的字节码有何区别?
答:
Record 类的底层实现基于编译器自动生成字节码。具体机制:① 编译器为每个 record 组件生成 private final 字段;② 生成全参构造器,并在构造器中为字段赋值;③ 生成 accessor 方法(方法名与组件名相同,如 name() 而非 getName()),返回对应字段值;④ 生成 equals():依次比较所有组件的值(使用 Objects.equals);⑤ 生成 hashCode():基于所有组件计算哈希(使用 Objects.hash 或等效逻辑);⑥ 生成 toString():输出类名及所有组件的键值对;⑦ Record 类隐式继承 java.lang.Record,该类是 final 的,因此 Record 不可再继承其他类。与普通类的字节码区别:① Record 类的 ACC_RECORD 标志位被设置;② 字节码中包含 Record 属性,记录组件名称与类型;③ 无法声明实例字段(编译器报错);④ 无法继承其他类(除 Record 外)。Record 的不可变性由 final 字段保证,但引用类型字段的内容仍可修改,需在紧凑构造器中做防御性拷贝。
问:Sealed Classes 的穷举检查(Exhaustiveness Checking)是如何实现的?
答:
Sealed Classes 的穷举检查由编译器在编译期完成。具体机制:① 编译器解析 sealed 类的 permits 子句,收集所有允许的子类列表;② 在 switch 语句中,若 switch 表达式/语句的条件类型为 sealed 类,编译器检查所有 case 分支是否覆盖了所有 permitted 子类;③ 若使用 Pattern Matching(如 case PaymentSuccess s -> ...),编译器根据模式类型匹配子类;④ 若所有子类均被覆盖,编译器认为 switch 是穷举的,无需 default 分支;⑤ 若有遗漏,编译报错 switch does not cover all possible cases。运行时支持:JVM 在 sealed 类的字节码中添加 PermittedSubclasses 属性,反射 API 可通过 Class.getPermittedSubclasses() 获取子类列表。穷举检查的优势:① 类型安全:新增子类时编译器自动提示未处理的分支;② 代码简洁:无需 default 兜底;③ 领域建模:明确表达"所有可能的情况"。注意:穷举检查仅在 switch 中使用 Pattern Matching 时生效,传统 switch(基于值)仍需 default。
问:Pattern Matching for instanceof 的作用域规则是什么?为什么在 || 中受限?
答:
Pattern Matching for instanceof 的模式变量作用域遵循"确定性赋值"原则。具体规则:① 在 if (obj instanceof String s) 中,模式变量 s 在 if 块内可用,类型为 String;② 在 && 表达式中,如 if (obj instanceof String s && s.length() > 5),s 在 && 右侧可用,因为若左侧为 false,右侧不会执行,s 必然已绑定;③ 在 || 表达式中,如 if (obj instanceof String s || s.length() > 5),s 在 || 右侧不可用,因为若左侧为 false(即 obj 不是 String),s 未绑定,右侧使用 s 会导致编译错误;④ 在 ! 表达式中,如 if (!(obj instanceof String s)),s 在后续代码中不可用,因为条件为 true 时 s 未绑定。这种设计的核心原则:模式变量仅在其绑定"确定性成立"的作用域内可用,避免未绑定变量的使用。
问:Text Blocks 的缩进处理机制是什么?如何精确控制输出格式?
答:
Text Blocks 的缩进处理由编译器在编译期完成,核心规则:① 确定"公共缩进基准":编译器找到所有行(包括结束引号所在行)的最小前导空白数;② 去除公共缩进:每行去除等于公共缩进数的前导空白;③ 结束引号位置决定基准:若结束引号 """ 在第 N 列,则每行去除 N 个前导空白。精确控制输出格式的技巧:① 调整结束引号位置:结束引号越靠右,保留的缩进越多;② 使用 \ 行尾续行:消除换行符,如 "Hello \" 续行后无换行;③ 使用 \s 保留尾部空格:Text Blocks 默认去除每行尾部空白,\s 可强制保留一个空格;④ 使用 \n 强制换行:在单行中插入换行符。示例:若希望输出保留 4 空格缩进,将结束引号放在第 4 列;若希望无缩进,将结束引号放在第 0 列。
问:Switch Expressions 与传统 Switch 语句的区别是什么?箭头语法(->)的优势是什么?
答:
Switch Expressions 与传统 Switch 的核心区别:① 返回值:Switch Expressions 可作为表达式返回值(如 int x = switch(...) { ... }),传统 Switch 是语句,无返回值;② 箭头语法:使用 -> 替代 :,无需 break,自动防止 fall-through;③ 多标签匹配:支持 case MONDAY, FRIDAY -> ... 一次匹配多个值;④ yield 关键字:在代码块中使用 yield 返回值(替代 break 的语义)。箭头语法的优势:① 安全性:消除忘记写 break 导致的 fall-through bug;② 简洁性:单行表达式无需 {} 和 break;③ 可读性:-> 直观表达"匹配则执行"的语义;④ 穷举检查:配合 sealed 类,编译器可检查是否覆盖所有分支。注意:箭头语法中若需多行代码,使用 { ... yield value; } 代码块形式。
问:Java 17 相比 Java 8 在性能方面有哪些提升?GC 有何改进?
答:
Java 17 相比 Java 8 在性能方面的核心提升:① JIT 编译器优化:GraalVM 支持、分层编译改进、内联缓存优化,热点代码执行速度提升 10%-30%;② 内存管理:ZGC(JDK 15 生产可用)和 Shenandoah GC 支持 TB 级堆内存,停顿时间 < 10ms,适合低延迟场景;③ 字符串压缩:Compact Strings(JDK 9)将 Latin-1 字符串存储为 byte\[\] 而非 char\[\],内存占用减少 30%-40%;④ 集合优化:Map.of()、List.of() 等不可变集合工厂方法,内存布局更紧凑;⑤ 启动时间:AppCDS(Application Class-Data Sharing)加速类加载,启动时间减少 20%-40%。GC 改进:① G1 GC 默认启用(JDK 9),替代 Parallel GC,平衡吞吐与延迟;② ZGC 支持并发标记、并发重定位,停顿时间恒定 < 10ms,不随堆大小增长;③ Shenandoah GC 类似 ZGC,OpenJDK 贡献,适合对延迟敏感的场景;④ 废弃 CMS GC(JDK 14 移除),推荐迁移至 G1 或 ZGC。
3. 场景题
问:在一个电商订单系统中,订单状态有"待支付、已支付、已发货、已完成、已取消"五种。如何使用 Sealed Classes 和 Pattern Matching 实现类型安全的状态处理?相比传统 if-else 或枚举有什么优势?
答:
实现方案:
java
// 1. 定义密封接口表示订单状态
public sealed interface OrderStatus
permits PendingPayment, Paid, Shipped, Completed, Cancelled {}
// 2. 每种状态用 Record 承载状态相关数据
public record PendingPayment(LocalDateTime createTime, BigDecimal amount)
implements OrderStatus {}
public record Paid(LocalDateTime payTime, String transactionId)
implements OrderStatus {}
public record Shipped(String trackingNumber, LocalDateTime shipTime)
implements OrderStatus {}
public record Completed(LocalDateTime completeTime, String reviewStatus)
implements OrderStatus {}
public record Cancelled(LocalDateTime cancelTime, String reason)
implements OrderStatus {}
// 3. 使用 Pattern Matching 进行类型安全的状态处理
public String handleOrder(Order order) {
return switch (order.getStatus()) {
case PendingPayment p -> {
if (Duration.between(p.createTime(), LocalDateTime.now()).toHours() > 24) {
yield "订单超时未支付,自动取消";
}
yield "等待支付,金额:" + p.amount();
}
case Paid p -> "已支付,交易号:" + p.transactionId();
case Shipped s -> "已发货,快递单号:" + s.trackingNumber();
case Completed c -> "订单完成,评价状态:" + c.reviewStatus();
case Cancelled c -> "订单取消,原因:" + c.reason();
// 无需 default,编译器穷举检查
};
}
相比传统方案的优势:① 类型安全:新增状态(如"退款中")时,编译器自动提示所有未处理的 switch 分支,避免遗漏;② 数据承载:每种状态可携带不同数据(如"已发货"携带快递单号),无需在枚举中定义大量可选字段;③ 代码简洁:消除 instanceof + 强转的样板代码;④ 领域建模:明确表达"订单状态只能是这五种之一",业务语义清晰。传统枚举的局限:① 所有枚举实例共享相同字段,导致大量字段可选(如"待支付"不需要快递单号);② 无法为每种状态定义不同的行为逻辑;③ 新增状态时需修改所有 switch/if-else,易遗漏。
问:在一个数据分析系统中,需要对百万级用户数据进行过滤、转换、分组统计。如何设计 Stream API 的处理管道以最大化性能?并行流在什么情况下会失效?
答:
性能优化方案:
java
// 1. 数据源优化:使用 ArrayList 或数组(支持高效 Spliterator 拆分)
List<User> users = ...; // ArrayList,非 LinkedList
// 2. 减少中间操作数量:合并 filter 条件
// ❌ 低效:多次 filter
users.parallelStream()
.filter(u -> u.getAge() > 18)
.filter(u -> u.getStatus().equals("ACTIVE"))
.filter(u -> u.getCity() != null);
// ✅ 高效:合并为单次 filter
users.parallelStream()
.filter(u -> u.getAge() > 18
&& "ACTIVE".equals(u.getStatus())
&& u.getCity() != null);
// 3. 避免装箱拆箱:使用原始类型 Stream
// ❌ 低效:Stream<Integer> 装箱
int sum = users.stream()
.map(User::getAge)
.reduce(0, Integer::sum);
// ✅ 高效:IntStream 无装箱
int sum = users.stream()
.mapToInt(User::getAge)
.sum();
// 4. 分组统计优化:使用 groupingBy + downstream collector
Map<String, Long> countByCity = users.parallelStream()
.collect(Collectors.groupingByConcurrent(
User::getCity,
Collectors.counting()
)); // groupingByConcurrent 支持并行分组
// 5. 短路操作提前终止
Optional<User> firstMatch = users.parallelStream()
.filter(u -> u.getScore() > 90)
.findFirst(); // 找到第一个即终止,无需处理全部数据
并行流失效的场景:① 数据源不可拆分:LinkedList、Stream.iterate()、I/O Channel 等无法高效分片,并行流退化为串行;② 操作有状态:如 sorted()、distinct() 需要全局视图,并行流需额外同步开销;③ 数据量小(< 10000):线程创建与任务分片开销超过并行收益;④ 共享 ForkJoinPool 繁忙:其他并行任务占用线程,导致当前任务等待;⑤ 操作本身很快:如简单字段访问,拆分开销占比过高。建议:使用 Spliterator.characteristics() 检查数据源特性(如 SIZED、SUBSIZED、ORDERED),仅在支持高效拆分且操作无状态时使用并行流。
问:在一个微服务系统中,需要调用三个外部服务(用户服务、订单服务、支付服务)并聚合结果。如何使用 CompletableFuture 实现异步编排?如何处理超时和异常?
答:
实现方案:
java
// 1. 定义异步任务
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(
() -> userService.getUser(userId), executor);
CompletableFuture<List<Order>> orderFuture = CompletableFuture.supplyAsync(
() -> orderService.getOrders(userId), executor);
CompletableFuture<Payment> paymentFuture = CompletableFuture.supplyAsync(
() -> paymentService.getPayment(orderId), executor);
// 2. 聚合结果(三个任务全部完成后)
CompletableFuture<UserProfile> profileFuture = userFuture
.thenCombine(orderFuture, (user, orders) -> new UserProfile(user, orders))
.thenCombine(paymentFuture, (profile, payment) -> {
profile.setPayment(payment);
return profile;
});
// 3. 设置超时(Java 9+ 支持 orTimeout,Java 17 推荐使用 completeOnTimeout)
CompletableFuture<UserProfile> result = profileFuture
.orTimeout(5, TimeUnit.SECONDS) // 超时抛 TimeoutException
.completeOnTimeout(UserProfile.defaultProfile(), 5, TimeUnit.SECONDS); // 超时返回默认值
// 4. 异常处理
result.exceptionally(ex -> {
if (ex.getCause() instanceof TimeoutException) {
log.warn("服务调用超时");
return UserProfile.fallbackProfile();
}
if (ex.getCause() instanceof ServiceException) {
log.error("服务异常", ex);
return UserProfile.errorProfile();
}
throw new CompletionException(ex); // 未预期的异常继续抛出
});
// 5. 获取结果(非阻塞)
result.thenAccept(profile -> {
// 处理聚合后的结果
response.send(profile);
});
关键要点:① 使用自定义线程池(executor)而非 ForkJoinPool.commonPool(),避免与其他并行任务竞争;② thenCombine 用于聚合两个 Future 的结果,allOf 用于等待多个 Future;③ orTimeout 在超时时抛异常,completeOnTimeout 在超时时返回默认值;④ exceptionally 捕获异常并返回降级值,handle 可同时处理正常结果和异常;⑤ 避免使用 get() 阻塞等待,使用 thenAccept 回调处理结果。常见问题:① 未设置超时导致线程长时间阻塞;② 异常未处理导致 Future 永远不完成;③ 使用默认线程池导致资源竞争。
问:在一个金融系统中,金额计算需要高精度。如何使用 Java 8 的新日期时间 API 和 BigDecimal 处理跨时区的交易时间戳和金额?如何避免精度丢失?
答:
实现方案:
java
// 1. 时间戳处理:统一使用 UTC 存储,展示时转换为用户时区
// 存储:使用 Instant(UTC 时间戳)
Instant transactionTime = Instant.now();
// 持久化到数据库:transactionTime.toEpochMilli() 或 transactionTime.toString()
// 展示:转换为用户时区
ZoneId userZone = ZoneId.of("Asia/Shanghai");
ZonedDateTime displayTime = transactionTime.atZone(userZone);
String formatted = displayTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// 2. 金额计算:使用 BigDecimal,避免 double/float
// ❌ 错误:使用 double
double amount = 100.00;
double fee = amount * 0.03; // 2.9999999999999996,精度丢失!
// ✅ 正确:使用 BigDecimal
BigDecimal amount = new BigDecimal("100.00"); // 使用 String 构造,避免 double 转换
BigDecimal feeRate = new BigDecimal("0.03");
BigDecimal fee = amount.multiply(feeRate).setScale(2, RoundingMode.HALF_UP); // 3.00
// 3. 跨时区交易:记录交易时区,统一转换为 UTC 对账
public record Transaction(
String id,
BigDecimal amount,
String currency,
Instant utcTime, // UTC 时间戳
ZoneId originalZone // 交易发生时区
) {}
// 4. 汇率计算:保留足够精度,最终结果四舍五入
BigDecimal exchangeRate = new BigDecimal("7.23456"); // 保留 5 位小数
BigDecimal usdAmount = new BigDecimal("100.00");
BigDecimal cnyAmount = usdAmount.multiply(exchangeRate)
.setScale(2, RoundingMode.HALF_UP); // 最终结果保留 2 位小数
// 5. 避免精度丢失的关键规则
// 规则 1:BigDecimal 使用 String 构造,不使用 double
// ❌ new BigDecimal(0.1) // 0.1000000000000000055511151231257827021181583404541015625
// ✅ new BigDecimal("0.1") // 0.1
// 规则 2:中间计算保留足够精度,最终结果再四舍五入
// 规则 3:使用 MathContext 控制精度
MathContext mc = new MathContext(10, RoundingMode.HALF_UP); // 10 位有效数字
BigDecimal result = amount.multiply(rate, mc);
核心要点:① 时间戳统一使用 Instant(UTC)存储,避免时区混乱;② 金额使用 BigDecimal,构造时使用 String 参数;③ 中间计算保留足够精度(如 10 位有效数字),最终结果四舍五入到业务精度(如 2 位小数);④ 使用 setScale 明确指定舍入模式(如 HALF_UP 四舍五入);⑤ 避免使用 double/float 进行金额计算。常见问题:① 使用 double 构造 BigDecimal 导致精度丢失;② 中间计算过早四舍五入导致累积误差;③ 未指定舍入模式导致 ArithmeticException。
问:在一个内容管理系统中,需要处理大量富文本内容(HTML、Markdown)。如何使用 Text Blocks 和 Record 简化模板引擎的配置和内容模型?如何避免 Text Blocks 的缩进陷阱?
答:
实现方案:
java
// 1. 使用 Record 定义内容模型
public record Article(
String id,
String title,
String author,
LocalDateTime publishTime,
String content // HTML 或 Markdown 内容
) {}
public record EmailTemplate(
String subject,
String htmlBody
) {}
// 2. 使用 Text Blocks 定义 HTML 模板
public class EmailService {
// 模板定义为常量,避免重复创建
private static final String EMAIL_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; }
.header { background: #4CAF50; color: white; padding: 20px; }
.content { padding: 20px; }
</style>
</head>
<body>
<div class="header">
<h1>%s</h1>
</div>
<div class="content">
<p>尊敬的 %s:</p>
<p>%s</p>
<p>此致,<br>团队</p>
</div>
</body>
</html>
""";
public EmailTemplate buildEmail(Article article) {
String html = EMAIL_TEMPLATE.formatted(
article.title(),
article.author(),
article.content()
);
return new EmailTemplate("新文章发布:" + article.title(), html);
}
}
// 3. 避免缩进陷阱的技巧
// 技巧 1:结束引号位置决定缩进基准
// ✅ 正确:结束引号在第 0 列,输出无缩进
String json = """
{
"name": "Alice",
"age": 30
}
""";
// ✅ 正确:结束引号在第 4 列,输出保留 4 空格缩进
String json = """
{
"name": "Alice",
"age": 30
}
""";
// 技巧 2:使用 \ 消除换行符
String sql = """
SELECT * \
FROM users \
WHERE status = 'ACTIVE'
"""; // 输出:SELECT * FROM users WHERE status = 'ACTIVE'(无换行)
// 技巧 3:使用 \s 保留尾部空格
String text = """
Line 1\s
Line 2\s
"""; // 每行末尾保留一个空格
// 技巧 4:使用 stripIndent() 手动控制缩进
String text = """
Line 1
Line 2
Line 3
""".stripIndent(); // 去除公共缩进
// 4. Markdown 内容处理
public record MarkdownContent(String rawMarkdown) {
// 紧凑构造器:校验与预处理
public MarkdownContent {
if (rawMarkdown == null || rawMarkdown.isBlank()) {
throw new IllegalArgumentException("Markdown content cannot be blank");
}
rawMarkdown = rawMarkdown.strip(); // 去除首尾空白
}
// 转换为 HTML(使用第三方库如 flexmark-java)
public String toHtml() {
// 简化示例,实际使用 Markdown 解析库
return rawMarkdown
.replaceAll("^# (.+)$", "<h1>$1</h1>")
.replaceAll("^## (.+)$", "<h2>$1</h2>")
.replaceAll("\\*\\*(.+?)\\*\\*", "<strong>$1</strong>");
}
}
核心要点:① 模板定义为 static final 常量,避免每次调用创建新字符串;② 使用 .formatted() 替代 String.format(),链式调用更简洁;③ 结束引号位置决定缩进基准,需根据输出需求调整;④ 使用 \ 消除换行符,\s 保留尾部空格;⑤ Record 的紧凑构造器可用于内容校验与预处理。常见问题:① 结束引号位置不当导致输出缩进异常;② 未使用常量导致模板重复创建;③ 未处理 Text Blocks 中的特殊字符(如 % 需转义为 %%)。
问:在一个配置中心系统中,配置项可能是字符串、整数、布尔值或嵌套对象。如何使用 Sealed Classes 和 Pattern Matching 实现类型安全的配置解析?如何处理配置缺失或类型错误?
答:
实现方案:
java
// 1. 定义密封接口表示配置值类型
public sealed interface ConfigValue
permits StringValue, IntValue, BooleanValue, ObjectValue, NullValue {}
// 2. 每种类型用 Record 承载
public record StringValue(String value) implements ConfigValue {}
public record IntValue(int value) implements ConfigValue {}
public record BooleanValue(boolean value) implements ConfigValue {}
public record ObjectValue(Map`<String, ConfigValue>` fields) implements ConfigValue {}
public record NullValue() implements ConfigValue {}
// 3. 配置解析器
public class ConfigParser {
public ConfigValue parse(JsonNode node) {
if (node == null || node.isNull()) {
return new NullValue();
}
if (node.isTextual()) {
return new StringValue(node.asText());
}
if (node.isInt() || node.isLong()) {
return new IntValue(node.asInt());
}
if (node.isBoolean()) {
return new BooleanValue(node.asBoolean());
}
if (node.isObject()) {
Map`<String, ConfigValue>` fields = new HashMap<>();
node.fields().forEachRemaining(entry ->
fields.put(entry.getKey(), parse(entry.getValue()))
);
return new ObjectValue(fields);
}
throw new ConfigException("Unsupported config type: " + node.getNodeType());
}
}
// 4. 类型安全的配置访问
public class ConfigAccessor {
private final ConfigValue root;
public ConfigAccessor(ConfigValue root) {
this.root = root;
}
// 获取字符串配置,类型不匹配或缺失时返回默认值
public String getString(String path, String defaultValue) {
return switch (getByPath(path)) {
case StringValue s -> s.value();
case NullValue n -> defaultValue;
case IntValue i -> String.valueOf(i.value()); // 自动转换
case BooleanValue b -> String.valueOf(b.value());
case ObjectValue o -> defaultValue; // 类型不匹配,返回默认值
};
}
// 获取整数配置
public int getInt(String path, int defaultValue) {
return switch (getByPath(path)) {
case IntValue i -> i.value();
case NullValue n -> defaultValue;
case StringValue s -> {
try {
yield Integer.parseInt(s.value());
} catch (NumberFormatException e) {
yield defaultValue;
}
}
case BooleanValue b -> b.value() ? 1 : 0;
case ObjectValue o -> defaultValue;
};
}
// 路径访问:支持 "database.host" 形式
private ConfigValue getByPath(String path) {
String[] keys = path.split("\\.");
ConfigValue current = root;
for (String key : keys) {
if (current instanceof ObjectValue obj) {
current = obj.fields().getOrDefault(key, new NullValue());
} else {
return new NullValue(); // 路径不存在
}
}
return current;
}
}
// 5. 使用示例
ConfigParser parser = new ConfigParser();
ConfigValue config = parser.parse(jsonNode);
ConfigAccessor accessor = new ConfigAccessor(config);
String dbHost = accessor.getString("database.host", "localhost");
int dbPort = accessor.getInt("database.port", 3306);
boolean debug = accessor.getBoolean("app.debug", false);
核心要点:① 使用 Sealed Classes 明确表达"配置值只能是这几种类型之一";② 每种类型用 Record 承载,类型安全且不可变;③ Pattern Matching 的穷举检查确保所有类型都被处理;④ 类型不匹配时返回默认值而非抛异常,提升容错性;⑤ 支持自动类型转换(如整数转字符串)。优势:① 新增配置类型(如"列表值")时,编译器提示所有未处理的分支;② 消除 instanceof + 强转的样板代码;③ 配置模型清晰,易于测试和维护。
问:在一个日志分析系统中,需要解析多种格式的日志(JSON、纯文本、XML)。如何使用 Sealed Classes 实现多态的日志解析器?如何设计扩展点以支持未来新增日志格式?
答:
实现方案:
java
// 1. 定义密封接口表示日志格式
public sealed interface LogFormat
permits JsonLogFormat, TextLogFormat, XmlLogFormat {}
// 2. 每种格式定义为 Record,携带解析配置
public record JsonLogFormat(String timestampField, String levelField)
implements LogFormat {}
public record TextLogFormat(String pattern)
implements LogFormat {} // 如 "yyyy-MM-dd HH:mm:ss [LEVEL] message"
public record XmlLogFormat(String rootElement, String entryElement)
implements LogFormat {}
// 3. 定义日志条目(统一模型)
public record LogEntry(
Instant timestamp,
LogLevel level,
String message,
Map`<String, String>` metadata
) {}
public enum LogLevel { DEBUG, INFO, WARN, ERROR }
// 4. 多态日志解析器(使用 Pattern Matching 分派)
public class LogParser {
public List`<LogEntry>` parse(String rawLog, LogFormat format) {
return switch (format) {
case JsonLogFormat json -> parseJson(rawLog, json);
case TextLogFormat text -> parseText(rawLog, text);
case XmlLogFormat xml -> parseXml(rawLog, xml);
// 穷举检查,无需 default
};
}
private List`<LogEntry>` parseJson(String rawLog, JsonLogFormat format) {
// JSON 解析逻辑
JsonNode node = objectMapper.readTree(rawLog);
Instant timestamp = Instant.parse(node.get(format.timestampField()).asText());
LogLevel level = LogLevel.valueOf(node.get(format.levelField()).asText().toUpperCase());
String message = node.get("message").asText();
return List.of(new LogEntry(timestamp, level, message, Map.of()));
}
private List`<LogEntry>` parseText(String rawLog, TextLogFormat format) {
// 正则解析逻辑
Pattern pattern = Pattern.compile(format.pattern());
Matcher matcher = pattern.matcher(rawLog);
List`<LogEntry>` entries = new ArrayList<>();
while (matcher.find()) {
// 提取字段并构建 LogEntry
entries.add(new LogEntry(/* ... */));
}
return entries;
}
private List`<LogEntry>` parseXml(String rawLog, XmlLogFormat format) {
// XML 解析逻辑
Document doc = DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.parse(new InputSource(new StringReader(rawLog)));
// 遍历节点构建 LogEntry
return List.of(/* ... */);
}
}
// 5. 扩展点设计:支持未来新增日志格式
// 方案 1:新增格式时,修改密封接口(需重新编译)
public sealed interface LogFormat
permits JsonLogFormat, TextLogFormat, XmlLogFormat, CsvLogFormat {} // 新增 CsvLogFormat
public record CsvLogFormat(String delimiter, String[] columns)
implements LogFormat {}
// 方案 2:使用 non-sealed 开放扩展(牺牲穷举检查)
public sealed interface LogFormat
permits JsonLogFormat, TextLogFormat, XmlLogFormat, CustomLogFormat {}
public non-sealed interface CustomLogFormat extends LogFormat {}
// 用户可实现 CustomLogFormat,无需修改密封接口
// 6. 使用示例
LogParser parser = new LogParser();
JsonLogFormat jsonFormat = new JsonLogFormat("@timestamp", "level");
List`<LogEntry>` entries = parser.parse(rawLog, jsonFormat);
设计要点:① 使用 Sealed Classes 明确表达"日志格式只能是这几种之一";② 每种格式用 Record 携带解析配置,实现参数化解析;③ Pattern Matching 的穷举检查确保所有格式都被处理;④ 扩展策略:若需严格控制格式类型,使用 sealed;若需开放扩展,使用 non-sealed 接口作为扩展点;⑤ 统一日志模型(LogEntry)屏蔽格式差异。优势:① 新增格式时编译器提示未处理的分支;② 解析逻辑与格式定义解耦;③ 配置化解析(如 JSON 字段名可配置)。
问:在一个任务调度系统中,任务可能是"一次性任务、周期性任务、Cron 表达式任务"。如何使用 Sealed Classes 和 Record 实现类型安全的任务模型?如何计算下次执行时间?
答:
实现方案:
java
// 1. 定义密封接口表示任务类型
public sealed interface ScheduledTask
permits OneTimeTask, PeriodicTask, CronTask {}
// 2. 每种任务类型用 Record 定义
public record OneTimeTask(
String id,
String name,
Instant executeTime,
Runnable action
) implements ScheduledTask {}
public record PeriodicTask(
String id,
String name,
Instant startTime,
Duration interval,
Runnable action
) implements ScheduledTask {}
public record CronTask(
String id,
String name,
String cronExpression,
ZoneId timeZone,
Runnable action
) implements ScheduledTask {}
// 3. 任务调度器(使用 Pattern Matching 计算下次执行时间)
public class TaskScheduler {
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(4);
public void schedule(ScheduledTask task) {
Instant nextExecution = calculateNextExecution(task);
long delayMillis = Duration.between(Instant.now(), nextExecution).toMillis();
executor.schedule(() -> {
task.action().run();
// 周期性任务需重新调度
if (task instanceof PeriodicTask periodic) {
reschedulePeriodic(periodic);
} else if (task instanceof CronTask cron) {
rescheduleCron(cron);
}
}, delayMillis, TimeUnit.MILLISECONDS);
}
// 计算下次执行时间(Pattern Matching 分派)
public Instant calculateNextExecution(ScheduledTask task) {
return switch (task) {
case OneTimeTask oneTime -> oneTime.executeTime();
case PeriodicTask periodic -> calculateNextPeriodic(periodic);
case CronTask cron -> calculateNextCron(cron);
};
}
private Instant calculateNextPeriodic(PeriodicTask task) {
Instant now = Instant.now();
if (now.isBefore(task.startTime())) {
return task.startTime();
}
// 计算从 startTime 开始,经过 N 个 interval 后的时间
long elapsed = Duration.between(task.startTime(), now).toMillis();
long intervals = elapsed / task.interval().toMillis();
return task.startTime().plus(task.interval().multipliedBy(intervals + 1));
}
private Instant calculateNextCron(CronTask task) {
// 使用 Cron 解析库(如 cron-utils)
Cron cron = CronDefinitionBuilder.instanceDefinitionFor(CronMapper.UTC)
.parse(task.cronExpression());
ExecutionTime executionTime = ExecutionTime.forCron(cron);
ZonedDateTime now = ZonedDateTime.now(task.timeZone());
return executionTime.nextExecution(now)
.orElseThrow(() -> new IllegalStateException("No next execution time"))
.toInstant();
}
private void reschedulePeriodic(PeriodicTask task) {
Instant next = calculateNextPeriodic(task);
long delay = Duration.between(Instant.now(), next).toMillis();
executor.schedule(() -> {
task.action().run();
reschedulePeriodic(task); // 递归调度
}, delay, TimeUnit.MILLISECONDS);
}
private void rescheduleCron(CronTask task) {
Instant next = calculateNextCron(task);
long delay = Duration.between(Instant.now(), next).toMillis();
executor.schedule(() -> {
task.action().run();
rescheduleCron(task);
}, delay, TimeUnit.MILLISECONDS);
}
}
// 4. 使用示例
TaskScheduler scheduler = new TaskScheduler();
// 一次性任务:10秒后执行
OneTimeTask oneTime = new OneTimeTask(
"task-1",
"Cleanup",
Instant.now().plusSeconds(10),
() -> System.out.println("Cleanup executed")
);
scheduler.schedule(oneTime);
// 周期性任务:每5分钟执行
PeriodicTask periodic = new PeriodicTask(
"task-2",
"HealthCheck",
Instant.now(),
Duration.ofMinutes(5),
() -> System.out.println("Health check executed")
);
scheduler.schedule(periodic);
// Cron 任务:每天凌晨2点执行
CronTask cron = new CronTask(
"task-3",
"DailyBackup",
"0 2 * * *", // 每天2:00
ZoneId.of("Asia/Shanghai"),
() -> System.out.println("Backup executed")
);
scheduler.schedule(cron);
设计要点:① 使用 Sealed Classes 明确表达"任务只能是这三种类型之一";② 每种任务类型用 Record 携带特定配置(如 Cron 表达式、时区);③ Pattern Matching 的穷举检查确保所有任务类型都被处理;④ 计算逻辑与任务模型分离,易于测试;⑤ 递归调度实现周期性执行。优势:① 新增任务类型(如"依赖任务")时编译器提示未处理的分支;② 每种任务类型的配置参数类型安全(如 Cron 任务必须有时区);③ 消除 instanceof + 强转的样板代码。
问:在一个消息队列系统中,消息可能是"文本消息、二进制消息、命令消息"。如何使用 Sealed Classes 实现类型安全的消息处理管道?如何处理消息的反序列化和路由?
答:
实现方案:
java
// 1. 定义密封接口表示消息类型
public sealed interface Message
permits TextMessage, BinaryMessage, CommandMessage {}
// 2. 每种消息类型用 Record 定义
public record TextMessage(
String id,
String topic,
String payload,
Map`<String, String>` headers
) implements Message {}
public record BinaryMessage(
String id,
String topic,
byte[] payload,
String contentType,
Map`<String, String>` headers
) implements Message {}
public record CommandMessage(
String id,
String topic,
String command,
Map`<String, Object>` parameters,
Map`<String, String>` headers
) implements Message {}
// 3. 消息处理器接口(泛型,类型安全)
public interface MessageHandler`<T extends Message>` {
void handle(T message);
boolean supports(Message message);
}
// 4. 具体消息处理器
public class TextMessageHandler implements MessageHandler`<TextMessage>` {
@Override
public void handle(TextMessage message) {
System.out.println("Processing text: " + message.payload());
// 文本处理逻辑
}
@Override
public boolean supports(Message message) {
return message instanceof TextMessage;
}
}
public class BinaryMessageHandler implements MessageHandler`<BinaryMessage>` {
@Override
public void handle(BinaryMessage message) {
System.out.println("Processing binary, size: " + message.payload().length);
// 二进制处理逻辑
}
@Override
public boolean supports(Message message) {
return message instanceof BinaryMessage;
}
}
public class CommandMessageHandler implements MessageHandler`<CommandMessage>` {
@Override
public void handle(CommandMessage message) {
System.out.println("Executing command: " + message.command());
// 命令执行逻辑
}
@Override
public boolean supports(Message message) {
return message instanceof CommandMessage;
}
}
// 5. 消息路由器(使用 Pattern Matching 分派)
public class MessageRouter {
private final List<MessageHandler<?>> handlers;
public MessageRouter() {
this.handlers = List.of(
new TextMessageHandler(),
new BinaryMessageHandler(),
new CommandMessageHandler()
);
}
public void route(Message message) {
// 方案 1:使用 Pattern Matching 直接分派(推荐)
switch (message) {
case TextMessage text -> handleText(text);
case BinaryMessage binary -> handleBinary(binary);
case CommandMessage command -> handleCommand(command);
}
// 方案 2:遍历处理器列表(扩展性更好)
// handlers.stream()
// .filter(h -> h.supports(message))
// .findFirst()
// .ifPresent(h -> handleWith(h, message));
}
private void handleText(TextMessage message) {
new TextMessageHandler().handle(message);
}
private void handleBinary(BinaryMessage message) {
new BinaryMessageHandler().handle(message);
}
private void handleCommand(CommandMessage message) {
new CommandMessageHandler().handle(message);
}
@SuppressWarnings("unchecked")
private `<T extends Message>` void handleWith(MessageHandler`<T>` handler, Message message) {
handler.handle((T) message);
}
}
// 6. 消息反序列化器(根据类型标识反序列化为对应 Record)
public class MessageDeserializer {
public Message deserialize(byte[] data, String typeHeader) {
return switch (typeHeader) {
case "TEXT" -> deserializeText(data);
case "BINARY" -> deserializeBinary(data);
case "COMMAND" -> deserializeCommand(data);
default -> throw new IllegalArgumentException("Unknown message type: " + typeHeader);
};
}
private TextMessage deserializeText(byte[] data) {
JsonNode node = objectMapper.readTree(data);
return new TextMessage(
node.get("id").asText(),
node.get("topic").asText(),
node.get("payload").asText(),
extractHeaders(node)
);
}
private BinaryMessage deserializeBinary(byte[] data) {
// 二进制反序列化逻辑
return new BinaryMessage(/* ... */);
}
private CommandMessage deserializeCommand(byte[] data) {
// 命令反序列化逻辑
return new CommandMessage(/* ... */);
}
private Map`<String, String>` extractHeaders(JsonNode node) {
Map`<String, String>` headers = new HashMap<>();
node.get("headers").fields().forEachRemaining(
entry -> headers.put(entry.getKey(), entry.getValue().asText())
);
return headers;
}
}
// 7. 消息消费者(整合反序列化与路由)
public class MessageConsumer {
private final MessageDeserializer deserializer;
private final MessageRouter router;
public void consume(byte[] rawData, Map`<String, String>` metadata) {
String type = metadata.get("X-Message-Type");
Message message = deserializer.deserialize(rawData, type);
router.route(message);
}
}
设计要点:① 使用 Sealed Classes 明确表达"消息只能是这三种类型之一";② 每种消息类型用 Record 承载特定数据(如二进制消息有 contentType);③ Pattern Matching 的穷举检查确保所有消息类型都被处理;④ 反序列化器根据类型标识分派,返回类型安全的 Record;⑤ 路由器使用 Pattern Matching 直接分派,或遍历处理器列表(扩展性更好)。优势:① 新增消息类型时编译器提示未处理的分支;② 消息模型类型安全,避免运行时类型错误;③ 反序列化与路由逻辑分离,易于测试和维护。
问:在一个工作流引擎中,流程节点可能是"开始节点、任务节点、判断节点、结束节点"。如何使用 Sealed Classes 和 Record 实现类型安全的流程定义与执行?如何验证流程的合法性?
答:
实现方案:
java
// 1. 定义密封接口表示流程节点类型
public sealed interface FlowNode
permits StartNode, TaskNode, DecisionNode, EndNode {}
// 2. 每种节点类型用 Record 定义
public record StartNode(
String id,
String name,
String nextNodeId
) implements FlowNode {}
public record TaskNode(
String id,
String name,
String assignee,
String nextNodeId
) implements FlowNode {}
public record DecisionNode(
String id,
String name,
String condition, // 条件表达式,如 "${amount > 1000}"
String trueNextNodeId,
String falseNextNodeId
) implements FlowNode {}
public record EndNode(
String id,
String name
) implements FlowNode {}
// 3. 流程定义(包含所有节点)
public record ProcessDefinition(
String id,
String name,
List`<FlowNode>` nodes
) {
// 紧凑构造器:流程合法性校验
public ProcessDefinition {
// 校验 1:必须有且仅有一个开始节点
long startCount = nodes.stream()
.filter(n -> n instanceof StartNode)
.count();
if (startCount != 1) {
throw new IllegalArgumentException("Process must have exactly one start node");
}
// 校验 2:必须至少有一个结束节点
long endCount = nodes.stream()
.filter(n -> n instanceof EndNode)
.count();
if (endCount == 0) {
throw new IllegalArgumentException("Process must have at least one end node");
}
// 校验 3:所有 nextNodeId 必须指向存在的节点
Set`<String>` nodeIds = nodes.stream()
.map(FlowNode::id)
.collect(Collectors.toSet());
for (FlowNode node : nodes) {
switch (node) {
case StartNode start -> validateNextNode(start.nextNodeId(), nodeIds);
case TaskNode task -> validateNextNode(task.nextNodeId(), nodeIds);
case DecisionNode decision -> {
validateNextNode(decision.trueNextNodeId(), nodeIds);
validateNextNode(decision.falseNextNodeId(), nodeIds);
}
case EndNode end -> {} // 结束节点无后续节点
}
}
nodes = List.copyOf(nodes); // 不可变副本
}
private void validateNextNode(String nextNodeId, Set`<String>` nodeIds) {
if (!nodeIds.contains(nextNodeId)) {
throw new IllegalArgumentException("Next node not found: " + nextNodeId);
}
}
// 辅助方法:获取节点 ID(Pattern Matching)
public static String id(FlowNode node) {
return switch (node) {
case StartNode start -> start.id();
case TaskNode task -> task.id();
case DecisionNode decision -> decision.id();
case EndNode end -> end.id();
};
}
}
// 4. 流程执行引擎
public class ProcessEngine {
private final Map`<String, ProcessDefinition>` definitions;
public ProcessEngine(Map`<String, ProcessDefinition>` definitions) {
this.definitions = definitions;
}
public void execute(String processId, Map`<String, Object>` variables) {
ProcessDefinition definition = definitions.get(processId);
if (definition == null) {
throw new IllegalArgumentException("Process not found: " + processId);
}
// 找到开始节点
FlowNode currentNode = definition.nodes().stream()
.filter(n -> n instanceof StartNode)
.findFirst()
.orElseThrow();
// 执行流程
while (!(currentNode instanceof EndNode)) {
currentNode = executeNode(currentNode, variables, definition);
}
System.out.println("Process completed: " + processId);
}
private FlowNode executeNode(FlowNode node, Map`<String, Object>` variables, ProcessDefinition definition) {
return switch (node) {
case StartNode start -> {
System.out.println("Start: " + start.name());
yield findNode(definition, start.nextNodeId());
}
case TaskNode task -> {
System.out.println("Task: " + task.name() + ", assignee: " + task.assignee());
// 模拟任务执行
variables.put("taskCompleted", true);
yield findNode(definition, task.nextNodeId());
}
case DecisionNode decision -> {
System.out.println("Decision: " + decision.name());
boolean conditionResult = evaluateCondition(decision.condition(), variables);
String nextId = conditionResult ? decision.trueNextNodeId() : decision.falseNextNodeId();
yield findNode(definition, nextId);
}
case EndNode end -> {
System.out.println("End: " + end.name());
yield end; // 返回自身,循环终止
}
};
}
private FlowNode findNode(ProcessDefinition definition, String nodeId) {
return definition.nodes().stream()
.filter(n -> ProcessDefinition.id(n).equals(nodeId))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Node not found: " + nodeId));
}
private boolean evaluateCondition(String condition, Map`<String, Object>` variables) {
// 简化的条件评估(实际使用表达式引擎如 SpEL、MVEL)
if (condition.equals("${amount > 1000}")) {
Object amount = variables.get("amount");
return amount instanceof Number && ((Number) amount).doubleValue() > 1000;
}
return false;
}
}
// 5. 使用示例
// 定义流程:开始 -> 提交申请 -> 判断金额 -> [>1000] 经理审批 -> 结束
// -> [<=1000] 直接通过 -> 结束
ProcessDefinition process = new ProcessDefinition(
"expense-approval",
"费用报销审批",
List.of(
new StartNode("start", "开始", "submit"),
new TaskNode("submit", "提交申请", "employee", "decision"),
new DecisionNode("decision", "判断金额", "${amount > 1000}", "manager-approve", "auto-approve"),
new TaskNode("manager-approve", "经理审批", "manager", "end"),
new TaskNode("auto-approve", "直接通过", "system", "end"),
new EndNode("end", "结束")
)
);
ProcessEngine engine = new ProcessEngine(Map.of("expense-approval", process));
// 执行流程:金额 1500,走经理审批
engine.execute("expense-approval", Map.of("amount", 1500));
// 执行流程:金额 500,直接通过
engine.execute("expense-approval", Map.of("amount", 500));
设计要点:① 使用 Sealed Classes 明确表达"节点只能是这四种类型之一";② 每种节点类型用 Record 携带特定配置(如判断节点有条件表达式、两个分支);③ Record 的紧凑构造器实现流程合法性校验(开始节点唯一、结束节点存在、节点引用有效);④ Pattern Matching 的穷举检查确保所有节点类型都被处理;⑤ 流程定义不可变(List.copyOf),保证线程安全。优势:① 新增节点类型(如"子流程节点")时编译器提示未处理的分支;② 流程合法性在构造时校验,避免运行时错误;③ 节点模型类型安全,避免运行时类型错误;④ 流程定义清晰,易于序列化和持久化。