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
点开3.5.4文档:https://docs.spring.io/spring-boot/system-requirements.html
从官方文档可以看到,SpringBoot3.5.4需要Java17到最新版本的Java24之间的版本,不管是Java8还是Java11都已经不在支持的范围内。
二、新版SpringFramework的要求
新版SpringFramework已经到6.2.9版本,而我还在用5.2.15,同样落后了一个大版本号:https://spring.io/projects/spring-framework#learn
打开6.2.9版本的文档,可以看到该版本对Java版本的要求:https://docs.spring.io/spring-framework/reference/overview.html
同样的,新版本SpringFramework要求Java也至少得是Java17版本。
三、Java版本的选择
从SpringBoot以及SpringFramework的最新版本的要求来看,他们的运行环境至少得是Java17,那么我们应该升级到Java17吗?
打开Java官网看看:https://www.oracle.com/java/technologies/
可以看到,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 的问题:List 和 Deque 有首尾操作,但 Set 和 Map 没有统一的首尾访问接口。想获取 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.
注意:本文归作者所有,未经作者允许,不得转载