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 | 正式 |
| 10 | var 局部变量类型推断 | 正式 |
| 11 | lambda 参数使用 var | 正式 |
| 12 | switch 表达式 | 预览 |
| 13 | 文本块 | 预览 |
| 14 | switch 表达式正式;record、instanceof 模式匹配 | 正式 / 预览 |
| 15 | 文本块正式;sealed class | 预览 |
| 16 | record 正式、instanceof 模式匹配正式 | 正式 |
| 17 | sealed 正式;switch 模式匹配 | 预览 |
| 19 | record 模式 | 预览 |
| 21 | switch 模式匹配、record 模式正式;未命名变量、字符串模板 | 正式 / 预览 |
按“敢不敢直接上生产”分类:
| 特性 | 最低版本 | 建议 |
|---|---|---|
var | 10 | 新代码可用,别滥用 |
| switch 表达式 | 14 | 可以直接用 |
| 文本块 | 15 | SQL / JSON / 测试数据首选 |
| record | 16 | DTO、配置、事件、查询结果 |
| sealed | 17 | 封闭领域模型、结果类型 |
| switch / record 模式 | 21 | 新模块优先,老代码渐进 |
var:局部变量类型推断(JDK 10)javavar 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; 编译不过,因为推断不出类型。Java 8 的写法:
javaString label;
switch (status) {
case NEW:
label = "新建";
break;
case PAID:
case SHIPPED:
label = "进行中";
break;
default:
label = "未知";
}
Java 14+:
javaString label = switch (status) {
case NEW -> "新建";
case PAID, SHIPPED -> "进行中";
default -> "未知";
};
需要多行逻辑时用 yield:
javaint weight = switch (status) {
case NEW -> 1;
case PAID -> {
int base = 10;
yield base * 2;
}
default -> 0;
};
它最大的价值不是少写几个 break,而是 switch 变成了表达式:可以直接赋值、可以作为方法返回值,编译器还能检查分支覆盖。枚举的所有常量都覆盖时,可以省掉 default。
javaString sql = """
select id, sku, qty
from orders
where status = ?
""";
适合写 SQL、JSON、HTML、证书、测试数据。几个细节:
""" 决定缩进基准,公共缩进会被去掉。\s 保留。javapublic 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、查询结果、配置、事件、值对象。不适合:
注意 record 是“浅不可变”:如果组件是 List、数组,内部仍然可变,需要自己防御性拷贝。
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);
}
sealed 用来限定“谁能实现我”,配合 record 可以非常准确地表达一个封闭的结果集:
javapublic sealed interface PaymentResult {
record Success(String txId, Instant paidAt) implements PaymentResult {}
record Failure(String code, String message) implements PaymentResult {}
}
如果所有子类都在同一个编译单元里,permits 可以省略。子类必须显式声明自己是 final、sealed 或 non-sealed:
javapublic sealed interface Shape permits Circle, FreeForm {}
public final class Circle implements Shape {}
public non-sealed class FreeForm implements Shape {}
它解决的是“类型集合是否封闭”的问题:编译器知道只有这几种结果,调用方就不需要写 default 兜底。
javaString 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。default。result 可能为 null,需要显式 case null -> ...,否则抛 NPE。txId()。record 模式可以嵌套:
javarecord 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 或访问者模式表达的“封闭分支”,现在可以用类型系统直接表达。
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 |
|---|---|
| 9 | List.of/Set.of/Map.of、Stream.ofNullable/takeWhile/dropWhile、Optional.ifPresentOrElse/or/stream |
| 10 | List.copyOf、orElseThrow() |
| 11 | String.isBlank/strip/repeat/lines、Files.readString/writeString、HttpClient、Predicate.not、Optional.isEmpty |
| 12 | Collectors.teeing、String.indent/transform、Files.mismatch |
| 15 | String.formatted |
| 16 | Stream.toList/mapMulti |
| 17 | HexFormat、RandomGenerator |
| 21 | SequencedCollection、Math.clamp |
javavar 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 有了统一的首尾访问:
javavar first = list.getFirst();
var last = list.getLast();
var reversed = list.reversed(); // 视图,不是拷贝
javavar recent = orders.stream()
.takeWhile(o -> o.createdAt().isAfter(cutoff))
.toList();
takeWhile / dropWhile 是 JDK 9 引入的,处理“有序数据的前缀/后缀”比 filter 更准确。
Collectors.teeing 可以一次遍历得到两个结果:
javarecord 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:
javaList<String> tags = orders.stream() .<String>mapMulti((order, downstream) -> { order.tags().forEach(downstream); }) .toList();
javarepo.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 语义,仍然应该抛自己的领域异常。
javaif (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);
其他顺手的:
javaString 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。
javavar 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、或者想显式利用虚拟线程的阻塞式调用。
javavar 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
数字格式化也有一个对中文场景很实用的补充:
javavar fmt = NumberFormat.getCompactNumberInstance(Locale.CHINA, NumberFormat.Style.SHORT);
fmt.format(12000); // "1.2万"
--enable-preview,类文件绑定 JDK 版本,后续版本还可能调整。List.of 不接受 null,Stream.toList() 不能修改;传给会修改列表的框架前先确认。var 是工具,不是风格。 类型明显时用,类型被隐藏时不用;团队要有统一的代码评审标准。语言和标准库的升级,核心收益是表达力:同样的业务规则,用 record + sealed + switch 写出来,类型更准确、分支更难漏、代码更短。
下一篇讲运行时:为什么升级后启动会报 InaccessibleObjectException、G1 和 ZGC 怎么选、UTF-8 默认对中文项目意味着什么。
本系列共三篇:
本文作者:Weee
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!