JDK17 新特性

从springboot3.0开始,已经不⽀持JDK8了。 从3.0开始,转变为JDK17。 

 官⽅维护的版本都是SpringBoot3.X的了,但是之前的版本也是可以⽤的,只是官⽅不再进⾏功能更新了.

综上所述,选⽤Java17,概括起来主要有下⾯⼏个主要原因:

1. JDK 17 是 LTS (⻓期⽀持版),可以免费商⽤到 2029 年。⽽且将前⾯⼏个过渡版(JDK 9-JDK 16)去其糟粕,取其精华的版本;
2. JDK 17 性能提升不少,⽐如重写了底层 NIO,⾄少提升 10% 起步;
3. ⼤多数第三⽅框架和库都已经⽀持,不会有什么⼤坑;

语法层面新特性

1、文本块

2 、Switch 表达式增强

yield关键字⽤于从case的代码块中返回值。

正常的switch

 public static void main(String[] args) {
        String data = "one" ;
        int result = 0 ; // 接收数据的返回值
        switch (data) {
            case "one":
                result = 1 ; // 为result重新赋值
                break ;
            case "two":
                result = 2 ; // 为result重新赋值
                break ;
            default:
                result = -1 ; // 为result重新赋值
                break ;
        }
        System.out.println(result) ;
    }

简化后的switch

  public static void main(String[] args) {
        String data = "one" ;
        int result = switch (data) {
            case "one"->1;
            case "two"->2;
            default->-1;
        };
        System.out.println(result) ;
    }

 如果不想使⽤指向符-> 可以使⽤yield来代替:

  public static void main(String[] args) {
        String data = "one" ;
        int result = switch (data) {
            case "one" : yield 1;
            case "two": yield 2;
            default : yield -1;
        };
        System.out.println(result) ;
    }

3、instanceof的模式匹配

 instanceof 增加了模式匹配的功能,如果变量类型经过instanceof判断能够匹配目标类型,则对应分支中无需再做类型强转。

if (o instanceof Integer i && i > 0) {
    System.out.println(i.intValue());
} else if (o instanceof String s && s.startsWith("t")) {
    System.out.println(s.charAt(0));
}

4、var 局部变量推导

对于某些可以直接推导出类型的局部变量,可以使用var进行声明。

    var nums = new int[] {1, 2, 3, 4, 5};
        var sum = Arrays.stream(nums).sum();
        System.out.println("数组之和为:" + sum);

5、空指针异常

出现异常的具体⽅法和原因都⼀⽬了然。如果你的⼀⾏代码中有多个⽅法、多个变量,可以快速定位问题所在,如果是 JDK8,有些情况下真的不太容易看出来。

JDK8:

 JDK17:

模块化及类封装

1、记录类 record

 在 JDK17 中,可以声明一种特殊的类,record 。被reocrd定义的类代表的是一种不可变的常量,只能用来描述一种简单的不可变的数据结构。这样我们未来或许就不用再定义一大堆的 BO、 VO 、 DTO 这些只用来进行值传递的复杂对象了。

例如可以这样声明一个带有x,y两个属性的Point类。

public record Point(int x, int y) {

}

然后,这个类只能初始化设置属性值,初始化后,不允许修改属性值,用反射也不行。唯一和我们自己写的 POJO 有点不同的是,获取属性的方法,与属性同名,而不再是getXXX这样的了。

public class RecordTest {

    @Test
    public void getPoint() throws IllegalAccessException {
        Point p = new Point(10,20);
        for (Method method : p.getClass().getMethods()) {
            System.out.println(method);
        }
        for (Field field : p.getClass().getDeclaredFields()) {
            System.out.println(field);
           // 不允许通过反射修改值。
            // field.setAccessible(true);
            // field.set(p,30);
        }
        System.out.println(p.x()+"===="+p.y());
    }
}

record记录类的实现原理,其实大致相当于给每个属性添加了private final声明。这样就不允许修改。另外,从字节码也能看到,对于record类,同时还实现了toString,hashcode,equals方法,而这些方法都被声明成了final,进一步阻止应用定制record相关的业务逻辑。 

2 、隐藏类 Hidden Classes

从 JDK15 开始,JDK 引入了一个很有意思的特性,隐藏类。隐藏类是一种不能被其他类直接使用的类。隐藏类不再依赖于类加载器,而是通过读取目标类字节码的方式,创建一个对其他类字节码隐藏的class对象,然后通过反射的方式创建对象,调用方法。

我们先来一个示例理解一下什么是隐藏类,再来思考隐藏类有什么用处。

​ 比如先编写一个普通的测试类

public class HiddenClass {
    public String sayHello(String name) {
        return "Hello, " + name;
    }

    public static void printHello(String name) {
        System.out.printf("""
                Hello, %s !
                Hello, HiddenClass !
                %n""", name);
    }
}

传统方式下,要使用这个类,就需要经过编译,然后类加载的整个过程。但是隐藏类机制允许直接从编译后的class字节码入手,并且绕过整个类加载的复杂过程,直接使用这个类。

​ 比如,我们可以使用下面的方法获取class字节数组:

public void printHiddenClassBytesInBase64(){
        //编译后的 class 文件地址
        String classPath = "/Users/roykingw/DevCode/JDK17Demo/demoModule/target/classes/com/roy/hidden/HiddenClass.class";
        try {
            byte[] bytes = Files.readAllBytes(Paths.get(classPath));
            System.out.println(Base64.getEncoder().encodeToString(bytes));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

 这样就可以拿到一串编码后的class文件的字节码。接下来,就可以用这个字节码直接生成这个类。例如:

public void testInvokeHiddenClass() throws Throwable {
        //class文件的字节码
        String CLASS_INFO = "yv66vgAAAD0ANgoAAgADBwAEDAAFAAYBABBqYXZhL2xhbmcvT2JqZWN0AQAGPGluaXQ+AQADKClWEgAAAAgMAAkACgEAF21ha2VDb25jYXRXaXRoQ29uc3RhbnRzAQAmKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1N0cmluZzsJAAwADQcADgwADwAQAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwgAEgEAI0hlbGxvLCAlcyAhCkhlbGxvLCBIaWRkZW5DbGFzcyAhCiVuCgAUABUHABYMABcAGAEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAZwcmludGYBADwoTGphdmEvbGFuZy9TdHJpbmc7W0xqYXZhL2xhbmcvT2JqZWN0OylMamF2YS9pby9QcmludFN0cmVhbTsHABoBABpjb20vcm95L2hpZGRlbi9IaWRkZW5DbGFzcwEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAcTGNvbS9yb3kvaGlkZGVuL0hpZGRlbkNsYXNzOwEACHNheUhlbGxvAQAEbmFtZQEAEkxqYXZhL2xhbmcvU3RyaW5nOwEACnByaW50SGVsbG8BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBAApTb3VyY2VGaWxlAQAQSGlkZGVuQ2xhc3MuamF2YQEAEEJvb3RzdHJhcE1ldGhvZHMPBgApCgAqACsHACwMAAkALQEAJGphdmEvbGFuZy9pbnZva2UvU3RyaW5nQ29uY2F0RmFjdG9yeQEAmChMamF2YS9sYW5nL2ludm9rZS9NZXRob2RIYW5kbGVzJExvb2t1cDtMamF2YS9sYW5nL1N0cmluZztMamF2YS9sYW5nL2ludm9rZS9NZXRob2RUeXBlO0xqYXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9pbnZva2UvQ2FsbFNpdGU7CAAvAQAISGVsbG8sIAEBAAxJbm5lckNsYXNzZXMHADIBACVqYXZhL2xhbmcvaW52b2tlL01ldGhvZEhhbmRsZXMkTG9va3VwBwA0AQAeamF2YS9sYW5nL2ludm9rZS9NZXRob2RIYW5kbGVzAQAGTG9va3VwACEAGQACAAAAAAADAAEABQAGAAEAGwAAAC8AAQABAAAABSq3AAGxAAAAAgAcAAAABgABAAAAAwAdAAAADAABAAAABQAeAB8AAAABACAACgABABsAAAA7AAEAAgAAAAcrugAHAACwAAAAAgAcAAAABgABAAAABQAdAAAAFgACAAAABwAeAB8AAAAAAAcAIQAiAAEACQAjACQAAQAbAAAAQAAGAAEAAAASsgALEhEEvQACWQMqU7YAE1exAAAAAgAcAAAACgACAAAACQARAA0AHQAAAAwAAQAAABIAIQAiAAAAAwAlAAAAAgAmACcAAAAIAAEAKAABAC4AMAAAAAoAAQAxADMANQAZ";
        byte[] classInBytes = Base64.getDecoder().decode(CLASS_INFO);
        Class<?> proxy = MethodHandles.lookup()
                .defineHiddenClass(classInBytes, true, MethodHandles.Lookup.ClassOption.NESTMATE)
                .lookupClass();

        // 输出类名
        System.out.println(proxy.getName());
        // 输出类有哪些函数
        for (Method method : proxy.getDeclaredMethods()) {
            System.out.println(method.getName());
        }
        // 2. 调用对应的方法
        MethodHandle mhPrintHello = MethodHandles.lookup().findStatic(proxy, "printHello", MethodType.methodType(void.class, String.class));
        mhPrintHello.invokeExact("loulan");
        Object proxyObj = proxy.getConstructors()[0].newInstance();
        MethodHandle mhSayHello = MethodHandles.lookup().findVirtual(proxy, "sayHello", MethodType.methodType(String.class, String.class));
        System.out.println(mhSayHello.invoke(proxyObj, "loulan"));
    }

3 、密封类 Sealed Classes

在 JDK8 中,每一个类都可以被任意多个子类继承,并修改其中的内置功能。比如 JDK8 中最重要的类加载双亲委派机制,在应用当中,程序员可以随意挑选一个内置的类加载器,继承出新的类加载器实现,随意打破双亲委派机制。这其实是不太安全的,意味着很多内置的行为得不到保护。而密封类就是用来限制每一个父类可以被哪些子类继承或者实现。

​ 首先在声明类或方法时,如果增加sealed修饰,那么还需要同时增加permits指定这个类可以被哪些类来继承或实现。例如:

public sealed abstract class Shape permits Circle, Rectangle, Square {

    public abstract int lines();
}

 接下来,Shape 的子类也会要收到限制。在声明类时,需要声明自己的密封属性。可以有三个选项:

  • final,表示这个子类不能再被继承了。
  • non-sealed 表示这个子类没有密封特性,可以随意继承。
  • sealed 表示这个子类有密封特性。再按照之前的方式声明他的子类。

例如针对 Shape,就可以声明这样的一些子类:

// 非密封子类,可以随意继承
public non-sealed class Square extends Shape{
    @Override
    public int lines() {
        return 4;
    }
}
//final 子类,不可再被继承
public final class Circle extends Shape{
    @Override
    public int lines() {
        return 0;
    }
}
// 密封子类,继续声明他所允许的子类。
public sealed class Rectangle extends Shape permits FilledRectangle {
    @Override
    public int lines() {
        return 3;
    }
}

public final class FilledRectangle extends Rectangle{
    @Override
    public int lines() {
        return 0;
    }
}

4、模块化 Module System

JDK8 中,我们写的 Java 代码是在一个一个的package下面的,模块化在包之上增加了更高级别的聚合,它包括一组密切相关的包和资源以及一个新的模块描述符文件。简单点说,module是 java 中package包的上一层抽象。通过module,java 可以更精确的分配对象的生效范围。

​ 比如,在 JDK8 的安装目录下,JDK 预设的功能是以一个一个jar包的形式存在的,但是在JDK17 的安装目录下,你就看不到那些jar包了,取而代之的,是一系列以jmod后缀的文件,这些就是一个一个的模块。

​ 安装 JDK17 后,也可以使用java --list-modules 查看到所有的系统模块。 

 可以看到,整个 JDK 都已经用模块化的方式进行了重组,并且,在 JDK 的安装目录下,也已经取消了 JRE 目录。这意味着,在新版本的 JDK 中,应用程序可以定制自己的JRE,只选择应用所需要的模块,而不再需要引入整个 JDK 庞大的后台功能。

 比如,我们如果只需要使用java.base模块中的类型,那么随时可以用一下指令打包出一个可以在服务器上运行的 JRE:

jlink -p $JAVA_HOME/jmods --add-modules java.base --output basejre 

  这个basejre就可以像安装 JDK 一样部署到一台新的服务器上运行了。

5 、GC调整

另外,从 JDK8 到 JDK17,还涉及到了非常多的调整,这些调整让 Java 具备了更多现代语言的特性,并且执行效率也在不断提高。

​ 比如,重写了Socket 底层的 API 代码,让这写代码使用更简单,实现也更易于维护和替换。

​ 默认禁用偏向锁。从 JDK1.6 开始,对synchronized关键字的实现就形成了 无锁 ->偏向锁->轻量级锁->重量级锁 的锁升级过程。而在 JDK15 中,默认就废弃了偏向锁。当然,目前还是可以通过添加参数 -XX:+UseBiasedLocking 手动开启偏向锁。

​ 不过 JDK 在升级过程中,一直保持着良好的向下兼容性,因此,有很多优化调整对开发工作影响都还不是太大。但是在这些调整当中,有一部分调整是大家需要额外关心一下的,那就是对于 GC 的调整。

1 、ZGC 转正

​ ZGC 在之前已经做过介绍,是现在最为强大的一个垃圾回收器,自 JDK11 开始引入,从 JDK15 开始正式投入了使用。现在使用-XX:+UseZGC参数就可以快速使用 ZGC 。

​ 另外,ZGC 的具体实现其实也在版本升级过程中不断优化。在 JDK17 中使用指令 java -XX:+PrintFlagsFinal -version 可以简单看到,与 ZGC 相关的系统不稳定参数已经基本没有了。G1 的还有一大堆 这也说明 ZGC 的算法优化已经相当成熟了。

​ 随 ZGC 登场的,还有 RedHat 推出的Shenandoah 垃圾回收器。尽管 Oracle 一直比较抵触这个非官方推出的垃圾回收器,但是最终也还是将Shennandoah 垃圾回收器以可选的方案集成了进来。现在可以使用 -XX:+UseShenandoahGC 参数手动选择shennandoah。

2 、废除 CMS

​ 虽然 CMS作为 G1 之前唯一的一款并发的垃圾回收器,在相当长的时间里,都扮演者非常重要的角色。在最为经典的 JDK8 时代,尽管 CMS 一直没有作为默认垃圾回收器登场过,但是关于 G1 和 CMS 的比较以及取舍,一直都是业界津津乐道的话题。但是,随着 G1 垃圾回收器发展得更为完善,以及后续ZGC,shennandoah等现代垃圾回收器开始登场,过于复杂的 CMS 垃圾回收器还是退出了历史舞台。

​ 在 JDK14 中,就彻底删除了 CMS 垃圾回收器。与 CMS 一起退场的,还有Parallel Scavenge +SerialOld的经典 GC 组合。SerialOld 这个最早的垃圾回收器其实早就应该退出历史舞台了,只不过由于他一直作为 CMS 的补充方案而一直保留。这次也终于随着 CMS 一起退出了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值