Java 8 至 Java 21 的核心特性梳理

0 阅读12分钟

为了满足线上安全审计的要求,公司很久前就完成从 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类型

自动生成构造函数GetterequalshashCodetoString (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类型 的解构

instanceofswitch 中直接对 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("不是窗口");
    }
}

密封类

使用 sealedpermits 关键字精确控制哪些类可以继承当前类(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);    // 状态流转
}

责任链模式

结合 Optionalswitch 的新增强,责任链可以写得非常扁平化,不再需要层层嵌套。

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 就行了.