为了满足线上安全审计的要求,公司很久前就完成从 JDK 8 到 JDK 21 的迁移。尽管生产环境已经‘先行一步’,但在实际开发中,我发现自己对新特性的运用仍处于‘原地踏步’的状态。趁着今日复盘,我决定梳理下从JDK8 升级到 JDK 21 后,有哪些好用的新特性.
语法特性
局部变量类型推断
通过 var 关键字减少繁琐的类型声明(JDK 10)
JDK 8 (旧写法):
// 类型声明非常冗长,左右两边重复定义
Map<String, List<UserDTO>> userMap = new HashMap<String, List<UserDTO>>();
// 迭代器声明也很繁琐
for (Map.Entry<String, List<UserDTO>> entry : userMap.entrySet()) {
List<UserDTO> users = entry.getValue();
}
JDK 21 (新写法):
// 编译器自动推断类型
var userMap = new HashMap<String, List<UserDTO>>();
// 在循环中使用,可读性较好
for (var entry : userMap.entrySet()) {
var users = entry.getValue();
}
文本块
使用 """ 编写多行字符串,再也不用手动拼 \n 和 + 号了(JDK 15)
JDK 8 (旧写法):
// 繁琐的引号和拼接方式
String json = "{\n" +
" "name": "jdk",\n" +
" "version": "21",\n" +
" "status": "LTS"\n" +
"}";
JDK 21 (新写法):
// 干净清爽
String json = """
{
"name": "jdk",
"version": "21",
"status": "LTS"
}
""";
未命名模式与变量
参照python等语言,用下划线 _ 代替不再使用的变量,显著提升代码的可读性,减少静态检查警告。
try {
int number = Integer.parseInt(input);
} catch (NumberFormatException _) { // 显式表示我们不关心异常的具体信息
System.out.println("输入格式有误,请输入数字。");
}
for (int _ = 0; _ < 5; _++) { // 只执行 5 次,不需要循环变量
System.out.println("正在初始化...");
}
instanceof 模式匹配
直接在 instanceof 判断后定义变量,无需强转。
JDK 8 (旧写法):
if (obj instanceof String) {
String s = (String) obj; // 显式强转
System.out.println(s.length());
}
JDK 21 (新写法):
if (obj instanceof String s) {
// 这里的 s 已经自动转换好类型了
System.out.println(s.length());
}
Switch 表达式与模式匹配
switch 已经从单纯的控制流语句进化成了表达式,并全面支持数据类型的模式匹配(JDK 14/21)。
Switch 作为表达式
String day = "MONDAY";
int numLetters = switch (day) {
case "MONDAY", "FRIDAY", "SUNDAY" -> 6;
case "TUESDAY" -> 7;
case null -> -1; // 支持 null 值处理
default -> 0;
};
类型模式匹配
public static String formatterPattern(Object obj) {
return switch (obj) {
case Integer i -> String.format("Integer: %d", i);
case Long l -> String.format("Long: %d", l);
case Double d -> String.format("Double: %f", d);
case String s -> String.format("String: %s", s);
default -> obj.toString();
};
}
守卫模式
在 case 标签后紧跟 when 表达式,只有当类型匹配 且 when 后面的布尔表达式为 true 时,该分支才会执行。
Object obj = 100;
switch (obj) {
case Integer i when i > 0 -> System.out.println("正数");
case Integer i when i < 0 -> System.out.println("负数");
case Integer i -> System.out.println("这是零");
default -> System.out.println("不是整数");
}
注意区别
-> (箭头) :代表“只选其一”,执行完即刻结束。
: (冒号) :代表“从这里开始”,如果不写 break 就会一直往下走。
Record类型
自动生成构造函数、Getter、equals、hashCode 和 toString (JDK 16)
JDK 8 (旧写法):
// 使用 Lombok 的传统写法
@Getter
@AllArgsConstructor
@EqualsAndHashCode
@ToString
public final class UserDTO { // 注意是 final
private final String name; // 注意是 final
private final Integer age; // 注意是 final
}
JDK 21 (新写法):
// 成员变量默认是final,且没有无参构造函数,对象一旦创建无法修改
public record UserDTO(String name, Integer age) {
// 由于 record 是不可变的, 若要更新字段,则需实现 Wither 模式. 即手动增加拷贝方法,这个方法通常以 "with" 开头
// 补充说明:
// Wither 模式通过提供类似 withXxx 的方法,返回一个包含更新值的新实例,而保持原对象完全不变。
// Setter 模式是传统面向对象编程中常见的做法,通过提供 setXxx 方法来直接修改对象实例内部的私有成员变量。
public UserDTO withAge(Integer newAge) {
return new UserDTO(this.name, newAge); // 返回一个新实例
}
}
Record类型 的解构
在 instanceof 或 switch 中直接对 Record 进行解构拆包(JDK 21)。
JDK 8 (旧写法):
if (obj instanceof Point p) {
int x = p.x();
int y = p.y();
System.out.println("X: " + x + ", Y: " + y);
}
JDK 21 (新写法):
instanceof 解构示例:
record Point(int x, int y) {}
public void print(Object obj) {
if (obj instanceof Point(int x, int y)) { // 直接解构得到变量x和变量y
System.out.println("X: " + x + ", Y: " + y);
}
}
switch 解构示例:
// 声明Record类
record Point(double x, double y) {}
record Window(Point topLeft, Point bottomRight)
public void checkWindow(Object obj) {
switch (obj) {
// 解构一层嵌套:直接得到 Window对象 里的两个 Point 对象
case Window(Point(double x1, double y1), Point(double x2, double y2))
when x1 == x2 ->
System.out.println("这是一个垂直窗口");
// 如果只需要 Window 本身
case Window w ->
System.out.println("普通窗口对象: " + w);
default -> System.out.println("不是窗口");
}
}
密封类
使用 sealed 和 permits 关键字精确控制哪些类可以继承当前类(JDK 17)
简单来说,在密封类出现之前,Java 的类继承只有两个极端:要么完全开放(默认),要么完全禁死(final)。密封类提供了一种“中间态”:我允许别人继承我,但必须是我指定的“亲儿子”。
// 要定义一个密封类,你需要使用 sealed 修饰父类,并用 permits 声明哪些子类有权继承它。
// 1. 定义密封父类:只允许三个“亲儿子”
public sealed class Light permits RedLight, GreenLight, YellowLight {}
// --- 子类的三种选择 ---
// 选择 A: final (到此为止,不准再有孙子类)
public final class RedLight extends Light {}
// 选择 B: non-sealed (彻底开放,谁都能继承我)
public non-sealed class GreenLight extends Light {}
// 选择 C: sealed (继续套娃,只允许特定的孙子类)
public sealed class YellowLight extends Light permits FlashingYellow {}
// 孙子类(属于 YellowLight 的白名单)
final class FlashingYellow extends YellowLight {}
同时,密封类的子类不能“含糊其辞”,它们必须在三个修饰符中选择其一,以明确继承链的未来:
final:禁止进一步继承。继承链到此为止。sealed:继续开启密封模式。它本身也是密封类,需要再次使用permits指定它的子类。non-sealed:打破密封限制,允许任何类继承它。这是 Java 为了保持灵活性留出的出口。
核心库增强
String 增强方法
isBlank(), lines(), strip(), repeat(n)。这些方法让很多原本依赖 StringUtils 的代码变得原生化。
// 1. 检查是否全是空格 (isBlank)
String input = " ";
// 以前:StringUtils.isBlank(input)
boolean isBlank = input.isBlank(); // true
// 2. 去除首尾空格 (strip) —— 比 trim() 更智能,支持中文全角空格
String name = " Java 21 ";
// 以前:name.trim()
String cleanName = name.strip(); // "Java 21"
// 3. 快速重复字符串 (repeat)
// 以前:StringUtils.repeat("*", 5)
String star = "*".repeat(5); // "*****"
// 4. 按行拆分 (lines)
String logs = "Error1\nError2";
// 以前:logs.split("\n")
long count = logs.lines().count(); // 2 (返回的是 Stream,处理更方便)
集合工厂方法
List.of(), Set.of(), Map.of() 快速创建不可变集合(JDK 9)。
// 1. List.of() - 快速创建不可变列表
List<String> list = List.of("Java", "Python", "Go");
// 2. Set.of() - 快速创建不可变集合 (注意:不能有重复元素,否则抛异常)
Set<Integer> set = Set.of(1, 2, 3);
// 3. Map.of() - 快速创建不可变映射 (最多支持 10 个键值对)
Map<String, Integer> map = Map.of("Apple", 10, "Banana", 20);
// 4. Map.ofEntries() - 超过 10 个键值对时使用
Map<String, Integer> bigMap = Map.ofEntries(
Map.entry("A", 1),
Map.entry("B", 2)
// ... 可以放无限个
);
Sequenced Collections (序列集合)
新增 SequencedCollection 接口,统一了获取集合首尾元素的操作(JDK 21)。
// 1. 列表 (List)
List<String> list = new ArrayList<>(List.of("A", "B", "C"));
// 2. 有序集合 (LinkedHashSet) - 以前很难拿最后一个元素
LinkedHashSet<String> set = new LinkedHashSet<>(List.of("X", "Y", "Z"));
// --- 统一的操作方式 ---
// 获取首尾
System.out.println(list.getFirst()); // "A"
System.out.println(set.getLast()); // "Z"
// 反转视图 (非常高效,不复制数据)
System.out.println(list.reversed()); // [C, B, A]
// 在首尾添加/删除 (注意:List 支持,Set 如果已存在则会移动位置)
list.addFirst("Start");
list.addLast("End");
list.removeFirst();
list.removeLast();
新的 HttpClient
原生的 HTTP/2 客户端(JDK 11),支持异步和 WebSocket,相比老旧的 HttpURLConnection 好用太多了。
// 1. 创建客户端 (可以配置超时、HTTP版本、重定向等)
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
// 2. 构建请求
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/get"))
.header("Accept", "application/json")
.GET()
.build();
// 3. 发送并处理响应 (BodyHandlers 帮你自动把流转成字符串)
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("状态码: " + response.statusCode());
System.out.println("响应体: " + response.body());
// 异步请求
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println)
.join(); // 等待异步完成
Stream API 增强
增加了toList() ,takeWhile, dropWhile等更实用的方法。
List<String> list = List.of("Apple", "Banana", "Cherry");
// 注意:生成的 result 是不可变集合
List<String> result = list.stream()
.filter(s -> s.startsWith("A"))
// .collect(Collectors.toUnmodifiableList()); // 以前
.toList(); //现在
截断流
List<Integer> numbers = List.of(1, 2, 3, 10, 4, 5);
// takeWhile: 只要小于 10 就一直拿,遇到 10 立即停止
// 结果:[1, 2, 3]
List<Integer> taken = numbers.stream()
.takeWhile(n -> n < 10)
.toList();
// dropWhile: 只要小于 10 就一直丢,遇到 10 开始拿剩下的所有
// 结果:[10, 4, 5]
List<Integer> dropped = numbers.stream()
.dropWhile(n -> n < 10)
.toList();
Optional 增强
增加了 ifPresentOrElse, or, stream 等方法。
Optional<String> username = Optional.ofNullable(getUser());
// 以前:if/else 模式
// 现在:声明式逻辑
username.ifPresentOrElse(
name -> System.out.println("欢迎你:" + name), // 存在时执行
() -> System.out.println("请先登录") // 不存在时执行
);
Optional<String> config = getCacheConfig() // 优先从缓存拿
.or(() -> getDatabaseConfig()) // 缓存没有,去数据库拿
.or(() -> Optional.of("DefaultConfig")); // 都没拿,给个默认 Optional
List<String> userIds = List.of("1", "2", "3");
List<User> users = userIds.stream()
.map(id -> findUserById(id)) // 返回 Optional<User>
.flatMap(Optional::stream) // 关键!自动过滤掉空的 Optional,并提取出里面的 User
.toList();
// 以前得写 .filter(Optional::isPresent).map(Optional::get)
并发与性能
虚拟线程
JDK 21 最核心的特性。 作为一种轻量级线程,它允许在普通硬件环境下轻松创建数百万个实例,显著提升了阻塞式编程(如基于 Servlet 架构的任务)的并发处理能力。由于虚拟线程本质上是在少量平台线程上进行多路复用,它极大地优化了 I/O 密集型场景的资源利用率,但并不适用于 CPU 密集型任务。
直接创建虚拟线程
Thread.startVirtualThread(() -> {
System.out.println("你好,我是虚拟线程: " + Thread.currentThread());
});
使用 ExecutorService (推荐):
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
// 执行耗时的 I/O 操作(如查数据库、调 API)
System.out.println("正在处理并发任务...");
});
} // 自动等待所有任务完成并关闭
ZGC垃圾回收器
ZGC 专为极低延迟设计。在 JDK 21 中,它正式支持了分代收集,能够更频繁地回收短命对象。无论内存是 8MB 还是 16TB,停顿时间均能控制在 1ms 以内,基本消灭了“Stop The World”带来的卡顿感。
结合使用
Record类 + 参数校验
利用 Record 的 紧凑构造函数 ,在构造对象时实现极简的参数校验
public record GeoLocation(double longitude, double latitude) {
// Record类 特有的紧凑构造函数写法,没有参数列表
public GeoLocation {
if (longitude < -180 || longitude > 180) {
throw new IllegalArgumentException("经度范围不正确");
}
if (latitude < -90 || latitude > 90) {
throw new IllegalArgumentException("纬度范围不正确");
}
}
}
Record结合Optional + Stream + Switch
优雅的处理数据
// 定义数据模型 (Record)
public record Order(String id, double amount, String category) {}
public class DiscountService {
public double getDiscountedAmount(Optional<Order> orderOpt) {
return orderOpt
// stream流式
.stream()
.filter(order -> order.amount() > 0)
// switch 表达式
.map(order -> switch (order) {
case Order(var id, var amt, var cat) when cat.equals("ELECTRONICS") -> amt * 0.9;
case Order(var id, var amt, var cat) when cat.equals("BOOKS") -> amt * 0.8;
case Order(var id, var amt, var cat) -> amt;
})
// optional处理
.findFirst().orElse(0.0);
}
}
密封类结合模式匹配
这是密封类最强大的地方。在 Java 17+ 的 switch 表达式中,如果你对密封类进行模式匹配,编译器可以自动检查你是否覆盖了所有可能的情况。
// 使用 sealed 关键字限制实现类
public sealed interface Shape permits Circle, Square {}
final class Circle implements Shape { double radius; }
final class Square implements Square { double side; }
// 因为 Shape 只准有 Circle 和 Square,所以这里不需要写 default 分支
double area = switch (shape) {
case Circle c -> Math.PI * c.radius * c.radius;
case Square s -> s.side * s.side;
};
设计模式新写法
策略模式
传统写法:定义一个接口,然后写一大堆实现类(如 CreditCardStrategy, WeChatPayStrategy),最后可能还要用简单工厂来创建。 JDK 21 写法:使用 Sealed Interface + Record + Switch 模式匹配。
// 定义策略(数据)
public sealed interface PayStrategy {
record CreditCard(String cardNumber) implements PayStrategy {}
record WeChatPay(String openId) implements PayStrategy {}
record Cash() implements PayStrategy {}
}
// 执行策略(逻辑)
public void processPayment(PayStrategy strategy, double amount) {
switch (strategy) {
case PayStrategy.CreditCard(var code) -> System.out.println("刷卡:" + code);
case PayStrategy.WeChatPay(var id) -> System.out.println("微信支付:" + id);
case PayStrategy.Cash() -> System.out.println("现金支付");
// sealed限制PayStrategy只会有三个子类,因此不必写default逻辑
}
}
逻辑不再分散在各个子类中,而是集中在业务处理器里。这对于逻辑不复杂、策略相对固定的场景极其高效。
// 1. 使用信用卡支付
service.processPayment(new PayStrategy.CreditCard("6222-xxxx-xxxx-0001"), 100.0);
// 2. 使用微信支付
service.processPayment(new PayStrategy.WeChatPay("wx_user_9527"), 50.5);
// 3. 使用现金支付
service.processPayment(new PayStrategy.Cash(), 20.0);
建造者模式
传统写法:手写或使用 Lombok 的 @Builder,创建一个内部静态类来逐步构建对象。
JDK 21 写法:Wither 模式 + 链式构造。
// 链式更新
var user = new UserDTO("张三", 18)
.withAge(19)
.withEmail("test@test.com");
状态模式
传统写法:每个状态都是一个子类,状态切换逻辑分散在各个子类的方法中。
JDK 21 写法:Sealed Classes + 类型覆盖检查。
sealed interface ConnectionState {}
record Disconnected() implements ConnectionState {}
record Connecting(int attempts) implements ConnectionState {}
record Connected(String sessionId) implements ConnectionState {}
public ConnectionState next(ConnectionState current) {
return switch (current) {
case Disconnected() -> new Connecting(1);
case Connecting(int a) when a < 3 -> new Connecting(a + 1);
case Connecting(int a) -> new Disconnected();
case Connected(String id) -> current;
};
}
利用 switch 对密封类的强校验。如果你增加了一个新的 Record 状态而没有在 switch 中处理,编译器会直接报错。这比传统状态模式更难出错。
ConnectionState state = new Disconnected();
for (int i = 0; i < 5; i++) {
state = next(state); // 状态流转
}
责任链模式
结合 Optional 和 switch 的新增强,责任链可以写得非常扁平化,不再需要层层嵌套。
record LogRequest(String message, String level) {}
record AuthRequest(String username, String password) {}
public void handleRequest(Object request) {
switch (request) {
case LogRequest(String msg, var level) -> System.out.println("Log: " + msg);
case AuthRequest(var user, var password) -> authenticate(user, password);
case null -> throw new IllegalArgumentException("Request cannot be null");
default -> System.out.println("Unknown request");
}
}
装饰器模式
Record类的不可变性和简洁性使得创建包装类更加轻量。
interface Renderer { String render(String text); }
record PlainRenderer() implements Renderer {
public String render(String text) { return text; }
}
// 装饰器本身也可以是一个 Record
record BoldRenderer(Renderer inner) implements Renderer {
public String render(String text) {
return "<b>" + inner.render(text) + "</b>";
}
}
个人使用总结
- 引入 Record 类简化 POJO 样板代码.
- 增强 Switch 表达式与模式匹配,大幅简化了分支判断的代码.
- 融入现代语言语法:引入文本块 (Text Blocks)、局部变量推断 (var) 等特性,好像借鉴了python的写法.
- 引入虚拟线程 , 虽在表现上对标 Python 协程,但通过“阻塞即挂起”实现了类似 Netty EventLoop 的非阻塞高吞吐。
- 垃圾回收器越来越高级了, 程序员顶多改改最大堆内存
-Xmx就行了.