Java8升级java21的一点思考

Published on 2025-08-10 13:12 in 分类: 随笔 with 狂盗一枝梅
分类: 随笔

Oracle 对 Java 8 的公开免费更新已于2019年终止,但是直到现在2025年,Java8还是非常流行,但是随着SpringBoot、SpringFramework等框架逐渐不再支持Java8环境下运行,升级Java8到新版本已经势在必行。

一、新版SpringBoot的要求

最近翻了翻SpringBoot的官方文档,突然发现SpringBoot已经发展到3.5.4了,而我还在用2.3.12,真的落后了好大一截。

https://spring.io/projects/spring-boot#learn

image-20250807153836530

点开3.5.4文档:https://docs.spring.io/spring-boot/system-requirements.html

image-20250807154107218

从官方文档可以看到,SpringBoot3.5.4需要Java17到最新版本的Java24之间的版本,不管是Java8还是Java11都已经不在支持的范围内。

二、新版SpringFramework的要求

新版SpringFramework已经到6.2.9版本,而我还在用5.2.15,同样落后了一个大版本号:https://spring.io/projects/spring-framework#learn

image-20250807154934664

打开6.2.9版本的文档,可以看到该版本对Java版本的要求:https://docs.spring.io/spring-framework/reference/overview.html

image-20250807155049633

同样的,新版本SpringFramework要求Java也至少得是Java17版本。

三、Java版本的选择

从SpringBoot以及SpringFramework的最新版本的要求来看,他们的运行环境至少得是Java17,那么我们应该升级到Java17吗?

打开Java官网看看:https://www.oracle.com/java/technologies/

image-20250807155426370

可以看到,Java11、Java17、Java21是LTS(Long Term Support)版本,即长期支持版。Java11比较特殊,它虽然是LTS,但是一直被人诟病,最后成了一个Java8升级的“过渡版本”,而且SpringBoot以及SpringFramework都从Java17开始支持,所以Java11不做考虑了。考虑到Java17之上还有Java21也是LTS版本,那么毫无疑问,直接升级到Java21会是更好的选择。

Oracle JDK下载地址:https://www.oracle.com/java/technologies/downloads/#jdk21-windows

Open JDK下载地址: https://jdk.java.net/archive/

四、Java21新特性

如果是从 Java 8 直接跨越到 Java 21,那这些新特性绝对会让你大开眼界。因为中间跳过了 Java 9~17 多个版本的积累,所以特性非常多。下面按类别逐一介绍,并附上代码示例。

完整可运行案例见项目 java21demo,所有案例均在 JDK 21 下编译运行通过。

1. 语言特性

这些是 Java 开发者每天写代码都会接触的核心语法改进,也是从 Java 8 升级后感受最直观的变化。

1.1 Records

记录类(JEP 395,Java 16 正式)。

Java 8 方式:需要手写 POJO 类,包含 private 字段、getter/setter、toString、equals、hashCode、构造器,即便用 Lombok 也只是掩盖了样板代码,且本质仍是可变类。

Java 21 方式record 关键字一行定义不可变数据载体,编译器自动生成全部样板代码:构造器、访问器(注意是 name() 而非 getName())、equals()hashCode()toString()

public record User(Long id, String name, String email) {
    // 紧凑型构造器:可做校验,自动完成字段赋值
    public User {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("name must not be blank");
        }
        if (email != null && !email.contains("@")) {
            throw new IllegalArgumentException("invalid email format");
        }
    }

    // 可以定义实例方法和静态方法
    public String displayName() {
        return "%s <%s>".formatted(name, email == null ? "no-reply" : email);
    }

    // 静态工厂方法
    public static User of(Long id, String name, String email) {
        return new User(id, name, email);
    }
}

// 使用
var user = new User(1L, "张三", "zhangsan@example.com");
System.out.println(user.id());         // 1(访问器,非 getId())
System.out.println(user.name());       // 张三
System.out.println(user.email());      // zhangsan@example.com
System.out.println(user.toString());  // User[id=1, name=张三, email=zhangsan@example.com]

var same = new User(1L, "张三", "zhangsan@example.com");
System.out.println(user.equals(same)); // true(自动基于字段比较)

紧凑型构造器校验

try {
    new User(3L, "", "test@example.com");
} catch (IllegalArgumentException e) {
    // name must not be blank
}
try {
    new User(4L, "测试", "invalid-email");
} catch (IllegalArgumentException e) {
    // invalid email format
}

支持的特性

  • 局部 record(方法内定义,作用域限制在此方法中)
  • 实现接口
  • 泛型 record
  • 注解
// 局部 record
record Point(int x, int y) {}
var p = new Point(10, 20);

// 实现接口
record Circle(double radius) implements Shape {
    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

不能做的事:record 不能继承其他类(已经隐式继承 java.lang.Record)、不能定义实例字段(但可以定义静态字段和实例方法)。

1.2 密封类

密封类(Sealed Classes,JEP 409,Java 17 正式)。

Java 8 方式:类的继承只有两个极端——final(禁止一切继承)或普通类(任何人都可以继承)。没有"允许指定子类继承"的中间选项。

Java 21 方式sealed 关键字明确声明该类/接口只允许 permits 列表中指定的类继承,精确控制继承层次。

密封类的直接子类必须使用以下三个关键字之一声明:

关键字 含义 示例
final 不能再被继承 final class Circle extends Shape
non-sealed 解除密封,允许任意继承 non-sealed class Rectangle extends Shape
sealed 继续限制继承 sealed class Triangle extends Shape permits EqTriangle

完整示例

sealed interface Shape permits Circle, Rectangle, Triangle {
    double area();
}

final class Circle implements Shape {           // final:禁止进一步继承
    private final double radius;
    Circle(double radius) { this.radius = radius; }
    @Override public double area() { return Math.PI * radius * radius; }
}

non-sealed class Rectangle implements Shape {   // non-sealed:开放继承
    private final double w, h;
    Rectangle(double w, double h) { this.w = w; this.h = h; }
    @Override public double area() { return w * h; }
}

// Rectangle 是 non-sealed 的,所以可以自由继承
final class Square extends Rectangle {
    Square(double side) { super(side, side); }
}

sealed class Triangle implements Shape permits EquilateralTriangle {
    // sealed:继续限定只允许 EquilateralTriangle 继承
}

final class EquilateralTriangle extends Triangle {
    EquilateralTriangle(double side) { super(side, Math.sqrt(3)/2*side); }
}

密封类的核心价值——switch 穷尽检查

配合模式匹配的 switch,编译器可以验证是否覆盖了所有子类,无需 default 分支

String classifyShape(Shape shape) {
    return switch (shape) {
        case Circle c -> "圆形,半径=" + c.radius;
        case Square s -> "正方形,边长=" + s.width;
        case Rectangle r -> "矩形," + r.width + "x" + r.height;
        case EquilateralTriangle et -> "等边三角形";
        case Triangle t -> "三角形";
        // 无需 default —— sealed 保证穷尽!
        // 如果添加了新的子类未在 switch 中处理,编译直接报错
    };
}

这意味着:新增子类时,IDE 会提示所有需要修改的 switch 语句,大幅降低遗漏风险。

1.3 模式匹配

模式匹配(Pattern Matching)是 Java 近几个版本最重要的语言改进之一,由多个 JEP 组成,贯穿 instanceof、switch、record 解构等多个场景。

instanceof 模式匹配(JEP 394,Java 16 正式):

Object obj = "Hello, Java 21!";

// ---- Java 8 方式 ----
if (obj instanceof String) {      // 1. 类型判断
    String s = (String) obj;      // 2. 强制转换(重复且易错)
    System.out.println(s.length());
}

// ---- Java 21 方式 ----
if (obj instanceof String s) {    // 一步到位:判断 + 绑定变量
    System.out.println(s.length());
}

// 结合条件判断更简洁
Object num = 42;
if (num instanceof Integer i && i > 10) {
    System.out.println("大于10的整数:" + i);
}

switch 模式匹配(JEP 441,Java 21 正式):

switch 在 Java 21 经历了脱胎换骨的改变:它变成了表达式(可以有返回值),可以匹配任意类型,支持 null 处理和守卫条件(when)。

// ---- Java 8 switch ----
// 只能匹配 int、char、enum、String,且只能作为语句(无返回值)
String result = "";
switch (day) {
    case MONDAY:
    case FRIDAY:
        result = "工作日";
        break;
    default:
        result = "其他";
}

// ---- Java 21 switch ----
// 可以是表达式、匹配任意类型、处理 null、守卫条件
String describe(Object value) {
    return switch (value) {
        case null -> "null值";                       // null 安全处理
        case String s when s.length() > 5 -> "较长字符串: " + s;  // 守卫条件
        case String s -> "较短字符串: " + s;
        case Integer i -> "整数: " + i;                // 匹配类型
        case Double d -> "浮点数: " + d;
        case User(Long id, String name, _) -> "用户: " + name;   // 解构 record
        case int[] arr -> "数组, 长度=" + arr.length;
        default -> "未知类型: " + value.getClass().getSimpleName();
    };
}

守卫条件(when 子句) 允许在匹配模式的基础上进一步过滤:

return switch (obj) {
    case String s when s.startsWith("http") -> "URL: " + s;
    case String s when s.matches("\\d+") -> "数字字符串: " + s;
    case String s -> "普通字符串: " + s;
    case null, default -> "其他";
};

记录模式(JEP 440,Java 21 正式):

解构 record 组件,支持嵌套解构,适用于多层数据结构:

// 嵌套 record
record Address(String city, String street, String zipCode) {}
record Person(String name, int age, Address address) {}
record Order(Long id, Person customer, double amount) {}

var order = new Order(1001L, 
    new Person("张三", 28, new Address("北京", "长安街", "100000")),
    299.99);

// ---- Java 8 方式:逐层调用 ----
String city = order.customer().address().city();

// ---- Java 21 方式:一步解构到底 ----
if (order instanceof Order(
        Long id,
        Person(String name, int age, Address(String city, _, _)),
        double amount
)) {
    System.out.printf("订单#%d: %s 收货城市: %s%n", id, name, city);
}

// switch 中结合记录模式
String result = switch (order) {
    case Order(Long id, Person(String name, _, _), double amt) when amt > 1000
            -> "大额订单: " + name;
    case Order(_, Person(String name, _, _), _) -> "普通订单: " + name;
};

未命名模式和变量(JEP 443,Java 21 正式):

_ 表示"不需要的值",避免取无意义的变量名:

// 未命名变量 —— 忽略不使用的变量
var _ = createUser();               // 忽略返回值

try (var _ = createResource()) {    // 忽略资源句柄
    // 使用资源
} catch (Exception _) {             // 忽略异常详情
    System.out.println("出错了");
}

// 未命名模式 —— 忽略 record 中不需要的字段
if (point instanceof Point(int x, int _)) {
    System.out.println("x = " + x);  // 忽略 y
}

switch (obj) {
    case Integer _ -> System.out.println("整数,具体值不重要");
    case String _ -> System.out.println("字符串,内容不重要");
    default -> System.out.println("其他");
}

#### 1.4 文本块

文本块(Text Blocks,JEP 378,Java 15 正式)。

**Java 8 方式**:多行字符串靠 `+` 和 `\n` 拼接,格式混乱、容易出错、可读性差。

```java
// Java 8 —— 拼接噩梦
String html = "<html>\n" +
              "    <body>\n" +
              "        <h1>Hello</h1>\n" +
              "    </body>\n" +
              "</html>";

Java 21 方式:使用 """ 三引号包裹文本块,编译器自动检测公共缩进并去除。保留原始格式,所见即所得。

// Java 21 —— 清晰明了
String html = """
        <html>
            <body>
                <h1>Hello</h1>
            </body>
        </html>
        """;

编译器如何处理缩进:它会找到所有行中最小的前导空白,以此为基准统一去除。所以上述示例中,最左列的内容(如 <html>)前面有 8 个空格,文本块输出时就是 8 个空格缩进。

常见使用场景——JSON 和 SQL

// JSON 模板
String json = """
        {
            "name": "张三",
            "age": 30,
            "address": {
                "city": "北京",
                "street": "长安街"
            }
        }
        """;

// SQL 查询
String sql = """
        SELECT u.id, u.name, o.amount
        FROM users u
        JOIN orders o ON u.id = o.user_id
        WHERE o.amount > 100
        ORDER BY o.created_at DESC
        """;

新的转义序列

  • \s——表示一个空格,防止行尾空格被去除
  • \——行连续符,连接两行内容不换行
// \s 保留尾随空格
String withTrailing = """
        第一行\s
        第二行\s
        """;  // 每行末尾保留一个空格

// \ 行连续
String continuous = """
        这是第一行 \
        但实际是同一行
        """;  // 输出:"这是第一行 但实际是同一行"

formatted() 方法——类似 String.format(),但更直观:

String msg = """
        === 用户信息 ===
        姓名:%s
        年龄:%d
        成绩:%.1f分
        """.formatted("张三", 28, 95.5);

indent() 方法——方便地调整缩进:

String block = "line1\nline2\nline3";
block.indent(4);  // 增加 4 空格缩进
block.indent(-1); // 减少 1 空格缩进

1.5 var

局部变量类型推断(JEP 286,Java 11 正式)。

Java 8 方式:变量声明两侧都要写完整类型,尤其在泛型嵌套时极其冗余。

// Java 8 —— 两侧重复
Map<String, List<Integer>> complex = new HashMap<String, List<Integer>>();
// 即使 Java 8 有菱形运算符,左侧仍然要写完整类型
Map<String, List<Integer>> complex = new HashMap<>();

Java 21 方式var 让编译器推断类型,减少重复代码,提升可读性。

// Java 21 —— 编译器推断
var complex = new HashMap<String, List<Integer>>();
var list = new ArrayList<String>();
var numbers = List.of(1, 2, 3);

var 的典型使用场景

// for 循环
for (var i = 0; i < 3; i++) { ... }
for (var entry : map.entrySet()) {
    System.out.println(entry.getKey() + "=" + entry.getValue());
}

// try-with-resources
try (var input = new FileInputStream("file.txt")) {
    // ...
}

// 匿名类(var 可以捕获匿名类的额外方法)
var obj = new Object() {
    String greet(String name) {
        return "Hello, " + name;
    }
};
System.out.println(obj.greet("张三"));  // var 让这行可以编译

var 的限制(不能使用的地方)

var x = null;                    // ❌ 编译错误:类型无法推断
var y;                           // ❌ 编译错误:无初始值
public var field = "test";       // ❌ 编译错误:不能用于字段
void method(var param) {}       // ❌ 编译错误:不能用于方法参数

原则var 的目的是提升可读性,当右侧表达式足够清晰时使用。如果右侧是 foo() 这样的方法调用,var result = foo() 就不知道类型了,此时显式类型更好。

2. 并发革命

Java 21 在并发领域带来了三次变革:虚拟线程(轻量级线程)、结构化并发(错误边界管理)、作用域值(上下文传递)。其中虚拟线程是 Java 21 最重要的特性,没有之一。

2.1 虚拟线程

虚拟线程(Virtual Threads,JEP 444,Java 21 正式)。

Java 8 方式的问题

平台线程(操作系统线程)的创建成本极高:

  • 内存开销大:每个线程默认分配 1MB+ 栈空间,1 万个线程就需要 10GB+ 内存
  • 创建销毁慢:从内核态到用户态的切换,每个线程创建需要毫秒级
  • 上下文切换代价高:CPU 需要保存/恢复线程上下文,大量线程竞争时性能急剧下降
  • 线程池也有限:即使使用线程池,通常也就几百个线程,无法应对高并发 IO 场景

所以 Java 8 下"一个请求一个线程"的模型很难支撑高并发,通常需要引入异步编程框架(如 CompletableFuture、响应式编程),代码复杂度大幅上升。

Java 21 的解决方案——虚拟线程

虚拟线程是 JVM 管理的轻量级线程,不直接对应操作系统线程:

  • 创建成本极低(微秒级),创建 10 万个毫无压力
  • 栈可动态调整,初始只有几百字节
  • IO 阻塞时不占 OS 线程——虚拟线程在 sleep/read/write 等阻塞操作时自动脱挂,底层的平台线程可以去执行其他虚拟线程
  • 依然适用 Thread 模型——代码写法与平台线程完全一致,无需学习新模型

虚拟线程的三种创建方式

// 方式一:Thread.ofVirtual()——最灵活
Thread t1 = Thread.ofVirtual()
        .name("vthread-1")          // 设置虚拟线程名称
        .start(() -> {
            System.out.println(Thread.currentThread());
        });

// 方式二:Executors.newVirtualThreadPerTaskExecutor()——批量任务推荐
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    var future = executor.submit(() -> {
        System.out.println("虚拟线程执行: " + Thread.currentThread());
        return "done";
    });
    System.out.println(future.get());
}
// 注意:executor 是 AutoCloseable,try-with-resources 自动等待所有任务完成

// 方式三:Thread.startVirtualThread()——快捷方式
Thread.startVirtualThread(() -> {
    System.out.println("快捷方式: " + Thread.currentThread());
});

实际基准测试——平台线程 vs 虚拟线程创建成本

10000 个线程创建耗时对比:
  平台线程: 734ms(每个约 73μs)
  虚拟线程:  22ms(每个约 2.2μs)
  虚拟线程快了约 33 倍

即使是空线程(不做任何操作),平台线程也要创建操作系统线程,这是硬性开销。而虚拟线程可以做到几十微秒创建数千个。

十万虚拟线程演示(模拟 IO 密集型场景)

var total = 100_000;
var latch = new CountDownLatch(total);
var start = Instant.now();

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < total; i++) {
        executor.submit(() -> {
            // 模拟 IO 操作(如数据库查询、REST 调用)
            Thread.sleep(Duration.ofMillis(10));
            latch.countDown();
            return i;
        });
    }
}

latch.await();
// 输出:启动 100000 个虚拟线程完成,耗时约 292ms
// 每个线程 sleep 10ms,总耗时仅约 292ms(并发执行)

注意,如果是串行执行 10 万个 sleep(10ms) 需要 1000 秒(约 17 分钟),但虚拟线程通过并发执行,只用了不到 300ms!因为 IO 阻塞时虚拟线程让出了底层的平台线程。

适用场景

场景 是否适合 原因
HTTP/RPC 远程调用 ✅ 非常适合 等待远程响应时大量虚拟线程共享少量平台线程
数据库查询(JDBC) ✅ 非常适合 等待数据库返回时释放平台线程给其他请求
文件读写 ✅ 适合 IO 阻塞期间不浪费平台线程
消息队列消费 ✅ 适合 高吞吐消费场景,大量虚拟线程并发处理
CPU 密集型计算 ❌ 不适合 如视频编码、加密解密,占用 CPU 与平台线程无差别
synchronized 块内长操作 ❌ 可能 pinning 虚拟线程在 synchronized 块内可能被 pinned 到平台线程
长时间 JNI 调用 ❌ 不适合 本地方法调用无法释放平台线程

pinning 问题:当虚拟线程进入 synchronized 块或执行 JNI 调用时,JVM 无法将虚拟线程从平台线程上脱挂,导致该平台线程被"钉住"。虽然不影响正确性,但会降低并发度。可以通过使用 ReentrantLock 替代 synchronized 来避免。

最佳实践

// 用 try-with-resources 替代手动关闭
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    var futures = tasks.stream()
            .map(task -> executor.submit(() -> process(task)))
            .toList();
    return futures.stream().map(Future::resultNow).toList();
}  // 自动等待所有任务完成

// Java 21 新增 Future.resultNow() / exceptionNow()
// 替代 future.get() 的 checked exception

2.2 结构化并发

结构化并发(Structured Concurrency,JEP 453,Java 21 预览,Java 22 正式)。

Java 8 方式的问题

// Java 8 —— 需要手动管理 Future
ExecutorService executor = Executors.newFixedThreadPool(3);
Future<String> info = executor.submit(() -> fetchUserInfo(userId));
Future<String> orders = executor.submit(() -> fetchUserOrders(userId));
Future<Integer> score = executor.submit(() -> fetchUserScore(userId));

// 逐个获取结果,如果某个任务失败,其他任务仍在后台运行
// 而且 executor 需要手动 shutdown,否则线程泄漏
try {
    String result = combine(info.get(), orders.get(), score.get());
} catch (Exception e) {
    // 问题:其他两个任务还在后台无意义地运行
    // 问题:executor 没有关闭 → 线程泄漏
}

核心痛点:任务之间没有关联,错误边界不清晰,容易遗漏线程清理

Java 21 的解决方案——StructuredTaskScope

StructuredTaskScope 将多个并发任务绑定到同一个代码作用域中:

  • 所有任务在作用域结束前必须全部完成(或被取消)
  • 任一子任务失败,自动取消其他未完成的任务
  • 使用 try-with-resources 语法,作用域结束自动清理,不会泄漏线程

ShutdownOnFailure(任一失败则取消全部)

这是最常用的模式——三个并发任务要么全部成功,要么一起失败。

String getUserDashboard(Long userId) throws Exception {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        // 提交三个并发子任务
        var userInfo = scope.fork(() -> fetchUserInfo(userId));
        var orders = scope.fork(() -> fetchUserOrders(userId));
        var score = scope.fork(() -> fetchUserScore(userId));

        // 等待全部完成
        scope.join();
        // 检查是否有任务失败(如有则抛出异常)
        scope.throwIfFailed();

        // 所有任务成功,合并结果
        return """
                === 用户面板 ===
                信息: %s
                订单: %s
                积分: %d
                """.formatted(userInfo.get(), orders.get(), score.get());
    }
    // 作用域结束:自动cancel所有未完成的任务 + 清理线程
}

异常自动取消机制

// 如果 userId=999 会导致订单服务抛出异常
try {
    getUserDashboard(999L);
} catch (Exception e) {
    // 捕获异常: 订单服务异常! (其他任务已被自动取消)
}

调用 scope.throwIfFailed() 时,如果任意子任务抛出了异常,会自动传播。其他未完成的任务通过 scope.close()(即 try-with-resources 的自动关闭)被取消。

ShutdownOnSuccess(任一成功即返回)

适用于"多个数据源,谁先返回用谁的"场景。

String getAnyUserInfo(Long userId) throws Exception {
    try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
        // 两个数据源竞争,谁先返回用谁的
        scope.fork(() -> fetchFromCache(userId));   // 缓存查询,通常快
        scope.fork(() -> fetchFromDB(userId));      // 数据库查询,通常慢

        // 返回最先成功的结果(第一个成功的结果返回后,自动取消另一个)
        return scope.join().result();
    }
}

结构化并发的核心优势

维度 Java 8 (ExecutorService) Java 21 (StructuredTaskScope)
错误边界 需手动管理 自动处理:任一失败取消全部
线程清理 需手动 shutdown try-with-resources 自动关闭
取消传播 无,任务各管各 自动 cancel 未完成的任务
异常传递 上游逐个 get 才感知 throwIfFailed 一步到位
任务关系 无关联,独立提交 结构化:作用域内创建,作用域结束前完成

此特性在 Java 21 为预览阶段,Java 22 正式发布。编译运行时需要 --enable-preview

2.3 作用域值

作用域值(Scoped Values,JEP 446,Java 21 预览,Java 22 正式)。

Java 8 方式——ThreadLocal 的痛点

ThreadLocal 是 Java 8 中最常用的线程上下文传递方式,但它有几个严重问题:

private static final ThreadLocal<String> requestId = new ThreadLocal<>();
private static final ThreadLocal<Long> currentUserId = new ThreadLocal<>();

// 问题 1:必须手动 remove(),否则内存泄漏
try {
    requestId.set("req-001");
    currentUserId.set(42L);
    processRequest();
} finally {
    requestId.remove();       // 容易忘记 → 线程池中的线程持有引用不释放
    currentUserId.remove();   // → OOM
}

// 问题 2:值可以被任意修改
ThreadLocal<String> ctx = new ThreadLocal<>();
ctx.set("值A");
ctx.set("值B");  // 可以随意覆盖,没有防御机制

// 问题 3:子线程不继承
new Thread(() -> {
    System.out.println(requestId.get());  // null!
}).start();

// InheritableThreadLocal 可以解决继承问题,但性能差且容易数据错乱

Java 21 的解决方案——ScopedValue

ScopedValue 是一种不可变的线程上下文传递方案:

  • 不可修改:没有 set/remove 方法,一旦绑定在作用域内只读
  • 自动回收:离开作用域值自动销毁,无需手动清理,无内存泄漏
  • 子线程自动继承:虚拟线程中自动传递绑定值
  • 天然线程安全:因为不可变,不需要同步

基础用法

// 1. 声明 ScopedValue 变量(通常作为 static final 字段)
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
private static final ScopedValue<Long> USER_ID = ScopedValue.newInstance();

// 2. 在限定作用域内绑定值
ScopedValue.where(REQUEST_ID, "req-001")
        .where(USER_ID, 42L)
        .run(() -> {
            // 作用域内任何方法都可以读取
            System.out.println("当前请求: " + REQUEST_ID.get());
            System.out.println("当前用户: " + USER_ID.get());
            processRequest();  // 内部也能读到
        });
// 3. 离开作用域后,REQUEST_ID.get() 会抛 NoSuchElementException

不可变性——编译时保证

ScopedValue.where(USER_NAME, "张三").run(() -> {
    System.out.println(USER_NAME.get());  // "张三"
    // USER_NAME.set("李四");  // ❌ 编译错误!ScopedValue 没有 set 方法
});

这提供了 编译期保证:一旦绑定,不可修改,完全消除误修改的问题。

与虚拟线程配合

private static final ScopedValue<Long> USER_ID = ScopedValue.newInstance();
private static final ScopedValue<String> USER_NAME = ScopedValue.newInstance();

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> {
        ScopedValue.where(USER_ID, userId)
                .where(USER_NAME, userName)
                .run(() -> {
                    // 虚拟线程自动继承 ScopedValue 绑定
                    System.out.println("处理: " + USER_NAME.get() + " (#" + USER_ID.get() + ")");
                });
        return null;
    });
}

边界情况——嵌套绑定

// 外层值和内层值互不干扰
ScopedValue.where(USER_NAME, "外层值").run(() -> {
    System.out.println(USER_NAME.get());  // "外层值"
    ScopedValue.where(USER_NAME, "内层值").run(() -> {
        System.out.println(USER_NAME.get());  // "内层值"(覆盖)
    });
    System.out.println(USER_NAME.get());  // "外层值"(还原)
});

有返回值的 call() 方法

var result = ScopedValue.where(USER_NAME, "返回值的例子")
        .call(() -> "Hello, " + USER_NAME.get());
// result = "Hello, 返回值的例子"

ThreadLocal vs ScopedValue 对比

维度 ThreadLocal ScopedValue
可变性 可修改(set/remove) 不可变(编译期保证)
内存泄漏 忘记 remove 导致 OOM 自动回收,无泄漏风险
子线程继承 需 InheritableThreadLocal 自动继承
性能 每个线程一个副本 不可变,无副本开销
入侵性 需要在 finally 中清理 try-with-resources 语义

此特性在 Java 21 为预览,Java 22 正式发布。编译运行时需要 --enable-preview

3. 集合与 Stream 增强

3.1 有序集合

有序集合(Sequenced Collections,JEP 431,Java 21 正式)。

Java 8 的问题ListDeque 有首尾操作,但 SetMap 没有统一的首尾访问接口。想获取 LinkedHashSet 的最后一个元素?只能遍历。想逆序遍历 LinkedHashMap?需要手动获取 entrySet 然后转为 ArrayList 反转。

Java 21 的解决方案

新增三个接口,统一了所有有序集合的操作:

SequencedCollection  ← List, Deque, ArrayDeque
SequencedSet         ← LinkedHashSet, TreeSet
SequencedMap         ← LinkedHashMap, TreeMap

SequencedCollection(List、Deque)新增的方法

var list = new ArrayList<>(List.of("A", "B", "C", "D"));

// Java 8 方式
String first = list.isEmpty() ? null : list.get(0);
String last = list.isEmpty() ? null : list.get(list.size() - 1);

// Java 21 方式(内置方法,直接抛 NoSuchElementException 而非静默 null)
list.getFirst();       // A
list.getLast();        // D
list.addFirst("First");
list.addLast("Last");
list.removeFirst();
list.removeLast();

// 逆序视图(原 list 的修改会反映到逆序视图)
var reversed = list.reversed();  // [D, C, B, A]
list.add("E");
System.out.println(reversed);    // [E, D, C, B, A] — 视图,实时更新

SequencedSet(LinkedHashSet、TreeSet)

var set = new LinkedHashSet<>(List.of("X", "Y", "Z"));
set.getFirst();         // X
set.getLast();          // Z
set.addFirst("NewFirst");
set.addLast("NewLast");
set.removeFirst();
set.removeLast();
set.reversed();         // [Z, Y, X]

// TreeSet 同样适用
var treeSet = new TreeSet<>(Set.of(5, 3, 1, 4, 2));
treeSet.getFirst();     // 1(最小值)
treeSet.getLast();      // 5(最大值)
treeSet.reversed();     // [5, 4, 3, 2, 1]

SequencedMap(LinkedHashMap、TreeMap)

var map = new LinkedHashMap<String, Integer>();
map.put("one", 1);
map.put("two", 2);
map.put("three", 3);

// 首尾 entry
map.firstEntry();        // one=1
map.lastEntry();         // three=3

// 添首/添尾
map.putFirst("zero", 0);
map.putLast("four", 4);

// 逆序
map.reversed();          // {three=3, two=2, one=1, zero=0}

// 有序遍历
map.sequencedEntrySet().forEach(e -> System.out.println(e.getKey() + "=" + e.getValue()));
// 逆序遍历
map.reversed().sequencedEntrySet().forEach(e -> System.out.println(e.getKey() + "=" + e.getValue()));

3.2 Stream

Stream API 增强(Java 9~16 持续引入)。

toList()(Java 16)

最常用的增强之一,比 collect(Collectors.toList()) 简洁得多,且返回不可变列表。

List<String> oldWay = stream.filter(w -> w.length() > 4)
                            .collect(Collectors.toList());   // 可变

var newWay = stream.filter(w -> w.length() > 4)
                   .toList();                                 // 不可变,更简洁

newWay.add("extra"); // ❌ 抛 UnsupportedOperationException

mapMulti()(Java 16)

flatMap 的高效替代,避免为每个元素创建中间 Stream 对象。

// flatMap 方式:每个元素需要创建一个 Stream
var flatMapped = words.stream()
        .flatMap(s -> s.chars().mapToObj(c -> (char) c))
        .toList();

// mapMulti 方式:直接向 consumer 推送,无中间对象
var multiMapped = words.stream()
        .mapMulti((String s, Consumer<Character> consumer) -> {
            for (char c : s.toCharArray()) {
                consumer.accept(c);
            }
        })
        .toList();

// mapMulti 更适合过滤+转换的场景
var numbers = List.of(1, "two", 3, "four", 5);
var onlyInts = numbers.stream()
        .mapMulti((Object item, Consumer<Integer> consumer) -> {
            if (item instanceof Integer i) {
                consumer.accept(i * 2);
            }
        })
        .toList();  // [2, 6, 10]

takeWhile / dropWhile(Java 9)

var numbers = List.of(2, 4, 6, 7, 8, 10, 11);

// takeWhile:取满足条件的元素,遇到第一个不满足的停止
numbers.stream().takeWhile(n -> n % 2 == 0).toList();  // [2, 4, 6]

// dropWhile:丢弃满足条件的元素,从第一个不满足的开始保留
numbers.stream().dropWhile(n -> n % 2 == 0).toList();  // [7, 8, 10, 11]

Stream.ofNullable()(Java 9)

// 安全处理可能的 null
Stream.ofNullable(null).count();  // 0

// 配合 Optional 链式安全访问
var value = Optional.ofNullable(map.get(key))
        .stream()                    // Java 9: Optional.stream()
        .flatMap(Stream::ofNullable)
        .toList();

Collectors.teeing()(Java 12)

同时应用两个收集器然后合并结果。

var numbers = List.of(3, 7, 2, 9, 5);

// 同时计算总和和数量,合并为平均值
var avg = numbers.stream()
        .collect(Collectors.teeing(
                Collectors.summingInt(Integer::intValue),   // 收集器1:求和
                Collectors.counting(),                       // 收集器2:计数
                (sum, count) -> sum / (double) count        // 合并函数
        ));
// 平均值为 5.2

4. API 升级

4.1 HttpClient

全新的 HTTP 客户端(Java 11 正式引入)。

Java 8 方式:使用 HttpURLConnection,API 极其古老繁琐,不支持 HTTP/2,不支持异步请求。

// Java 8 —— HttpURLConnection
URL url = new URL("http://example.com/api");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
conn.connect();

int status = conn.getResponseCode();
InputStream in = conn.getInputStream();  // 同步阻塞
// 读取流... 手动处理编码...
conn.disconnect();

Java 11+ 方式java.net.http.HttpClient,支持 HTTP/1.1 和 HTTP/2,同步/异步都支持,API 设计现代化。

// 构建客户端(可复用)
var client = HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_2)
        .followRedirects(HttpClient.Redirect.NORMAL)
        .connectTimeout(Duration.ofSeconds(10))
        .build();

// 同步 GET 请求
var request = HttpRequest.newBuilder()
        .uri(URI.create("https://httpbin.org/get?name=张三"))
        .GET()
        .timeout(Duration.ofSeconds(5))
        .build();

HttpResponse<String> response = client.send(request,
        HttpResponse.BodyHandlers.ofString());

System.out.println("状态码: " + response.statusCode());
System.out.println("响应体: " + response.body());

POST 请求(JSON 格式)

var json = """
        {"username": "zhangsan", "email": "zhang@example.com"}
        """;

var request = HttpRequest.newBuilder()
        .uri(URI.create("https://httpbin.org/post"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(json))
        .build();

var response = client.send(request, HttpResponse.BodyHandlers.ofString());

异步请求(CompletableFuture)

// 异步 GET —— 不阻塞当前线程
CompletableFuture<String> future = client
        .sendAsync(request, HttpResponse.BodyHandlers.ofString())
        .thenApply(r -> "状态码: " + r.statusCode() + ", 长度: " + r.body().length());

// 可以组合多个异步请求
String result = future.orTimeout(10, TimeUnit.SECONDS).join();
System.out.println(result);

并发请求 + 虚拟线程

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    var futures = urls.stream()
            .map(url -> executor.submit(() -> {
                var req = HttpRequest.newBuilder()
                        .uri(URI.create(url)).GET().build();
                var resp = client.send(req, BodyHandlers.ofString());
                return url + " -> " + resp.statusCode();
            }))
            .toList();
    futures.forEach(f -> System.out.println(f.get()));
}

4.2 String

Java 11~15 给 String 类添加了大量实用方法,从日常使用的角度来说,这些改进的实用性甚至超过一些语言特性。

isBlank()(Java 11)——比 isEmpty() 更实用

"   ".isBlank();        // true(纯空格也是空)
"".isBlank();           // true
"\t\n".isBlank();       // true(换行制表符也算空)
"abc".isBlank();        // false

lines()(Java 11)——按行分割为 Stream

var multiline = """
        第一行
        第二行
        第三行
        """;

multiline.lines().count();       // 3
multiline.lines()
    .map(line -> "> " + line)
    .collect(Collectors.joining("\n"));

strip() / stripLeading() / stripTrailing()(Java 11)——比 trim() 更强

var text = "   Hello, World!  ";  // 全角空格

text.trim();             // ❌ "   Hello, World!"(不识别全角空格)
text.strip();            // ✅ "Hello, World!"(全角空格也能去除)
text.stripLeading();     // ✅ "Hello, World!  "
text.stripTrailing();    // ✅ "   Hello, World!"

repeat()(Java 11)——重复字符串

"*".repeat(5);            // "*****"
"=-".repeat(3);           // "=-=-=-"

// 实用场景:格式化分隔线
"-".repeat(30);
// 输出:------------------------------

transform()(Java 12)——链式转换

var result = "  hello world  "
        .transform(String::strip)         // "hello world"
        .transform(String::toUpperCase)   // "HELLO WORLD"
        .transform(s -> new StringBuilder(s).reverse().toString());
        // "DLROW OLLEH"

// 也可以转成其他类型
var number = "42".transform(Integer::parseInt);
// number 类型为 Integer

formatted()(Java 15)——String.format 的实例方法

// Java 8
String old = String.format("姓名: %s, 年龄: %d", "张三", 25);

// Java 15+
String msg = "姓名: %s, 年龄: %d".formatted("张三", 25);

// 在文本块中尤其好用
String template = """
        ===========
        用户: %s
        角色: %s
        ===========
        """.formatted("zhangsan", "管理员");

4.3 数字格式化

Java12引入

var shortFmt = NumberFormat.getCompactNumberInstance(
        Locale.CHINA, NumberFormat.Style.SHORT);
var longFmt = NumberFormat.getCompactNumberInstance(
        Locale.CHINA, NumberFormat.Style.LONG);

shortFmt.format(1000);          // "1000"(Short 格式 1000 以下不缩略)
shortFmt.format(1_000_000);     // "100万"
shortFmt.format(1_234_567);     // "123万"
shortFmt.format(50_000_000_000L); // "500亿"

longFmt.format(1_000_000);      // "100万"(中文语境 LONG 和 SHORT 相同)

4.4 Emoji 检测

Java 21引入

Character.isEmoji(0x1F600);                // true(😀笑脸)
Character.isEmojiPresentation(0x1F600);    // true
Character.isEmoji('A');                    // false
Character.isEmoji('中');                   // false
Character.isEmoji('3');                    // true(数字 3 的 emoji 变体)

5. 集合工厂方法

Java 8 方式:创建不可变集合需要 Collections.unmodifiableList() 包装,步骤繁琐。

Java 9+ 方式List.of() / Set.of() / Map.of() 一步到位,直接创建不可变集合。

var list = List.of("A", "B", "C");         // 不可变列表
var set = Set.of(1, 2, 3);                  // 不可变集合
var map = Map.of("k1", "v1", "k2", "v2");   // 不可变映射

list.add("X");  // ❌ 抛 UnsupportedOperationException

6. 杂项 API 增强

// Optional.orElseThrow()(Java 10)
Optional.of("Hello").orElseThrow();
Optional.empty().orElseThrow();  // 抛 NoSuchElementException

// Optional.isEmpty()(Java 11)
Optional.empty().isEmpty();  // true

// Collection.toArray(String[]::new)(Java 11)
String[] arr = list.toArray(String[]::new);

// Files.mismatch()(Java 12)比较两个文件
Files.mismatch(file1, file2);  // -1 相同, 否则返回第一个差异位置

// InputStream.readAllBytes()(Java 9)
byte[] bytes = inputStream.readAllBytes();

// Objects.requireNonNullElse(Java 9)
Objects.requireNonNullElse(name, "默认名称");

// Compact Number Format(Java 12)
var fmt = NumberFormat.getCompactNumberInstance(Locale.CHINA, Style.SHORT);
fmt.format(1_000_000);  // "100万"

// Character.isEmoji(Java 21)
Character.isEmoji(0x1F600);  // true

7. 预览特性

以下特性在 Java 21 中是预览/孵化状态,需要 --enable-preview 编译运行,在后续版本中已转为正式。

Foreign Function & Memory API(JEP 454,Java 21 第三次预览,Java 22 正式):

安全操作堆外内存,替代 sun.misc.Unsafe

// Arena 管理堆外内存生命周期
try (var arena = Arena.ofConfined()) {
    // 分配 100 字节堆外内存
    var segment = arena.allocate(100);

    // 写入
    for (int i = 0; i < 10; i++) {
        segment.set(ValueLayout.JAVA_BYTE, i, (byte) ('A' + i));
    }

    // 读取
    for (int i = 0; i < 10; i++) {
        System.out.print((char) segment.get(ValueLayout.JAVA_BYTE, i));
    }
    // 输出:ABCDEFGHIJ
}
// 离开 try 块,Arena 自动释放所有堆外内存

// 结构化布局
var pointLayout = MemoryLayout.structLayout(
        ValueLayout.JAVA_INT.withName("x"),
        ValueLayout.JAVA_INT.withName("y")
);
try (var arena = Arena.ofConfined()) {
    var segment = arena.allocate(pointLayout);
    segment.set(ValueLayout.JAVA_INT, 0, 42);   // x
    segment.set(ValueLayout.JAVA_INT, 4, 99);   // y
}

总结

从 Java 8 升级到 Java 21,不仅仅是版本号的跳跃,更是开发效率和程序性能的全面飞跃:

维度 Java 8 Java 21
代码简洁性 样板代码多 Records 减少 90% 样板代码
类型匹配 instanceof + 强制转换 模式匹配一步到位
switch 语句,仅支持整型/枚举/字符串 表达式,支持任何类型 + null + 守卫
多行字符串 拼接噩梦 文本块优雅解决
并发模型 平台线程 + 线程池 虚拟线程百万级并发
继承控制 要么 final 要么开放 sealed 精确控制
HTTP 调用 HttpURLConnection 繁琐 HttpClient 简洁优雅
字符串操作 方法有限 isBlank/strip/repeat 等实用方法
不可变集合 需要包装 List.of/Set.of/Map.of 直接创建
有序集合 首尾操作需要遍历 SequencedCollection 统一支持

升级 Java 21,不仅是满足 SpringBoot 3.x 的版本要求,更重要的是用更少的代码、更高的性能来完成工作。



END.


#java #spring #springboot
目录
复制 复制成功