前言
这些年我见证过太多项目从JDK6一路升级到JDK8,然后就停在了那里。
"你发任你发,我用Java 8"成了很多团队的座右铭 。
但最近两年,情况开始变化了。
JDK17作为长期支持版本发布后,越来越多的新项目开始尝试拥抱现代Java。
美团的技术团队分享过一组数据:他们将核心服务升级到JDK17后,机器成本降低了约10%,性能和稳定性也大幅提升 。
有些小伙伴可能还不太适应新写法,看到项目里的代码第一反应是:"这是Java?"
今天,我就带大家系统性地看看JDK8到JDK17,代码风格到底发生了怎样的巨变。
希望对你会有所帮助。
更多项目实战在项目实战网:java突击队
01 模式匹配:告别冗余的类型转换
这是日常开发中最常用的改进。
在JDK8时代,我们写instanceof时,总要先判断类型,再强制转换:
java
// JDK8及之前:繁琐的强制转换
Object obj = "Hello Java";
if (obj instanceof String) {
// 必须显式转换
String str = (String) obj;
System.out.println("字符串长度:" + str.length());
}
JDK17带来了模式匹配,类型检查和变量绑定一步到位:
java
// JDK17:模式匹配,一步到位
Object obj = "Hello Java";
if (obj instanceof String str) { // 直接绑定变量
System.out.println("字符串长度:" + str.length()); // str直接可用
}
这种写法的好处不仅是少写了一行代码。更重要的是,变量作用域更合理 ------str只在类型判断为真后才存在,避免了不必要的类型转换风险 。
02 记录类:终结样板代码
在JDK8时代,定义一个纯数据载体类(DTO、VO等)是一件很痛苦的事情:
java
// JDK8:传统的POJO类
public class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
JDK16正式引入的**记录类(Record)**让这一切变得极其简单:
java
// JDK17:一行代码搞定
public record User(String name, int age) { }
没错,就这么简单!编译器会自动生成:
- 规范的构造器
name()和age()访问器(注意不是getName())equals()、hashCode()和toString()方法
记录类特别适合那些只承载数据、没有额外逻辑的类,代码量减少了70%以上 。
而且它是不可变的,天然适合并发场景 。
03 Switch表达式:从语句到表达式
传统的switch语句有很多痛点:容易忘记break导致穿透、不能直接返回值、语法冗长:
java
// JDK8:传统的switch语句
String dayType;
switch (day) {
case MONDAY:
case TUESDAY:
case WEDNESDAY:
case THURSDAY:
case FRIDAY:
dayType = "工作日";
break; // 容易遗漏
case SATURDAY:
case SUNDAY:
dayType = "周末";
break;
default:
throw new IllegalArgumentException("无效日期");
}
JDK14正式引入了switch表达式,语法更简洁、语义更安全:
java
// JDK17:switch表达式
String dayType = switch (day) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "工作日";
case SATURDAY, SUNDAY -> "周末";
default -> throw new IllegalArgumentException("无效日期");
}; // 注意这里需要分号
如果分支逻辑复杂,可以用代码块配合yield返回值:
java
int daysInMonth = switch (month) {
case JAN, MAR, MAY, JUL, AUG, OCT, DEC -> 31;
case APR, JUN, SEP, NOV -> 30;
case FEB -> {
if (isLeapYear) {
yield 29; // 使用yield返回值
} else {
yield 28;
}
}
};
新switch的箭头语法避免了穿透问题 ,同时可以直接返回值,让代码更简洁也更安全 。
04 文本块:终结字符串拼接地狱
在JDK8中写多行字符串(如JSON、SQL、HTML)简直是噩梦:
java
// JDK8:转义和拼接的噩梦
String json = "{\n" +
" \"name\": \"张三\",\n" +
" \"age\": 30,\n" +
" \"city\": \"北京\"\n" +
"}";
String sql = "SELECT *\n" +
"FROM users\n" +
"WHERE age > 18\n" +
"ORDER BY create_time DESC";
JDK15正式引入的文本块彻底改变了这一局面:
java
// JDK17:文本块,保持原始格式
String json = """
{
"name": "张三",
"age": 30,
"city": "北京"
}
""";
String sql = """
SELECT *
FROM users
WHERE age > 18
ORDER BY create_time DESC
""";
文本块的好处不仅仅是少写几个加号和转义符。
代码的可读性大幅提升,你可以直接复制粘贴JSON或SQL,无需任何修改 。
在MyBatis的注解中写SQL时,这种优势尤为明显 。
05 密封类:精准控制继承
在JDK8中,类的继承是开放的。只要一个类不是final的,理论上任何人都可以继承它。这在领域建模时可能导致设计被破坏。
JDK17引入了密封类(Sealed Classes),让你可以精确控制哪些类可以继承:
java
// JDK17:密封类定义
public sealed class Shape permits Circle, Rectangle, Triangle {
// 抽象方法定义
public abstract double area();
}
// 允许的继承类必须使用final、non-sealed或sealed修饰
public final class Circle extends Shape {
private double radius;
public Circle(double radius) { this.radius = radius; }
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public non-sealed class Rectangle extends Shape {
private double length, width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override
public double area() {
return length * width;
}
}
// 编译错误!Square不在permits列表中
// public class Square extends Shape { }
密封类通过permits关键字明确列出允许的子类,其他类无法继承。
这种设计让领域模型更加严谨和安全 。
结合模式匹配,可以实现非常优雅的多态处理。
06 API层面的增强
除了语言特性,JDK17还带来了大量API层面的改进。
6.1 集合工厂方法
在JDK8中创建不可变集合需要好几行代码:
java
// JDK8:创建不可变集合
List<String> list = Collections.unmodifiableList(
Arrays.asList("Java", "Python", "Go")
);
Set<Integer> set = Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(1, 2, 3))
);
Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
map = Collections.unmodifiableMap(map);
JDK9引入了集合工厂方法,一行搞定:
java
// JDK17:集合工厂方法
List<String> list = List.of("Java", "Python", "Go");
Set<Integer> set = Set.of(1, 2, 3);
Map<String, Integer> map = Map.of("A", 1, "B", 2, "C", 3);
// 对于超过10个键值对的情况,可以使用Map.ofEntries
这些方法创建的集合是不可变的,不允许修改,非常适合作为常量或返回只读数据 。
6.2 字符串增强
JDK11开始,String类增加了几个实用方法:
java
// JDK11+:字符串增强
String text = " Hello World ";
text.strip(); // "Hello World" (去除前后空格)
text.stripLeading(); // "Hello World " (只去除前面空格)
text.stripTrailing(); // " Hello World" (只去除后面空格)
"".isBlank(); // true (空或只包含空白字符)
" \n ".isBlank(); // true
"line1\nline2\nline3".lines().count(); // 3 (获取行数)
"Java".repeat(3); // "JavaJavaJava" (重复字符串)
这些小工具在日常开发中非常实用,可以替代很多手写的判断逻辑 。
6.3 Stream API增强
JDK16为Stream增加了toList()方法:
java
// JDK8:将Stream转换为List
Stream<String> stream = Stream.of("a", "b", "c");
List<String> list1 = stream.collect(Collectors.toList()); // 需要collect
// JDK17:更简洁
List<String> list2 = stream.toList(); // 直接toList()
注意stream.toList()返回的是不可变列表 ,这一点和collect(Collectors.toList())不同 。
6.4 更友好的NullPointerException
这是调试体验的大幅提升。
在JDK8中,链式调用出现空指针时,只能知道是哪一行,很难定位具体是哪个对象为空:
java
// JDK8:难以定位
Address address = new Address();
User user = new User();
user.setAddress(address);
// 假设address.getCity()返回null
System.out.println(user.getAddress().getCity().toLowerCase());
// 抛出:Exception in thread "main" java.lang.NullPointerException
// at NpeDemo.main(Main.java:6) // 只知道第6行出错
JDK16+增强了NPE信息,会明确指出是哪个方法的返回值导致了空指针:
java
// JDK17:精确定位
// 抛出:Exception in thread "main" java.lang.NullPointerException:
// Cannot invoke "String.toLowerCase()" because the return value of "Address.getCity()" is null
// at NpeDemo.main(Main.java:6)
这个改进在日常开发中极大提升了调试效率 。
07 JVM层面的进化
除了语言特性和API,JDK17在JVM层面也有很多重要改进。
7.1 默认GC:G1成为主流
JDK8的默认垃圾收集器是Parallel GC,适合批处理、注重吞吐量的场景。JDK9开始,G1(Garbage First)成为默认GC 。G1在保持高吞吐的同时,能更好地控制停顿时间。
7.2 低延迟GC:ZGC和Shenandoah
对于低延迟要求的应用,JDK17提供了更成熟的ZGC和Shenandoah:
- ZGC:GC停顿时间不超过10ms,甚至可以达到亚毫秒级
- Shenandoah:同样致力于降低GC停顿,与ZGC实现方式不同
启用方式很简单:
bash
java -XX:+UseZGC -jar myapp.jar
java -XX:+UseShenandoahGC -jar myapp.jar
美团的技术团队分享过,升级到JDK17并启用ZGC后,核心服务性能大幅提升,机器成本降低了约10% 。
7.3 容器感知
JDK8在容器中运行时,需要手动设置参数来感知内存和CPU限制。
JDK10+开始,JVM能自动识别容器资源限制 :
bash
# JDK8需要手动设置
java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -jar app.jar
# JDK17自动感知,无需额外参数
java -jar app.jar
08 升级迁移实用指南
如果你决定从JDK8升级到JDK17,以下是一些实用建议。
8.1 使用工具检查依赖
JDK17强封装了内部API,有些代码在JDK8下能用,在JDK17下可能无法编译或运行。可以使用jdeps工具分析依赖:
bash
jdeps -jdkinternals myapp.jar
这个命令会列出代码中依赖的JDK内部API,并提供替换建议 。
8.2 已移除或废弃的特性
升级时需要注意以下变化:
| 特性 | JDK8状态 | JDK17状态 | 替代方案 |
|---|---|---|---|
| Applet | 可用 | 已移除 | 寻找Web替代方案 |
| Java Web Start | 可用 | 已移除 | 使用应用打包工具 |
| Nashorn JavaScript引擎 | 可用 | 已移除 | GraalVM JavaScript |
| CMS垃圾收集器 | 可用 | 已移除 | G1或ZGC |
| Security Manager | 可用 | 弃用 | 使用其他安全机制 |
8.3 分阶段迁移策略
建议采用渐进式迁移策略 :
- 第一阶段:升级运行时,不改造代码。先验证兼容性和性能。
- 第二阶段:引入不影响架构的特性(记录类、文本块、Switch表达式)。
- 第三阶段:使用模式匹配、密封类等特性优化核心代码。
- 第四阶段:评估GC优化,对延迟敏感模块启用ZGC。
8.4 新工具链
JDK17带来了几个实用工具:
- jlink:创建自定义JRE镜像,显著减小部署体积
- jpackage:将应用打包为原生安装包(exe、dmg、deb)
- jshell:REPL工具,可以快速测试代码片段
09 演进路线图
从JDK8到JDK17,Java的演进呈现出清晰的脉络:

这张图清晰地展示了Java的演进路径:
- JDK8奠定了函数式编程基础
- JDK9-16在语法糖和API层面持续优化
- JDK17将多个预览特性转正,并强化了安全性和性能
总结
回到文章标题------为什么说JDK17前后的写法,差点让人认不出是Java?
让我们用一个完整的例子来感受这种差异:
JDK8风格:
java
public class User {
private String name;
private int age;
// 省略构造器、getter/setter、equals、hashCode、toString...
}
public class OrderService {
public String process(Object data) {
if (data instanceof User) {
User user = (User) data;
String json = "{\n" +
" \"name\": \"" + user.getName() + "\",\n" +
" \"age\": " + user.getAge() + "\n" +
"}";
return json;
}
return "{}";
}
}
JDK17风格:
java
public record User(String name, int age) { }
public class OrderService {
public String process(Object data) {
if (data instanceof User(String name, int age)) { // 模式匹配+解构
return """
{
"name": "%s",
"age": %d
}
""".formatted(name, age); // 文本块
}
return "{}";
}
}
更多项目实战在项目实战网:java突击队
这种差异不仅仅是代码量的减少,更是编程思维的进化:
- 声明式代替命令式:关注"是什么"而不是"怎么做"
- 编译时安全代替运行时检查:更多错误在编译期被发现
- 不可变设计代替可变状态:适应高并发时代的需求
美团升级JDK17的经验表明,这不仅是技术栈的更新,更是开发体验和代码质量的全面提升 。
随着JDK21的虚拟线程等革命性特性逐渐成熟,Java正在云原生和高并发领域重新焕发活力。
如果你还在JDK8上犹豫,不妨从今天开始,尝试在个人项目或新模块中使用JDK17。