JDK17的语法变化真大,这是Java?

前言

这些年我见证过太多项目从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 分阶段迁移策略

建议采用渐进式迁移策略 :

  1. 第一阶段:升级运行时,不改造代码。先验证兼容性和性能。
  2. 第二阶段:引入不影响架构的特性(记录类、文本块、Switch表达式)。
  3. 第三阶段:使用模式匹配、密封类等特性优化核心代码。
  4. 第四阶段:评估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。

相关推荐
014-code2 小时前
Spring Boot 自动配置原理深度解析
java·spring boot·后端·spring
用户76280782528632 小时前
SpringBoot入门实战指南
后端
老李的勺子2 小时前
Agent Skills 实战(1):Summarize
后端
sdanss2 小时前
Spring Boot接收参数的19种方式
java·spring boot·后端
金銀銅鐵2 小时前
Byte Buddy 生成的类的结构如何?(第一篇)
java·后端
umeelove352 小时前
Spring框架
java·后端·spring
轩情吖2 小时前
MySQL之表的约束
android·数据库·c++·后端·mysql·开发·约束
xjt_09012 小时前
用 LiteLLM 打通 Codex CLI 与 Claude Code(有key即可实现编程自由)
后端·python·flask
Tzarevich2 小时前
Agent记忆模块:让大模型“记住”你,还能省Token!
后端·langchain·agent