编辑
2026-10-25
学习记录
00

目录

一、语言特性时间线
二、语法:从“写得少”到“表达得准”
1. var:局部变量类型推断(JDK 10)
2. switch 表达式(JDK 14 正式)
3. 文本块(JDK 15 正式)
4. record(JDK 16 正式)
5. instanceof 模式匹配(JDK 16 正式)
6. sealed(JDK 17 正式)
7. switch 模式匹配 + record 解构(JDK 21 正式)
8. 其他小特性
三、标准库:日常最常用的那些
1. 集合工厂与不可变集合
2. Stream
3. Optional
4. String 与 Files
5. HttpClient(JDK 11 正式)
6. 其他小工具
四、使用建议
五、小结

JDK 8 到 21 的语言变化,可以概括成一句话:让类型系统承担更多表达责任,让模板代码更少。

这篇是系列第一篇,只讲语言和标准库;JVM、GC、字符集、模块系统放到运行时篇,Spring 的落地放到迁移篇。

TL;DR

  • 语言主线:var(10)→ switch 表达式(14)→ 文本块(15)→ record(16)→ sealed(17)→ switch / record 模式匹配(21)。
  • 标准库优先用:List.of、Stream.toList、Optional.ifPresentOrElse、Files.readString、HttpClient。
  • 生产不要用预览特性;record 不要做 JPA 实体;List.of 不接受 null。

一、语言特性时间线

版本特性状态
9接口私有方法、try-with-resources 支持 effectively final 变量、匿名类支持 diamond正式
10var 局部变量类型推断正式
11lambda 参数使用 var正式
12switch 表达式预览
13文本块预览
14switch 表达式正式;record、instanceof 模式匹配正式 / 预览
15文本块正式;sealed class预览
16record 正式、instanceof 模式匹配正式正式
17sealed 正式;switch 模式匹配预览
19record 模式预览
21switch 模式匹配、record 模式正式;未命名变量、字符串模板正式 / 预览

按“敢不敢直接上生产”分类:

特性最低版本建议
var10新代码可用,别滥用
switch 表达式14可以直接用
文本块15SQL / JSON / 测试数据首选
record16DTO、配置、事件、查询结果
sealed17封闭领域模型、结果类型
switch / record 模式21新模块优先,老代码渐进

二、语法:从“写得少”到“表达得准”

1. var:局部变量类型推断(JDK 10)

java
var orders = orderRepository.findByStatus(OrderStatus.PAID); var total = orders.stream() .map(Order::amount) .reduce(BigDecimal.ZERO, BigDecimal::add); for (var order : orders) { log.info("order {}", order.id()); }

建议:

  • 只用于局部变量,不要用在字段、方法参数、返回值上。
  • 右侧类型一眼能看出来时用;链式调用很长、类型被隐藏时,反而降低可读性。
  • var 不会让 Java 变成动态类型,编译期类型仍然是确定的。
  • var x = null; 编译不过,因为推断不出类型。

2. switch 表达式(JDK 14 正式)

Java 8 的写法:

java
String label; switch (status) { case NEW: label = "新建"; break; case PAID: case SHIPPED: label = "进行中"; break; default: label = "未知"; }

Java 14+:

java
String label = switch (status) { case NEW -> "新建"; case PAID, SHIPPED -> "进行中"; default -> "未知"; };

需要多行逻辑时用 yield:

java
int weight = switch (status) { case NEW -> 1; case PAID -> { int base = 10; yield base * 2; } default -> 0; };

它最大的价值不是少写几个 break,而是 switch 变成了表达式:可以直接赋值、可以作为方法返回值,编译器还能检查分支覆盖。枚举的所有常量都覆盖时,可以省掉 default。

3. 文本块(JDK 15 正式)

java
String sql = """ select id, sku, qty from orders where status = ? """;

适合写 SQL、JSON、HTML、证书、测试数据。几个细节:

  • 结尾的 """ 决定缩进基准,公共缩进会被去掉。
  • 文本块里不需要转义双引号。
  • 行尾空格可以用 \s 保留。
  • 不要在文本块里拼接用户输入,SQL 注入不会因为换了语法就消失。

4. record(JDK 16 正式)

java
public record OrderView(long id, String sku, int qty, OrderStatus status) { public OrderView { if (qty < 0) { throw new IllegalArgumentException("qty must be >= 0"); } } }

record 自动生成:

  • 全参构造器
  • id()、sku() 这样的访问器
  • equals、hashCode、toString

它适合 DTO、查询结果、配置、事件、值对象。不适合:

  • JPA 实体:Hibernate 需要无参构造器、可变属性和代理,实体继续用普通类。
  • 需要大量可变状态的领域对象。
  • 需要继承的模型(record 不能继承其他类,但可以实现接口)。

注意 record 是“浅不可变”:如果组件是 List、数组,内部仍然可变,需要自己防御性拷贝。

5. instanceof 模式匹配(JDK 16 正式)

java
// Java 8 if (payload instanceof CreateOrderRequest) { CreateOrderRequest req = (CreateOrderRequest) payload; if (req.qty() > 0) { ... } } // Java 16+ if (payload instanceof CreateOrderRequest req && req.qty() > 0) { ... }

在 equals 里也很好用:

java
@Override public boolean equals(Object o) { return o instanceof Order that && id.equals(that.id); }

6. sealed(JDK 17 正式)

sealed 用来限定“谁能实现我”,配合 record 可以非常准确地表达一个封闭的结果集:

java
public sealed interface PaymentResult { record Success(String txId, Instant paidAt) implements PaymentResult {} record Failure(String code, String message) implements PaymentResult {} }

如果所有子类都在同一个编译单元里,permits 可以省略。子类必须显式声明自己是 final、sealed 或 non-sealed:

java
public sealed interface Shape permits Circle, FreeForm {} public final class Circle implements Shape {} public non-sealed class FreeForm implements Shape {}

它解决的是“类型集合是否封闭”的问题:编译器知道只有这几种结果,调用方就不需要写 default 兜底。

7. switch 模式匹配 + record 解构(JDK 21 正式)

java
String describe(PaymentResult result) { return switch (result) { case PaymentResult.Success(String txId, Instant paidAt) -> "支付成功: " + txId + " @ " + paidAt; case PaymentResult.Failure(String code, String msg) when code.equals("TIMEOUT") -> "超时,可重试"; case PaymentResult.Failure(String code, String msg) -> "失败: " + code + " " + msg; }; }

几个关键点:

  • when 是守卫条件,类似 if。
  • 因为是 sealed 类型,且两个子类型都覆盖了,不需要 default。
  • 如果 result 可能为 null,需要显式 case null -> ...,否则抛 NPE。
  • record 模式可以直接解构组件,不需要再调 txId()。

record 模式可以嵌套:

java
record Point(int x, int y) {} record Line(Point start, Point end) {} String describe(Object o) { return switch (o) { case Line(Point(int x1, int y1), Point(int x2, int y2)) -> "线段 (" + x1 + "," + y1 + ") -> (" + x2 + "," + y2 + ")"; case Point(int x, int y) -> "点 (" + x + "," + y + ")"; default -> "未知图形"; }; }

这是 8 → 21 里对业务代码风格影响最大的特性:以前用枚举 + if/else 或访问者模式表达的“封闭分支”,现在可以用类型系统直接表达。

8. 其他小特性

java
// JDK 9:try-with-resources 可以使用 effectively final 变量 var in = Files.newInputStream(path); try (in) { ... } // JDK 9:接口私有方法 interface PriceCalculator { default BigDecimal calculate(Order order) { return order.amount().multiply(rate()); } private BigDecimal rate() { return BigDecimal.ONE; } } // JDK 11:lambda 参数加注解 / var list.stream().map((@Nullable var s) -> s.trim()).toList();

JDK 21 还有两个预览特性值得关注,但不要在生产环境押注:

  • 未命名变量 _(JDK 22 转正)
  • 字符串模板 STR."..."(后来被撤回,不要基于它设计代码)

三、标准库:日常最常用的那些

版本API
9List.of/Set.of/Map.of、Stream.ofNullable/takeWhile/dropWhile、Optional.ifPresentOrElse/or/stream
10List.copyOf、orElseThrow()
11String.isBlank/strip/repeat/lines、Files.readString/writeString、HttpClient、Predicate.not、Optional.isEmpty
12Collectors.teeing、String.indent/transform、Files.mismatch
15String.formatted
16Stream.toList/mapMulti
17HexFormat、RandomGenerator
21SequencedCollection、Math.clamp

1. 集合工厂与不可变集合

java
var roles = List.of("admin", "user"); // 不可变,元素为 null 会 NPE var copy = List.copyOf(roles); // JDK 10 var names = orders.stream().map(Order::sku).toList(); // JDK 16,不可变

注意:

  • List.of、Set.of、Map.of 都是 null-hostile,元素为 null 直接 NPE。
  • Stream.toList() 返回不可变列表;需要继续修改时用 collect(Collectors.toList())。
  • List.copyOf 做的是浅拷贝,元素对象本身仍然共享。

JDK 21 的 SequencedCollection 让 List、Deque、LinkedHashSet 有了统一的首尾访问:

java
var first = list.getFirst(); var last = list.getLast(); var reversed = list.reversed(); // 视图,不是拷贝

2. Stream

java
var recent = orders.stream() .takeWhile(o -> o.createdAt().isAfter(cutoff)) .toList();

takeWhile / dropWhile 是 JDK 9 引入的,处理“有序数据的前缀/后缀”比 filter 更准确。

Collectors.teeing 可以一次遍历得到两个结果:

java
record OrderStats(long count, BigDecimal total) {} var stats = orders.stream().collect(Collectors.teeing( Collectors.counting(), Collectors.reducing(BigDecimal.ZERO, Order::amount, BigDecimal::add), OrderStats::new));

需要把一个元素展开成多个元素,又不想创建中间集合时,可以用 mapMulti:

java
List<String> tags = orders.stream() .<String>mapMulti((order, downstream) -> { order.tags().forEach(downstream); }) .toList();

3. Optional

java
repo.findById(id).ifPresentOrElse( order -> log.info("found {}", order.id()), () -> log.warn("order {} not found", id)); var order = repo.findById(id).orElseThrow(); var names = optional.map(Order::sku).stream().toList();

ifPresentOrElse、or、stream 是 JDK 9;orElseThrow() 无参版本是 JDK 10;isEmpty() 是 JDK 11。

注意:orElseThrow() 抛的是 NoSuchElementException。如果这是 API 的 404 语义,仍然应该抛自己的领域异常。

4. String 与 Files

java
if (sku.isBlank()) { ... } var lines = text.lines() .map(String::strip) .filter(s -> !s.isEmpty()) .toList(); var json = "{\"id\":%d}".formatted(id); // JDK 15 var content = Files.readString(Path.of("config.yaml")); // JDK 11 Files.writeString(Path.of("out.txt"), content);

其他顺手的:

java
String repeated = "ab".repeat(3); // JDK 11 String stripped = " x ".strip(); // JDK 11 String indented = "line".indent(4); // JDK 12 int pos = Files.mismatch(path1, path2); // JDK 12,返回第一个不同字节的位置

注意:Files.readString 适合本地文件,不适合 classpath 资源;Spring 里读 classpath 还是用 Resource。

5. HttpClient(JDK 11 正式)

java
var client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(2)) .version(HttpClient.Version.HTTP_2) .build(); var request = HttpRequest.newBuilder(URI.create("https://api.example.com/orders/" + id)) .header("Accept", "application/json") .timeout(Duration.ofSeconds(5)) .GET() .build(); var response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() != 200) { throw new IllegalStateException("HTTP " + response.statusCode()); } var body = response.body();

HttpClient 是不可变、线程安全的,应该复用,不要每次请求都 new。

在 Spring 项目里,业务代码优先用 RestClient / WebClient;直接使用 HttpClient 的场景通常是:非 Spring 模块、需要 HTTP/2、或者想显式利用虚拟线程的阻塞式调用。

6. 其他小工具

java
var hex = HexFormat.of().formatHex(bytes); // JDK 17 var random = RandomGeneratorFactory.of("L64X128MixRandom").create(42L); // JDK 17 var clamped = Math.clamp(value, 0, 100); // JDK 21

数字格式化也有一个对中文场景很实用的补充:

java
var fmt = NumberFormat.getCompactNumberInstance(Locale.CHINA, NumberFormat.Style.SHORT); fmt.format(12000); // "1.2万"

四、使用建议

  1. 新代码优先用新语法,老代码只在改动时顺手重构。 不要为了升级做全量重写,风险和收益不成正比。
  2. record 用在边界上。 Controller 的请求/响应、配置、事件、查询结果都很合适;JPA 实体、需要可变状态的领域对象不要用。
  3. sealed + switch 用在“分支必须穷尽”的地方。 支付结果、订单状态、事件类型这类封闭集合收益最大。
  4. 不要用预览特性上生产。 预览 API 需要 --enable-preview,类文件绑定 JDK 版本,后续版本还可能调整。
  5. 注意不可变集合的语义。 List.of 不接受 null,Stream.toList() 不能修改;传给会修改列表的框架前先确认。
  6. var 是工具,不是风格。 类型明显时用,类型被隐藏时不用;团队要有统一的代码评审标准。

五、小结

语言和标准库的升级,核心收益是表达力:同样的业务规则,用 record + sealed + switch 写出来,类型更准确、分支更难漏、代码更短。

下一篇讲运行时:为什么升级后启动会报 InaccessibleObjectException、G1 和 ZGC 怎么选、UTF-8 默认对中文项目意味着什么。

本系列共三篇:

  1. 语言篇:语法现代化与标准库
  2. 运行时篇:JPMS、GC、字符集与启动
  3. Spring 迁移篇:Boot 2.x → 3.x → 虚拟线程

本文作者:Weee

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!