Java 在 JDK 8 之后经历了一段"沉默期",但从 JDK 11(LTS)开始,特别是 JDK 17(LTS)和 JDK 21(LTS),Oracle 和 OpenJDK 社区以每六个月一次的发布节奏,持续交付了大量改变编程范式的特性。本文聚焦生产环境可用的核心特性,展示从"样板代码地狱"到"表达力爆炸"的蜕变。
1. Records:不可变数据载体
1.1 为什么需要 Record
传统 POJO 的痛点在于 样板代码爆炸:
// Java 16 之前: 40+ 行代码
public class UserDTO {
private final String name;
private final int age;
private final String email;
public UserDTO(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
public String getName() { return name; }
public int getAge() { return age; }
public String getEmail() { return email; }
@Override public boolean equals(Object o) { /*...*/ }
@Override public int hashCode() { /*...*/ }
@Override public String toString() { /*...*/ }
}
// Java 16+: 1 行代码
public record UserDTO(String name, int age, String email) {}
1.2 Record 的语义与限制
public record Point(int x, int y) {
// 编译器自动生成:
// - private final int x, y;
// - 全参构造器
// - x(), y() 访问器(注意不是 getX())
// - equals, hashCode, toString
// 可以添加自定义构造器
public Point {
if (x < 0 || y < 0) {
throw new IllegalArgumentException("坐标不能为负");
}
// 这里 x, y 已经被赋值,这是紧凑构造器(compact constructor)
}
// 可以添加方法
public double distanceToOrigin() {
return Math.sqrt(x * x + y * y);
}
// 可以实现接口
// 但 record 是 final,不可被继承,也不能显式 extends
}
Record 的约束清单:
| 特性 | 支持 | 说明 |
|---|---|---|
final 类 | ✅ 自动 | 不可被继承 |
| 组件字段 | ✅ | 自动为 private final |
| 自定义方法 | ✅ | 如 distanceToOrigin() |
| 实现接口 | ✅ | record Point(int x, int y) implements Comparable<Point> |
| 额外字段(非组件) | ❌ | 不能声明实例字段 |
| 继承类 | ❌ | 不能 extends |
abstract | ❌ | 不能声明为抽象 |
| 框架序列化 | ⚠️ 需注意 | Jackson 2.12+ 原生支持 |
1.3 Record 与 DDD 值对象
public record Money(BigDecimal amount, Currency currency) {
public Money {
Objects.requireNonNull(amount);
Objects.requireNonNull(currency);
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("金额不能为负");
}
amount = amount.setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_UP);
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("币种不一致");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public static Money zero(Currency currency) {
return new Money(BigDecimal.ZERO, currency);
}
}
1.4 Record 模式匹配(JDK 21 Preview → JDK 24 Standard)
// 解构 Record
record Point(int x, int y) {}
static String describe(Object obj) {
return switch (obj) {
case Point(int x, int y) when x == 0 && y == 0 -> "原点";
case Point(int x, int y) when x == y -> "对角线: (%d, %d)".formatted(x, y);
case Point(int x, int y) -> "坐标点: (%d, %d)".formatted(x, y);
case null -> "null";
default -> "未知类型";
};
}
// 嵌套解构
record Segment(Point start, Point end) {}
static int horizontalLength(Segment s) {
if (s instanceof Segment(Point(int x1, _), Point(int x2, _))) {
return Math.abs(x2 - x1);
}
return 0;
}
2. Sealed Classes:受控继承
2.1 问题背景
Java 继承默认是开放的——任何类都可以 extends 你的类。这在设计外部 API 时带来风险:
- 子类可能破坏不变量
switch无法穷尽所有情况- 安全敏感类被恶意继承
2.2 Sealed Class 语法
// 父类声明允许的直接子类
public sealed abstract class Payment
permits CreditCardPayment, AlipayPayment, WechatPayment {
public abstract BigDecimal calculateFee(BigDecimal amount);
}
// 子类必须是 final、sealed 或 non-sealed
public final class CreditCardPayment extends Payment {
private final String cardNumber;
@Override public BigDecimal calculateFee(BigDecimal amount) {
return amount.multiply(new BigDecimal("0.015"));
}
}
public final class AlipayPayment extends Payment {
@Override public BigDecimal calculateFee(BigDecimal amount) {
return amount.multiply(new BigDecimal("0.006"));
}
}
public non-sealed class WechatPayment extends Payment {
// 非密封,允许进一步继承(如 WechatMiniProgramPayment)
@Override public BigDecimal calculateFee(BigDecimal amount) {
return amount.multiply(new BigDecimal("0.006"));
}
}
2.3 Sealed + Record = 代数数据类型(ADT)
public sealed interface Result<T, E>
permits Result.Ok, Result.Err {
record Ok<T>(T value) implements Result<T, Void> {}
record Err<E>(E error) implements Result<Void, E> {}
default boolean isOk() { return this instanceof Ok; }
default boolean isErr() { return this instanceof Err; }
}
// 使用
Result<Order, String> orderResult = fetchOrder(orderId);
switch (orderResult) {
case Result.Ok(Order order) -> process(order);
case Result.Err(String error) -> log.error("获取订单失败: {}", error);
}
编译器保证穷尽性:如果新增一种 Result 实现但忘记更新 switch,编译期报错。
3. Pattern Matching for Switch(JDK 21 正式版)
3.1 类型模式
// Java 17 之前: instanceof + 强制转换
static String format(Object obj) {
if (obj instanceof String s) {
return s.toUpperCase();
} else if (obj instanceof Integer i) {
return "int: " + i;
} else if (obj instanceof Double d) {
return "double: " + d;
} else {
return "unknown";
}
}
// Java 21+: switch 模式匹配
static String format(Object obj) {
return switch (obj) {
case String s -> s.toUpperCase();
case Integer i -> "int: " + i;
case Double d -> "double: " + d;
case null -> "null value";
default -> "unknown: " + obj.getClass().getSimpleName();
};
}
3.2 守卫模式(Guarded Patterns)
static String classifyNumber(int n) {
return switch (n) {
case 0 -> "零";
case int i when i > 0 && i < 10 -> "个位数正数";
case int i when i >= 10 && i < 100 -> "两位数正数";
case int i when i < 0 -> "负数: " + i;
default -> "三位数及以上";
};
}
3.3 枚举穷尽性检查
sealed interface PaymentMethod {
enum CreditCard implements PaymentMethod { VISA, MASTERCARD, AMEX }
enum EWallet implements PaymentMethod { ALIPAY, WECHAT }
}
static String paymentFee(PaymentMethod method) {
return switch (method) {
case PaymentMethod.CreditCard cc -> switch (cc) {
case VISA, MASTERCARD -> "1.5%";
case AMEX -> "2.5%";
};
case PaymentMethod.EWallet ew -> switch (ew) {
case ALIPAY -> "0.6%";
case WECHAT -> "0.6%";
};
}; // 无需 default,编译器证明已穷尽
}
4. Virtual Threads(JDK 21):轻量级并发
4.1 为什么需要 Virtual Threads
传统 Java 线程(平台线程)的问题:
- 每个线程对应一个 OS 线程,创建成本 ~1MB 栈
- 高并发 = 大量线程 → 上下文切换开销剧增
- 阻塞 I/O 时线程空转,资源浪费
// 传统方式:1000 并发请求需要 1000 OS 线程
try (var executor = Executors.newFixedThreadPool(1000)) {
IntStream.range(0, 1000).forEach(i ->
executor.submit(() -> fetchData(i)) // 阻塞 I/O
);
}
4.2 Virtual Threads 入门
// 方式一: 直接创建
Thread.startVirtualThread(() -> {
System.out.println("Virtual Thread: " + Thread.currentThread());
});
// 方式二: 使用 ExecutorService
public List<String> fetchAll(List<String> urls) throws InterruptedException {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var futures = urls.stream()
.map(url -> executor.submit(() -> fetch(url)))
.toList();
return futures.stream()
.map(f -> {
try { return f.get(); }
catch (Exception e) { throw new RuntimeException(e); }
})
.toList();
} // 虚拟线程自动回收
}
4.3 虚拟线程的原理
Platform Thread (OS 线程, 1MB)
├─ Carrier Thread 1
│ ├─ Virtual Thread A → 阻塞 I/O → 卸载(unmount)
│ ├─ Virtual Thread B(被挂载 mount)
│ └─ Virtual Thread C(等待中)
└─ Carrier Thread 2
├─ Virtual Thread D
└─ Virtual Thread E
ForkJoinPool (carrier 线程池) 调度虚拟线程
- 虚拟线程阻塞时自动卸载,carrier 线程执行其他虚拟线程
- 阻塞结束,虚拟线程重新挂载到可用 carrier
4.4 Monitor Pinning 问题与解决方案
// 危险: 在 synchronized 块内执行 I/O,导致"pinned"虚拟线程
synchronized (lock) {
// 虚拟线程无法在此处卸载!carrier 线程被占用
httpClient.send(request, responseHandler);
}
// 解决方案一: 改用 ReentrantLock
lock.lock();
try {
httpClient.send(request, responseHandler); // 可以正常卸载
} finally {
lock.unlock();
}
// 解决方案二: JDK 24 实验性引入 <-ynchronized 卸载
// -XX:+UnlockExperimentalVMOptions -XX:+VMContinuations
4.5 何时使用 / 避免 Virtual Threads
| 场景 | 建议 |
|---|---|
| 大量并发 I/O(HTTP、数据库) | ✅ 首选虚拟线程 |
| CPU 密集型计算 | ❌ 使用平台线程 + parallel() |
| 低延迟关键路径(<1ms) | ⚠️ 测试对比,虚拟线程调度有开销 |
大量 synchronized + I/O | ⚠️ 改用 ReentrantLock 避免 pinning |
| 使用 JNI / 本地代码 | ⚠️ 虚拟线程与本地代码栈交互有限 |
5. String Templates(JDK 21 Preview → JDK 24 Standard)
5.1 传统字符串拼接的问题
// 低效: 运行时拼接
time = "2024-01-28 10:30:00";
String json = "{\"user\":\"" + user + "\",\"time\":\"" + time + "\"}";
// 安全漏洞: SQL 注入风险
String sql = "SELECT * FROM users WHERE name = '" + name + "'";
5.2 String Template 语法
String name = "Alice";
int age = 30;
// STR 处理器: 简单插值
String greeting = STR."Hello, \{name}! You are \{age} years old.";
// Hello, Alice! You are 30 years old.
// FMT 处理器: 格式化插值
String formatted = FMT."Price: %10.2f\{price}";
// RAW 处理器: 原始字符串
String raw = RAW."Name: \{name}"; // 不执行插值
// 自定义处理器: 自动转义 JSON
static final StringTemplate.Processor<String, RuntimeException> JSON =
StringTemplate.Processor.of((stringTemplate) -> {
StringBuilder sb = new StringBuilder();
var fragments = stringTemplate.fragments().iterator();
var values = stringTemplate.values().iterator();
sb.append(fragments.next());
while (values.hasNext()) {
sb.append(escapeJson(values.next()));
sb.append(fragments.next());
}
return sb.toString();
});
String json = JSON."{\"name\":\{name},\"age\":\{age}}";
6. 其他重要特性速览
6.1 Text Blocks(JDK 15)
String html = """
<div class="container">
<h1>%s</h1>
<p>Welcome, %s!</p>
</div>
""".formatted(title, username);
6.2 Switch 表达式(JDK 14 标准)
// 返回值、无 fall-through
String dayType = switch (day) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "工作日";
case SATURDAY, SUNDAY -> "周末";
};
6.3 instanceof 模式匹配(JDK 16 标准)
// 之前
if (obj instanceof Person) {
Person p = (Person) obj;
p.getName();
}
// 之后
if (obj instanceof Person p) {
p.getName(); // p 在范围内可用
}
6.4 不可变集合工厂方法(JDK 9)
List<String> list = List.of("a", "b", "c");
Set<Integer> set = Set.of(1, 2, 3);
Map<String, Integer> map = Map.of("a", 1, "b", 2);
7. 升级路线与兼容性
7.1 LTS 版本迁移路径
JDK 8 → JDK 11 → JDK 17 → JDK 21 → JDK 25(LTS)
↑
模块化系统(JPMS)
↑
关键中间站,先到这里再往后
JDK 8 → 11 的关键变化:
- 模块系统:检查是否使用了内部 API(
sun.misc.Unsafe等) - 删除 Java EE / CORBA 模块:改用 Maven 依赖
- G1GC 成为默认 GC
7.2 类文件版本对应
| JDK 版本 | 字节码版本 | 特性示例 |
|---|---|---|
| 8 | 52 | Lambda |
| 11 | 55 | var、HTTP Client |
| 17 | 61 | Sealed Classes、Records 标准版 |
| 21 | 65 | Virtual Threads、Pattern Matching |
7.3 使用 jdeps 分析依赖
# 检查应用是否使用已移除的模块
jdeps --jdk-internals -recursive myapp.jar
# 生成模块依赖图
jdeps --dot-output . -s myapp.jar
8. 实战建议
- 项目现状为 JDK 8:先升级到 11,验证兼容性后再到 17
- 新项目直接 JDK 17+:Records + Sealed + Pattern Matching 大幅提升代码清晰度
- I/O 密集场景: JDK 21 + Virtual Threads,可支撑百万级并发连接
- Spring Boot 3.x:基线 JDK 17,天然支持所有现代特性
- IDE 配置:IntelliJ IDEA 2023.2+ 完整支持 JDK 21 特性
延伸阅读
- Java 高并发编程精要 — Virtual Threads 与 JUC 的协作模式
- JVM 内存模型与类加载机制 — 理解虚拟线程的底层实现
- Spring Boot 核心原理与自动配置 — Spring Boot 3.x 对 JDK 17+ 的支持
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。