JDK9新特性

x33g5p2x  于2022-02-07 转载在 其他  
字(25.9k)|赞(0)|评价(0)|浏览(587)

模块化系统

目标

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

步骤

  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()方法进行资源关闭

误区一把实践范围缩小了,而误区二把实践范围夸大了。那么什么样的资源可以被自动关闭呢?答案就是实现了AutoCloseable或Closeable接口的类可以自动的进行资源关闭。

public interface Closeable extends AutoCloseable {
    public void close() throws IOException;
}

Closeable接口继承自AutoCloseable接口,二者都包含close()方法。

如果你自定义的占用系统资源的类需要进行资源回收,请实现这两个接口之一,并在close()方法中进行资源回收与关闭。这样你自定义的类,也可以使用try-with-resources语法进行资源回收与关闭。

try-with-resources在Java 9中的改进

try-with-resources语法在java 9 中进行了改进,try-with-resources语法的try()可以包含变量,多个变量用分号隔开。

\这样的改进目的是让语义更加明确,将资源创建代码与尝试资源回收的语法分离。

  • 语义一:尝试去执行代码段,如果抛出异常,对异常进行处理
  • 语义二:尝试去自动关闭资源,关闭谁?关闭被try()包含的变量
@Test
    void testJava9Try() throws IOException {
        String fileName = "D:\\data\\test\\testStream.txt";

        FileOutputStream fos = new FileOutputStream(fileName);
        OutputStreamWriter osw = new OutputStreamWriter(fos);
        BufferedWriter bw = new BufferedWriter(osw);

        try(bw;osw;fos){  //注意这里:尝试去回收这三个对象对应的资源,和上文中的java 7代码对比
            bw.write("Java9-可以被自动调用close()方法");
            bw.flush();
        }
    }

标识符优化

jdk9之前

public class Demo1 {
    public static void main(String[] args) {
        String _ = "hello";
        System.out.println(_);
    }
}

以上代码不会报错,允许_作为标识符

JDK9开始

public class Demo1 {
    public static void main(String[] args) {
        String _ = "hello";
        System.out.println(_);
    }
}

以上代码报错,jdk9开始不允许_作为标识符

小结

jdk9为标识符定义了什么的新规则

  • _不能作为单独的标识符。

String底层结构的变化

了解String底层变化的动机是什么

原文:

The current implementation of the String class stores characters in a char array, using two bytes
(sixteen bits) for each character. Data gathered from many different applications indicates that
strings are a major component of heap usage and, moreover, that most String objects contain
only Latin-1 characters. Such characters require only one byte of storage, hence half of the space
in the internal char arrays of such String objects is going unused.

翻译:

string类的当前实现将字符存储在char数组中,每个字符使用两个字节(16位)。

从许多不同的应用程序收集的数据表明,字符串是堆使用的主要组成部分,而且,大多数字符串对象只包含拉丁-1字符。

这样的字符只需要一个字节的存储空间,因此这样的字符串对象的内部字符数组中的一半空间将被闲置

小结

  • String底层变化的动机是什么?
节省内存空间。
  • 具体的变化有什么?
Stirng类内部维护一个字符数组变化为维护一个字节数组。

StringBuffer与StringBuilder底层变化

由于String类底层已经发生变化,所以StringBuilder与StringBuffer底层也相应的发生了改变。

String-related classes such as AbstractStringBuilder, StringBuilder, and StringBuffer will be

集合工厂方法:快速创建只读集合

新增的方法

调用集合中静态方法 of(),可以将不同数量的参数传输到此工厂方法中。

此功能可用于 Set 和 List,也可用于 Map 的类似形式。此时得到 的集合,是不可变的:

List.of
Set.of
Map.of

构建Set集合对象

Java 9 提供了一系列的工厂方法of()来更加简便的构建Set集合对象。使用of()方法构建java.util.Set我们就不用一个元素一个元素的add()数据了。但需要注意的是:这种方法构建的Set集合类对象一旦构建就不能更改,不能再新增集合元素。

Set<Integer> integers = Set.of(2, 6, 7, 10);
//顺序改变是因为set底层是hash桶
System.out.println(integers);  //[2, 10, 6, 7]

还有许多重载的of()工厂方法供我们使用

of()//空的set
 of(E)
 of(E, E)
 of(E, E, E)
 //更多 ......
 of(E, E, E, E, E, E, E, E, E, E )// 一直到十个元素
 of(E...)//更多参数

构建List集合对象

和Set集合类似,List集合类也新增了一系列的of()工厂函数,创建不可变的List集合对象。

List<Integer> integers = List.of(2, 6, 7, 10);
 System.out.println(integers); // [2, 6, 7, 10]

构建Map对象

虽然Map不是Colleaction的子类,但是我们一般也把它当作集合类学习。

Map类同样新增了一系列的of()工厂函数,创建不可变的Map对象。

唯一的区别是使用Key/Value的形式传递参数。

Map<Integer, String> map = Map.of(2, "two", 6, "six");
 System.out.println(map); // {2=two, 6=six}

使用Map.ofEntries() 和 Map.entry()

工厂方法Map.ofEntries接受Map.Entry作为varargs。还有另一个相关的新静态方法Map.entry(K, V)来创建Entry实例。

Map<Integer, String> map = Map.ofEntries(Map.entry(2, "two"), Map.entry(4, "four"));
 System.out.println(map); //{2=two, 4=four}

注意

通过of方法创建的集合对象是不能修改集合中的元素的,如果强行修改就会出现 UnsupportedOperationException

Arrays

Arrays是我们进行集合操作的工具类,在Java 9 版本中也进行了增强。

Arrays.mismatch()

这些新方法用于查找两个数组之间的第一个不匹配索引

例如,下面的代码查找两个整数数组之间的第一个不匹配项。如果没有不匹配,则此方法将返回-1。

int[] ints1 = {1, 3, 5, 7, 9};
int[] ints2 = {1, 3, 5, 6, 7};
int i = Arrays.mismatch(ints1, ints2);
System.out.println(i); //3

另一个这样的方法接受fromIndextoIndex参数来查找相对不匹配的索引。例如:

int mismatch(int[] a, int aFromIndex, int aToIndex, 
                int[] b, int bFromIndex, int bToIndex)

从arrayA的索引第1个元素,arrayB索引第2个元素开始的区间内,进行第一个不匹配项目的查找。

int[] arrayA = {-2, 1, 3, 5, 7, 9};
 int[] arrayB = {-1, 0, 1, 3, 5, 7, 10};
 int j = Arrays.mismatch(arrayA, 1, arrayA.length, arrayB, 2, arrayB.length);
 System.out.println(j);

Arrays.equals()

Arrays.mismatch()有些相似,Arrays.equals() 用来判断两个数组区间内的元素是否相等。

新方法为两个被比较的数组新增了fromIndex和toIndex参数。

这些方法根据两个数组的相对索引位置检查两个数组的相等性。

String[] sa = {"d", "e", "f", "g", "h"};
 String[] sb = {"a", "b", "c", "d", "e", "f"};
 boolean b = Arrays.equals(sa, 0, 2, sb, 3, 5);
 System.out.println(b);  //true

从sa数组的索引0-2,与sb数组的索引3-5的元素进行比对。结果为true相等。

Stream流API的增强

在Java 9中对Java Util Stream的语法进行了优化和增强,下面我就和大家一起看一下有哪些比较有价值的使用方法。

Stream.takeWhile(Predicate)

翻译名字:保留集合中的元素直到不满足指定条件为止,保留所有满足条件的元素

在进行Stream流的管道数据处理的时候,提供的Predicate条件返回false之后,将跳过剩余的数据元素直接返回。

在下面的示例中,一旦Predicate条件!"orange" .equals(s)返回false,则将跳过其他元素:

String[] fruits = {"apple", "banana", "orange", "mango", "peach"};
 Stream<String> stream = Arrays.stream(fruits)
                               .takeWhile(s -> !"orange".equals(s));
 stream.forEach(System.out::println);

控制台输出结果为,依次对数组中元素过滤,到orange元素满足了!"orange" .equals(s) === false,流式处理不再继续直接返回。

apple
 banana

需要注意的是:对于无序Stream,如果存在多个与提供的Predicate匹配的元素(多个orange),则此操作返回值是不确定的。

这种方法看上去和Java 8中的Stream.filter()很相似,但是它们的不同之处在于filter()方法只是跳过了不匹配的元素,然后继续进行处理。

然而takeWhile()方法在存在不匹配项之后会跳过所有剩余的元素,有点像continue和break的区别。以下是具有相同流和Predicate的filter()方法示例:

String[] fruits = {"apple", "banana", "orange", "mango", "peach"};
 Stream<String> stream = Arrays.stream(fruits).filter(s -> !"orange".equals(s));
 stream.forEach(System.out::println);

控制台输出如下,只是把orange过滤掉了。

apple
 banana
 mango
 peach

Stream.dropWhile(Predicate)

翻译名字:删除集合中的元素直到不满足指定条件为止,即删除掉所有满足条件的元素

提供的Predicate条件在管道流中返回false之后,此元素后面的所有数据元素作为返回值返回。

String[] fruits = {"apple", "banana", "orange", "mango", "peach"};
 Stream<String> stream = Arrays.stream(fruits)
                            .dropWhile(s -> !"orange".equals(s));
 stream.forEach(System.out::println);

在上面示例中,一旦Predicate条件!"orange".equals(s) 返回false管道流中剩余的元素将被接受(不被过滤),作为返回值返回

orange
 mango
 peach

Stream Stream.iterate(T,Predicate,UnaryOperator)

循环遍历一直对种子进行处理,直到不满足条件为止,遍历过程中对种子的处理过程交给UnaryOperator(单元素处理器)处理

一旦Predicate条件返回false,此方法将返回一个顺序流,该顺序流将停止迭代操作。T为初始值,迭代操作由UnaryOperator来提供

功能其实下面这样

//第一个对应T,第二个对应Predicate,第三个对应UnaryOperator
 for (T index=seed; hasNext.test(index); index = next.apply(index)) {       ...   }
Stream<String> iterate = Stream.iterate("-", 
                                  s -> s.length() < 5,    //Predicate条件
                                  s -> s + "-");   //迭代操作
 iterate.forEach(System.out::println);

控制台打印输出的结果,只输出四个横杠,到第五个的时候停止。这是由Predicate条件决定的。

-
 --
 ---
 ----

Stream Stream.ofNullable(T)

ofNullable()Java 8 中 Stream 不能完全为 null(一个元素不能为 null 多个元素是可以存在 null),否则会报空指针异常。而 Java 9 中的 ofNullable 方法允许我们创建一个单元素 Stream,可以包含一个非空元素,也可 以创建一个空 Stream。

此方法返回一个包含单个元素的顺序Stream。如果提供的元素为null,则此方法返回空Stream。当我们要将非空单个元素附加到流时,此方法很有用。例如:

String nullableItem = "peach";
 Stream<String> stream = Stream.of("apple", "banana", "orange");
 Stream<String> stream2 = Stream.concat(stream, Stream.ofNullable(nullableItem));
 stream2.forEach(System.out::println);

控制台打印输出结果如下:

apple
 banana
 orange
 peach

IntStream,LongStream和DoubleStream方法

下面的这些XxxStream类也具有与Stream类等效的方法(ofNullable()方法除外)。

IntStream.of(2, 4, 6, 8, 9, 10, 11)
          .takeWhile(i -> i % 2 == 0)
          .forEach(System.out::println);   //2,4,6,8
          
 IntStream.of(2, 4, 6, 8, 9, 10, 11)
          .dropWhile(i -> i % 2 == 0)
          .forEach(System.out::println);  // 9,10,11
          
 IntStream.iterate(0, i -> i < 10, i -> i + 1)
          .forEach(System.out::print); // 0123456789

支持多分辨率图片

java.awt.image 包下新增了支持多分辨率图片的API,用于支持多分辨率的图片。

  1. 将不同分辨率的图像封装到一张(多分辨率的)图像中,作为它的变体。
  2. 获取这个图像的所有变体。
  3. 获取特定分辨率的图像变体,表示一张已知分辨率单位为 DPI 的特定尺寸大小的逻辑图像,并且
    这张图像是最佳的变体。。
  4. 通过接口的getResolutionVariant(double destImageWidth, double destImageHeight)方法,
    根据分辨率获取图像。

全新的 HTTPClinet

这里只做简单使用演示,后续会出对HTTPClinet使用详细解析篇

HttpClient的基本概述

httpClient的作用就是用于获取网络资源的,Java 9 中有新的方式来处理 HTTP 调用。

它提供了一个新的 HTTP 客户端(HttpClient ), 它 将替代仅适用于 blocking 模式的 HttpURLConnection(HttpURLConnection是在HTTP 1.0的时代创建的, 并使用了协议无关的方法),并提供对WebSocket 和 HTTP/2 的支持。

全新的HTTP客户端API可以从jdk.incubator.httpclient模块中获取。

因为在默认情况下,这个模块是不能根据 classpath 获取的,需要使 用 add modules 命令选项配置这个模块,将这个模块添加到classpath 中。

代码演示

因为我这里使用的是jdk11,所以讲一下jdk11中对httpClient的改变

变化:

一: 从java9的jdk.incubator.httpclient模块迁移到java.net.http模块,包名由jdk.incubator.http改为java.net.http

二: 原来的诸如HttpResponse.BodyHandler.asString()方法变更为HttpResponse.BodyHandlers.ofString(),变化一为BodyHandler改为BodyHandlers,变化二为asXXX()之类的方法改为ofXXX(),由as改为of

public class DhyHttpClient {
    public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
        //1.创建HttpClient对象
        HttpClient httpClient = HttpClient.newHttpClient();
        //2.创建请求的构造器
        HttpRequest request = HttpRequest.newBuilder(new URI("http://www.baidu.com"))
                //设置请求头
                .header("user-agent", "dhy")
                //设置请求方式
                .GET()
                .build();
        //发送请求,并设置返回值处理器
        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println("响应状态码: "+response.statusCode());
        System.out.println("响应的信息: "+response.body());
    }
}

小结

HttpClient发送请求的步骤

  • 创建HttpClient的客户端
  • 创建请求构造器
  • 使用请求构造器创建请求
  • 使用客户端发送请求,并且得到响应对象
  • 查看响应的内容

被废弃的API

并且随着对安全要求的提高,主流浏览器已经取消对 Java 浏览器插件的支持。HTML5 的出现也进一步加速了它的消亡。开发者现在可以使用像 Java Web Start 这样的技术来代替 Applet,它可以实现从浏览器启动应用程序或者安装应用程序。

javadoc 的 HTML5 支持

JDK8 生成的java帮助文档是在 HTML4 中。而HTML4 已经是很久的标准了。

JDK9 的javadoc,现支持HTML5 标准。

下图是JDK8 API的HTML页面

java的动态编译器

动态编译器出现的目的就是为了提高编译的效率。 sjavac(smarter java compilation)最早在openjdk8中提供了初级版本,其初衷是用来加速jdk自己的编译。在9中进行过一版优化,使其更加稳定可靠,能够用来编译任意的大型java项目。sjavac在javac的基础上实现了:

  • 增量编译 – 只重新编译必要的内容
  • 并行编译 – 在编译期间使用多个核心

钻石操作符使用升级

钻石操作符是在 java 7 中引入的,可以让代码更易读,但它不能用于匿名的内部类。

在 java 9 中, 它可以与匿名的内部类一起使用,从而提高代码的可读性。

考虑以下 Java 9 之前的代码:

public class Tester {
   public static void main(String[] args) {
      Handler<Integer> intHandler = new Handler<Integer>(1) {
         @Override
         public void handle() {
            System.out.println(content);
         }
      };
      intHandler.handle();
      Handler<? extends Number> intHandler1 = new Handler<Number>(2) {
         @Override
         public void handle() {
            System.out.println(content);
         }
      };
      intHandler1.handle();
      Handler<?> handler = new Handler<Object>("test") {
         @Override
         public void handle() {
            System.out.println(content);
         }
      };
      handler.handle();    
   }  
}
abstract class Handler<T> {
   public T content;
 
   public Handler(T content) {
      this.content = content; 
   }
   
   abstract void handle();
}

在 Java 9 中,我们可以在匿名类中使用 <> 操作符,如下所示:

public class Tester {
   public static void main(String[] args) {
      Handler<Integer> intHandler = new Handler<>(1) {
         @Override
         public void handle() {
            System.out.println(content);
         }
      };
      intHandler.handle();
      Handler<? extends Number> intHandler1 = new Handler<>(2) {
         @Override
         public void handle() {
            System.out.println(content);
         }
      };
      intHandler1.handle();
      Handler<?> handler = new Handler<>("test") {
         @Override
         public void handle() {
            System.out.println(content);
         }
      };
 
      handler.handle();    
   }  
}
 
abstract class Handler<T> {
   public T content;
 
   public Handler(T content) {
      this.content = content; 
   }
   
   abstract void handle();
}

Java.util.Optional优化与增强

Java9的ifPresentOrElse(Consumer,Runnable)

Java 9 中的增强

如果存在值,则此新方法将执行给定的Consumer操作,否则运行给定的Runnable操作。下面的代码先使用Java 8的的Stream流过滤3的倍数,然后通过findFirst找到第一个3的倍数。如果找到一个这样的值,就print控制台打印出来;如果没找到一个这样的值,就输出"没有找到3的倍数"
ifPresentOrElse(Consumer,Runnable)的语义可以解释为:ifPresent就Consumer,OrElse就Runnable。这是Java 9 才有的增强方法。

IntStream.of(1, 2, 4)
                .filter(i -> i % 3 == 0)
                .findFirst()
                .ifPresentOrElse(System.out::println, () -> {
                    System.out.println("没有找到3的倍数");
                });

在1、2、4中没有3的倍数,所以输出结果如下

没有找到3的倍数

如果是下面的2、6、8数组被过滤,最终控制台输出结果为:6

IntStream.of(2, 6, 8)
          .filter(i -> i % 3 == 0)
          .findFirst()
          .ifPresentOrElse(System.out::println, () -> {
              System.out.println("没有找到3的倍数");
          });   // 6

回顾一下Java 8中的写法

Java 8 Optional.isPresent():

如果使用Java 8 ,没有ifPresentOrElse(Consumer,Runnable)方法,上文中同样的代码你应该是这样写的:自己去写if和else进行判断。同样输出:没有找到3的倍数

OptionalInt opt = IntStream.of(1, 2, 4)
                            .filter(i -> i % 3 == 0)
                            .findFirst();
 if (opt.isPresent()) {
     System.out.println(opt.getAsInt());
 } else {
     System.out.println("没有找到3的倍数");
 }

Java 8 Optional.ifPresent():

ifPresent()方法在值不存在的时候,没有提供一个可选的操作。所以下面的代码在执行之后,没有orElse动作,没有任何输出,这样不是很友好。

IntStream.of(1, 2, 4)
          .filter(i -> i % 3 == 0)
          .findFirst()
          .ifPresent(System.out::println);

Java 8 Optional.orElse():

orElse()方法在value返回值为空的之后,给出一个默认值。下文代码中给出一个默认值-1。

int result = IntStream.of(1, 2, 4)
                       .filter(i -> i % 3 == 0)
                       .findFirst()
                       .orElse(-1);
 System.out.println(result);   //-1

Java9的Optional.or(Supplier)

该方法在找不到值的时候,生成一个新的Optional出来。下文代码过滤数组['a', 'b', 'c'],isDigit判断数组中是否有数字字符,明显没有,所以findFirst找不到一个这样的值。所以生成一个默认值: Optional.of('0')

char digit = Stream.of('a', 'b', 'c')
                    .filter(e -> Character.isDigit(e))
                    .findFirst()
                    .or(() -> Optional.of('0')).get();
 System.out.println(digit);   //0

Java8 中的 orElseGet()

Java 8中的Optional.orElseGet()方法也具备同样的功能。下文代码过滤数组[‘a’, ‘b’, ‘c’],isDigit判断数组中是否有数字字符,明显没有,所以findFirst找不到一个这样的值。所以通过orElseGet获取一个默认值: ‘0’

char result = Stream.of('a', 'b', 'c')
                    .filter(c -> Character.isDigit(c))
                    .findFirst()
                    .orElseGet(()->'0');
 System.out.println(result);   //0

Java9的Optional.stream()

在本例中Optional.stream()方法返回仅包含一个最大值元素的Stream流。如果该值不存在,则返回空Stream:

OptionalInt opt1 = IntStream.of(2, 5, 6).max();  //求最大值
 OptionalInt opt2 = IntStream.of(1, 3, 7).max();  //求最大值
 IntStream.concat(opt1.stream(), opt2.stream())  //将2个流合并
          .forEach(System.out::println);   //将合并后的流数据打印

控制台输出的结果如下:

6
 7

Reactive Stream API响应式编程

Java 9的 Reactive Streams是对异步流式编程的一种实现。它基于异步发布和订阅模型,具有非阻塞“背压”数据处理的特点。

Non-blocking Back Pressure(非阻塞背压):它是一种机制,让发布订阅模型中的订阅者避免接收大量数据(超出其处理能力),订阅者可以异步通知发布者降低或提升数据生产发布的速率。它是响应式编程实现效果的核心特点!

Java9 Reactive Stream API

Java 9提供了一组定义响应式流编程的接口。所有这些接口都作为静态内部接口定义在java.util.concurrent.Flow类里面。

下面是Java 响应式编程中的一些重要角色和概念,先简单理解一下

  • 发布者(Publisher)是潜在的无限数量的有序数据元素的生产者。它根据收到的需求(subscription)向当前订阅者发布一定数量的数据元素。
  • 订阅者(Subscriber)从发布者那里订阅并接收数据元素。与发布者建立订阅关系后,发布者向订阅者发送订阅令牌(subscription),订阅者可以根据自己的处理能力请求发布者发布数据元素的数量。
  • 订阅令牌(subscription)表示订阅者与发布者之间建立的订阅关系。 当建立订阅关系后,发布者将其传递给订阅者。
    订阅者使用订阅令牌与发布者进行交互,例如请求数据元素的数量或取消订阅。

Java响应式编程四大接口

Subscriber Interface(订阅者订阅接口)

public static interface Subscriber<T> {
    public void onSubscribe(Subscription subscription);
    public void onNext(T item);
    public void onError(Throwable throwable);
    public void onComplete();
}
  • onSubscribe:在发布者接受订阅者的订阅动作之后,发布任何的订阅消息之前被调用。新创建的Subscription订阅令牌对象通过此方法传递给订阅者。
  • onNext:下一个待处理的数据项的处理函数
  • onError:在发布者或订阅遇到不可恢复的错误时调用
  • onComplete:当没有订阅者调用(包括onNext()方法)发生时调用。

Subscription Interface (订阅令牌接口)

订阅令牌对象通过Subscriber.onSubscribe()方法传递

public static interface Subscription {
    public void request(long n);
    public void cancel();
}
  • request(long n)是无阻塞背压概念背后的关键方法。订阅者使用它来请求n个以上的消费项目。这样,订阅者控制了它当前能够接收多少个数据。
  • cancel()由订阅者主动来取消其订阅,取消后将不会在接收到任何数据消息。

Publisher Interface(发布者接口)

@FunctionalInterface
public static interface Publisher<T> {
    public void subscribe(Subscriber<? super T> subscriber);
}

调用该方法,建立订阅者Subscriber与发布者Publisher之间的消息订阅关系。

Processor Interface(处理器接口)

处理者Processor 可以同时充当订阅者和发布者,起到转换发布者——订阅者管道中的元素的作用。用于将发布者T类型的数据元素,接收并转换为类型R的数据并发布。

public static interface Processor<T,R> extends Subscriber<T>, Publisher<R> {
}

实战案例

现在我们要去实现上面的四个接口来完成响应式编程

  • Subscription Interface订阅令牌接口通常不需要我们自己编程去实现,我们只需要在知道request()方法和cancle()方法含义即可。
  • Publisher Interface发布者接口,Java 9 已经默认为我们提供了实现SubmissionPublisher,该实现类除了实现Publisher接口的方法外,提供了一个方法叫做submit()来完成消息数据的发送。
  • Subscriber Interface订阅者接口,通常需要我们自己去实现。因为在数据订阅接收之后,不同的业务有不同的处理逻辑。
  • Processor实际上是 Publisher Interface和Subscriber Interface的集合体,有需要数据类型转换及数据处理的需求才去实现这个接口

下面的例子实现的式字符串的数据消息订阅处理

实现订阅者Subscriber Interface

import java.util.concurrent.Flow;

public class MySubscriber implements Flow.Subscriber<String> {

  private Flow.Subscription subscription;  //订阅令牌

  @Override
  public void onSubscribe(Flow.Subscription subscription) {
      System.out.println("订阅关系建立onSubscribe: " + subscription);
      this.subscription = subscription;
      subscription.request(2);
  }

  @Override
  public void onNext(String item) {
      System.out.println("item: " + item);
      // 一个消息处理完成之后,可以继续调用subscription.request(n);向发布者要求数据发送
      //subscription.request(n);
  }

  @Override
  public void onError(Throwable throwable) {
      System.out.println("onError: " + throwable);
  }

  @Override
  public void onComplete() {
      System.out.println("onComplete");
  }
}

SubmissionPublisher消息发布者

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;

public class SubmissionPublisherExample {
  public static void main(String[] args) throws InterruptedException {
      ExecutorService executor = Executors.newFixedThreadPool(1);
      SubmissionPublisher<String> sb = new SubmissionPublisher<>(executor, Flow.defaultBufferSize());
      sb.subscribe(new MySubscriber());   //建立订阅关系,可以有多个订阅者
      sb.submit("数据 1");  //发送消息1
      sb.submit("数据 2"); //发送消息2
      sb.submit("数据 3"); //发送消息3

      executor.shutdown();
  }
}

控制台打印输出结果

订阅关系建立
onSubscribe: java.util.concurrent.SubmissionPublisher$BufferedSubscription@27e81a39
item: 数据 1
item: 数据 2

请注意:即使发布者submit了3条数据,MySubscriber也仅收到了2条数据进行了处理。是因为我们在MySubscriber#onSubscribe()方法中使用了subscription.request(2);这就是“背压”的响应式编程效果,我有能力处理多少数据,就会通知消息发布者给多少数据

Java 9 改进的 @Deprecated 注解

注解 @Deprecated 可以标记 Java API 状态,可以是以下几种:

  • 使用它存在风险,可能导致错误
  • 可能在未来版本中不兼容
  • 可能在未来版本中删除
  • 一个更好和更高效的方案已经取代它。

Java 9 中注解增加了两个新元素:since 和 forRemoval。

  • since: 元素指定已注解的API元素已被弃用的版本。
  • forRemoval: 元素表示注解的 API 元素在将来的版本中被删除,应该迁移 API。

以下实例为 Java 9 中关于 Boolean 类的说明文档,文档中 @Deprecated 注解使用了 since 属性:Boolean Class。

以下实例为在 Java 9 中关于系统类的说明文档,文档中 @Deprecated 注解使用了 forRemoval 属性:System Class。

Java 9 改进的 CompletableFuture API

Java 8 引入了 CompletableFuture<T> 类,可能是 java.util.concurrent.Future<T> 明确的完成版(设置了它的值和状态),也可能被用作java.util.concurrent.CompleteStage 。支持 future 完成时触发一些依赖的函数和动作。

Java 9 引入了一些CompletableFuture 的改进:

  • 支持 delays 和 timeouts
  • 提升了对子类化的支持
  • 新的工厂方法

支持 delays 和 timeouts

public CompletableFuture<T> completeOnTimeout(T value, long timeout, TimeUnit unit)

timeout(单位在 java.util.concurrent.Timeunits units 中,比如 MILLISECONDS )前以给定的 value 完成这个 CompletableFutrue。返回这个 CompletableFutrue

public CompletableFuture<T> orTimeout(long timeout, TimeUnit unit)

如果没有在给定的 timeout 内完成,就以 java.util.concurrent.TimeoutException 完成这个 CompletableFutrue,并返回这个 CompletableFutrue。

增强了对子类化的支持

做了许多改进使得 CompletableFuture 可以被更简单的继承。比如,你也许想重写新的 public Executor defaultExecutor() 方法来代替默认的 executor

另一个新的使子类化更容易的方法是:

public <U> CompletableFuture<U> newIncompleteFuture()

新的工厂方法

Java 8引入了 <U> CompletableFuture<U> completedFuture(U value) 工厂方法来返回一个已经以给定 value 完成了的 CompletableFutureJava 9以 一个新的 <U> CompletableFuture<U> failedFuture(Throwable ex) 来补充了这个方法,可以返回一个以给定异常完成的 CompletableFuture

除此以外,Java 9 引入了下面这对 stage-oriented 工厂方法,返回完成的或异常完成的 completion stages:

  • <U> CompletionStage<U> completedStage(U value): 返回一个新的以指定 value完成的CompletionStage ,并且只支持 CompletionStage 里的接口。
  • <U> CompletionStage<U> failedStage(Throwable ex):返回一个新的以指定异常完成的CompletionStage ,并且只支持 CompletionStage 里的接口。

相关文章

目录