JDK9新特性

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK9新特性相关的知识,希望对你有一定的参考价值。

JDK9新特性


模块化系统

目标

认识模块化的好处,如何建立模块与模块之间的访问

步骤

  1. 模块化出现的背景与概念
  2. 模块系统好处
  3. 模块的输出与访问

模块化出现的背景与概念

咱就是说,为啥咋好端端的要引入模块化的概念?

  • Java运行环境的膨胀和臃肿。每次JVM启动的时候,至少会有30~60MB的内存加载,主要原因是JVM需要加载rt.jar,不管
    其中的类是否被classloader加载,第一步整个jar都会被JVM加载到内存当中去(而模块化可以根据模块的需要加载程序运行
    需要的class)
  • 当代码库越来越大,创建复杂,盘根错节的“意大利面条式代码”的几率呈指数级的增长。不同版本的类库交叉依赖导致让
    人头疼的问题,这些都阻碍了 Java 开发和运行效率的提升。
  • 很难真正地对代码进行封装, 而系统并没有对不同部分(也就是 JAR 文件)之间的依赖关系有个明确的概念。每一个公共类都可以被类路径之下任何其它的公共类所访问到,这样就会导致无意中使用了并不想被公开访问的 API。
  • 类路径本身也存在问题: 你怎么知晓所有需要的 JAR 都已经有了, 或者是不是会有重复的项?

模块化的概念

  1. 模块化的目标

模块独立、化繁为简

模块化(以 Java 平台模块系统的形式)将 JDK 分成一组模块,可以在编译时,运行时或者构建时进行组合

主要目的在于减少内存的开销

只须必要模块,而非全部jdk模块,可简化各种类库和大型应用的开发和维护

改进其安全性,可维护性,提高性能

咱就是讲,模块(module)的概念,其实就是package外再裹一层,也就是说,用模块来管理各个package,通过声明某个package暴露,不声明默认就是隐藏。因此,模块化使得代码组织上更安全,因为它可以指定哪些部分可以暴露,哪些部分隐藏。


演示

下面就来具体演示一下如何玩转模块化系统

如何让MoudleB指定A的存在呢? 首先,在模块A中将Utils类暴露出来:

module MoudleA 
    //暴露com文件夹下面所有类
     exports com;

小结

  1. 认识模块化的好处
  • 提高效率
  • 可以实现包隐藏,从而实现包里面的所有类隐藏。
  1. 如何建立模块与模块之间的访问
  • 定义一个输出模块信息
  • 定义一个输入模块信息
  • 添加依赖

交互式编程

交互式编程的概念

java的编程模式是:编辑,保存,编译,运行和调试。

有时候我们需要快速看到某个语句的结果的时候,还需要写上public static void main(String[] args)这些无谓的语句,减低我们的开发效率。

JDK9引入了交互式编程,通过jshell工具即可实现,交互式编程就是指我们不需要编写类我们即可直接声明变量,方法,执行语句,不需要编译即可马上看到效果。

交互式编程的作用:即时反馈

Jshell工具使用

  • 打开jshell工具


前提配置了系统环境变量,jdk版本为9+

  • 直接声明变量、方法

  • /list 查看当前所有的代码(仅限于当前的会话,当前控制台)

  • /methods查看所有的方法

  • /var 查看所有的变量

  • /edit 打开编辑器

  • /open 路径 执行外部的java代码文件

  • /imports 查看默认导入的包

  • /exit 退出jshell工具

小结

交互编程是什么,其作用是什么?

即时反馈, 马上能够看到语句的执行结果

多版本兼用jar的作用

多版本JAR(MR JAR)可能包含同一类的多个变体,每个变体都针对特定的Java版本。 在运行时,类的正确变体将被自动加载,这取决于所使用的Java版本。这允许库作者在早期利用新的Java版本,同时保持与旧版本的兼容性。

在Java 9增强了JAR多版本字节码文件格式的支持,同一个Jar包可以包含多个Java版本的class文件。使用这个功能,我们可以将应用程序/库升级到新的Java版本,而不必强迫用户升级到相同的Java版本。

应用场景:比如某个架构师开发了一个工具类MyUtils,该工具类里面使用了jdk9的新特性,这时候该工具在推广的时候会遇到很大的阻力,因为很多用户还没有升级jdk版本,JDK9推出了多版本兼用jar的特性就允许该架构师编写一个同类名的工具MyUtils,并在该工具类中不使用jdk9的新特性,然后两个同类名的类一起打包成为一个jar,提供给用户去使用,这时候即可根据用户当前使用的jdk版本而选择不同的工具类了。


简言之:该jar包在java 8中可以执行最上层的MyUtils.class,在Java 9中自动选择执行目录9下的MyUtils.class。


基本使用方法

多版本的字节码发行jar包,需要在其MANIFEST.MF中做以下的声明:

Multi-Release: true

jar包的META-INF/versions文件目录里面可以包含多个版本的class文件,编译结果目录结构如下:

jar root
  - A.class
  - B.class
  - META-INF
     - versions
        - 9
           - A.class

假设上文中的根目录是使用java 8 或之前版本编译的字节码文件A.calss。META-INF/versions/9/是使用java 9 编写的java代码的编译结果A.class。

  • 如果jar包是在JDK 8的运行时环境下运行,将使用根目录下面的class文件进行程序运行。
  • 如果jar包是在JDK 9的运行时环境下运行,将使用META-INF/versions/9/下面的class文件进行程序运行。

假设未来这个项目升级JDK 10,决定在A.java中使用Java 10的一些新特性,可以单独针对A.class进行语法升级,并将编译结果a.class放置在META-INF/versions/10/下面

jar root
  - A.class
  - B.class
  - META-INF
     - versions
        - 9
           - A.class
        - 10
           - A.class

现在,上面的jar包含了可以以三种Java版本运行的字节码文件,A.class兼容JDK 8、9、10。


使用演示

java 8代码

下面的类文件代码我们让它运行在Java 8的环境下

package com.example;

public class IOUtil 
  public static String convertToString(InputStream inputStream) throws IOException 
      System.out.println("IOUtil 使用java 8 版本");
      Scanner scanner = new Scanner(inputStream, "UTF-8");
      String str = scanner.useDelimiter("\\\\A").next();
      scanner.close();
      return str;
  

增加一个Main.java的应用程序入口文件,调用IOUtil.convertToString方法将InputStream转换成String。

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

public class Main 
  public static void main(String[] args) throws IOException 
          InputStream inputStream = new ByteArrayInputStream("测试字符串".getBytes());
          String result = IOUtil.convertToString(inputStream);
          System.out.println(result);
      

Java 9代码

在Java 9 发布之后,我们决定使用Java 9 的新的语法重写IOUtil.convertToString方法。

public class IOUtil 
  public static String convertToString(InputStream inputStream) throws IOException 
      System.out.println("IOUtil 使用java 9 版本");
      try (inputStream)   //Java9版本的增强try-with-resources
          String str = new String(inputStream.readAllBytes());
          return str;
      
  

如上的代码所示,我们使用了Java 9的两个新特性带有inputStream引用的try-with-resource块和新的InputStream.readAllBytes()方法。

编译

将Java8 、Java9的IOUtil.java代码分别在JDK8、JDK9的版本下分别编译成class字节码文件,并将class文件按照如下的目录结构打成保存,并打jar包。(先按java8版本打成jar包,然后修改MANIFEST.MF文件,添加java 9字节码class文件即可)

D:\\multi-release-jar-example\\my-lib-jar>tree /A /F
+---com
|   \\---example
|           IOUtil.class
|           Main.class
|           
\\---META-INF
    |   MANIFEST.MF
    |   
    \\---versions
        \\---9
            \\---com
                \\---example
                        IOUtil.class

运行 Main class

在JDK 9的环境下运行这个jar包

D:\\multi-release-jar-example>java -cp my-lib.jar com.example.Main
IOUtil 使用java 9 版本
测试字符串

在JDK 8的环境下运行这个jar包

D:\\multi-release-jar-example>C:\\jdk1.8.0_151\\bin\\java -cp my-lib.jar com.example.Main
IOUtil 使用java 8 版本
测试字符串

小结

多版本兼用jar的作用。

根据当前用户安装的jdk版本选择性的使用具体类。


接口方法私有化

接口方法私有化的作用

当我们在一个接口里写多个默认方法或者静态方法的时候,可能会遇到程序重复的问题。我们可以把这些重复的程序提取出来,创建一个新的方法,用private进行修饰,这样就创造了一个只有接口可以调用的私有方法。

这些私有方法将改善接口内部的代码可重用性。例如,如果需要两个默认方法来共享代码,则私有接口方法将允许它们共享代码,但不将该私有方法暴露给它的实现类调用(后文中会给大家举一个例子)。

在接口中使用私有方法有四个规则:

  • 接口中private方法不能是abstract抽象方法。因为abstract抽象方法是公开的用于给接口实现类实现的方法,所以不能是private。
  • 接口中私有方法只能在接口内部的方法里面被调用。
  • 接口中私有静态方法可以在其他静态和非静态接口方法中使用。
  • 接口中私有非静态方法不能在私有静态方法内部使用。
interface CustomInterface 

    public abstract void abstractMethod();  //抽象方法不能是私有的

    public default void defaultMethod() 
        privateMethod(); //可以调用接口中的私有方法
        privateStaticMethod(); //可以调用接口中的私有静态方法
        System.out.println("普通方法被调用");
    

    public static void staticMethod() 
        privateStaticMethod(); //public静态方法可以调用private静态方法
        System.out.println("静态方法被调用");
    

    private void privateMethod() 
        System.out.println("private私有方法被调用");
    

    private static void privateStaticMethod() 
        System.out.println("private私有静态方法被调用");
    

按照上面四个规则,上面的代码定义都是正确的


public interface UserDao 
    default void methodA() 
        System.out.println("methodA...");
        System.out.println("A....");
        System.out.println("B....");
        System.out.println("C....");
    

    default void methodB() 
        System.out.println("methodB...");
        System.out.println("A....");
        System.out.println("B....");
        System.out.println("C....");
    

存在的问题: 以上代码的methodA与methodB存在着代码冗余问题,我们可以把这部分公共的方法抽取成私有的方法提供给接口内部去使用。

接口私有方法的作用:解决接口中默认方法与静态方法代码重复的问题

接口定义私有化方法

public interface UserDao 
    default void methodA() 
        System.out.println("methodA...");
        commons();
    

    default void methodB() 
        System.out.println("methodB...");
        commons();
    

    //定一个私有的方法,把重复部分的代码抽离出来。然后在methodA与methodB方法内部去调 用。 
    // 私有方法只能在本类中调用,这里包括接口的实现类也不能调用。 
    private void commons() 
        System.out.println("A....");
        System.out.println("B....");
        System.out.println("C....");
    

测试代码

public class UserDaoImpl implements UserDao 
    public static void main(String[] args) 
        UserDaoImpl userDao = new UserDaoImpl();
        userDao.methodA();
        userDao.methodB();
    

一个例子:分别计算奇数与偶数的和

接口定义如下,下文中add方法采用了java8 的Stream流操作,分别使用lambda表达式作为过滤条件,并求和。

核心是:addEvenNumbers偶数求和函数和addOddNumbers奇数求和函数,都调用了add接口私有方法。

import java.util.function.IntPredicate;
import java.util.stream.IntStream;

public interface CustomCalculator 

    default int addEvenNumbers(int... nums)  //非抽象,java8 开始可以定义default方法
        return add(n -> n % 2 == 0, nums);   //过滤偶数并求和,调用private私有方法
    

    default int addOddNumbers(int... nums)  //非抽象,java8 开始可以定义default方法
        return add(n -> n % 2 != 0, nums);  //过滤奇数并求和,调用private私有方法
    

    //按照过滤条件过滤奇数或偶数并sum求和:java9开始可以定义private私有方法
    private int add(IntPredicate predicate, int... nums) 
        return IntStream.of(nums)   //java8 Stream流
                .filter(predicate)   //java8 predicate及过滤器
                .sum();  //sum求和
    

接口实现类MainCalculator 实现CustomCalculator接口

public class MainCalculator implements CustomCalculator 

    public static void main(String[] args) 
        CustomCalculator demo = new MainCalculator ();

        int sumOfEvens = demo.addEvenNumbers(1,2,3,4,5,6,7,8,9);
        System.out.println(sumOfEvens);   //过滤所有偶数并求和,结果是20

        int sumOfOdds = demo.addOddNumbers(1,2,3,4,5,6,7,8,9);
        System.out.println(sumOfOdds);   //过滤所有奇数并求和,结果是25
    


小结

清楚接口方法私有化的目的?

  • 解决静态或者是默认方法代码重复的问题。

如何在接口中定义私有化的方法以及调用?

  • 在接口中使用private修饰方法即可。
  • 在方法的内部去调用。

Java9改进try-with-resources语法

先说Java7的try-with-resources(Java9改进版在后文)

Java 7之前没有try-with-resources语法,所有的流的销毁动作,全都需要自己在finally方法中手动的写代码进行关闭。

如下文中的代码,将一个字符串写入到一个文件里面。

public class ResourcesTest 

    @Test
    void testStream() throws IOException 

        String fileName="D:\\\\data\\\\test\\\\testStream.txt";

        FileOutputStream fos = new FileOutputStream(fileName);  //创建IO管道流
        OutputStreamWriter osw = new OutputStreamWriter(fos);
        BufferedWriter bw = new BufferedWriter(osw);

        try
            bw.write("手写代码进行Stream流的关闭");
            bw.flush();
        finally
            bw.close();   //手动关闭IO管道流
            osw.close();
            fos.close();
        
    

Java 7版本开始提供了try-with-resources语法,我们只需要把管道流用try()包含起来,在try代码段执行完成之后,IO管道流就会自动的关闭,不需要我们手写代码去关闭,这很简洁!

    @Test
    void testTry() throws IOException 
        String fileName = "D:\\\\data\\\\test\\\\testStream.txt";
        try (FileOutputStream fos = new FileOutputStream(fileName);
             OutputStreamWriter osw = new OutputStreamWriter(fos);
             BufferedWriter bw = new BufferedWriter(osw);) 
            bw.write("IO管道流被自动调用close()方法");
            bw.flush();
        
    

注意:JDK8开始已经不需要我们再手动关闭资源,只需要把要关闭资源的代码放入try语句中即可,但是要求初始化资源的语句必须位于try语句中

避免走入误区

很多小伙伴在知道try-with-resources语法之后,容易陷入误区

  • 误区一:只有IO管道流才能使用try-with-resources语法,进行自动的资源关闭
  • 误区二:所有带有close()方法的类对象,都会自动的调用close()方法进行资源关闭

以上是关于JDK9新特性的主要内容,如果未能解决你的问题,请参考以下文章

JDK9 新特性

java新特性--05--JDK9

JDK9 新特性

JDK9的新特性之一:集合接口的of()方法

JDK9的新特性之一:集合接口的of()方法

JDK9新特性