15. Java 17/21 现代语言特性实战

从 Records 到 Sealed Classes,从 Pattern Matching 到 Virtual Threads,系统掌握 Java 17 和 21 带来的生产力革命

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 版本字节码版本特性示例
852Lambda
1155var、HTTP Client
1761Sealed Classes、Records 标准版
2165Virtual Threads、Pattern Matching

7.3 使用 jdeps 分析依赖

# 检查应用是否使用已移除的模块
jdeps --jdk-internals -recursive myapp.jar

# 生成模块依赖图
jdeps --dot-output . -s myapp.jar

8. 实战建议

  1. 项目现状为 JDK 8:先升级到 11,验证兼容性后再到 17
  2. 新项目直接 JDK 17+:Records + Sealed + Pattern Matching 大幅提升代码清晰度
  3. I/O 密集场景: JDK 21 + Virtual Threads,可支撑百万级并发连接
  4. Spring Boot 3.x:基线 JDK 17,天然支持所有现代特性
  5. IDE 配置:IntelliJ IDEA 2023.2+ 完整支持 JDK 21 特性

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java-enterprise」更多文章

  1. 限流算法深度解析:令牌桶、漏桶与滑动窗口计数
  2. Java 代码质量:SonarQube、Checkstyle 与 SpotBugs 工程化实践
  3. Spring IoC 容器与依赖注入原理深度剖析