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

0 阅读10分钟

前言

这些年我见证过太多项目从JDK6一路升级到JDK8,然后就停在了那里。

“你发任你发,我用Java 8”成了很多团队的座右铭 。

但最近两年,情况开始变化了。

JDK17作为长期支持版本发布后,越来越多的新项目开始尝试拥抱现代Java。

美团的技术团队分享过一组数据:他们将核心服务升级到JDK17后,机器成本降低了约10%,性能和稳定性也大幅提升 。

有些小伙伴可能还不太适应新写法,看到项目里的代码第一反应是:“这是Java?”

今天,我就带大家系统性地看看JDK8到JDK17,代码风格到底发生了怎样的巨变。

希望对你会有所帮助。

更多项目实战在项目实战网:java突击队

01 模式匹配:告别冗余的类型转换

这是日常开发中最常用的改进。

在JDK8时代,我们写instanceof时,总要先判断类型,再强制转换:

// JDK8及之前:繁琐的强制转换
Object obj = "Hello Java";
if (obj instanceof String) {
    // 必须显式转换
    String str = (String) obj;
    System.out.println("字符串长度:" + str.length());
}

JDK17带来了模式匹配,类型检查和变量绑定一步到位:

// JDK17:模式匹配,一步到位
Object obj = "Hello Java";
if (obj instanceof String str) {  // 直接绑定变量
    System.out.println("字符串长度:" + str.length());  // str直接可用
}

这种写法的好处不仅是少写了一行代码。更重要的是,变量作用域更合理——str只在类型判断为真后才存在,避免了不必要的类型转换风险 。

02 记录类:终结样板代码

在JDK8时代,定义一个纯数据载体类(DTO、VO等)是一件很痛苦的事情:

// 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)**让这一切变得极其简单:

// JDK17:一行代码搞定
public record User(String name, int age) { }

没错,就这么简单!编译器会自动生成:

  • 规范的构造器
  • name()age()访问器(注意不是getName()
  • equals()hashCode()toString()方法

记录类特别适合那些只承载数据、没有额外逻辑的类,代码量减少了70%以上

而且它是不可变的,天然适合并发场景 。

03 Switch表达式:从语句到表达式

传统的switch语句有很多痛点:容易忘记break导致穿透、不能直接返回值、语法冗长:

// 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表达式,语法更简洁、语义更安全:

// JDK17:switch表达式
String dayType = switch (day) {
    case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "工作日";
    case SATURDAY, SUNDAY -> "周末";
    default -> throw new IllegalArgumentException("无效日期");
};  // 注意这里需要分号

如果分支逻辑复杂,可以用代码块配合yield返回值:

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)简直是噩梦:

// 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正式引入的文本块彻底改变了这一局面:

// 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),让你可以精确控制哪些类可以继承:

// 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中创建不可变集合需要好几行代码:

// 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引入了集合工厂方法,一行搞定:

// 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类增加了几个实用方法:

// 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()方法:

// 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中,链式调用出现空指针时,只能知道是哪一行,很难定位具体是哪个对象为空:

// 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信息,会明确指出是哪个方法的返回值导致了空指针:

// 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实现方式不同

启用方式很简单:

java -XX:+UseZGC -jar myapp.jar
java -XX:+UseShenandoahGC -jar myapp.jar

美团的技术团队分享过,升级到JDK17并启用ZGC后,核心服务性能大幅提升,机器成本降低了约10%

7.3 容器感知

JDK8在容器中运行时,需要手动设置参数来感知内存和CPU限制。

JDK10+开始,JVM能自动识别容器资源限制

# JDK8需要手动设置
java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -jar app.jar

# JDK17自动感知,无需额外参数
java -jar app.jar

08 升级迁移实用指南

如果你决定从JDK8升级到JDK17,以下是一些实用建议。

8.1 使用工具检查依赖

JDK17强封装了内部API,有些代码在JDK8下能用,在JDK17下可能无法编译或运行。可以使用jdeps工具分析依赖:

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风格

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风格

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。