Java语法新特性_java5到java11

一、前言

万万没想到,都0202年了,Sun都亡了,老夫还要从Java5的新特性开始写,还要重点写Java8的新特性。。。

其实网上这种玩意一大堆,为啥老夫还要写呢?

  1. 因为领导们觉得大家平时太(真)忙(懒),没有时间去学习,所以集中讲一下,来个速(应)成(付)。
  2. 网上的资料讲的都挺好,都很全面,但对于我们外(死)包(跑)开(龙)发(套)来说,应该重点关注那些能提高生(搬)产(砖)效率的新特性。
  3. 之所以还是从Java5的新特性开始讲起,表面的原因是更能清楚地看到java发展的脉络。真实原因大家自己慢慢品。。。

这个速成版资料特点有:

  1. 主要讲对开发影响比较大的新特性,比如Java类库API的新增或增强,编译(javac)层面的语法糖等等。其他的在字节码层面,Java整体架构层面,以及虚拟机内部的改进,因为对开发几乎没有影响,这个速成版资料就略过不讲了。(老夫也讲不好。。。)
  2. Java5到Java7的新特性只是简单过一遍,重点讲Java8的新特性。
  3. Java9,Java10是过度版本,因此它们的新特性会跟Java11放到一起讲。
  4. 相关代码均在JDK11 + IDEA 2019.1 环境上运行通过。
当然,光说不练假把式,这个速成版资料所有示例代码位于下面的github或gitee仓库,请各位自行下载,在本地准备好Java与IDE后,自行参考并练习:(代码位于 src/test/java下)

https://github.com/zhaochuninhefei/study-czhao/tree/master/jdk11-test
或 : https://gitee.com/XiaTangShaoBing/study/tree/master/jdk11-test

二、Java5到Java7的新特性

这一章主要快速地讲一下Java5到Java7在语法上的一些重要的新特性,以及一些重要的新的类库API。

2.1 Java5的新特性

Java5新特性比较多,但大部分我们都已经很熟悉了,简单过一下:

  • 泛型 Generics :
泛型即参数化类型(Parameterized Type)。引入泛型之后,允许指定集合里元素的类型,免去了强制类型转换,并且能在编译时刻进行类型检查。泛型是长度可变的参数列表(vararg)、注解(annotation)、枚举(enumeration)、集合(collection)的基石。
List lst01 = new ArrayList();

// 用 ? 表示接受任何类型,可以避免调用方法时类型检查警告。
private void test01(List list) {
    for (Iterator i = list.iterator(); i.hasNext(); ) {
        System.out.println((i.next().toString()));
    }
}

// 限制类型,此处表示参数类型必须继承TestCase01Generic
private  void test02(T t) {
    t.doSomething();
}
  • 枚举 Enumeration :
枚举类是一种特殊的类,它和普通的类一样,有自己的成员变量、成员方法、构造器 (只能使用 private 访问修饰符,所以无法从外部调用构造器,构造器只在构造枚举值时被调用);enum 定义的枚举类默认继承了 java.lang.Enum 类,并实现了 java.lang.Seriablizable 和 java.lang.Comparable 两个接口;所有的枚举值默认都是 public static final 的(无需显式添加),且非抽象的枚举类不能再派生子类;枚举类的所有实例(枚举值)必须在枚举类的第一行显式地列出,否则这个枚举类将永远不能产生实例。列出这些实例(枚举值)时,系统会自动添加 public static final 修饰,无需程序员显式添加。
enum Color {
    black, white, red, yellow
}

// 枚举经常用于switch语句
private void test01(Color color) {
    switch (color) {
        case red:
            System.out.println("霜叶红于二月花");
            break;
        case black:
            System.out.println("黑云压城城欲摧");
            break;
        case white:
            System.out.println("一行白鹭上青天");
            break;
        case yellow:
            System.out.println("故人西辞黄鹤楼");
            break;
    }

    System.out.println(Color.black.compareTo(color));
    System.out.println(Color.white.compareTo(color));
    System.out.println(Color.red.compareTo(color));
    System.out.println(Color.yellow.compareTo(color));
}
  • 自动装箱拆箱 autoboxing & unboxing :
八种primitive类型与其封装引用类型的自动装箱与拆箱:Boolean、Byte、Short、Character、Integer、Long、Float、Double
List lstInt = new ArrayList();
lstInt.add(1);
lstInt.add(2);
lstInt.add(3);

for (int i = 0; i < lstInt.size(); i++) {
    System.out.println(lstInt.get(i).toString());
    System.out.println(lstInt.get(i) + 1);
}
  • 长度可变的参数列表 varargs number of arguments : 参数类型相同时,把重载函数合并到一起。
me.test01("One ring to rule them all,");
me.test01("one ring to find them,", "One ring to bring them all ", "and in the darkness bind them.");

private void test01(String ... args) {
    for (String s : args) {
        System.out.println(s);
    }
}
  • 注解 Annotations :

注解用于为 Java 代码提供元数据。一般来说注解不会直接影响代码执行,很多注解的作用就是做数据约束和标准定义,可以将其理解成代码的规范标准(代码的模板),但有一些注解可以存活到JVM运行时,因此可以结合其他手段(如反射)来影响实际运行的代码逻辑。所以注解的目的一般来说有二:一则规范代码;二则动态注入(需要配合其他手段实现)。

通常注解可以分为四类:

  1. Java自带的标准注解,如@Override、@Deprecated、@SuppressWarnings等,通常编译时就会依据这些注解对代码进行检查;
  2. 元注解,用于定义注解的注解,包括@Retention、@Target、@Inherited、@Documented等。
  3. 第三方注解,如spring,mybatis,lombok都提供了自己的注解。
  4. 自定义注解,使用@interface与元注解配合定义。
// 编译器看到 @Override 注解,就知道这个方法须是重写父类的方法
// 因此会严格检查方法声明信息是否与父类对应方法相同
// 如返回值类型,参数列表等等
@Override
public String toString() {
    return "解落三秋叶,能开二月花。";
}

// 一个自定义注解的例子,用于AOP中对方法参数进行非空检查
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ParamNotEmpty {
}
  • foreach循环 : 迭代器循环的一个语法糖。
List numbers = new ArrayList();
for (int i = 0; i < 10; i++) {
    numbers.add(i + 1);
}

for(Integer number : numbers) {
    System.out.println(number);
}
  • 静态导入 import static : 没啥好说的,直接看代码,不建议使用。
package java5;

import static java5.TestCase07ImportStatic.TestInner.test;
import static java.lang.System.out;
import static java.lang.Integer.*;

/**
 * @author zhaochun
 */
public class TestCase07ImportStatic {
    public static void main(String[] args) {
        test();
        out.println(MIN_VALUE);
        out.println(toBinaryString(100));
    }

    static class TestInner {
        public static void test() {
            System.out.println("TestInner");
        }
    }
}
  • 格式化 : Java5中新增了printf-style格式化字符串的解释器。
private void test01_formatter() {
    StringBuilder sb = new StringBuilder();
    Formatter formatter = new Formatter(sb);
    // "  前不见古人,  后不见来者。 念天地之悠悠, 独怆然而涕下。"
    formatter.format("%4$7s,%3$7s。%2$7s,%1$7s。%n", "独怆然而涕下", "念天地之悠悠", "后不见来者", "前不见古人");
    // "祖冲之的迷之数字 : +3.1415927 "
    formatter.format("祖冲之的迷之数字 : %+5.7f %n", Math.PI);
    // "某款手机价格 : ¥ 5,988.00"
    formatter.format("某款手机价格 : ¥ %(,.2f", 5988.0);
    System.out.println(formatter.toString());
    formatter.close();
}

private void test02_printf() {
    List lines = new ArrayList<>();
    lines.add("人闲桂花落,");
    lines.add("夜静春山空。");
    lines.add("月出惊山鸟,");
    lines.add("时鸣春涧中。");
    for (int i = 0; i < lines.size(); i++) {
        System.out.printf("Line %d: %s%n", i + 1, lines.get(i));
    }
}

private void test03_stringFormat() {
    Calendar c = new GregorianCalendar(2020, Calendar.MAY, 28);
    System.out.println(String.format("今天是个好日子: %1$tY-%1$tm-%1$te", c));
}

private void test04_messageFormat() {
    String msg = "您好,{0}!有您的快递哦!请到{1}号柜拿取您的快递,每超时{2}小时要收费{3}元哦~~~";
    MessageFormat mf = new MessageFormat(msg);
    String fmsg = mf.format(new Object[]{"张三", 3, 8, 2});
    System.out.println(fmsg);
}

private void test05_dateFormat() {
    String str = "2020-05-28 14:55:21";
    SimpleDateFormat format1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    SimpleDateFormat format2 = new SimpleDateFormat("yyyyMMddHHmmss");
    try {
        System.out.println(format2.format(format1.parse(str)));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

其他诸如ProcessBuilder,Scanner,增强反射,增强集合框架,StringBuilder,concurrent并发工具包,等等,因为要么用得少,要么大家已经很熟悉了,这里就不再一一介绍了。

2.2 Java6的新特性

Java6的新特性很少,对开发几乎没有影响,简单看一下。

  • WebService注解支持
  • 引入了一个可以运行Javascript,python等脚本语言的引擎
  • Compiler API,运行期动态编译java源码
  • Http Server API
  • 通用的Annotations支持
  • JDBC 4.0
  • 集合框架增强,增加了一些不常用的接口,类和方法。

还有一些其他的,不列了。

2.3 Java7的新特性

Java7的新特性也不多,但相比Java6,还是有几个新语法或新类库API能改善开发效率的,我们来看一下。

  • switch支持String
private String test01_switch(String title) {
    switch (title) {
        case "鹿柴":
            return "空山不见人,但闻人语响。返景入深林,复照青苔上。";
        case "山中送别":
            return "山中相送罢,日暮掩柴扉。春草明年绿,王孙归不归。";
        case "渭城曲":
            return "渭城朝雨浥轻尘,客舍青青柳色新。劝君更尽一杯酒,西出阳关无故人。";
        default:
            return "";
    }
}
  • 实例化时自动推断泛型类型
List tempList = new ArrayList<>();
  • autoclose接口 : 部分资源管理类,如文件IO,JDBC的Conection等,实现了AutoCloseable接口,它们可以使用try-with-resources新语法。
String filePath = "/home/work/sources/jdk11-test/src/test/java/java7/TestCaseForJava7.java";
try (FileInputStream fis = new FileInputStream(filePath);
        InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8);
        BufferedReader br = new BufferedReader(isr)) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}
  • 捕获多个异常
try {
    if (n < 0) {
        throw new FileNotFoundException();
    }
    if (n > 0) {
        throw new SQLException();
    }
    System.out.println("No Exceptions.");
} catch (FileNotFoundException | SQLException e) {
    e.printStackTrace();
}
  • 数字增强 : java7支持用下划线分割较长的数字,支持用0b开头直接写二进制数字。
int num1 = 1_000_000;
System.out.println(num1);

int num2 = 0b11;
System.out.println(num2);
  • New IO 2.0 : Java7提供了一些新的文件操作API,如Path,并且提供了对指定目录进行监视的WatchService,能够监听指定目录下文件的增删改事件。(但并不能直接监听文件变化内容)
private void test06_newIO2() {
    Path path = Paths.get("/home/zhaochun/test");
    System.out.printf("Number of nodes: %s %n", path.getNameCount());
    System.out.printf("File name: %s %n", path.getFileName());
    System.out.printf("File root: %s %n", path.getRoot());
    System.out.printf("File parent: %s %n", path.getParent());

    try {
        Files.deleteIfExists(path);
        Files.createDirectory(path);
        watchFile(path);
    } catch (IOException | InterruptedException e) {
        e.printStackTrace();
    }
}

private void watchFile(Path path) throws IOException, InterruptedException {
    WatchService service = FileSystems.getDefault().newWatchService();
    Path pathAbs = path.toAbsolutePath();
    pathAbs.register(service,
            StandardWatchEventKinds.ENTRY_CREATE,
            StandardWatchEventKinds.ENTRY_MODIFY,
            StandardWatchEventKinds.ENTRY_DELETE);
    while (true) {
        WatchKey key = service.take();
        for (WatchEvent event : key.pollEvents()) {
            String fileName = event.context().toString();
            String kind = event.kind().name();

            System.out.println(String.format("%s : %s", fileName, kind));
            if ("end".equals(fileName) && "ENTRY_DELETE".equals(kind)) {
                return;
            }
        }
        key.reset();
    }
}
  • JDBC 4.1 : Connection接口增加了一些方法,如果以前有对JDBC Connection的实现或封装,升级到Java7后会编译不过。如果一直使用各个数据库提供的JDBC驱动包,只需要确认版本支持 JDBC 4.1 以上即可。
  • Fork/Join 框架 : Java7增加了一个新的多线程编程框架,fork/join。很少直接用,而且Java8以后基于这个并行编程框架增加了一种集合操作的并行模式,所以我们后面学习Java8的新特性时会简单讲一下这个fork/join的机制,这里就不说了。

Java7还有一些其他的新特性,但对开发影响不大,这里就不一一讲述了。

三、Java8的新特性

Java8是Java继Java5之后又一个具有里程碑意义的大版本,有很多革命性的新特性。

当然,Java8新特性虽多,但我们主要讲那些对开发影响比较大的,语法上的新特性:

  • lambda表达式
  • Stream API
  • 接口默认方法
  • Optional
  • Map操作及HashMap性能优化
  • Date API
  • CompletableFuture

3.1 lambda表达式

Java8最重要的新特性就是添加了对lambda表达式的支持,使得Java可以进行函数式编程(functional programming)。

3.1.1 什么是lambda表达式

Lambda表达式就是可按引用传递的代码块,类似于其他语言的闭包的概念:它们是实现某项功能的代码,可接受一个或多个输入参数,而且可返回一个结果值。闭包是在一个上下文中定义的,可访问来自上下文的值。

而在Java8中,lambda表达式可以被具体地表述为函数式接口的一个具体实现。所谓函数式接口,就是只定义了一个抽象方法的interface。(函数式接口可以通过添加注解@FunctionalInterface,从而在编译时强制检查该接口是否只有一个抽象方法。但这个注解不是必须的。)

我们先看一个具体的例子:

假设我们有这样一个接口,它只有一个抽象方法,是一个函数式接口:

@FunctionalInterface
interface TestLambda {
    String join(String a, String b);
}

以及一个使用它的方法:(显然这个方法并不需要知道具体实现TestLambda接口的类是谁)

private String joinStr(TestLambda testLambda, String a, String b) {
    return testLambda.join(a, b);
}

接下来,我们尝试使用joinStr方法来连接两个字符串。在Java8之前,我们往往使用匿名内部类在需要的地方直接实现TestLambda接口:

String s1 = joinStr(new TestLambda() {
    @Override
    public String join(String a, String b) {
        return a + ", " + b;
    }
}, "问君能有几多愁", "恰似一江春水向东流");
System.out.println(s1);

很显然,匿名内部类很臃肿,语义上也不够直观,大家受够了没有?

从Java8开始,你可以使用lambda表达式代替匿名内部类,就是下面代码中的(a, b) -> a + ", " + b;这种写法,很简洁,语义直观,更接近自然语言:

TestLambda simpleJoin = (a, b) -> a + ", " + b;
String s2 = joinStr(simpleJoin, "高堂明镜悲白发", "朝如青丝暮成雪");
System.out.println(s2);

或直接写为:

String s3 = joinStr((a, b) -> a + ", " + b, "高堂明镜悲白发", "朝如青丝暮成雪");
System.out.println(s3);

当你要实现的接口逻辑比较复杂时,你可以用{}把代码块包起来;你还可以给每个入参声明类型:

TestLambda joinWithCheck = (String a, String b) -> {
    if (a != null && b != null) {
        return a + ", " + b;
    } else {
        return "空空如也";
    }
};
String s4 = joinStr(joinWithCheck, null, null);
System.out.println(s4);

至此我们可以知道:

  • 对于那些参数为函数式接口的方法,可以在调用时传入一个lambda表达式,这个lambda表达式就是接口的一个具体实现。
  • lambda表达式在形式上表现为(函数的参数列表) -> {函数实现}
  • 用来包裹函数实现的{}在仅有一行时可以省略。
  • 仅有一行实现没有{}时,默认返回这一行代码的计算结果(函数有返回值时)。
  • 多行实现有{}时,需要显式返回对应类型的计算结果(函数有返回值时)。
  • lambda表达式在效果上,可以认为等同于之前的匿名内部类。(但两者实现机制并不相同,lambda表达式并不能简单视为匿名内部类的高级语法糖。)
  • lambda表达式可以内联,也可以提为单独的变量或方法引用。
  • lambda表达式实现的函数式接口为啥只能定义一个抽象方法呢?因为lambda表达式不使用方法名啊。。。方法弄多了不晓得该调用哪个方法了。。。

3.1.2 lambda表达式对上下文的访问限制

lambda表达式内部是可以访问外部变量的。但要注意的是,如果这个外部变量是局部变量,那么这个局部变量必须是final的(可以不声明为final,但不能对其二次赋值,即,需要隐式final)。

private void test02_finalVars() {
    String a = "王维";
    new Thread(() -> {
        // lambda表达式里可以使用外部的final局部变量(不用显式声明final)
        System.out.println(a);
        // 下面这句编译不过,不能对"lambda表达式里使用的外部局部变量"重新赋值。
        // 即lambda内部使用的外部局部变量是隐式final的。
//            a = "李白";
    }).start();
    // 在lambda外面也不能对a重新赋值,因为需要在lambda表达式里使用,因此a是隐式final的。
//        a = "李白";
}
注意是局部变量不能重新赋值。对于实例变量,静态变量来说,可以在lambda表达式里随意访问,包括重新赋值。

3.1.3 方法引用

Java8除了提供比较标准(对比其他语言)的lambda表达式以外,还提供了一种叫做方法引用的简便形式。

  • 对象实例的方法引用 instance::method
new Thread(this::test02_finalVars).start();
// 上面这句等价于下面这句:
new Thread(() -> this.test02_finalVars()).start();

test02_finalVars是前面例子里的一个实例方法。

  • 类的静态方法引用 Class::static_method
new Thread(TestCase01Lambda::printSomething).start();
// 等价于:
new Thread(() -> TestCase01Lambda.printSomething()).start();
...
private static void printSomething() {
    System.out.println("大漠孤烟直,长河落日圆。");
}
  • 类的实例方法引用 Class::method
List lines = new ArrayList<>();
lines.add("a005");
lines.add("a001");
lines.add("a003");
Collections.sort(lines, String::compareTo);
// 等价于:
Collections.sort(lines, (o1, o2) -> o1.compareTo(o2));
System.out.println(lines);
  • 构造器引用 Class::new
Set lineSet = transferElements(lines, HashSet::new);
// 等价于
lineSet = transferElements(lines, () -> new HashSet<>());
System.out.println(lineSet);
...
private static , DEST extends Collection> DEST transferElements(
        SOURCE sourceCollection,
        Supplier collectionFactory) {

    DEST result = collectionFactory.get();
    result.addAll(sourceCollection);
    return result;
}

3.1.4 标准函数式接口

之前我们说过了,lambda表达式实现的只能是函数式接口,即,只有一个抽象方法定义的接口。Java8还为了lambda接口的广泛使用,增加了新的java.util.function包,定义了一些可以广泛使用lambda的函数式接口。

  • Function:接受一个参数,基于参数值返回结果
  • Predicate:接受一个参数,基于参数值返回一个布尔值
  • BiFunction:接受两个参数,基于参数值返回结果
  • Supplier:不接受参数,返回一个结果
  • Consumer:接受一个参数,无结果 (void)

这些标准函数式接口在Stream的操作中得到了广泛的应用,我们后面讲到Stream的时候会处处看到它们的身影。

如果你现在就去看这些接口的源码,你会发现它们虽然都只定义了一个抽象方法,但内部往往还有一些default的实例方法。是不是有点懵逼,接口不是没有实例方法的吗?这个我们后面讲到Java8的另一个新特性(接口默认方法)的时候再具体说。

3.2 Stream API

Java8中新增的Stream API是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高了编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用fork/join并行方式(Java7的新特性,因为很少直接用,我们没有讲这个) 来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。

所谓聚合操作,就是对数据集的各种统计操作,比如:平均值,总和,最小,最大,计数等等。在我们开发的信息系统中,这些聚合操作往往是通过关系型数据库的各种查询SQL完成的。如果想在Java应用中完成这些操作,那么我们需要自己开发集合操作,通过不停的显式地对集合进行遍历,循环执行运算逻辑来达成。这些程序不但开发繁琐,也不易维护,同时不小心就会出现性能问题。

而Java8提供的Stream API,则让聚合操作的开发变得非常简单;代码可读性更高;在多核机器上面对耗时的可并发聚合操作时,使用并行模式的性能表现也会更好。

3.2.1 Stream总体介绍

现在我们有了个初步的概念,就是Stream是在对数据集做聚合操作。我们先看一个典型的Stream完成聚合操作的例子:

int sum = Stream.of("", "1", null, "2", " ", "3")
        .filter(s -> s != null && s.trim().length() > 0)
        .map(s -> Integer.parseInt(s))
        .reduce((left, right) -> right += left)
        .orElse(0);
这个例子是在计算一个集合中所有数字的合计值。

先简单讲解一下上面这个Stream操作的过程:

  1. Stream.of("", "1", null, "2", " ", "3") : 获取数据源的Stream对象;
  2. .filter(s -> s != null && s.trim().length() > 0) : 过滤前面返回的Stream对象,并返回过滤后的新的Stream对象;
  3. .map(s -> Integer.parseInt(s)) : 将前面返回的Stream对象中的字符串转换为数字,并返回新的Stream对象;
  4. .reduce((left, right) -> right += left) : 对前面返回的Stream对象执行合计操作,并返回合计值(Optional对象,包括最后的orElse,是Java8另外的新特性。后面再讲,这里先无视)。
先讲Stream操作的基本流程

从上述经典示例中,我们可以看到,一个Stream操作可以分为三个基本步骤:

1.获取数据源 Source --> 2.数据转换 Transform --> 3.执行操作 Operation

再细致一点,可以将其视为一个管道流操作:

数据集:Stream | filter:Stream | map:Stream | reduce

其中,filter与map属于数据转换 Transform,而reduce属于执行操作 Operation。每次Transform的时候,不会改变原有的Stream对象,而是返回一个新的Stream对象,因此允许对其进行链式操作,从而形成一个管道。

获取数据源的方式主要有:

1.从 Collection 和数组

Collection.stream()
Collection.parallelStream()
Arrays.stream(T array) or Stream.of()

2.从 BufferedReader

java.io.BufferedReader.lines()

3.静态工厂

java.util.stream.IntStream.range()
java.nio.file.Files.walk()

4.自己构建

java.util.Spliterator

5.其它

Random.ints()
BitSet.stream()
Pattern.splitAsStream(java.lang.CharSequence)
JarFile.stream()

稍后讲Stream操作示例的时候会有所介绍,不必着急。

Stream操作类型

Stream操作类型:

  • Intermediate: 中间操作,对应前面的Transform,其目的是打开前一个Stream对象,定义要执行的数据映射或过滤等转换处理(Transform),然后返回一个新的Stream对象,交给下一个操作使用。语法上,多个Intermediate操作可以链式连接在一起。但这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法时,并没有真正开始Stream的遍历。
常见的Intermediate操作:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
  • Terminal: 终点站操作,对应前面的Operation,一次对数据集的Stream操作只能有一次terminal操作。当这个操作执行时,前面的链式的中间操作返回的最后一个Stream对象(也可能没有中间操作,直接是数据源的Stream对象)就真正开始遍历数据集,之后就无法再操作这个Stream对象了。所以这必定是最后一个操作。Terminal 操作执行时,才会真正开始数据集遍历,并产生结果。
常见的Terminal操作:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
  • Short-circuiting: 短路操作,并不与前两者冲突,一个短路操作同时也是Intermediate或Terminal。它是在处理无限大的Stream时,需要返回一个有限的Stream对象(Intermediate),或有限的计算结果(Terminal)。但短路操作用在有限Stream对象也是完全没问题的。
常见的Short-circuiting操作:anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

多次的Intermediate操作并不会导致多次的数据集遍历,因为这些Intermediate是惰性的,这些转换操作只会在 Terminal 操作的时候融合起来,一次遍历完成。

至于Stream的哪些操作是Intermediate,哪些是Terminal,一个简单的标准就是看方法返回值是不是Stream。

3.2.2 Stream常见操作的使用

如果你没有用过Stream,那么你看完前面对Stream的介绍估计就只能是雾里看花了。来,骚年,跟老夫一起动手把代码撸起来。

先准备一个数据集,它的元素如下(Poet,诗人):

class Poet {
        private String name;
        private int age;
        private int evaluation;

        public Poet() {
        }

        public Poet(String name, int age, int evaluation) {
            this.name = name;
            this.age = age;
            this.evaluation = evaluation;
        }

        @Override
        public String toString() {
            return "Poet{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", evaluation=" + evaluation +
                    '}';
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        public int getEvaluation() {
            return evaluation;
        }

        public void setEvaluation(int evaluation) {
            this.evaluation = evaluation;
        }
    }

然后准备一个唐代著名诗人的集合:

List poets = preparePoets();
...
private List preparePoets() {
    List poets = new ArrayList<>();
    // 年龄未必准确,评价不能当真
    poets.add(new Poet("王维", 61, 4));
    poets.add(new Poet("李白", 61, 5));
    poets.add(new Poet("杜甫", 58, 5));
    poets.add(new Poet("白居易", 74, 4));
    poets.add(new Poet("李商隐", 45, 4));
    poets.add(new Poet("杜牧", 50, 4));
    poets.add(new Poet("李贺", 26, 4));
    return poets;
}
  • foreach:
// foreach 等价于 poets.stream().forEach(System.out::println);
poets.forEach(System.out::println);

注意,不能对同一个Stream反复操作,演示如下:

Stream poetStream = poets.stream();
poetStream.forEach(System.out::println);
try {
    // 不能对同一个stream对象做两次操作,stream是流,不能回头,操作过一次之后就不能再操作了。
    poetStream.forEach(System.out::println);
} catch (Throwable t) {
    System.out.println("stream has already been operated upon or closed. 别人嚼过的甘蔗你就别嚼了。。。");
}
// 但是重新从集合获取stream是可以重复操作的,因为是一个新的stream对象。
poets.stream().forEach(System.out::println);
  • map -> Collectors
String strPoets = poets.stream()
        .map(poet -> poet.getName() + " 唐代大诗人")
        .collect(Collectors.joining(","));
System.out.println(strPoets);
Collectors提供了很多操作,可以对各个元素进行连接操作,可以将元素导入其他Collection(List或Set),等等。
  • filter + map + collect 倒入set集合中
Set poetsLi = poets.stream()
        .filter(poet -> poet.getName().startsWith("李"))
        .map(poet -> "唐诗三李 之 " + poet.getName())
        .collect(Collectors.toSet());
System.out.println(poetsLi);
之前说对同一个stream对象只能操作一次,为何这里链式多次操作?
因为 map, filter这些方法是Intermediate操作,返回了一个新的stream对象。
  • filter + findAny/findFirst 查找一个满足条件的数据
Poet topPoet = poets.stream()
        .filter(poet -> poet.getEvaluation() > 4)
        .findAny()
//      .findFirst()
        // 关于 orElse, 后面讲 Optional 的时候再解释
        .orElse(new Poet("杜甫", 58, 5));
System.out.println("最牛的诗人之一:" + topPoet.getName());
  • allMatch 和 anyMatch
boolean all50plus = poets.stream()
        .allMatch(poet -> poet.getAge() > 50);
System.out.println("大诗人们都活了50岁以上吗?" + (all50plus ? "是的" : "并没有"));

boolean any50plus = poets.stream()
        .anyMatch(poet -> poet.getAge() > 50);
System.out.println("大诗人们有活到50岁以上的吗?" + (any50plus ? "有的有的" : "居然没有");
  • count max min sum
// 5星诗人数量 count
System.out.println("5星诗人数量:" + poets.stream()
        .filter(poet -> poet.getEvaluation() == 5)
        .count());
// 年龄最大的诗人
System.out.println("年龄最大的诗人:" + poets.stream()
        .max(Comparator.comparingInt(Poet::getAge))
        .orElse(null));
// 年龄最小的诗人
System.out.println("年龄最小的诗人:" + poets.stream()
        .min(Comparator.comparingInt(Poet::getAge))
        .orElse(null));
// 年龄合计
System.out.println("诗人们年龄合计:" + poets.stream()
        .mapToInt(Poet::getAge)
        .sum());
Java8的Stream API为int,long,double专门提供了 mapToInt(), mapToLong(), mapToDouble()三个方法。从语义上来说,你自己写map操作得到一个泛型为Integer/Long/Double的Stream对象,然后做后续操作当然可以。但直接使用 mapToInt()可以提高性能表现,因为会省去后续操作的循环中的自动装箱解箱处理。
  • reduce 一个专门做统计的操作,比如这里我们也可以用reduce计算合计
int sumAge = poets.stream()
        .mapToInt(Poet::getAge)
        .reduce((age, sum) -> sum += age)
//      .reduce(Integer::sum)
        .orElse(0);
System.out.println("reduce计算出的年龄合计:" + sumAge);

注意,reduce做统计是可以有起始值的,例如:

// 假设唐代其他诗人们的评价合计已经有了,假设是 100,但还未包括前面的7位,这里从 100 开始继续统计评价总值
int sumEvaluation = poets.stream()
        .mapToInt(Poet::getEvaluation)
        .reduce(100, (left, right) -> right += left);
//      .reduce(100, Integer::sum);
System.out.println("reduce计算出的有起始值的评价合计:" + sumEvaluation);
  • limit
System.out.println("生成一个等差数组,限制长度为10:");
Stream.iterate(1, n -> n + 3).limit(10). forEach(x -> System.out.print(x + " "));
  • distinct
String distinctEvaluation = poets.stream()
        .map(poet -> String.valueOf(poet.getEvaluation()))
        .distinct()
        .collect(Collectors.joining(","));
System.out.println("诗人们的评价分数(去重):" + distinctEvaluation);
  • sorted
System.out.println("诗人们按年龄排序:");
poets.stream()
        .sorted(Comparator.comparingInt(Poet::getAge))
        .forEach(System.out::println);
  • group
Map> poetsByAge = poets.stream()
        .collect(Collectors.groupingBy(poet -> {
            int age = poet.getAge();
            if (age < 20) {
                return "1~19";
            } else if (age < 30) {
                return "20~29";
            } else if (age < 40) {
                return "30~39";
            } else if (age < 50) {
                return "40~49";
            } else if (age < 60) {
                return "50~59";
            } else if (age < 70) {
                return "60~69";
            } else {
                return "70~";
            }
        }));
System.out.println("将诗人们按年龄分组:");
poetsByAge.keySet().stream()
        .sorted(String::compareTo)
        .forEach(s -> System.out.println(
                String.format("%s : %s", s, poetsByAge.get(s).stream().map(Poet::getName).collect(Collectors.joining(",")))));
  • flatmap [(poet1, poet2, poet3),(poet4,poet5)] --> [poet1, poet2, poet3, poet4, poet5]
System.out.println("通过flatmap将分组后的诗人集合扁平化:");
List lstFromGroup = poetsByAge.values().stream()
        .flatMap(poets1 -> poets1.stream())
        .collect(Collectors.toList());
lstFromGroup.forEach(System.out::println);

3.2.3 Stream的并行模式

刚刚的例子,都是Stream的串行模式,现在我们通过parallelStream获取Stream的并行模式。要注意并行模式与串行模式有时执行相同操作会得到不同的结果:

System.out.println("findAny:");
for (int i = 0; i < 10; i++) {
    Poet topPoet1 = poets.parallelStream()
            .filter(poet -> poet.getEvaluation() > 4)
            .findAny()
            .orElse(new Poet("XX", 50, 5));
    System.out.println("最牛的诗人之一:" + topPoet1.getName());
}

System.out.println("findFirst:");
for (int i = 0; i < 10; i++) {
    Poet topPoet2 = poets.parallelStream()
            .filter(poet -> poet.getEvaluation() > 4)
            .findFirst()
            .orElse(new Poet("XX", 50, 5));
    System.out.println("最牛的诗人之一:" + topPoet2.getName());
}
上述代码的执行结果中,findFirst与串行并无不同,但findAny有时与串行结果不一样。想想为什么。

parallelStream使用要谨慎,并不是所有的运算都可以并行执行的。

int sumEvaluation = poets.parallelStream()
        .mapToInt(Poet::getEvaluation)
        .reduce(100, Integer::sum);
System.out.println("reduce计算有初始值时,不应该用并行运算:" + sumEvaluation);
并行模式很吸引人,但前提是你要清楚什么时候才能使用。这个例子很好的说明了带起始值的reduce操作并不适合用并行模式。
  • parallelStream的机制是基于Java7引入的Fork/Join框架。了解即可。

Fork/Join的本质和Hadoop的MapReduce一样,都是基于分而治之的思想,将一个任务拆成多个可以并行的小任务执行(Map、fork),最后集中到一起(Reduce、join)。当然Hadoop更复杂,处理的是在不同节点上的分布式进程,而Fork/Join是一个进程(JVM)里的多个线程。

为什么我们很少直接用Fork/Join呢?因为用起来麻烦。。。还是简单说一下吧。。。

  1. 首先你需要像线程池那样定义一个ForkJoinPool,然后定义一个执行任务的ForkJoinTask,在ForkJoinPool中提交这个ForkJoinTask;
  2. 然后你的ForkJoinTask需要自己实现什么样的条件或阈值下,把你要处理的数据集拆开,对应new 几个新的ForkJoinTask,然后调用这些子task的fork方法,再调用它们的join方法(即分而治之);
  3. Fork/Join中关键的机制叫做Work-stealing策略,它将子任务放到不同的双端队列中,每个队列对应一个线程去获取并执行队列中的子任务。所谓双端队列,就是正常来说线程从队列的一端获取接下来要执行的子任务,而当某个线程空闲时,它会从其他线程的队列的另一端偷子任务来执行。。。Work-stealing的优势是能充分利用线程进行并行计算;缺点是队列中任务较少时,为了避免线程对子任务的竞争,需要同步机制,此时会产生额外的性能损耗。(所以后面我们验证Stream的性能时,会发现,数据量较少时,parallelStream有时会更慢,就有这里所说的原因。)

3.2.4 将lambda表达式重构出来

在Stream操作中,有时我们需要写很长的lambda函数,这时我们可以灵活运用IDE的重构功能,将较长的lambda表达式重构为变量或方法。

Predicate poetPredicate = poet -> poet.getEvaluation() < 5;
Consumer poetConsumer = poet -> System.out.println(poet.getName());
poets.stream()
        .filter(poetPredicate)
        .forEach(poetConsumer);

Function poetStringFunction = poet -> {
    int age = poet.getAge();
    if (age < 20) {
        return "1~19";
    } else if (age < 30) {
        return "20~29";
    } else if (age < 40) {
        return "30~39";
    } else if (age < 50) {
        return "40~49";
    } else if (age < 60) {
        return "50~59";
    } else if (age < 70) {
        return "60~69";
    } else {
        return "70~";
    }
};
Map> poetsByAge = poets.stream()
        .collect(Collectors.groupingBy(poetStringFunction));
System.out.println("将诗人们按年龄分组:");
Consumer stringConsumer = s -> System.out.println(
        String.format("%s : %s", s, poetsByAge.get(s).stream().map(Poet::getName).collect(Collectors.joining(","))));
poetsByAge.keySet().stream()
        .sorted(String::compareTo)
        .forEach(stringConsumer);

3.2.5 Stream的性能

Stream的性能不能简单地表述为比以前的集合遍历操作快或者慢,而是要根据具体场景的不同的性能约束条件去确认。

这里简单考虑三种场景:

  1. 单个数据集的简单遍历操作;
  2. 两个数据集的join操作;
  3. 单个数据集的复杂转换操作。

下面的代码使用的硬件环境:

老夫本地可供程序使用的CPU资源:6 core (i7 4核8线程,但有两个core常年被虚拟机占用,因此共 6 个core可以使用。)
单个数据集简单遍历

对于单个数据集的简单遍历来说,总体上讲,Stream的串行操作的性能表现大约介于fori循环与迭代器循环之间;而Stream的并行模式,在运行平台具有多核,且循环中的单次操作比较耗时的前提下,确实可以有效提高性能表现(比fori,迭代器,Stream串行都要更好)。

对于单个数据集的遍历来说,从下面的示例代码中,我们可以发现会影响性能表现的约束条件最少包括以下几点:

  1. 机器硬件条件,比如是不是多核,核数有多少。(两核未必能保证并行比串行效率高,因为要考虑线程上下文切换的损耗。)
  2. 数据集件数,数据集的件数在不同量级(百件,千件,万件,十万,百万,千万。。。)下,不同的遍历方式的性能表现的差别是显著的。
  3. 单次循环的耗时,比如纳秒级别的耗时,那么Stream的并行模式并无优势(同样因为线程上下文切换的损耗),但耗时达到数百毫秒的级别时,并行模式的优势就相当明显了。(当然要在多核机器上运行)

对于下面的代码,建议大家多尝试一下不同的约束条件,比如:

  1. sleep时间调整,例如,从没有sleep,到sleep 500毫秒;
  2. 数据集件数调整,例如,从1百件,到1千,1万,10万,百万,千万。。。(当然,件数大的时候适当减少sleep甚至去除sleep,免得跑太久)
  3. 不同硬件条件的机器,这个有条件的可以试试CPU核数差距较大的机器上运行并行模式的结果,没有条件就算了。

另外,代码中的LocalDateTimeDuration是Java8的又一个新特性,后面会介绍,现在不用在意。

List numbers = new ArrayList<>();
for (int i = 0; i < 100; i++) {
    numbers.add("a" + i);
}

System.out.println("=== loop with fori ===");
LocalDateTime startTime = LocalDateTime.now();
for (int i = 0; i < numbers.size(); i++) {
    String whatever = numbers.get(i) + "b";
    try {
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
LocalDateTime stopTime = LocalDateTime.now();
System.out.println("loop with fori time(millis):" + Duration.between(startTime, stopTime).toMillis());

System.out.println("=== loop with Iterator ===");
startTime = LocalDateTime.now();
for (String num : numbers) {
    String whatever = num + "b";
    try {
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
stopTime = LocalDateTime.now();
System.out.println("loop with Iterator time(millis):" + Duration.between(startTime, stopTime).toMillis());

System.out.println("=== loop with stream ===");
startTime = LocalDateTime.now();
numbers.stream().forEach(num -> {
    String whatever = num + "b";
    try {
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
stopTime = LocalDateTime.now();
System.out.println("loop with stream time(millis):" + Duration.between(startTime, stopTime).toMillis());

System.out.println("=== loop with parallelStream ===");
startTime = LocalDateTime.now();
numbers.parallelStream().forEach(num -> {
    String whatever = num + "b";
    try {
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
stopTime = LocalDateTime.now();
System.out.println("loop with parallelStream time(millis):" + Duration.between(startTime, stopTime).toMillis());
上面的代码在本地运行的时候,切记件数大的时候把sleep调小甚至注释掉,省的跑半天出不来结果。。。
两个数据集的join

上面的例子仅仅是单个数据集的遍历,但在实际开发当中,我们往往会更多地遇到更复杂的数据集操作。比如最典型的,两个数据集的join操作。

首先我们在Poet之外,再定义两个Class:EvaluationPoetExt

class Evaluation {
    private int evaluation;
    private String description;

    public Evaluation() {
    }

    public Evaluation(int evaluation, String description) {
        this.evaluation = evaluation;
        this.description = description;
    }

    public int getEvaluation() {
        return evaluation;
    }

    public void setEvaluation(int evaluation) {
        this.evaluation = evaluation;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

class PoetExt extends Poet {
    private String description;

    public PoetExt(String name, int age, int evaluation, String description) {
        super(name, age, evaluation);
        this.description = description;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public String toString() {
        return "PoetExt{" +
                "name='" + this.getName() + '\'' +
                ", description='" + description + '\'' +
                '}';
    }
}

很显然,Poet对应诗人的定义数据,Evaluation对应评价的定义数据。我们需要实现的需求是,poets 与 evaluations 做 join 获得 PoetExt集合。这个用关系型数据库的SQL来说,就是主表为Poet,副表为Evaluation,以Poet.evaluation = Evaluation.evaluation为条件连接查询数据。

Java8之前,如果我们需要在Java应用中实现这样的两个数据集的join操作,那么我们往往采用的是显式的双层迭代器循环嵌套的写法,而Java8开始,我们可以利用Stream操作实现两个数据集的join操作。而根据该场景的需求,我们还可以使用Stream的并行模式。

代码如下所示,分别比较了三种写法的性能表现(显式双层迭代器遍历,Stream,并行Stream):

// poets件数
int n = 100000;
// evaluations件数
int m = 100000;
List poets = new ArrayList<>();
for (int i = 0; i < n; i++) {
    String name = String.format("诗人%010d", i + 1);
    poets.add(new Poet(name, (int) (80 * Math.random()) + 10, (int) (m * Math.random()) + 1));
}
List evaluations = new ArrayList<>();
for (int i = 0; i < m; i++) {
    evaluations.add(new Evaluation(i + 1, (i + 1) + "星"));
}

// 要实现的逻辑是,poets 与 evaluations 做 join 获得 PoetExt集合

// 显式双层迭代器循环嵌套的写法:
List poetExts = new ArrayList<>();
System.out.println("=== 显式双层迭代器循环 ===");
LocalDateTime startTime = LocalDateTime.now();
for(Poet poet : poets) {
    int eva = poet.getEvaluation();
    for(Evaluation evaluation : evaluations) {
        if (eva == evaluation.getEvaluation()) {
            PoetExt poetExt = new PoetExt(poet.getName(), poet.getAge(), eva, evaluation.getDescription());
            poetExts.add(poetExt);
            break;
        }
    }
}
LocalDateTime stopTime = LocalDateTime.now();
System.out.println("显式双层迭代器循环 time(millis):" + Duration.between(startTime, stopTime).toMillis());
System.out.printf("%s 的件数: %d 与第一件结果: %s %n", "显式双层迭代器循环", poetExts.size(), poetExts.get(0).toString());

// Stream写法:
System.out.println("=== Stream ===");
startTime = LocalDateTime.now();
poetExts = poets.stream()
        .map(poet -> {
            Evaluation eva = evaluations.stream()
                    .filter(evaluation -> evaluation.getEvaluation() == poet.getEvaluation())
                    .findAny()
                    .orElseThrow();
            return new PoetExt(poet.getName(), poet.getAge(), poet.getEvaluation(), eva.getDescription());
        })
        .collect(Collectors.toList());
stopTime = LocalDateTime.now();
System.out.println("Stream time(millis):" + Duration.between(startTime, stopTime).toMillis());
System.out.printf("%s 的件数: %d 与第一件结果: %s %n", "Stream", poetExts.size(), poetExts.get(0).toString());

// parallelStream
System.out.println("=== parallelStream ===");
startTime = LocalDateTime.now();
poetExts = poets.parallelStream()
        .map(poet -> {
            Evaluation eva = evaluations.parallelStream()
                    .filter(evaluation -> evaluation.getEvaluation() == poet.getEvaluation())
                    .findAny()
                    .orElseThrow();
            return new PoetExt(poet.getName(), poet.getAge(), poet.getEvaluation(), eva.getDescription());
        })
        .collect(Collectors.toList());
stopTime = LocalDateTime.now();
System.out.println("parallelStream time(millis):" + Duration.between(startTime, stopTime).toMillis());
System.out.printf("%s 的件数: %d 与第一件结果: %s %n", "parallelStream", poetExts.size(), poetExts.get(0).toString());

老夫本地不同约束条件下的运行结果:时间单位:毫秒

poets件数 evaluations件数 显式双层迭代器循环 Stream parallelStream
1000 1000 53 44 145
10000 10000 772 603 520
100000 100000 27500 48351 11958
10000 100000 4375 4965 1510
100000 10000 3078 5053 1915
100000 1000000 421999 787188 186758
1000000 100000 278927 497239 122923
100000 100 140 306 895
100 100000 111 110 111

由此可见,在老夫本地硬件环境下(6个core可用),数据量较小时(join双方数据集件数都在1万以下),三者区别不大,显式双层迭代器循环与Stream接近,而parallelStream在1千的数据量时甚至会慢一点;而当数据量来到10万件以上的规模时,三者性能表现出现较为明显的差距,parallelStream优势明显,显式双层迭代器循环次之,Stream串行最慢。

  • 两个数据集数据量都较小时,Stream无论串行模式还是并行模式,与显式双层迭代器循环的性能表现差距不大,都在一个数量级。
  • 两个数据集数据量都较大时,parallelStream > 显式双层迭代器循环 > Stream
  • 主数据集数据量较大而副数据集数据量较小时,显式双层迭代器循环 > Stream > parallelStream
  • 副数据集数据量较大而主数据集数据量较小时,三者接近

但要注意:

  1. 上述三个join操作并没有考虑空间换时间的算法优化,比如将evaluations先转换到HashMap中,之后在遍历poets时,通过HashMap直接获取目标evaluation。没有考虑这个优化,是因为这里相比较的就是Stream的隐式双层遍历与以前的显式双层遍历之间的性能表现。利用HashMap的优化方法,这三者都可以使用。。。
  2. 显式双层遍历就没有考虑fori循环了,因为fori的性能本来就比不上迭代器循环,没必要在这里丢人现眼了。。。
  3. 数据集数量较大还是较小的判断标准取决于硬件环境,不能一概而论。
  4. 上述测试比较简陋,每种case都只测了一次。如果大家有时间,建议每种数据量的case都测试10次以上取平均值。
单个数据集的复杂转换操作

其实比较完上述两个场景的性能表现之后,我们大约已经可以得到一个粗略的印象:

  1. 数据量小的时候性能其实都差不多;
  2. 数据量较大时,只要业务允许,硬件足够就尽量并行;
  3. 只能串行时,又对性能有一定要求,那还是显式迭代器循环快一点。

但这里老夫仍然要说,没有极致的性能要求的话,优先用Stream操作。

我们看这样的一个例子:单个数据集的多次数据转换操作。

首先仍然是诗人集合与评价集合:

// poets件数
int n = 100000;
// evaluations件数
int m = 1000;
List poets = new ArrayList<>();
for (int i = 0; i < n; i++) {
    String name = String.format("诗人%010d", i + 1);
    poets.add(new Poet(name, (int) (80 * Math.random()) + 10, (int) (m * Math.random()) + 1));
}
List evaluations = new ArrayList<>();
for (int i = 0; i < m; i++) {
    evaluations.add(new Evaluation(i + 1, (i + 1) + "星"));
}

为了避免双层遍历,我们把评价集合转换为HashMap:

Map evaluationMap = evaluations.stream()
        .collect(Collectors.toMap(Evaluation::getEvaluation, Evaluation::getDescription));

下面我们模拟这样一段逻辑:从 poets 中找到所有评价 > m/2 的诗人,把它们拼接为"诗人名:评价描述"的字段,然后再过滤掉"诗人名:评价描述"中不包含0的记录。

虽然上述逻辑可以在一次循环中实现,但在实际开发中,往往有更复杂的逻辑导致我们经常按业务逻辑把它拆成数个循环处理。因此下面我们的模拟代码并未做一次循环搞定的优化。
System.out.println("=== 多次循环实现数据转换逻辑 ===");
LocalDateTime startTime = LocalDateTime.now();
List betterPoets = new ArrayList<>();
for(Poet poet : poets) {
    if (poet.getEvaluation() > m / 2) {
        betterPoets.add(poet);
    }
}
List poetWithEva2 = new ArrayList<>();
for(Poet poet : betterPoets) {
    poetWithEva2.add(poet.getName() + ":" + evaluationMap.get(poet.getEvaluation()));
}
List poetWithEva3 = new ArrayList<>();
for(String s : poetWithEva2) {
    if (s != null && s.contains("0")) {
        poetWithEva3.add(s);
    }
}
LocalDateTime stopTime = LocalDateTime.now();
System.out.println("多次循环实现数据转换逻辑 time(millis):" + Duration.between(startTime, stopTime).toMillis());

然后我们用Stream实现相同的逻辑:

System.out.println("=== Stream实现数据转换逻辑 ===");
startTime = LocalDateTime.now();
List poetWithEva = poets.stream()
        .filter(poet -> poet.getEvaluation() > m / 2)
        .map(poet -> poet.getName() + ":" + evaluationMap.get(poet.getEvaluation()))
        .filter(s -> s.contains("0"))
        .collect(Collectors.toList());
stopTime = LocalDateTime.now();
System.out.println("Stream实现数据转换逻辑 time(millis):" + Duration.between(startTime, stopTime).toMillis());

再将三次显式迭代器循环优化为一次循环:

System.out.println("=== 一次循环实现数据转换逻辑 ===");
startTime = LocalDateTime.now();
List lastLst = new ArrayList<>();
for(Poet poet : poets) {
    if (poet.getEvaluation() > m / 2) {
        String tmp = poet.getName() + ":" + evaluationMap.get(poet.getEvaluation());
        if (tmp.contains("0")) {
            lastLst.add(tmp);
        }
    }
}
stopTime = LocalDateTime.now();
System.out.println("一次循环实现数据转换逻辑 time(millis):" + Duration.between(startTime, stopTime).toMillis());

从运行结果上看,Stream与一次循环(迭代器)的差距微乎其微,但都比多次循环优势明显。原因当然很浅显,因为Stream也是最后一次遍历。

但Stream在开发效率上具有巨大的优势:语义简单明了,不需要开发人员先按逻辑写多个循环,然后再优化成一次循环。

当然了,水平高点的程序员也是可以一次写出优化后的一次循环的,但你看两者的代码,就问你哪个优雅?哪个更容易读懂代码的目的?结果是显而易见的,Stream在易读性和可维护性上,远比显式循环的写法更有优势。

因此再强调一遍:没有极致的性能要求的话,优先用Stream操作。

Stream与parallelStream的使用建议

直接给结论:

  1. 能用Stream的地方,尽量用Strem(开发效率高,代码易读易维护,性能接近迭代器循环);
  2. 只要没有用Stream达不到的性能需求,就不要用parallelStream。一是因为并非所有的数据集操作都可以并行操作,二是并行操作严重依赖硬件特别是CPU核数,在一个复杂的有并发请求的应用中可能会导致其他业务的请求抢不到足够的资源。。。
关于并行模式的CPU消耗,各位在本地运行前面的性能测试代码时,可以打开本地的资源监视器,看看Stream串行与并行模式下的CPU使用率。你会发现,Stream串行与显式迭代器循环在运行时,基本上只有一个core的使用率达到100%,而并行模式时,所有core的使用率都会达到100%。如果这时你的应用有其他并发的,也比较消耗CPU的请求过来,你猜它会比平时慢呢,还是慢呢,还是慢呢?如果你的应用还是个高并发的系统,那你能否保证对CPU产生大量消耗的并行操作只发生在并发低的时间段呢?(当然是假设的你高并发系统是有高并发峰值时间段的,峰值时间段以外不存在高并发场景。。。)

3.2.6 强行总结一波Stream

  • Stream到底是啥?
Stream 其实并不是集合或集合的元素,它本身不是数据结构,不保存数据,它其实是对集合的一种运算框架。它更像一个高级版本的迭代器 Iterator。但不同于Iterator只能显式地一个一个遍历元素,Stream 只要开发者给出操作意图及其函数实现(即做什么和怎么做),比如 "过滤掉小于0的数字"、"给每个字符串从左补足10位"等,Stream 就会隐式地在内部进行遍历,并做出相应的数据转换。

做什么就是你需要调用Stream的哪个方法,而怎么做就是你需要给Stream的方法传入什么样的函数,即,lambda表达式!

  • 所以为啥叫Stream呢?
首先,Stream是管道流操作。从前面的Stream操作的代码示例中我们可以看到,整个Stream操作就是一个管道流操作,开始和中间操作总是返回一个新的Stream对象,后面继续对这个Stream对象进行操作,犹如接力,直到最后执行操作获得结果。

其次,Stream就如同一个迭代器(Iterator)那样,最后的Terminal对数据集的遍历是单向的,不可往复的。数据只能遍历一次,遍历过一次后就结束了,不可逆转,恰似黄河之水天上来,奔流到海不复回。

故名Stream

  • Stream与以前的集合操作相比,有哪些特点呢?
Stream与以前的集合操作相比,不同的地方在于,以前的集合操作(包括Iterator)只能命令式的,串行的操作。而Stream具有如下特点:
  1. 通过lambda表达式实现了对函数式编程的支持,语义更接近自然语言,代码更易读;
  2. 支持管道流的链式操作,可以将大量遍历逻辑更加简洁地统一在一起;
  3. 支持并行模式,可以将数据分成多个片段,在不同的线程中执行,最后再合并输出,而且不用显式写多线程操作。
这么多好处,就问你爽不爽,漫卷诗书喜欲狂了没?
  • 关于Stream的并行模式,它是有发展轨迹的。

从Java的并行编程API(或者说多线程编程)的角度来看,我们可以看到其在Java各个大版本中的发展壮大过程大致如下:

  1. Java1到Java4 中的 java.lang.Thread
  2. Java5开始提供,Java6继续增强的 java.util.concurrent
  3. Java7引入的 Fork/Join 框架
  4. Java8新增的Stream并行模式

3.3 接口默认方法

前面讲Lamdba表达式的标准函数式接口的时候,各位敏锐的小伙伴们应该就发现了,这些接口里面居然有已经实现了的方法。。。这是怎么回事呢?岂不是违反了Java自己关于接口没有实现方法的规定?

emmm,确实违反了,当然这是有原因的,后面我们再说。。。先看看接口里的方法实现是怎么回事。

3.3.1 给接口添加default方法

Java8开始,你可以给接口添加default方法。如下所示:

public interface Printer {
    default void print() {
        System.out.println("众鸟高飞尽");
    }

    default void printAnathor() {
        System.out.println("孤云独去闲");
    }
}

这些默认的实现不要求implements该接口的Class重写就可以直接使用,如下所示:

PrintClass printClass = new PrintClass();
printClass.print();
printClass.printAnathor();
...
class PrintClass implements Printer {
}

当然你偏要重写接口的default方法也是没有问题的。

3.3.2 如何避免defaut方法冲突

接口不同于抽象类,抽象类使用继承,而Java是单继承的,因此不会出现继承的方法冲突问题。但接口可以写default方法后,就有了方法冲突的可能。因为Java中一个类可以实现多个接口,那么当这些接口中有相同的default方法时,就会出现default方法冲突。

例如接口Printer2中也实现了方法print

public interface Printer2 {
    default void print() {
        System.out.println("只有敬亭山");
    }
}

此时如果一个类同时实现接口PrinterPrinter2

class PrintClass2 implements Printer, Printer2 {
}

此时就会因为default方法冲突而编译错误。

如何解决呢?我们可以在PrintClass2中重写print方法:

class PrintClass2 implements Printer, Printer2 {
    @Override
    public void print() {
        System.out.println("相看两不厌");
    }
}

但如果想要调用某个接口中的default方法怎么办呢?这时可以通过Printer2.super.print();这种特殊写法实现:

class PrintClass2 implements Printer, Printer2 {
    @Override
    public void print() {
        System.out.println("相看两不厌");
        Printer2.super.print();
    }
}

总的规则如下:

  1. 类优先级高于接口。如果在继承链中有方法体或抽象的方法声明,那么就可以忽略接口中定义的方法。
  2. 子优先级高于父。如果一个接口继承了另一个接口,且两个接口都定义了一个默认方法,那么子接口中定义的方法胜出。
  3. 如果上面两条规则都不适用,子类要么需要实现该方法,要么将该方法声明为抽象方法。

3.3.3 静态方法实现

Java8的接口中不仅可以写default方法,还可以写static方法:

public interface Printer2 {
    default void print() {
        System.out.println("只有敬亭山");
    }

    static void printHello(String name) {
        System.out.println("Hello " + name);
    }

    static void printBye(String name) {
        System.out.println("Goodbye " + name);
    }
}

调用时使用接口.静态方法即可:

class PrintClass2 implements Printer, Printer2 {
    @Override
    public void print() {
        System.out.println("相看两不厌");
        Printer2.super.print();
    }

    public void helloAndBye() {
        Printer2.printHello("Java8");
        Printer2.printBye("Java8");
    }
}

3.3.4 接口默认方法的讨论

Java8给接口增加默认方法是引起不同意见比较多的一个新特性。不喜欢的认为这一点破坏了Java作为面向对象语言的规范性,容易引起方法引用混乱不易维护,比如以前的老夫;喜欢的觉得增加了Java的灵活性,只要能控制住范围还挺好用的,比如现在觉得真香的老夫。。。

为什么Java要增加接口的默认方法?

  • Java8之所以给接口加上default方法,一方面是为了配合Stream API与Lambda表达式,比如Collection的stream()方法,试想如果接口不能提供默认方法,那么就需要为所有的Collection类中实现stream()方法。。。
  • 另一方面,default方法带来了一个好处,就是在扩展一个新的简单功能时,可以直接在相关接口中加一个新的默认方法,而不用加一个新的实现类。增加新实现类可能会破坏现有的代码继承体系,经常增加新的实现类甚至可能会引起类爆炸。

但不管是以前添加新的实现类,还是现在可以直接在接口中添加默认方法,都是不可以滥用的。前者会破坏代码的类继承体系甚至引起类爆炸,导致代码难以维护;后者可能导致方法引用混乱,进而同样导致代码难以维护。运用之妙存乎一心,可以用,但不能滥用。

目前的话,老夫有个小小的建议:

  1. 广大应用层开发者,因为代码的变动频率比较高,且人员流动快,所谓铁打的项目流水的程序员,这个时候还是不要使用接口默认方法了。不挖坑从我做起!
  2. 但是,然而,如果英雄您已经可以做共通或者框架底层开发了,那么在需要的时候不妨一试。毕竟能做共通或框架的程序员,其基本素质我们还是要相信一下的。

3.4 Optional

前面讲Stream的时候,看到有的Terminal操作会返回一个Optional对象,我们对其进行orElse之类的操作。

Optional是Java8新增的用来解决NullPointerException的一个容器类,其中包含对其他对象的引用。

这货其实有点高冷,你对它不够熟悉的话,它其实不是很好用。。。熟了以后你才会觉得真香。。。

不逼逼,直接看代码。

在Java8之前,我们的代码中总需要大量的非空判断:

private void printLineOld(String line) {
    if (line != null) {
        System.out.println(line.trim());
    }
}

Java8之后,你可以使用Optional优雅的完成不够优雅的非空判断。。。

首先,你需要用Optional把不知道是不是null的对象包起来:

// 如果确定line不是null
Optional line1 = Optional.of(line);
// 如果line是null,需要使用ofNullable
Optional empty = Optional.ofNullable(line);

还有其他的一些创建Optional对象的方法,这里不再一一介绍。面对未知是否是null的变量,老夫建议使用Optional.ofNullable将其封装起来。

然后,在使用变量的地方,改为使用Optional对象:

// 假设 line 是一个 Optional 类型的对象
try {
    System.out.println(line.get().trim());
} catch (NoSuchElementException e) {
    System.out.println("Optional.get 如果line是null,get会抛NoSuchElementException异常!");
}
// 仅在原来对象非null时执行传入的lambda表达式
line.ifPresent(s -> System.out.println(s.trim()));
// 利用orElse,当原来对象是null时,使用orElse传入的默认值
System.out.println(line.orElse(""));
// 利用orElseGet,当原来对象是null时,使用orElseGet传入的lambda表达式
System.out.println(line.orElseGet(() -> "天生我材必有用," + "千金散尽还复来。"));
// 利用orElseThrow,当原来对象是null时,抛出自己定义的异常
System.out.println(line.orElseThrow(() -> new RuntimeException("也可以抛出自己定义的异常!")));

其中:

  • ifPresent: 只有对象非null时才会执行后面的lambda表达式;
  • orElse: 如果对象是null就返回后面传入的默认值;
  • orElseGet:如果对象是null就执行后面传入的lambda表达式来获取返回值;
  • orElseThrow:如果对象是null就抛出自己定义的异常。

但要注意的是,使用Optional需要一个正确的打开姿势。。。

先看一个不正确的姿势:

// 不推荐将参数类型设计为Optional,Optional适合用于返回值类型
public void printLine(Optional line) {
    ...
}
这里直接将方法参数设计为Optional,这是不推荐的设计方式。因为作为方法的提供者,你怎么保证智商无下限的调用者一定会用 Optional.ofNullable显式传入参数呢?你挡不住人家放飞自我直接传 null的。。。

除此以外,Optional也尽量不要用于实例变量,因为它不能被序列化,当做字段属性时可能会出问题。

来,看看正确的打开姿势:

private void test02_returnOptional(String line) {
    Optional lineOpt = createLineOptional(line);

    // 仅在原来对象非null时执行传入的lambda表达式
    lineOpt.ifPresent(s -> System.out.println(s.trim()));
    // 利用orElse,当原来对象是null时,使用orElse传入的默认值
    System.out.println(lineOpt.orElse(""));
    // 利用orElseGet,当原来对象是null时,使用orElseGet传入的lambda表达式
    System.out.println(lineOpt.orElseGet(() -> "天生我材必有用," + "千金散尽还复来。"));
    // 利用orElseThrow,当原来对象是null时,抛出自己定义的异常
    System.out.println(lineOpt.orElseThrow(() -> new RuntimeException("也可以抛出自己定义的异常!")));
}

private Optional createLineOptional(String line) {
    // 实际开发中,这里也许会有比较复杂的逻辑,用于返回一个对象,而该方法不保证返回对象不为null;
    // 因此使用该方法的地方必须判断返回值是否为null。。。
    // 但如果我们将返回值用Optional包起来,那么对于调用该方法的地方而言,非空判断就可以很优雅了。
    return Optional.ofNullable(line);
}

3.5 Map操作及HashMap性能优化

之前讲Stream的时候,细心的小伙伴会发现,没有从Map生成Stream的操作。是的,Map没有stream()方法,没法直接获取Map的Stream对象,因为Java到现在也不支持元组甚至二维元组,因此Map的元素键值对(key, value)没法作为Stream的泛型来使用。。。

当然,Java8的Map提供了一些新的方法来满足我们日常操作的需要。

3.5.1 增强的Map操作

我们先看看一个集合(List或Set)如何转换为Map。

// 还是之前的诗人集合
List poets = Poet.preparePoets();
// 利用 Collectors.toMap 将Stream中的数据集转换为Map
Map poetMap = poets.stream().collect(Collectors.toMap(Poet::getName, poet -> poet));
之前Stream的示例代码中也有类似的例子。。。强大的Collectors大家要多多亲近。。。

接下来,让我们看看Map都有哪些好用的新方法:

  • foreach
poetMap.forEach((s, poet) -> {
    System.out.printf("%s 活了 %s 岁。 %n", s, poet.getAge());
    System.out.printf("%s 评价 : %s 。 %n", s, poet.getEvaluation());
});
这个看起来已经很像一个二维元组了。。。
  • putIfAbsent : 判断map中是否已经存在目标key,没有或为null的话put一个value进去。
Poet censhen = poetMap.get("岑参");
if (censhen == null) {
    censhen = new Poet("岑参", 51, 4);
    poetMap.put("岑参", censhen);
}
System.out.println(censhen);
// 上面的代码现在可以直接使用 putIfAbsent 了。
poetMap.putIfAbsent("岑参", new Poet("岑参", 51, 5));
// 结果 "岑参" 的评价依旧是 4 而不是 5,因为 putIfAbsent 不会替换已经存在的value。
System.out.println(poetMap.get("岑参"));
比较一下以前的写法和现在的写法,是不是优雅了很多?优雅就是战斗力,优雅即正义。。。
  • computeIfPresent : 如果指定键的值存在且非空,则尝试在给定键及其当前映射值的情况下计算新映射。
// "岑参"已经加入了poetMap
poetMap.computeIfPresent("岑参", (s, poet) -> new Poet(s, 51,4));
// computeIfPresent会替换已经存在的value
System.out.println(poetMap.get("岑参"));
// "孟浩然"尚未加入poetMap
poetMap.computeIfPresent("孟浩然", (s, poet) -> new Poet(s, 51,3));
// computeIfPresent只在key已经存在时替换value
System.out.println(poetMap.containsKey("孟浩然"));
  • computeIfAbsent : 只在key不存在时put一个非空的value
poetMap.computeIfAbsent("孟浩然", s -> new Poet(s, 51,3));
System.out.println(poetMap.get("孟浩然"));
computeIfAbsent 与 putIfAbsent 区别在于传入参数不同,一个是lambda表达式,一个是具体的value。
  • remove(key, value) : key , value 都匹配时删除
poetMap.remove("孟浩然", new Poet("孟浩然", 51,3));
// 删除失败,因为value不是一个对象
System.out.println(poetMap.containsKey("孟浩然"));
poetMap.remove("孟浩然", poetMap.get("孟浩然"));
// 删除成功
System.out.println(poetMap.containsKey("孟浩然"));
  • getOrDefault
System.out.println(poetMap.getOrDefault("孟浩然", new Poet("XX", 20, 1)));
  • merge : key不存在时添加新value,key存在时根据lambda表达式merge value
Map lines = new HashMap<>();
lines.merge("杜甫名句", "星垂平野阔,", (value, newValue) -> value.concat(newValue));
System.out.println(lines.get("杜甫名句"));
lines.merge("杜甫名句", "月涌大江流。", String::concat);
System.out.println(lines.get("杜甫名句"));

3.5.2 HashMap的性能优化

Java8对HashMap的性能也做了一定的优化。

这节都是理论,对HashMap机制不熟悉的小伙伴要回头自己补补课了。。。

不管我们知道还是假装知道hashmap的机制,这里都简单回顾一下(Java8之前):

  1. Java中的hashmap存储是一个node数组,通过key的hash值数组长度-1与运算得到每个key在数组中的存储下标;
  2. 当不同的key计算得到的存储下标冲突时(很多资料叫hash冲突,其实大部分情况并不是hash值冲突),就把对应的不同的key-value都放到这个下标对应的node链表里(对,node是链表结构)。
  3. 数组长度不是不变的,初始容量是2的4次方,负载系数默认0.75,数组长度超过容量×负载系数时,HashMap就会乘2扩容,即2的指数加1,然后大家重新排排坐(重新算下标)。
  4. 从HashMap查找一个key对应的value时,是先根据key的hash值计算下标,再遍历下标对应的node链表,找到该key对应的value。

在Java8以前,HashMap的性能瓶颈主要有两个地方:

  1. 下标冲突较多时,从HashMap取值要先根据key计算下标位置,然后遍历该位置的node链表,直到找到key对应的value;
  2. HashMap每次扩容时,要重新计算所有元素的下标。

在Java8中,对这两点做了一定的优化:

  1. node不再总是链表,当链表长度超过8且node数组容量超过64时,就将链表改为红黑树。红黑树是一种读写性能比较均衡的特殊的平衡二叉树。关于二叉树,平衡二叉树,红黑树,有兴趣的小伙伴自己去了解吧。。。为什么还要指定node数组容量超过64呢?因为容量比较小时,下标冲突可能性比较大,这时应该优先扩容。
  2. 扩容时,不再重新一个一个计算元素的hash值,而是直接对原来的下标值做位移(因为容量总是以乘2的规律扩张)。
扩容时,可能会导致红黑树又被拆分为两个链表。

3.6 Date API

Java 8 在包java.time下包含了一组全新的时间日期API,功能更强大,也更安全。

3.6.1 Clock与时区

  1. Clock类提供了访问当前日期和时间的方法,Clock是时区敏感的,可以用来取代 System.currentTimeMillis() 来获取当前的微秒数。某一个特定的时间点也可以使用Instant类来表示,Instant类也可以用来创建老的java.util.Date对象。
  2. 在新API中时区使用ZoneId来表示。时区可以很方便的使用静态方法of来获取到。 时区定义了到UTS时间的时间差,在Instant时间点对象到本地日期对象之间转换的时候是极其重要的。
// 系统Clock对象 采用系统默认时区
Clock clock = Clock.systemDefaultZone();
System.out.println(clock);

// 系统当前微妙数
long millis = clock.millis();
System.out.println(millis);

// 获取以前的Date对象
Instant instant = clock.instant();
Date legacyDate = Date.from(instant);
System.out.println(legacyDate);

// 获取可用时区
System.out.println(ZoneId.getAvailableZoneIds());
// 获取指定时区
ZoneId zoneSh = ZoneId.of("Asia/Shanghai");
System.out.println(zoneSh.getRules());
ZoneId zoneTk = ZoneId.of("Asia/Tokyo");
System.out.println(zoneTk.getRules());
ZoneId zoneNy = ZoneId.of("America/New_York");
System.out.println(zoneNy.getRules());

3.6.2 LocalTime、LocalDate与LocalDateTime

LocalTime、LocalDate与LocalDateTime都是Java8提供的新的日期API,它们具有如下特点:

  1. 不可变的,因此线程安全;
  2. 配合DateTimeFormatter做格式化,线程安全;
  3. 配合Duration,ChronoUnit等做时间差运算更方便;
  4. 获取系统当前时间更方便;
  5. 等等。。。
  • LocalTime 定义了一个没有时区信息的时间,例如 晚上10点,或者 17:30:15。
// LocalTime 没有年月日和时区信息,只有时分秒及以下
LocalTime localTimeNowDefault = LocalTime.now(ZoneId.systemDefault());
System.out.println(localTimeNowDefault);
LocalTime localTimeNowTk = LocalTime.now(ZoneId.of("Asia/Tokyo"));
System.out.println(localTimeNowTk);
// 计算时间差
long hoursBetween = ChronoUnit.HOURS.between(localTimeNowDefault, localTimeNowTk);
System.out.println(hoursBetween);
long minutesBetween = ChronoUnit.MINUTES.between(localTimeNowDefault, localTimeNowTk);
System.out.println(minutesBetween);
// 获取一个任意时间的 LocalTime
LocalTime late = LocalTime.of(23, 59, 59);
System.out.println(late);
// 根据格式转换字符串为 LocalTime (因为LocalTime只有小时以下,因此格式有限制,只能用FormatStyle.SHORT)
DateTimeFormatter dtf_localtime = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
        .withLocale(Locale.GERMAN);
LocalTime leetTime = LocalTime.parse("13:37", dtf_localtime);
System.out.println(leetTime);
  • LocalDate 表示了一个确切的日期,比如 2014-03-11。
// LocalDate 年月日
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
LocalDate yesterday = tomorrow.minusDays(2);
LocalDate new_year_day = LocalDate.of(2020, Month.JANUARY, 1);
DayOfWeek dayOfWeek = new_year_day.getDayOfWeek();
System.out.printf("今天是%s,明天是%s,昨天是%s,元旦是%s,%s。 %n", today, tomorrow, yesterday, new_year_day, dayOfWeek);
// 格式化
DateTimeFormatter dtf_localdate = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.GERMAN);
LocalDate children_day = LocalDate.parse("01.06.2020", dtf_localdate);
System.out.println(children_day);
  • LocalDateTime 同时表示了时间和日期
// LocalDateTime 日期加时间
LocalDateTime now = LocalDateTime.now();
System.out.println(now);
LocalDateTime laborDay = LocalDateTime.of(2020, Month.MAY, 1, 14, 41, 3);
System.out.println(laborDay);
System.out.println(laborDay.getDayOfWeek());
System.out.println(laborDay.getMonth());
System.out.println(laborDay.getLong(ChronoField.MINUTE_OF_DAY));
// 通过时间点Instance对象转换为Date
Instant laborInstant = laborDay.atZone(ZoneId.systemDefault()).toInstant();
Date laborDate = Date.from(laborInstant);
System.out.println(laborDate);

// 自定义格式化
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
String strNow = formatter.format(LocalDateTime.now());
System.out.println(strNow);
LocalDateTime ldtNow = LocalDateTime.parse(strNow, formatter);
System.out.println(ldtNow);

// 计算时间差
System.out.println(ChronoUnit.DAYS.between(ldtNow, laborDay));
System.out.println(Duration.between(ldtNow, laborDay).toDays());

3.7 CompletableFuture

Java8之前,多线程开发中,如果主线程需要子线程结束后再进行下一步的处理,那么只能同步阻塞的等待,无论你是在主线程中调用子线程的join方法,还是用Future的get方法。

Java8增加了新的CompletableFuture类,可以配合lamda表达式,给子线程传入函数用于子线程执行结束后的回调。

看个简单的例子:

CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "明月出天山,苍茫云海间。";
});
completableFuture.thenApply(s -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return s.concat("\n").concat("长风几万里,吹度玉门关。");
}).thenApply(s -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return s.concat("\n").concat("汉下白登道,胡窥青海湾。");
}).thenApply(s -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return s.concat("\n").concat("由来征战地,不见有人还。");
}).thenApply(s -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return s.concat("\n").concat("戍客望边邑,思归多苦颜。");
}).thenApply(s -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return s.concat("\n").concat("高楼当此夜,叹息未应闲。");
}).thenAccept(System.out::println);

System.out.println("关山月 唐 李白");
try {
    Thread.sleep(8000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println("==================");
这个例子中, CompletableFuture.supplyAsync定义了一个子线程,异步执行传入的lamda表达式,它返回一个CompletableFuture对象。 supplyAsync方法被重载为两个方法,一个如上面示例,只有一个参数。另一个重载的方法有两个参数,一个是传入的子线程处理逻辑(lambda表达式),另一个是线程池对象。不传入线程池对象时,使用默认线程池(对于多核机器来说是一个forkjoin线程池)。

CompletableFuture对象的thenApply方法传入了一个回调函数,这个回调函数会在子线程执行结束后被子线程回调,且回调函数以子线程的执行返回为入参,并返回本次回调处理的结果。可以看到,当连续用thenApply方法传入多个回调函数时,这些回调函数会被串行回调。

而CompletableFuture对象的thenAccept传入的回调函数只接收子线程的执行结果,本身没有返回值。

一串的thenApply最后接一个thenAccept是一种常见用法。

再看一个例子:

CompletableFuture futurePrice = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep((long) (Math.random() * 1000));
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    double price = Math.random() * 100;
    System.out.println("Price is " + price);
    return price;
});
CompletableFuture futureCount = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep((long) (Math.random() * 1000));
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    int count = (int) (Math.random() * 100);
    System.out.println("Count is " + count);
    return count;
});
CompletableFuture futureTotal = futurePrice.thenCombine(futureCount, (price, count) -> price * count);
futureTotal.thenAccept(total -> System.out.println("Total is " + total));

System.out.println("鬼知道要多久。。。该干嘛干嘛去。。。");
try {
    Thread.sleep(3000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
这个例子中,我们需要先计算出price和count,然后相乘得到总价。假设求取price和count的处理不知何时方能完成。

所以我们先分别异步执行price和count的子线程,然后通过thenCombine方法,执行这样一个逻辑:等两个子线程都结束以后,将它们的返回值作为参数执行回调函数。

这样我们就达成了等两个子线程都结束后再回调的逻辑,同时主线程依然该干嘛干嘛,不会阻塞。

CompletableFuture提供了很多方法,上面我们的例子理解之后,就可以自行去看看这些方法都是什么功能,适用于什么场景了:

  • 用于创建CompletableFuture的静态方法:supplyAsync, runAsync。前面我们用了supplyAsync,这是有返回值的子线程,runAsync是没有返回值的子线程。它们都有带线程池参数的重载方法。
  • CompletableFuture对象提供的用于指定回调函数的实例方法:thenAccept,thenApply,thenRun;thenCombine,thenAcceptBoth,runAfterBoth;applyToEither,acceptEither,runAfterEither;exceptionally;whenComplete,handle 等等
  • 获取CompletableFuture对象的静态方法:allOf,anyOf

3.8 其他新特性

Java8还有很多新特性,比如多重注解,Arrays.parallelSort,StampedLock等等,这里不再一一介绍,有需要的小伙伴可以自行学习。

四、Java9~Java11的新特性

因为java9和Java10都是过渡版本,我们直接以Java11(Java8之后第一个LTS版本)为边界来讲讲9到11有哪些比较影响我们开发的新特性。

Java11相对Java8,在语法上的新特性并不多。主要有:

  • 本地变量类型推断
  • HttpClient
  • Collection增强
  • Stream增强
  • Optional增强
  • String增强
  • InputStream增强

4.1 本地变量类型推断

Java10以后可以用var定义一个局部变量,不用显式写出它的类型。但要注意,被var定义的变量仍然是静态类型,编译器会试图去推断其类型。

String strBeforeJava10 = "strBeforeJava10";
var strFromJava10 = "strFromJava10";
System.out.println(strBeforeJava10);
System.out.println(strFromJava10);

因此,要注意:

  • 不兼容的类型是不能重新赋值的!
// 例如下面的语句编译会失败,"InCompatible types."
strFromJava10 = 10;
  • 只要编译器无法推断出变量类型,就会编译错误!
// 例如下面这些都无法通过编译:
var testVarWithoutInitial;
var testNull = null;
var testLamda = () -> System.out.println("test");
var testMethodByLamda = () -> giveMeString();
var testMethod2 = this::giveMeString;

而推荐使用类型推断的场景有:

  • 简化泛型声明
// 如下所示,Map >类型,可以被简化为单个var关键字
var testList = new ArrayList>>();
for (var curEle : testList) {
    // curEle能够被推断出类型是 Map>
    if (curEle != null) {
        curEle.put("test", new ArrayList<>());
    }
}
  • lambda参数
// 从Java 11开始,lambda参数也允许使用var关键字:
Predicate predNotNull = (var a) -> a != null && a.trim().length() > 0;
String strAfterFilter = Arrays.stream((new String[]{"a", "", null, "x"}))
        .filter(predNotNull)
        .collect(Collectors.joining(","));
System.out.println(strAfterFilter);

4.2 HttpClient

Java 9开始引入HttpClient API来处理HTTP请求。 从Java 11开始,这个API正式进入标准库包。参考网址:http://openjdk.java.net/groups/net/httpclient/intro.html

HttpClient具有以下特性:

  1. 同时支持 HTTP1.1 和 HTTP2 协议,并支持 websocket
  2. 同时支持同步和异步编程模型
  3. 将请求和响应主体作为响应式流(reactive-streams)处理,并使用构建器模式

HttpClient

要发送http请求,首先要使用其构建器创建一个HttpClient。这个构建器能够配置每个客户端的状态:

  • 首选协议版本 ( HTTP/1.1 或 HTTP/2 )
  • 是否跟随重定向
  • 代理
  • 身份验证

一旦构建完成,就可以使用HttpClient发送多个请求。

HttpRequest

HttpRequest是由它的构建器创建的。请求的构建器可用于设置:

  • 请求URI
  • 请求Method ( GET, PUT, POST )
  • 请求主体(如果有)
  • 超时时间
  • 请求头

HttpRequest构建之后是不可变的,但可以发送多次。

Synchronous or Asynchronous

请求既可以同步发送,也可以异步发送。当然同步的API会导致线程阻塞直到HttpResponse可用。异步API立即返回一个CompletableFuture,当HttpResponse可用时,它将获取HttpResponse并执行后续处理。

CompletableFuture是Java 8添加的新特性,用于可组合的异步编程。

Data as reactive-streams

请求和响应的主体作为响应式流(具有非阻塞背压的异步数据流)供外部使用。HttpClient实际上是请求正文的订阅者和响应正文字节的发布者。BodyHandler接口允许在接收实际响应体之前检查响应代码和报头,并负责创建响应BodySubscriber。

HttpRequest和HttpResponse类型提供了许多便利的工厂方法,用于创建请求发布者和响应订阅者,以处理常见的主体类型,如文件、字符串和字节。这些便利的实现要么累积数据,直到可以创建更高级别的Java类型(如String),要么就文件流传输数据。BodySubscriber和BodyPublisher接口可以实现为自定义反应流处理数据。

HttpRequest和HttpResponse还提供了转换器,用于将 java.util.concurrent.Flow 的 Publisher/Subscriber 类型转换为 HTTP Client的 BodyPublisher/BodySubscriber 类型。

HTTP/2

Java HTTP Client支持 HTTP/1.1 和 HTTP/2。默认情况下,客户端将使用 HTTP/2 发送请求。发送到尚不支持 HTTP/2 的服务器的请求将自动降级为 HTTP/1.1。以下是HTTP/2带来的主要改进:

  • 标头压缩。 HTTP/2 使用 HPACK 压缩,从而减少了开销。
  • 与服务器的单一连接减少了建立多个TCP连接所需的往返次数。
  • 多路复用。 在同一连接上,同时允许多个请求。
  • 服务器推送。 可以将其他将来需要的资源发送给客户端。
  • 二进制格式。 更紧凑。

由于HTTP/2是默认的首选协议,并且在需要的地方无缝地实现回退到HTTP/1.1,那么当HTTP/2被更广泛地部署时,Java HTTP客户端就无需修正它的应用代码。

API文档

https://docs.oracle.com/en/ja...

演示代码

代码中请求的网址中,localhost:30001的相关uri来自工程https://github.com/zhaochuninhefei/study-czhao/tree/master/jdk11-test

package jdk11;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.WebSocket;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;

/**
 * HttpClient
 *
 * @author zhaochun
 */
 public class TestCase02HttpClient {
    public static void main(String[] args) throws Exception {
        TestCase02HttpClient me = new TestCase02HttpClient();
        me.testHttpClientGetSync();
        me.testHttpClientGetAsync();
        me.testHttpClientPost();

        // 同一个HttpClient先登录网站获取token,再请求受限制资源,从而爬取需要认证的资源
        me.testLogin();

        // HttpClient支持websocket
        me.testWebsocket();
    }

    private void testHttpClientGetSync() {
        var url = "https://openjdk.java.net/";
        var request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .GET()
                .build();
        var client = HttpClient.newHttpClient();
        try {
            System.out.println(String.format("send begin at %s", LocalDateTime.now()));
            // 同步请求
            HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
            System.out.println(String.format("send end at %s", LocalDateTime.now()));
            System.out.println(String.format("receive response : %s", response.body().substring(0, 10)));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void testHttpClientGetAsync() {
        var url = "https://openjdk.java.net/";
        var request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .GET()
                .build();
        var client = HttpClient.newHttpClient();
        try {
            System.out.println(String.format("sendAsync begin at %s", LocalDateTime.now()));
            // 异步请求
            client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
                    .thenApply(stringHttpResponse -> {
                        System.out.println(String.format("receive response at %s", LocalDateTime.now()));
                        return stringHttpResponse.body();
                    })
                    .thenAccept(s -> System.out.println(String.format("receive response : %s at %s", s.substring(0, 10), LocalDateTime.now())));
            System.out.println(String.format("sendAsync end at %s", LocalDateTime.now()));

            // 为了防止异步请求尚未返回主线程就结束(jvm会退出),这里让主线程sleep 10秒
            System.out.println("Main Thread sleep 10 seconds start...");
            Thread.sleep(10000);
            System.out.println("Main Thread sleep 10 seconds stop...");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void testHttpClientPost() {
        var url = "http://localhost:30001/jdk11/test/helloByPost";
        var request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .header("Content-Type", "text/plain")
                .POST(HttpRequest.BodyPublishers.ofString("zhangsan"))
                .build();
        var client = HttpClient.newHttpClient();
        try {
            HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
            System.out.println(response.statusCode());
            System.out.println(response.body());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void testLogin() throws Exception {
        var client = HttpClient.newHttpClient();
        // 某测试环境用户登录URL
        var urlLogin = "http://x.x.x.x:xxxx/xxx/login";
        var requestObj = new HashMap();
        requestObj.put("username", "xxxxxx");
        requestObj.put("password", "xxxxxxxxxxxxxxxx");
        var objectMapper = new ObjectMapper();
        var requestBodyJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(requestObj);
        var requestLogin = HttpRequest.newBuilder()
                .uri(URI.create(urlLogin))
                .header("Content-Type", "application/json;charset=UTF-8")
                .POST(HttpRequest.BodyPublishers.ofString(requestBodyJson))
                .build();
        HttpResponse responseLogin = client.send(requestLogin, HttpResponse.BodyHandlers.ofString());
        // 这里的登录网站使用token,而没有使用session,因此我们需要从返回的报文主体中查找token信息;
        // 如果是使用session的网站,这里需要从响应的headers中查找"set-cookie"从而获取session id,并在后续请求中,将sid设置到header的Cookie中。
        // 如: responseLogin.headers().map().get("set-cookie")获取cookies,再从中查找sid。
        var loginResponse = responseLogin.body();
        var mpLoginResponse = objectMapper.readValue(loginResponse, Map.class);
        var dataLogin = (Map) mpLoginResponse.get("data");
        var token = dataLogin.get("token").toString();
        // 测试环境获取某资源的URL
        var urlGetResource = "http://xxxx:xxxx/xxx/resource";
        var requestRes = HttpRequest.newBuilder()
                .uri(URI.create(urlGetResource))
                .header("Content-Type", "application/json;charset=UTF-8")
                // 注意,token并非一定设置到header的Authorization中,这取决于网站验证的方式,也有可能token也放到cookie里。
                // 但对于使用session的网站,sid都是设置在cookie里的。如: .header("Cookie", "JSESSIONID=" + sid)
                .header("Authorization", token)
                .GET()
                .build();
        HttpResponse responseResource = client.send(requestRes, HttpResponse.BodyHandlers.ofString());
        var response = responseResource.body();
        System.out.println(response);
    }

    private void testWebsocket() {
        var wsUrl = "ws://localhost:30001/ws/test";
        var httpClient = HttpClient.newHttpClient();
        WebSocket websocketClient = httpClient.newWebSocketBuilder()
                .buildAsync(URI.create(wsUrl), new WebSocket.Listener() {
                    @Override
                    public void onOpen(WebSocket webSocket) {
                        System.out.println("onOpen : webSocket opened.");
                        webSocket.request(1);
                    }

                    @Override
                    public CompletionStage onText(WebSocket webSocket, CharSequence data, boolean last) {
                        System.out.println("onText");
                        webSocket.request(1);
                        return CompletableFuture.completedFuture(data)
                                .thenAccept(System.out::println);
                    }

                    @Override
                    public CompletionStage onClose(WebSocket webSocket, int statusCode, String reason) {
                        System.out.println("ws closed with status(" + statusCode + "). cause:" + reason);
                        webSocket.sendClose(statusCode, reason);
                        return null;
                    }

                    @Override
                    public void onError(WebSocket webSocket, Throwable error) {
                        System.out.println("error: " + error.getLocalizedMessage());
                        webSocket.abort();
                    }
                }).join();

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // last参数用于指示websocketClient,本次发送的数据是否是完整消息的最后部分。
        // 如果是false,则websocketClient不会把消息发送给websocket后台的listener,只会把数据缓存起来;
        // 当传入true时,会将之前缓存的数据和这次的数据拼接起来一起发送给websocket后台的listener。
        websocketClient.sendText("test1", false);
        websocketClient.sendText("test2", true);

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        websocketClient.sendText("org_all_request", true);

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        websocketClient.sendText("employee_all_request", true);

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        websocketClient.sendClose(WebSocket.NORMAL_CLOSURE, "Happy ending.");
    }
}

4.3 Collection增强

List,Set,Map有了新的增强方法:ofcopyOf

List的of与copyOf

List.of根据传入的参数列表创建一个新的不可变List集合;List.copyOf根据传入的list对象创建一个不可变副本。

var listImmutable = List.of("a", "b", "c");
var listImmutableCopy = List.copyOf(listImmutable);

由于拷贝的集合本身就是一个不可变对象,因此拷贝实际上并没有创建新的对象,直接使用了原来的不可变对象。

// 结果为true
System.out.println(listImmutable == listImmutableCopy);
// 不可变对象不能进行修改
try {
    listImmutable.add("d");
} catch (Throwable t) {
    System.out.println("listImmutable can not be modified!");
}
try {
    listImmutableCopy.add("d");
} catch (Throwable t) {
    System.out.println("listImmutableCopy can not be modified!");
}

如果想快速新建一个可变的集合对象,可以直接使用之前的不可变集合作为构造参数,创建一个新的可变集合。

var listVariable = new ArrayList<>(listImmutable);
var listVariableCopy = List.copyOf(listVariable);

新创建的可变集合当然是一个新的对象,从这个新对象拷贝出来的不可变副本也是一个新的对象,并不是之前的不可变集合。

System.out.println(listVariable == listImmutable); // false
System.out.println(listVariable == listVariableCopy); // false
System.out.println(listImmutable == listVariableCopy); // false
// 新的可变集合当然是可以修改的
try {
    listVariable.add("d");
} catch (Throwable t) {
    System.out.println("listVariable can not be modified!");
}
// 可变集合拷贝出来的副本依然是不可变的
try {
    listVariableCopy.add("d");
} catch (Throwable t) {
    System.out.println("listVariableCopy can not be modified!");
}

Set的of和copyOf

Set的of和copyOf与List类似。

var set = Set.of("a", "c", "r", "e");
var setCopy = Set.copyOf(set);
System.out.println(set == setCopy);

但要注意,用of创建不可变Set时,要确保元素不重复,否则运行时会抛出异常: "java.lang.IllegalArgumentException: duplicate element"

try {
    var setErr = Set.of("a", "b", "a");
} catch (Throwable t) {
    t.printStackTrace();
}

当然创建可变set后添加重复元素不会抛出异常,但会被去重

var setNew = new HashSet<>(set);
setNew.add("c");
System.out.println(setNew.toString());

Map的of和copyOf

Map的of和copyOf与list,set类似,注意of方法的参数列表是依次传入key和value:

var map = Map.of("a", 1, "b", 2);
var mapCopy = Map.copyOf(map);
System.out.println(map == mapCopy);

当然也要注意创建不可变Map时,key不能重复

try {
    var mapErr = Map.of("a", 1, "b", 2, "a", 3);
} catch (Throwable t) {
    t.printStackTrace();
}

4.4 Stream增强

Java8开始引入的stream,Java11提供了一些扩展:

  • 单个元素直接构造为Stream对象
  • dropWhile与takeWhile
  • 重载iterate方法用于限制无限流范围

单个元素直接构造为Stream对象

注意null与""的区别:

long size1 = Stream.ofNullable(null).count();
System.out.println(size1); // 0
long size2 = Stream.ofNullable("").count();
System.out.println(size2); // 1

dropWhile与takeWhile

dropWhile,对于有序的stream,从头开始去掉满足条件的元素,一旦遇到不满足元素的就结束

List lst1 = Stream.of(1, 2, 3, 4, 5, 4, 3, 2, 1)
        .dropWhile(e -> e < 3)
        .collect(Collectors.toList());
System.out.println(lst1); // [3, 4, 5, 4, 3, 2, 1]

takeWhile,对于有序的stream,从头开始保留满足条件的元素,一旦遇到不满足的元素就结束

List lst2 = Stream.of(1, 2, 3, 4, 5, 4, 3, 2, 1)
        .takeWhile(e -> e < 3)
        .collect(Collectors.toList());
System.out.println(lst2); // [1, 2]

即使把剩下的元素都收集到了无序的set中,但在此之前,stream对象是有序的,因此结果包含了原来stream中最后的[a2]和[a1]:

Set set1 = Stream.of("a1", "a2", "a3", "a4", "a5", "a4", "a3", "a2", "a1")
        .dropWhile(e -> "a3".compareTo(e) > 0)
        .collect(Collectors.toSet());
System.out.println(set1); // [a1, a2, a3, a4, a5]

如果先创建一个无序不重复的set集合,set无序更准确的说法是不保证顺序不变,事实上是有顺序的。
因此这里会发现,dropWhile还是按set当前的元素顺序判定的,一旦不满足条件就结束。

Set set = new HashSet<>();
for (int i = 1; i <= 100 ; i++) {
    set.add("test" + i);
}
System.out.println(set);
Set setNew = set.stream()
        .dropWhile(s -> "test60".compareTo(s) > 0)
        .collect(Collectors.toSet());
System.out.println(setNew);

重载iterate方法用于限制无限流范围

java8里可以创建一个无限流,比如下面这个数列,起始值是1,后面每一项都在前一项的基础上 * 2 + 1,通过limit限制这个流的长度:

Stream streamInJava8 = Stream.iterate(1, t -> 2 * t + 1);
// 打印出该数列的前十个: 1,3,7,15,31,63,127,255,511,1023
System.out.println(streamInJava8.limit(10).map(Object::toString).collect(Collectors.joining(",")));

从Java9开始,iterate方法可以添加一个判定器,例如,限制数的大小不超过1000

Stream streamFromJava9 = Stream.iterate(1, t -> t < 1000, t -> 2 * t + 1);
// 这里打印的结果是 1,3,7,15,31,63,127,255,511
System.out.println(streamFromJava9.map(Objects::toString).collect(Collectors.joining(",")));

4.5 Optional增强

可以将Optional对象直接转为stream

Optional.of("Hello openJDK11").stream()
        .flatMap(s -> Arrays.stream(s.split(" ")))
        .forEach(System.out::println);

可以为Optional对象提供一个默认的Optional对象

System.out.println(Optional.empty()
        .or(() -> Optional.of("default"))
        .get());

4.6 String增强

String方面,针对空白字符(空格,制表符,回车,换行等),提供了一些新的方法。

isBlank

判断目标字符串是否是空白字符。以下结果全部为true

// 半角空格
System.out.println(" ".isBlank());
// 全角空格
System.out.println(" ".isBlank());
// 半角空格的unicode字符值
System.out.println("\u0020".isBlank());
// 全角空格的unicode字符值
System.out.println("\u3000".isBlank());
// 制表符
System.out.println("\t".isBlank());
// 回车
System.out.println("\r".isBlank());
// 换行
System.out.println("\n".isBlank());
// 各种空白字符拼接
System.out.println(" \t\r\n ".isBlank());

strip,stripLeading与stripTrailing

去除首尾的空白字符:

// 全角空格 + 制表符 + 回车 + 换行 + 半角空格 + <内容> + 全角空格 + 制表符 + 回车 + 换行 + 半角空格
var strTest = " \t\r\n 你好 jdk11 \t\r\n ";

// strip 去除两边空白字符
System.out.println("[" + strTest.strip() + "]");
// stripLeading 去除开头的空白字符
System.out.println("[" + strTest.stripLeading() + "]");
// stripTrailing 去除结尾的空白字符
System.out.println("[" + strTest.stripTrailing() + "]");

repeat

重复字符串内容,拼接新的字符串:

var strOri = "jdk11";
var str1 = strOri.repeat(1);
var str2 = strOri.repeat(3);
System.out.println(str1);
System.out.println(str2);
// repeat传入参数为1时,不会创建一个新的String对象,而是直接返回原来的String对象。
System.out.println(str1 == strOri);

lines

lines方法用 r 或 n 或 rn 对字符串切割并返回stream对象:

var strContent = "hello java\rhello jdk11\nhello world\r\nhello everyone";
// lines方法用 \r 或 \n 或 \r\n 对字符串切割并返回stream对象
strContent.lines().forEach(System.out::println);
System.out.println(strContent.lines().count());

4.7 InputStream增强

InputStream提供了一个新的方法transferTo,将输入流直接传输到输出流:

inputStream.transferTo(outputStream);

完整示例代码

package jdk11;

import java.io.*;

/**
 * InputStream增强
 *
 * @author zhaochun
 */
public class TestCase07InputStream {
    public static void main(String[] args) {
        TestCase07InputStream me = new TestCase07InputStream();
        me.test01_transferTo();
    }

    private void test01_transferTo() {
        var filePath = "/home/work/sources/test/jdk11-test/src/main/resources/application.yml";
        var tmpFilePath = "/home/work/sources/test/jdk11-test/src/main/resources/application.yml.bk";

        File tmpFile = new File(tmpFilePath);
        if (tmpFile.exists() && tmpFile.isFile()) {
            tmpFile.delete();
        }

        try(InputStream inputStream = new FileInputStream(filePath);
            OutputStream outputStream = new FileOutputStream(tmpFilePath)) {
            // transferTo将 InputStream 的数据直接传输给 OutputStream
            inputStream.transferTo(outputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4.8 其他新特性

Java9到Java11还有一些其他的新特性,比如模块化开发,REPL交互式编程,单文件源代码程序的直接执行,新的垃圾回收器等等,对目前的开发来说,影响比较小,有兴趣的小伙伴可以查阅老夫另一篇文章:

你可能感兴趣的:(java)