06、Java8新特性(完结)

Java 8新特性

1、概述

Java 8发布于2014-03-18,发布至今已经8年了,是目前企业中使用最广泛的一个版本。Java 8是一次重大的版本升 级,带来了很多的新特性。

JDK 8新特性

  • Lambda表达式
  • 集合之Stream流式操作
  • 接口的增强
  • 并行数组排序
  • Optional中避免Null检查
  • 新的时间和日期 API
  • 可重复注解

1.1为什么学?

  • 能够看懂公司里的代码
  • 大数量下处理集合的效率高
  • 代码可读性高
  • 消灭嵌套地狱

1.2函数式编程思想

关注对数据进行操作

优点:

  • 代码简洁,开发快速
  • 接近自然语言,易于理解
  • 易于并发编程

2、Lambda表达式

Lambda是JDK8中的一个语法糖,可以对某些匿名内部类的写法进行简化,它是函数式编程思想的一个重要体现,让我们不用关注是什么对象,而是关注我们对数据进行了什么操作。

核心原则

可推导可省略

2.1 基本格式

(参数列表)—>{
	代码
}

2.2 示例

匿名内部类创建线程

new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名内部类创建线程");
            }
        }
).start();

Lambda表达式写法:

new Thread(()->{
    System.out.println("Lambda表达式创建线程");
}).start();

2.2.1 无参返回值的Lambda

public interface Swimmable {
    public abstract void run();
}
public class DemoLambdaUse {
    public static void main(String[] args) {
        //匿名内部类写法
        goRun(new Swimmable() {
            @Override
            public void run() {
                System.out.println("匿名内部类写法");
            }
        });

        //Lambda表达式写法
        goRun(()->{
            System.out.println("Lambda表达式写法");
        });
    }

    public static void goRun(Swimmable swimmable){
        swimmable.run();
    }
}

2.2.2 有参数返回的Lambda

下面举例演示 java.util.Comparator 接口的使用场景代码,其中的抽象方法定义为:

public abstract int compare(T o1, T o2);

当需要对一个对象集合进行排序时, Collections.sort 方法需要一个 Comparator 接口实例来指定排序的规则。

ArrayList list = new ArrayList<>();
list.add(1);
list.add(9);
list.add(5);

//匿名内部类写法
Collections.sort(list, new Comparator(){

    @Override
    public int compare(Integer o1, Integer o2) {
        return o1 - o2;
    }
});

//Lambda表达式写法
Collections.sort(list, (Integer o1, Integer o2) ->{
    return o1 - o2;
});

for (Integer integer : list) {
    System.out.println(integer);
}
List asList = Arrays.asList(11, 33, 22, 44);
//匿名内部类写法
asList.forEach(new Consumer() {
    @Override
    public void accept(Integer integer) {
        System.out.println(integer);
    }
});

//Lambda表达式写法
asList.forEach((s) -> {
    System.out.println(s);
});

//Lambda表达式写法 变形
asList.forEach(s->System.out.println(s));

asList.forEach(s->{
	if (s > 20){
		System.out.println(s);
	}
});

2.3 Lambda的实现原理

匿名内部类实现原理

说实现原理前先说说匿名内部类实现原理

public class DemoLambdaUse {
    public static void main(String[] args) {
        //匿名内部类写法
        goRun(new Swimmable() {
            @Override
            public void run() {
                System.out.println("匿名内部类写法");
            }
        });
    }

    public static void goRun(Swimmable swimmable){
        swimmable.run();
    }
}

匿名内部类会在编译后产生一个类: DemoLambdaImpl$1.class

06、Java8新特性(完结)_第1张图片

反编译后的内容

package com.sl.java8;

final class DemoLambdaUse$1 implements Swimmable {
    DemoLambdaUse$1() {
    }

    public void run() {
        System.out.println("匿名内部类写法");
    }
}

Lambda表达式实现原理

public class DemoLambdaUse {
    public static void main(String[] args) {
        //Lambda表达式写法
        goRun(()->{
            System.out.println("Lambda表达式写法");
        });
    }

    public static void goRun(Swimmable swimmable){
        swimmable.run();
    }
}

06、Java8新特性(完结)_第2张图片

运行程序,控制台可以得到预期的结果,但是并没有出现一个新的类,也就是说Lambda并没有在编译的时候产生一 个新的类

我们使用 JDK自带的一个工具: javap ,对字节码进行反汇编,查看字节码指令。 在DOS命令行输入:

javap -c -p 文件名.class
-c:表示对代码进行反汇编
-p:显示所有类和成员
F:\workspace\java_projcet\study\java8\target\classes\com\sl\java8>javap -c -p DemoLambdaUse.class
Compiled from "DemoLambdaUse.java"
public class com.sl.java8.DemoLambdaUse {
  public com.sl.java8.DemoLambdaUse();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: invokedynamic #2,  0              // InvokeDynamic #0:run:()Lcom/sl/java8/Swimmable;
       5: invokestatic  #3                  // Method goRun:(Lcom/sl/java8/Swimmable;)V
       8: return

  public static void goRun(com.sl.java8.Swimmable);
    Code:
       0: aload_0
       1: invokeinterface #4,  1            // InterfaceMethod com/sl/java8/Swimmable.run:()V
       6: return

  private static void lambda$main$0();
    Code:
       0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #6                  // String Lambda表达式写法
       5: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

多了一个私有的静态方法

private static void lambda$main$0();

其实Lambda在运行的时候会生成一个内部类,为了验证是否生成内部类,可以在运行时加 上 -Djdk.internal.lambda.dumpProxyClasses ,加上这个参数后,运行时会将生成的内部类class码输出到一个文 件中。使用java命令如下:

F:\workspace\java_projcet\study\java8\target\classes>java -Djdk.internal.lambda.dumpProxyClasses com.sl.java8.DemoLambdaUse
Lambda表达式写法

06、Java8新特性(完结)_第3张图片

反编译DemoLambdaUse$$Lambda$1.class

package com.sl.java8;

import java.lang.invoke.LambdaForm.Hidden;

// $FF: synthetic class
final class DemoLambdaUse$$Lambda$1 implements Swimmable {
    private DemoLambdaUse$$Lambda$1() {
    }

    @Hidden
    public void run() {
        DemoLambdaUse.lambda$main$0();
    }
}

可以看到这个匿名内部类实现了 Swimmable 接口,并且重写了 swimming 方法, swimming 方法调用 Demo04LambdaImpl.lambda$main$0() ,也就是调用Lambda中的内容。最后可以将Lambda理解为:

public class Demo04LambdaImpl {
	public static void main(String[] args) {
		goSwimming(new Swimmable() {
			public void swimming() {
				Demo04LambdaImpl.lambda$main$0();
			}
		});
	}

	private static void lambda$main$0() {
		System.out.println("Lambda表达式游泳");
	}
	
	public static void goSwimming(Swimmable swimmable) {
		swimmable.swimming();
	}
}

总结:

Lambda在程序运行的时候形成一个类

  1. 在类中新增一个方法,这个方法的方法体就是Lambda表达式中的代码
  2. 还会形成一个匿名内部类,实现接口,重写抽象方法
  3. 在接口的重写方法中会调用新生成的方法.

2.4 Lambda省略格式

在Lambda标准格式的基础上,使用省略写法的规则为:

  1. 小括号内参数的类型可以省略
  2. 如果小括号内有且仅有一个参数,则小括号可以省略
  3. 如果大括号内有且仅有一个语句,可以同时省略大括号、return关键字及语句分号
(int a) -> {
	return new Person();
}

省略后

a -> new Person()

2.5 Lambda的前提条件

Lambda表达式不是随便使用的,使用时有几个条件要特别注意:

  1. 方法的参数或局部变量类型必须为接口才能使用Lambda
  2. 接口中有且仅有一个抽象方法

Lambda表达式的前提条件:

  1. 方法的参数或变量的类型是接口
  2. 这个接口中只能有一个抽象方法

2.6 函数式接口

函数式接口在Java中是指:有且仅有一个抽象方法的接口

ava 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注 解可用于一个接口的定义上:

//检测这个接口是不是只有一个抽象方法
@FunctionalInterface
public interface Swimmable {
    public abstract void run();
}

一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即 使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。

2.7 常用内置函数式接口

Supplier接口

它们主要在 java.util.function 包中。下面是最常用的几个接口。

Supplier接口

java.util.function.Supplier 接口,它意味着"供给" , 对应的Lambda表达式需要“对外提供”一个符合泛型类 型的对象数据。

@FunctionalInterface
public interface Supplier {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

示例

public class TestSupplier {

    public static void printMax(Supplier supplier){
        System.out.println(supplier.get());
    }

    public static void main(String[] args) {

        printMax(()->{

            int[] arry = {11, 33, 22};
            Arrays.sort(arry);
            return arry[arry.length -1];
        });
    }

}

Consumer接口

java.util.function.Consumer 接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。

public interface Consumer {
    void accept(T t);
    default Consumer andThen(Consumer after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

默认方法:andThen

如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。

java.util.Objects 的 requireNonNull 静态方法将会在参数为null时主动抛出 NullPointerException 异常。

这省去了重复编写if语句和抛出空指针异常的麻烦。

示例:

public class TestConsumer {
    public static void test(Consumer c1, Consumer c2){
        String s = "HelloWorld";
        c1.accept(s);

    }

    public static void main(String[] args) {
        test(s -> {
            System.out.println(s.toLowerCase());
        });
    }
}

示例2:

public class TestConsumer {

    public static void test(Consumer c1, Consumer c2){
        String s = "HelloWorld";
        c1.andThen(c2).accept(s);

    }

    public static void main(String[] args) {
        test(s -> {
            System.out.println(s.toLowerCase());
        },s -> {
            System.out.println(s.toUpperCase());
        });
    }
}

Function接口

java.util.function.Function 接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件, 后者称为后置条件。有参数有返回值。

@FunctionalInterface
public interface Function {
    R apply(T t);

    default  Function compose(Function before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default  Function andThen(Function after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static  Function identity() {
        return t -> t;
    }
}

示例:

public class TestFunction {

    public static void test(Function function){
        Integer apply = function.apply("10");
        System.out.println("in:" + (apply+ 5));
    }

    public static void main(String[] args) {

        test(s->{
            return Integer.parseInt(s);
        });
    }

}

默认方法:andThen

Function 接口中有一个默认的 andThen 方法,用来进行组合操作。

该方法同样用于“先做什么,再做什么”的场景,和 Consumer 中的 andThen 差不多:

public class Demo09FunctionAndThen {
    public static void main(String[] args) {
        // Lambda表达式
        test((String s) -> {
        		return Integer.parseInt(s);
        	}, (Integer i) -> {
        		return i * 10;
        });
    }
    public static void test(Function f1, Function f2) {
        // Integer in = f1.apply("66"); // 将字符串解析成为int数字
        // Integer in2 = f2.apply(in);// 将上一步的int数字乘以10
        Integer in3 = f1.andThen(f2).apply("66");
        System.out.println("in3: " + in3); // 660
    }
}

请注意,Function的前置条件泛型和后置条件泛型可以相同。

Predicate接口

有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用 java.util.function.Predicate 接口。

@FunctionalInterface
public interface Predicate {
    boolean test(T t);

    default Predicate and(Predicate other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate negate() {
        return (t) -> !test(t);
    }

    default Predicate or(Predicate other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static  Predicate isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}

示例:

public class TestPredicate {

    private static void test(Predicate predicate){
        boolean test = predicate.test("1");
        System.out.println(test);
    }

    public static void main(String[] args) {

        test(s->{
            return s.equals("1");
        });

    }
}

2.8 Lambda表达式和匿名内部类

匿名内部类 Lambda表达式
所需类型 可以是类,抽象类,接口 必须是接口
抽象方法数量 随意 只能有一个抽象方法
实现原理 在编译后会形成class 在程序运行时动态生成class

总结:

当接口中只有一个抽象方法时,建议使用Lambda表达式,其他其他情况还是需要使用匿名内部类

3、JDK8接口新增的两个方法

接口增强介绍

jdk8之前:

public interface Swimmable {
	静态常量;
    抽象方法;
}

增强后:

可以有默认方法静态方法

interface 接口名 {
    静态常量;
    抽象方法;
    默认方法;
    静态方法;
}

3.1默认方法

格式

interface 接口名 {
	修饰符 default 返回值类型 方法名() {
		代码;
	}
}

public interface Swimmable {
    //默认方法;
    default void func(){
        System.out.println("接口中的抽象方法");
    }
}

class SwimmableImpl implements Swimmable{
    
    public static void main(String[] args) {
        new SwimmableImpl().func();
    }

}

使用

  1. 实现接口,直接调用接口中的默认方法
  2. 实现接口,重写默认方法,调用重写的方法

3.2静态方法

格式

interface 接口名 {
	修饰符 static 返回值类型 方法名() {
		代码;
	}
}

例如

public interface Swimmable {
    //静态方法;
    static void staticFunc(){
        System.out.println("接口中的静态方法");
    }

    public static void main(String[] args) {
    	//直接接口名.静态方法调用
        Swimmable.staticFunc();
    }

}

注意:

静态方法不可以重写

4、方法引用

传统写法

public class TestConsumer {

    public static void test(Consumer consumer){
        int[] arry = {10, 20, 30};
        consumer.accept(arry);

    }
    public static void main(String[] args) {
        //Lambda表达式
        test(s->{
            int count = 0;
            for (int i : s) {
                count+=i;
            }
            System.out.println(count);
        });

    }
}

方法引用

public class TestConsumer {

    public static void test(Consumer consumer){
        int[] arry = {10, 20, 30};
        consumer.accept(arry);

    }

    public static void getMax(int [] arry){
        int count = 0;
        for (int i : arry) {
            count+=i;
        }
        System.out.println(count);

    }

    public static void main(String[] args) {
        //方法引用
        test(TestConsumer::getMax);
    }
}

方法引用的格式

符号表示 ::

符号说明 : 双冒号为方法引用运算符,而它所在的表达式被称为方法引用。

应用场景 : 如果Lambda所要实现的方案 , 已经有其他方法存在相同方案,那么则可以使用方法引用。

常见引用方式

方法引用在JDK 8中使用方式相当灵活,有以下几种形式:

  1. instanceName::methodName 对象::方法名

  2. ClassName::staticMethodName 类名::静态方法

  3. ClassName::methodName 类名::普通方法

  4. ClassName::new 类名::new 调用的构造器

  5. TypeName[]::new String[]::new 调用数组的构造器

    小结

    首先了解Lambda表达式的冗余情况,体验了方法引用,了解常见的方法引用方式

对象名::引用成员方法

这是最常见的一种用法,与上例相同。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法,代码为:

// 对象::实例方法
@Test
public void test01() {
    Date now = new Date();
    Supplier supp = () -> {
        return now.getTime();
    };

    System.out.println(supp.get());
    Supplier supp2 = now::getTime;
    System.out.println(supp2.get());
}

方法引用的注意事项

  1. 被引用的方法,参数要和接口中抽象方法的参数一样
  2. 当接口抽象方法有返回值时,被引用的方法也必须有返回值

类名::引用静态方法

由于在 java.lang.System 类中已经存在了静态方法 currentTimeMillis ,所以当我们需要通过Lambda来调用该 方法时,可以使用方法引用 , 写法是:

// 类名::静态方法
@Test
public void test02() {
    Supplier supp = () -> {
        return System.currentTimeMillis();
    };
    System.out.println(supp.get());
    Supplier supp2 = System::currentTimeMillis;
    System.out.println(supp2.get());
}

类名::引用实例方法

Java面向对象中,类名只能调用静态方法,类名引用实例方法是有前提的,实际上是拿第一个参数作为方法的调用者。

// 类名::实例方法
@Test
public void test03() {
    Function f1 = s -> {
        return s.length();
    };
    System.out.println(f1.apply("abc"));

    Function f2 = String::length;
    System.out.println(f2.apply("abc"));

    BiFunction bif = String::substring;
    String hello = bif.apply("hello", 2);
    System.out.println("hello = " + hello);
}

类名::new引用构造器

由于构造器的名称与类名完全一样。所以构造器引用使用 类名称::new 的格式表示。首先是一个简单的 Person 类:

public class Person {

    private String name;
    private int age;

    public Person() {
        System.out.println("无参构造方法");
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("参构造方法 name:" + name + ",age:" + age);
    }
}

要使用这个函数式接口,可以通过方法引用传递:

// 类名::new
@Test
public void test04() {
    Supplier sup = () -> {
        return new Person();
    };
    System.out.println(sup.get());

    Supplier sup2 = Person::new;
    System.out.println(sup2.get());

    BiFunction fun2 = Person::new;
    System.out.println(fun2.apply("张三", 18));
}

数组::new 引用数组构造器

数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。

// 类型[]::new
@Test
public void test05() {
    Function fun = (len) -> {
        return new String[len];
    };
    String[] arr1 = fun.apply(10);
    System.out.println(arr1 + ", " + arr1.length);
    Function fun2 = String[]::new;
    String[] arr2 = fun.apply(5);
    System.out.println(arr2 + ", " + arr2.length);
}

小结

方法引用是对Lambda表达式符合特定情况下的一种缩写,它使得我们的Lambda表达式更加的精简,也可以理解为 Lambda表达式的缩写形式 , 不过要注意的是方法引用只能"引用"已经存在的方法!

Stream流

集合处理数据的弊端

当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。我们来体验 集合操作数据的弊端,需求如下:

一个ArrayList集合中存储有以下数据:张无忌,周芷若,赵敏,张强,张三丰 需求:
1.拿到所有姓张的 
2.拿到名字长度为3个字的 
3.打印这些数据
    @Test
    public void test06(){
        ArrayList arrayList = new ArrayList<>();
        Collections.addAll(arrayList, "张无忌", "周芷若", "赵敏", "张强", "张三丰");

        // 1.拿到所有姓张的
        // {"张无忌", "张强", "张三丰"}
        ArrayList zhangList = new ArrayList<>();
        for (String name : arrayList) {
            if (name.startsWith("张")) {
                zhangList.add(name);
            }
        }
        // 2.拿到名字长度为3个字的
        // {"张无忌", "张三丰"}
        ArrayList threeList = new ArrayList<>();
        for (String name : zhangList) {
            if (name.length() == 3) {
                threeList.add(name);
            }
        }

        // 3.打印这些数据
        for (String name : threeList) {
            System.out.println(name);
        }
    }

循环遍历的弊端

这段代码中含有三个循环,每一个作用不同:

  1. 首先筛选所有姓张的人;
  2. 然后筛选名字有三个字的人;
  3. 最后进行对结果进行打印输出。

每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循环是做事情的方式,而不是目的。每个需求都要循环一次,还要搞一个新集合来装数据,如果希望再次遍历,只能再使 用另一个循环从头开始。

Stream的更优写法

@Test
public void test06(){
    ArrayList arrayList = new ArrayList<>();
    Collections.addAll(arrayList, "张无忌", "周芷若", "赵敏", "张强", "张三丰");

    //stream流写法
    arrayList.stream()
            .filter(s -> s.startsWith("张"))
            .filter(s -> s.length()==3)
            .forEach(System.out::println);

}

直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:

获取流、过滤姓张、过滤长度为3、逐一打印。

我们真 正要做的事情内容被更好地体现在代码中。

Stream流式思想概述

Stream流式思想类似于工厂车间的“生产流水线”,Stream流不是一种数据结构,不保存数据,而是对数据进行加工 处理。Stream可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品。

Stream API能让我们快速完成许多复杂的操作,如筛选、切片、映射、查找、去除重复,统计,匹配和归约。

06、Java8新特性(完结)_第4张图片

06、Java8新特性(完结)_第5张图片

获取Stream流的两种方式

获取一个流非常简单,有以下几种常用的方式:

  • 所有的 Collection 集合都可以通过 stream 默认方法获取流;
  • Stream 接口的静态方法 of 可以获取数组对应的流。

方式1 : 根据Collection获取流

java.util.Collection 接口中加入了default方法 stream 用来获取流,所以其所有实现类均可获取流。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P8tz9P6k-1646227178503)(C:/Users/86178/AppData/Roaming/Typora/typora-user-images/image-20220211212226552.png)]

@Test
public void test07(){
    ArrayList arrayList = new ArrayList<>();
    Collections.addAll(arrayList, "张无忌", "周芷若", "赵敏", "张强", "张三丰");
    //list
    Stream stream = arrayList.stream();

    //set
    HashSet hashSet = new HashSet<>();
    Stream stringStream = hashSet.stream();

    //queue
    Deque arrayDeque = new ArrayDeque();
    Stream arrayDequeStream = arrayDeque.stream();
}

java.util.Map 接口不是 Collection 的子接口,所以获取对应的流需要分key、value或entry等情况:

//map
HashMap hashMap = new HashMap<>();

Stream keyStream = hashMap.keySet().stream();
Stream valueStream = hashMap.values().stream();

Stream> entryStream = hashMap.entrySet().stream();

方式2 : Stream中的静态方法of获取流

由于数组对象不可能添加默认方法,所以 Stream 接口中提供了静态方法 of ,使用很简单:

@Test
public void test08(){

    Stream stringStream = Stream.of("a", "b", "c");

    String[] arr = {"a", "b", "c"};
    Stream arr1 = Stream.of(arr);

    Integer[] arr2 = {1, 2, 3};
    Stream integerStream = Stream.of(arr2);

    //注意:基本数据类型的数组不行, 下面这种写法,将int数据作为一个数据
    int[] arr3 = {1, 2, 3};
    Stream stream = Stream.of(arr3);
}

Stream常用方法和注意事项

Stream常用方法

Stream流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:

06、Java8新特性(完结)_第6张图片

终结方法:返回值类型不再是 Stream 类型的方法,不再支持链式调用。本小节中,终结方法包括 count 和 forEach 方法。

非终结方法:返回值类型仍然是 Stream 类型的方法,支持链式调用。(除了终结方法外,其余方法均为非终结 方法。)

Stream注意事项(重要)

  1. Stream只能操作一次
  2. Stream方法返回的是新的流
  3. Stream不调用终结方法,中间的操作不会执行

Stream流终结方法

forEach方法

forEach 用来遍历流中的数据

    @Test
    public void test07(){
        ArrayList arrayList = new ArrayList<>();
        Collections.addAll(arrayList, "张无忌", "周芷若", "赵敏", "张强", "张三丰");
        Stream stream = arrayList.stream();

        //forEach遍历数据
        //stream.forEach(s -> System.out.println(s));

        //简写
        stream.forEach(System.out::println);
    }

count方法

Stream流提供 count 方法来统计其中的元素个数:

@Test
public void test07(){
    ArrayList arrayList = new ArrayList<>();
    Collections.addAll(arrayList, "张无忌", "周芷若", "赵敏", "张强", "张三丰");
    //count统计数据个数
    System.out.println(arrayList.stream().count());
}

Stream流中间操作方法

filter方法

filter用于过滤数据,返回符合过滤条件的数据

可以通过 filter 方法将一个流转换成另一个子集流。

06、Java8新特性(完结)_第7张图片

筛选出张开头的数据

@Test
public void test07(){
    ArrayList arrayList = new ArrayList<>();
    Collections.addAll(arrayList, "张无忌", "周芷若", "赵敏", "张强", "张三丰");

    //filter过滤数据,返回符合过滤条件的数据
    Stream stream = arrayList.stream().filter(s -> s.startsWith("张"));
    stream.forEach(System.out::println);
}
//打印数据
张无忌
张强
张三丰

limit方法

limit 方法可以对流进行截取,只取用前n个。

06、Java8新特性(完结)_第8张图片

Stream limit(long maxSize);

参数是一个long型,如果集合当前长度大于参数则进行截取。否则不进行操作。基本使用:

@Test
public void test07(){
    ArrayList arrayList = new ArrayList<>();
    Collections.addAll(arrayList, "张无忌", "周芷若", "赵敏", "张强", "张三丰");

    //limit 对流进行截取,只取用前n个
    arrayList.stream().limit(3).forEach(System.out::println);
}
//打印数据
张无忌
周芷若
赵敏

skip方法

06、Java8新特性(完结)_第9张图片

如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流。

如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。基本使用:

@Test
public void test07(){
    ArrayList arrayList = new ArrayList<>();
    Collections.addAll(arrayList, "张无忌", "周芷若", "赵敏", "张强", "张三丰");

    //skip 跳过前几个数据,得到一个新的流
    arrayList.stream().skip(3).forEach(System.out::println);
}
//打印数据
张强
张三丰

map方法

06、Java8新特性(完结)_第10张图片

如果需要将流中的元素映射到另一个流中,可以使用 map 方法。

该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。

@Test
public void test08(){
    Stream stringStream = Stream.of("11", "22", "33");
    Stream integerStream = stringStream.map(Integer::parseInt);
    integerStream.forEach(s-> System.out.println(s + 10));
}

这段代码中, map 方法的参数通过方法引用,将字符串类型转换成为了int类型(并自动装箱为 Integer 类对象)。

mapToInt方法

如果需要将Stream中的Integer类型数据转成int类型,可以使用 mapToInt 方法。方法签名:

IntStream mapToInt(ToIntFunction mapper);

06、Java8新特性(完结)_第11张图片

    @Test
    public void test11(){

        // Integer占用的内存比int多,在Stream流操作中会自动装箱和拆箱
        Stream stream = Arrays.stream(new Integer[]{1, 2, 3, 4, 5});
        // 先将流中的Integer数据转成int,后续都是操作int类型
        IntStream intStream = stream.mapToInt(Integer::intValue);

        //找到大于3的数据并打印
        intStream.filter(n-> n > 3).forEach(System.out::println);
    }

IntStream和Stream相互转换

@Test
public void test12(){
    //int[] -> IntStream -> Stream -> Integer[]
    int[] num = {3, 4, 5};

    //1. int[] -> IntStream
    IntStream stream = Arrays.stream(num);

    //2. IntStream -> Stream
    Stream boxed = stream.boxed();

    //3. Stream -> Integer[]
    Integer[] result = boxed.toArray(Integer[]::new);

    System.out.println(Arrays.toString(result));

    // one line
    Integer[] oneLineResult = Arrays.stream(num).boxed().toArray(Integer[]::new);
    System.out.println(Arrays.toString(oneLineResult));
}

sorted方法

如果需要将数据排序,可以使用 sorted 方法。方法签名:

Stream sorted(); 
Stream sorted(Comparator comparator);

基本使用

public void test08(){
    Stream stringStream = Stream.of("b", "a", "c");
    //sorted(): 根据元素的自然顺序排序
    stringStream.sorted().forEach(System.out::println);

    Stream.of(33, 45, 23, 10).sorted().forEach(System.out::println);
    //10
    //23
    //33
    //45

	// sorted(Comparator comparator): 根据比较器指定的规则排序
    Stream.of(33, 45, 23, 10).sorted((s1, s2) -> s2 - s1).forEach(System.out::println);
    //45
    //33
    //23
    //10
}

distinct方法

06、Java8新特性(完结)_第12张图片

如果需要去除重复数据,可以使用 distinct 方法。方法签名:

@Test
public void test08(){
    Stream stringStream = Stream.of("b", "a", "c", "c", "b");
    stringStream.distinct().forEach(System.out::println);
}
//打印数据
b
a
c

如果是自定义类型如何是否也能去除重复的数据呢?

@Override
public boolean equals(Object o) {
    if (this == o) {
        return true;
    }
    if (o == null || getClass() != o.getClass()) {
        return false;
    }
    Person person = (Person) o;
    return age == person.age && Objects.equals(name, person.name);
}

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

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


@Test
public void testDistinct2() {
    Stream.of(
            new Person("刘德华", 58),
            new Person("张学友", 56),
            new Person("张学友", 56),
            new Person("黎明", 52))
            .distinct()
            .forEach(System.out::println);
}
//打印数据
Person{name='刘德华', age=58}
Person{name='张学友', age=56}
Person{name='黎明', age=52}

自定义类型是根据对象的hashCode和equals来去除重复元素的。

match方法

如果需要判断数据是否匹配指定的条件,可以使用 Match 相关方法。

boolean allMatch(Predicate predicate); 
boolean anyMatch(Predicate predicate); 
boolean noneMatch(Predicate predicate);
@Test
public void test09(){
    Stream integerStream = Stream.of(11, 44, 33);
    //allMatch: 元素是否全部满足条件
    //boolean b = integerStream.allMatch(integer -> integer > 0);
    //System.out.println(b);

    //anyMatch: 元素是否任意有一个满足条件
    //boolean b1 = integerStream.anyMatch(integer -> integer > 0);
    //System.out.println(b1);

    //noneMatch: 元素是否全部不满足条件
    boolean b2 = integerStream.noneMatch(integer -> integer > 0);
    System.out.println(b2);
}

find方法

06、Java8新特性(完结)_第13张图片

如果需要找到某些数据,可以使用 find 相关方法。方法签名:

Optional findFirst(); 
Optional findAny();
@Test
public void test07(){
    ArrayList arrayList = new ArrayList<>();
    Collections.addAll(arrayList, "张无忌", "周芷若", "赵敏", "张强", "张三丰");

    //findFirst 获取第一个数据
    Optional first = arrayList.stream().findFirst();
    System.out.println(first.get());

    System.out.println(arrayList.stream().findAny().get());
}

findFirst是从流中找出第一个元素。

findAny则是从流中找出任意一个元素。

findAny()操作,返回的元素是不确定的,对于同一个列表多次调用findAny()有可能会返回不同的值。使用findAny()是为了更高效的性能。如果是数据较少,串行地情况下,一般会返回第一个结果,如果是并行的情况,那就不能确保是第一个。

max和min方法

06、Java8新特性(完结)_第14张图片

如果需要获取最大和最小值,可以使用 max 和 min 方法。方法签名:

Optional max(Comparator comparator); 
Optional min(Comparator comparator);
@Test
public void test09(){

    Stream integerStream = Stream.of(11, 44, 33, -1, 15);
    //max
    Optional max = integerStream.max((o1, o2) -> o1 - o2);
    System.out.println(max.get());

    //min
    Stream integerStream2 = Stream.of(11, 44, 33, -1, 15);
    System.out.println(integerStream2.min((o1, o2) -> o1 - o2).get());
}

reduce方法

image-20220212140903926

如果需要将所有数据归纳得到一个数据,可以使用 reduce 方法。方法签名:

T reduce(T identity, BinaryOperator accumulator);
@Test
public void test09(){

    Stream integerStream = Stream.of(4, 5, 3, 9);

    //T reduce(T identity, BinaryOperator accumulator);
    //T identity  默认值
    //BinaryOperator accumulator  对数据处理方式

    // reduce:
    // 第一次将默认做赋值给x, 取出第一个元素赋值给y,进行操作
    // 第二次,将第一次的结果赋值给x, 取出二个元素赋值给y,进行操作
    // 第三次,将第二次的结果赋值给x, 取出三个元素赋值给y,进行操作
    // 第四次,将第三次的结果赋值给x, 取出四个元素赋值给y,进行操作
    //求和1
    Integer reduce = integerStream.reduce(0, (x, y) -> x + y);
    System.out.println(reduce);

    //求和2
    Stream integerStream1 = Stream.of(4, 5, 3, 9);
    System.out.println(integerStream1.reduce(0, Integer::sum));

    //取最大值
    Stream integerStream2 = Stream.of(4, 5, 3, 9);
    System.out.println(integerStream2.reduce(0, (x, y)-> x > y ? x : y ));
}

06、Java8新特性(完结)_第15张图片

map和reduce组合使用

@Test
public void testDistinct2() {
    // 求出所有年龄的总和
    int totalAge = Stream.of(
            new Person("刘德华", 58),
            new Person("张学友", 56),
            new Person("郭富城", 54),
            new Person("黎明", 52))
            .map((p) -> p.getAge())
            .reduce(0, (x, y) -> x + y);
    System.out.println("totalAge = " + totalAge);

    // 找出最大年龄
    // 求出所有年龄的总和
    int maxAge = Stream.of(
            new Person("刘德华", 58),
            new Person("张学友", 56),
            new Person("郭富城", 54),
            new Person("黎明", 52))
            .map((p) -> p.getAge())
            .reduce(0, (x, y) -> x > y ? x :y);
    System.out.println("maxAge = " + maxAge);
}

concat方法

如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat :

static  Stream concat(Stream a, Stream b)
@Test
public void test08(){
    Stream stringStream1 = Stream.of("a");
    Stream stringStream2 = Stream.of("b");
    Stream concat = Stream.concat(stringStream1, stringStream2);
    concat.forEach(System.out::println);
}

综合训练

@Test
public void test08(){
    /**
     * 现在有两个 ArrayList 集合存储队伍当中的多个成员姓名,要求使用传统的for循环(或增强for循环)依次进行以下
     * 若干操作步骤:
     * 1. 第一个队伍只要名字为3个字的成员姓名;
     * 2. 第一个队伍筛选之后只要前3个人;
     * 3. 第二个队伍只要姓张的成员姓名;
     * 4. 第二个队伍筛选之后不要前2个人;
     * 5. 将两个队伍合并为一个队伍;
     * 6. 根据姓名创建 Person 对象;
     * 7. 打印整个队伍的Person对象信息。
     */
    ArrayList one = new ArrayList<>();
    Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子", "洪七公");
    ArrayList two = new ArrayList<>();
    Collections.addAll(two, "古力娜扎", "张无忌", "张三丰", "赵丽颖", "张二狗", "张天爱", "张三");

    //1. 第一个队伍只要名字为3个字的成员姓名
    //2. 第一个队伍筛选之后只要前3个人;
    Stream oneStream = one.stream().filter(s -> s.length() == 3).limit(3);

    //3. 第二个队伍只要姓张的成员姓名;
    //4. 第二个队伍筛选之后不要前2个人;
    Stream twoStream = two.stream().filter(s -> s.startsWith("张")).skip(2);

    //5. 将两个队伍合并为一个队伍;
    //6. 根据姓名创建 Person 对象;
    //7. 打印整个队伍的Person对象信息。
    Stream.concat(oneStream, twoStream).map(Person::new).forEach(System.out::println);
}

保存Stream流中的结果

对流操作完成之后,如果需要将流的结果保存到数组或集合中,可以收集流中的数据。

Stream流提供 collect 方法,其参数需要一个 java.util.stream.Collector 接口对象来指定收集到哪种集合中。java.util.stream.Collectors 类提供一些方法,可以作为 Collector`接口的实例:

public static  Collector> toList() //转换为 List 集合。 
public static  Collector> toSet()  //转换为 Set 集合。
@Test
public void test09(){
    //collect 将流中的数据收集到list集合中
    List list = Stream.of(4, 5, 3, 9).collect(Collectors.toList());
    System.out.println("list = " + list);

    //将流中的数据收集到set集合中
    Set set = Stream.of(4, 5, 3, 9).collect(Collectors.toSet());
    System.out.println("set = " + set);

    //将流中的数据收集到指定(ArrayList)集合中
    ArrayList arrayList = Stream.of(4, 5, 3, 9).collect(Collectors.toCollection(ArrayList::new));
    System.out.println("arrayList = " + arrayList);

    //将流中的数据收集到指定(HashSet)集合中(自动去重)
    HashSet hashSet = Stream.of(4, 5, 3, 9).collect(Collectors.toCollection(HashSet::new));
    System.out.println("hashSet = " + hashSet);

    //toArray将流转为Object数组
    Object[] toArray = Stream.of(4, 5, 3, 9).toArray();
    for (Object o : toArray) {
        System.out.println("o = " + o);
    }

    //toArray将流转为指定类型数组
    Integer[] integers = Stream.of(4, 5, 3, 9).toArray(Integer[]::new);
    System.out.println("integers = " + integers);
}
 
  

对流中数据进行聚合计算

当我们使用Stream流处理数据后,可以像数据库的聚合函数一样对某个字段进行操作。比如获取最大值,获取最小 值,求总和,平均值,统计数量。

@Test
public void testDistinct2() {
    // 求出所有年龄的总和
    Stream personStream = Stream.of(
            new Person("刘德华", 58),
            new Person("张学友", 56),
            new Person("郭富城", 54),
            new Person("黎明", 52));

    //最大值
    //Optional personMax = personStream.collect(Collectors.maxBy((p1, p2) -> p1.getAge() - p2.getAge()));
    //System.out.println("personMax.get() = " + personMax.get());

    //最小值
    //Optional min = personStream.collect(Collectors.minBy((p1, p2) -> p1.getAge() - p2.getAge()));
    //System.out.println("min.get() = " + min.get());

    //求和
    int sum = personStream.collect(Collectors.summingInt(s -> s.getAge()));
    System.out.println("sum = " + sum);

    //求平均值
    //Double averaging = personStream.collect(Collectors.averagingInt(s -> s.getAge()));
    //System.out.println("averaging = " + averaging);

    //统计数量
    //Long count = personStream.collect(Collectors.counting());
    //System.out.println("count = " + count);
}

对流中数据进行分组

@Test
public void testDistinct2() {
    // 求出所有年龄的总和
    Stream personStream = Stream.of(
            new Person("刘德华", 58),
            new Person("张学友", 40),
            new Person("郭富城", 54),
            new Person("黎明", 20));
    //分组
    Map> groupMap = personStream.collect(Collectors.groupingBy(s -> {
        if (s.getAge() > 50) {
            return "老年";
        } else  {
            return "青年";
        }
    }));
    groupMap.forEach((k, v)-> System.out.println(k + "::" + v));
}
//打印结果
青年::[Person{name='张学友', age=40}, Person{name='黎明', age=20}]
老年::[Person{name='刘德华', age=58}, Person{name='郭富城', age=54}]

多级分组

@Test
public void testDistinct2() {
    // 求出所有年龄的总和
    Stream personStream = Stream.of(
            new Person("刘德华", 58),
            new Person("张学友", 40),
            new Person("郭富城", 54),
            new Person("小明", 15),
            new Person("黎明", 20));

    //多级分组
    Map> groupMap = personStream.collect(Collectors.groupingBy(s -> {
        if (s.getAge() >= 50) {
            return "老年";
        } else if (s.getAge() >= 30 && s.getAge() < 50){
            return "中年";
        } else if (s.getAge() >= 18 && s.getAge() < 30){
            return "青年";
        } else {
            return "未成年";
        }
    }));
    groupMap.forEach((k, v)-> System.out.println(k + "::" + v));
}
//数据打印
青年::[Person{name='黎明', age=20}]
未成年::[Person{name='小明', age=15}]
老年::[Person{name='刘德华', age=58}, Person{name='郭富城', age=54}]
中年::[Person{name='张学友', age=40}]

对流中数据进行分区

Collectors.partitioningBy 会根据值是否为true,把集合分割为两个列表,一个true列表,一个false列表。

06、Java8新特性(完结)_第16张图片

@Test
public void testDistinct2() {
    // 求出所有年龄的总和
    Stream personStream = Stream.of(
            new Person("刘德华", 58),
            new Person("张学友", 40),
            new Person("郭富城", 54),
            new Person("小明", 15),
            new Person("黎明", 20));

    //partitioningBy 分区
    Map> collect = personStream.collect(Collectors.partitioningBy(s -> s.getAge() > 50));
    collect.forEach((k, v) -> System.out.println(k + "::" + v));
}
//打印结果
false::[Person{name='张学友', age=40}, Person{name='小明', age=15}, Person{name='黎明', age=20}]
true::[Person{name='刘德华', age=58}, Person{name='郭富城', age=54}]

对流中数据进行拼接

Collectors.joining 会根据指定的连接符,将所有元素连接成一个字符串。

@Test
public void testDistinct2() {
    // 求出所有年龄的总和
    Stream personStream = Stream.of(
            new Person("刘德华", 58),
            new Person("张学友", 40),
            new Person("郭富城", 54),
            new Person("小明", 15),
            new Person("黎明", 20));

    //joining 拼接
    String collect = personStream.map(p -> p.getName()).collect(Collectors.joining("_", "<", ">"));
    System.out.println("collect = " + collect);
}
//打印结果
collect = <刘德华_张学友_郭富城_小明_黎明>

总结:

收集Stream流中的结果

到集合中: Collectors.toList()/Collectors.toSet()/Collectors.toCollection()

到数组中: toArray()/toArray(int[]::new)

聚合计算: Collectors.maxBy/Collectors.minBy/Collectors.counting/Collectors.summingInt/Collectors.averagingInt

分组: Collectors.groupingBy

分区: Collectors.partitionBy

拼接: Collectors.joinging

并行的Stream流

串行的Stream流

目前我们使用的Stream流是串行的,就是在一个线程上执行。

@Test
public void test0Serial() {
    long count = Stream.of(4, 5, 3, 9, 1, 2, 6)
            .filter(s -> {
                System.out.println(Thread.currentThread() + ", s = " + s);
                return true;
            })
            .count();
    System.out.println("count = " + count);
}
Thread[main,5,main], s = 4
Thread[main,5,main], s = 5
Thread[main,5,main], s = 3
Thread[main,5,main], s = 9
Thread[main,5,main], s = 1
Thread[main,5,main], s = 2
Thread[main,5,main], s = 6
count = 7

并行的Stream流

parallelStream其实就是一个并行执行的流。它通过默认的ForkJoinPool,可能提高多线程任务的速度。

获取并行Stream流的两种方式

  1. 直接获取并行的流(parallelStream)
  2. 将串行流转成并行流
@Test
public void testgetParallelStream() {
    ArrayList list = new ArrayList<>();
    // 直接获取并行的流
    Stream parallelStream = list.parallelStream();

    // 将串行流转成并行流
    Stream integerStream = list.stream().parallel();
}

并行操作代码:

@Test
public void test13(){
    long count = Stream.of(4, 5, 3, 9, 1, 2, 6)
            .parallel()
            .filter(s -> {
                System.out.println(Thread.currentThread() + ", s = " + s);
                return true;
            })
            .count();
    System.out.println("count = " + count);
}
//打印结果
Thread[main,5,main], s = 1
Thread[ForkJoinPool.commonPool-worker-2,5,main], s = 4
Thread[ForkJoinPool.commonPool-worker-1,5,main], s = 5
Thread[ForkJoinPool.commonPool-worker-3,5,main], s = 3
Thread[ForkJoinPool.commonPool-worker-5,5,main], s = 9
Thread[ForkJoinPool.commonPool-worker-4,5,main], s = 6
Thread[main,5,main], s = 2
count = 7

并行和串行Stream流的效率对比

使用for循环,串行Stream流,并行Stream流来对5亿个数字求和。看消耗的时间。

private static long times = 50000000000L;
private long start;

@Before
public void init(){
    start = System.currentTimeMillis();
}

@After
public void destory(){
    long end = System.currentTimeMillis();
    System.out.println("消耗时间:" + (end - start));
}

//并发Stream 消耗时间:3520
@Test
public void parallelStream(){
    LongStream.rangeClosed(0, times)
            .parallel().
            reduce(0, Long::sum);
}

//串行Stream 消耗时间:16013
@Test
public void serialStream() {
    LongStream.rangeClosed(0, times)
            .reduce(0, Long::sum);
}

//for循环 消耗时间:10972
@Test
public void forAdd() {
    long result = 0L;
    for (long i = 0L; i <= times; i++) {
        result += i;
    }
}

我们可以看到parallelStream的效率是最高的。

Stream并行处理的过程会分而治之,也就是将一个大任务切分成多个小任务,这表示每个任务都是一个操作。

parallelStream线程安全问题

@Test
public void test14(){
    ArrayList list = new ArrayList<>();
    //将1-1000个数,循环add到list中
    IntStream.rangeClosed(1, 1000).parallel().forEach( i -> list.add(i));
    System.out.println(list.size());//957
}

正常list.size()=1000,但是由于是并行的,list.sizez没有1000个。

并行问题解决方案

//并发问题解决方案一:加锁
Object o = new Object();
IntStream.rangeClosed(1, 1000).parallel().forEach( i -> {
    synchronized (o) {
        list.add(i);
    }
});
System.out.println(list.size());

//并发问题解决方案二:使用线程安全的集合
Vector integers = new Vector<>();
IntStream.rangeClosed(1, 1000).parallel().forEach( i -> integers.add(i));
System.out.println(integers.size());

//并发问题解决方案三:使用Collections.synchronizedList
ArrayList list2 = new ArrayList<>();
List synchronizedList = Collections.synchronizedList(list2);
IntStream.rangeClosed(1, 1000).parallel().forEach( i -> synchronizedList.add(i));
System.out.println(synchronizedList.size());

//并发问题解决方案四:调用collect/toArray
List integerList = IntStream.rangeClosed(1, 1000).parallel().boxed().collect(Collectors.toList());
System.out.println(integerList.size());

Fork/Join框架介绍

parallelStream使用的是Fork/Join框架。Fork/Join框架自JDK 7引入。Fork/Join框架可以将一个大任务拆分为很多小 任务来异步执行。 Fork/Join框架主要包含三个模块:

06、Java8新特性(完结)_第17张图片

Fork/Join原理-分治法

ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。

典型的应用比如快速排序算法, ForkJoinPool需要使用相对少的线程来处理大量的任务。比如要对1000万个数据进行排序,那么会将这个任务分割成 两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会做出同样的分割处 理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割,转而使用插入排序对它们进行排序。那么到最后,所有的任务加起来会有大概2000000+个。问题的关键在 于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。

06、Java8新特性(完结)_第18张图片

Fork/Join原理-工作窃取算法

Fork/Join最核心的地方就是利用了现代硬件设备多核,在一个操作时候会有空闲的cpu,那么如何利用好这个空闲的 cpu就成了提高性能的关键,而这里我们要提到的工作窃取(work-stealing)算法就是整个Fork/Join框架的核心理念。

Fork/Join工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。

06、Java8新特性(完结)_第19张图片

那么为什么需要使用工作窃取算法呢?假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖 的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来 执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的 任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就 去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任 务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永 远从双端队列的尾部拿任务执行。

优点:充分利用线程进行并行计算,并减少了线程间的竞争,

缺点:在某些情况下还是存在竞争, 比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。

上文中已经提到了在Java 8引入了自动并行化的概念。它能够让一部分Java代码自动地以并行的方式执行,也就是我们使用了ForkJoinPool的ParallelStream。

对于ForkJoinPool通用线程池的线程数量,通常使用默认值就可以了,即运行时计算机的处理器数量。可以通过设置 系统属性:java.util.concurrent.ForkJoinPool.common.parallelism=N (N为线程数量),来调整ForkJoinPool的线 程数量,可以尝试调整成不同的参数来观察每次的输出结果。

Fork/Join案例

//1.创建一个求和的任务
class SumRecursiveTask extends RecursiveTask{

    //数据拆分临界值
    private static final long THRESHOLD = 5000L;
    //起始值
    private final long start;
    //结束值
    private final long end;

    SumRecursiveTask(long start, long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        long length = end - start;

        if (length < THRESHOLD){
            // 任务不用再拆分了.可以计算了
            long sum = 0;
            for (long i = start; i <= end; i++) {
                sum +=i;
            }
            System.out.println("计算: " + start + " -> " + end + ",结果为: " + sum);
            return sum;
        }else {
            // 数量大于预定的数量,任务还需要再拆分
            long middle = (start + end) / 2;
            System.out.println("拆分: 左边 " + start + " -> " + middle + ", 右边 " + (middle + 1) + " -> " + end);

            SumRecursiveTask left = new SumRecursiveTask(start, middle);
            left.fork();

            SumRecursiveTask right = new SumRecursiveTask(middle + 1, end);
            right.fork();

            return left.join() + right.join();
        }
    }
}

@Test
public void testForkJoin(){
    //1、创建ForkJoinPool
    ForkJoinPool forkJoinPool = new ForkJoinPool();
    //2、创建task
    SumRecursiveTask sumRecursiveTask = new SumRecursiveTask(1, 1000000L);
    //3、执行
    Long result = forkJoinPool.invoke(sumRecursiveTask);
    System.out.println("result = " + result);
}

小结

  1. parallelStream是线程不安全的
  2. parallelStream适用的场景是CPU密集型的,只是做到别浪费CPU,假如本身电脑CPU的负载很大,那还到处用 并行流,那并不能起到作用
  3. I/O密集型 磁盘I/O、网络I/O都属于I/O操作,这部分操作是较少消耗CPU资源,一般并行流中不适用于I/O密集 型的操作,就比如使用并流行进行大批量的消息推送,涉及到了大量I/O,使用并行流反而慢了很多
  4. 在使用并行流的时候是无法保证元素的顺序的,也就是即使你用了同步集合也只能保证元素都正确但无法保证 其中的顺序

Optional类

以前对null的处理方式

@Test
public void test15() {
    String userName = "凤姐";
    // String userName = null;
    if (userName != null) {
        System.out.println("用户名为:" + userName);
    } else {
        System.out.println("用户名不存在");
    }
}

Optional类介绍

Optional是一个没有子类的工具类,Optional是一个可以为null的容器对象。它的作用主要就是为了解决避免Null检 查,防止NullPointerException。

06、Java8新特性(完结)_第20张图片

Optional的基本使用

Optional类的创建方式:

//Optional.of(T t) : 创建一个 Optional 实例
//Optional.empty() : 创建一个空的 Optional 实例
//Optional.ofNullable(T t):若 t 不为 null,创建 Optional 实例,否则创建空实例

上面的例子用Optional:

@Test
public void test16() {
	Optional userNameO = Optional.of("凤姐");
    // isPresent() : 判断是否包含值,包含值返回true,不包含值返回false。
    if (userNameO.isPresent()) {
    // get() : 如果Optional有值则将其返回,否则抛出NoSuchElementException。
        String userName = userNameO.get();
        System.out.println("用户名为:" + userName);
    } else {
        System.out.println("用户名不存在");
    }
}

Optional类的常用方法

//isPresent() : 判断是否包含值,包含值返回true,不包含值返回false
//get() : 如果Optional有值则将其返回,否则抛出NoSuchElementException
//orElse(T t) : 如果调用对象包含值,返回该值,否则返回参数t
//orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回 s 获取的值
//map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回 Optional.empty()

使用示例:

@Test
public void test15() {
    String userName = "凤姐";
    // String userName = null;
    if (userName != null) {
        System.out.println("用户名为:" + userName);
    } else {
        System.out.println("用户名不存在");
    }

    //Optional.of(T t) : 创建一个 Optional 实例
    //Optional.empty() : 创建一个空的 Optional 实例
    //Optional.ofNullable(T t):若 t 不为 null,创建 Optional 实例,否则创建空实例
    Optional optional1 = Optional.of("凤姐");
    Optional optional2 = Optional.empty();
    Optional optional3 = Optional.ofNullable(null);

    //isPresent() : 判断是否包含值,包含值返回true,不包含值返回false
    System.out.println("optional1.isPresent() = " + optional1.isPresent());
    System.out.println("optional2.isPresent() = " + optional2.isPresent());
    System.out.println("optional3.isPresent() = " + optional3.isPresent());

    //ifPresent():如果有值执行函数
    optional1.ifPresent(s-> System.out.println("s = " + s));
    optional2.ifPresent(s-> System.out.println("s = " + s));

    //(java9特性)存在做的什么,不存在做点什么
    /*
    optional1.ifPresentOrElse(s -> System.out.println("用户名为" + s)
            , () -> System.out.println("用户名不存在"));
    optional2.ifPresentOrElse(s -> System.out.println("用户名为" + s)
            , () -> System.out.println("用户名不存在"));
     */

    //get() : 如果Optional有值则将其返回,否则抛出NoSuchElementException
    System.out.println("optional1.get() = " + optional1.get());
    //System.out.println("optional2.get() = " + optional2.get());
    //System.out.println("optional3.get() = " + optional3.get());

    //orElse(T t) : 如果调用对象包含值,返回该值,否则返回参数t
    String orElse1 = optional1.orElse("没有值");
    System.out.println("orElse1 = " + orElse1);

    String orElse2 = (String) optional2.orElse("没有值");
    System.out.println("orElse2 = " + orElse2);

    Object orElse3 = optional3.orElse("没有值");
    System.out.println("orElse3 = " + orElse3);

    //orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回 s 获取的值
    String s1 = optional1.orElseGet(()->"没有值");
    System.out.println("s1 = " + s1);

    String s2 = (String) optional2.orElseGet(()->"没有值");
    System.out.println("s2 = " + s2);

    Object s3 = optional3.orElseGet(() -> "没有值");
    System.out.println("s3 = " + s3);

    //map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回 Optional.empty()
    String s4 = optional1.map(s -> s.toUpperCase()).orElse(null);
    System.out.println("s4 = " + s4);

    String s5 = optional2.map(s -> s.toString().toUpperCase()).orElse(null);
    System.out.println("s5 = " + s5);
}
 
  

小结

Optional是一个可以为null的容器对象。orElse,ifPresent,ifPresentOrElse,map等方法避免对null的判断,写出 更加优雅的代码。

JDK8新的日期和时间API

旧版日期时间 API 存在的问题

  1. 设计很差: 在java.util和java.sql的包中都有日期类,java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期。

    此外用于格式化和解析的类在java.text包中定义。

  2. 非线程安全:java.util.Date 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一。

  3. 时区处理麻烦:日期类并不提供国际化,没有时区支持,因此Java引入了java.util.Calendar和 java.util.TimeZone类,但他们同样存在上述所有的问题。

新日期时间 API介绍

JDK 8中增加了一套全新的日期时间API,这套API设计合理,是线程安全的。新的日期及时间API位于 java.time 包 中,下面是一些关键类。

  • LocalDate :表示日期,包含年月日,格式为 2019-10-16
  • LocalTime :表示时间,包含时分秒,格式为 16:38:54.158549300
  • LocalDateTime :表示日期时间,包含年月日,时分秒,格式为 2018-09-06T15:33:56.750
  • DateTimeFormatter :日期时间格式化类。
  • Instant:时间戳,表示一个特定的时间瞬间。
  • Duration:用于计算2个时间(LocalTime,时分秒)的距离
  • Period:用于计算2个日期(LocalDate,年月日)的距离
  • ZonedDateTime :包含时区的时间

Java中使用的历法是ISO 8601日历系统,它是世界民用历法,也就是我们所说的公历。平年有365天,闰年是366 天。

此外Java 8还提供了4套其他历法,分别是:

  • ThaiBuddhistDate:泰国佛教历
  • MinguoDate:中华民国历
  • JapaneseDate:日本历
  • HijrahDate:伊斯兰历

JDK 8新的日期和时间 API的优势:

  1. 新版的日期和时间API中,日期和时间对象是不可变的。操纵的日期不会影响老值,而是新生成一个实例。
  2. 新的API提供了两种不同的时间表示方式,有效地区分了人和机器的不同需求。
  3. TemporalAdjuster可以更精确的操纵日期,还可以自定义日期调整器。
  4. 是线程安全的

JDK 8的日期和时间类

LocalDate类

表示日期,包含年月日,格式为 2019-10-16

@Test
public void test01() {
    // 创建指定日期
    LocalDate fj = LocalDate.of(1985, 9, 23);
    System.out.println("fj = " + fj); // 1985-09-23
    // 得到当前日期
    LocalDate nowDate = LocalDate.now();
    System.out.println("nowDate = " + nowDate); // 2019-10-16
    // 获取日期信息
    System.out.println("年: " + nowDate.getYear());      //
    System.out.println("月: " + nowDate.getMonth());     //月: FEBRUARY
    System.out.println("月: " + nowDate.getMonthValue());//月: 2
    System.out.println("日: " + nowDate.getDayOfMonth());//
    System.out.println("星期: " + nowDate.getDayOfWeek());//星期: SUNDAY
    System.out.println("星期: " + nowDate.getDayOfWeek().getValue());//星期: 7
}

LocalTime类

LocalTime类: 获取时间信息。格式为 16:38:54.158549300

@Test
public void test02() {
    // 得到指定的时间
    LocalTime time = LocalTime.of(12, 15, 28, 129_900_000);
    System.out.println("time = " + time);

    // 得到当前时间
    LocalTime nowTime = LocalTime.now();
    System.out.println("nowTime = " + nowTime);
    // 获取时间信息
    System.out.println("小时: " + nowTime.getHour());
    System.out.println("分钟: " + nowTime.getMinute());
    System.out.println("秒: " + nowTime.getSecond());
    System.out.println("纳秒: " + nowTime.getNano());
}

LocalDateTime类

LocalDateTime类: 获取日期时间信息。格式为 2018-09-06T15:33:56.750

@Test
public void test03(){

    // 得到指定的时间
    LocalDateTime fj = LocalDateTime.of(1985, 9, 23, 9, 10, 20);
    System.out.println("fj = " + fj);

    // 得到当前日期时间
    LocalDateTime now = LocalDateTime.now();
    System.out.println("now = " + now); //now = 2022-02-13T20:59:13.723

    //年
    System.out.println("now.getYear() = " + now.getYear());
    //年份的天数
    System.out.println("now.getDayOfYear() = " + now.getDayOfYear());
    //月
    System.out.println("now.getMonth() = " + now.getMonth());
    System.out.println("now.getMonthValue() = " + now.getMonthValue());
    //日
    System.out.println("now.getDayOfMonth() = " + now.getDayOfMonth());
    //时分秒
    System.out.println("now.getHour() = " + now.getHour());
    System.out.println("now.getMinute() = " + now.getMinute());
    System.out.println("now.getSecond() = " + now.getSecond());
    System.out.println("now.getNano() = " + now.getNano());
}

对日期时间的修改,对已存在的LocalDate对象,创建它的修改版,最简单的方式是使用withAttribute方法。 withAttribute方法会创建对象的一个副本,并按照需要修改它的属性。以下所有的方法都返回了一个修改属性的对 象,他们不会影响原来的对象。

日期时间的修改

@Test
public void test05() {
    LocalDateTime now = LocalDateTime.now();
    System.out.println("now = " + now);
    // 修改日期时间
    LocalDateTime setYear = now.withYear(2078);
    System.out.println("修改年份: " + setYear);
    System.out.println("now == setYear: " + (now == setYear));
    System.out.println("修改月份: " + now.withMonth(6));
    System.out.println("修改小时: " + now.withHour(9));
    System.out.println("修改分钟: " + now.withMinute(11));
    // 在当前对象的基础上加上或减去指定的时间
    //加5天,返回新对象
    LocalDateTime localDateTime = now.plusDays(5);
    System.out.println("5天后: " + localDateTime);
    System.out.println("now == localDateTime: " + (now == localDateTime));
    System.out.println("10年后: " + now.plusYears(10));
    System.out.println("20月后: " + now.plusMonths(20));
    System.out.println("20年前: " + now.minusYears(20));
    System.out.println("5月前: " + now.minusMonths(5));
    System.out.println("100天前: " + now.minusDays(100));
}

日期时间的比较

// 日期时间的比较
@Test
public void test06() {
    // 在JDK8中,LocalDate类中使用isBefore()、isAfter()、equals()方法来比较两个日期,可直接进行比较。
    LocalDate now = LocalDate.now();
    LocalDate date = LocalDate.of(2018, 8, 8);
    System.out.println(now.isBefore(date)); // false
    System.out.println(now.isAfter(date)); // true
    System.out.println(now.equals(date));
}

时间格式化与解析

通过 java.time.format.DateTimeFormatter 类可以进行日期时间解析与格式化。

// 日期格式化
@Test
public void test04() {
    // 得到当前日期时间
    LocalDateTime now = LocalDateTime.now();
    System.out.println("now = " + now);

    // 将日期时间格式化为字符串
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    String format = now.format(formatter);
    System.out.println("format = " + format);

    // 将字符串解析为日期时间
    LocalDateTime dateTime = LocalDateTime.parse("1985-09-23 10:12:22", formatter);
    System.out.println("dateTime = " + dateTime);
}
//打印数据
now = 2022-02-13T22:21:51.182
format = 2022-02-13 22:21:51
dateTime = 1985-09-23T10:12:22

Instant 类

@Test
public void test08(){

    Instant now = Instant.now();
    System.out.println("当前时间戳 = " + now);

    //得到秒
    System.out.println("now.getEpochSecond() = " + now.getEpochSecond());
    // 得到纳秒
    System.out.println("now.getNano() = " + now.getNano());

    //加操作
    Instant plusSeconds = now.plusSeconds(20);
    System.out.println("plusSeconds = " + plusSeconds);
    //减操作
    Instant minusSeconds = now.minusSeconds(20);
    System.out.println("minusSeconds = " + minusSeconds);
}

Duration/Period类:计算日期时间差

Duration/Period类: 计算日期时间差。

  1. Duration:计算2个时间(LocalTime,时分秒)的距离
  2. Period:计算2个日期(LocalDate,年月日)的距离
@Test
public void test09(){
    //Duration:计算2个时间(LocalTime,时分秒)的距离 
    LocalTime now = LocalTime.now();
    LocalTime ofTime = LocalTime.of(22, 42, 0);
    Duration duration = Duration.between(ofTime, now);
    System.out.println("相差的天数 = " + duration.toDays());
    System.out.println("相差的小时数 = " + duration.toHours());
    System.out.println("相差的分钟数 = " + duration.toMinutes());
    System.out.println("相差的秒数 = " + duration.getSeconds());
    System.out.println("相差的毫秒数 = " + duration.toMillis());
    System.out.println("相差的纳秒数 = " + duration.toNanos());

    // Period:计算2个日期(LocalDate,年月日)的距离
    LocalDate date = LocalDate.now();
    LocalDate date1 = LocalDate.of(2021, 3, 3);
    Period between = Period.between(date1, date);
    System.out.println("相差的年 = " + between.getYears());
    System.out.println("相差的月 = " + between.getMonths());
    System.out.println("相差的天 = " + between.getDays());
}

JDK8的时间校正器

有时我们可能需要获取例如:将日期调整到“下一个月的第一天”等操作。可以通过时间校正器来进行。

  • TemporalAdjuster : 时间校正器。
  • TemporalAdjusters : 该类通过静态方法提供了大量的常用TemporalAdjuster的实现。
//TemporalAdjuster
@Test
public void test10(){
    LocalDateTime now = LocalDateTime.now();
    //将日期调整到 下个月的第一天
    TemporalAdjuster adjuster = temporal -> {
        LocalDateTime temporal1 = (LocalDateTime) temporal;
        return temporal1.plusMonths(1).withDayOfMonth(1);//下一个月的第一天
    };

    LocalDateTime localDateTime = now.with(adjuster);
    System.out.println("localDateTime = " + localDateTime);
}

TemporalAdjusters提供了大量的常用TemporalAdjuster的实现。

@Test
public void test10(){
    LocalDateTime now = LocalDateTime.now();

    //获取当月第一天
    System.out.println("当月第一天:"+now.with(TemporalAdjusters.firstDayOfMonth()));
    //获取下月第一天
    System.out.println("下月第一天:"+now.with(TemporalAdjusters.firstDayOfNextMonth()));
    //获取明年第一天
    System.out.println("明年第一天:"+now.with(TemporalAdjusters.firstDayOfNextYear()));
    //获取本年第一天
    System.out.println("本年第一天:"+now.with(TemporalAdjusters.firstDayOfYear()));
    //获取当月最后一天
    System.out.println("当月最后一天:"+now.with(TemporalAdjusters.lastDayOfMonth()));
    //获取本年最后一天
    System.out.println("本年最后一天:"+now.with(TemporalAdjusters.lastDayOfYear()));
    //获取当月第三周星期五
    System.out.println("当月第三周星期五:"+now.with(TemporalAdjusters.dayOfWeekInMonth(3, DayOfWeek.FRIDAY)));
    //获取上周一
    System.out.println("上周一:"+now.with(TemporalAdjusters.previous(DayOfWeek.MONDAY)));
    //获取下周日
    System.out.println("下周日:"+now.with(TemporalAdjusters.next(DayOfWeek.SUNDAY)));
}

JDK8设置日期时间的时区

Java8 中加入了对时区的支持,LocalDate、LocalTime、LocalDateTime是不带时区的,带时区的日期时间类分别为:ZonedDate、ZonedTime、ZonedDateTime。

其中每个时区都对应着 ID,ID的格式为 “区域/城市” 。例如 :Asia/Shanghai 等。

ZoneId:该类中包含了所有的时区信息。

@Test
public void test11(){
    // 设置日期时间的时区
    // 1、获取所有的时区ID
    ZoneId.getAvailableZoneIds().forEach(System.out::println);

    //默认不带时区
    LocalDateTime now = LocalDateTime.now();  //中国使用的东八区的时区
    System.out.println("now = " + now);

    // 2、带时区的类
    //now(Clock.systemUTC()) :创建世界标准时间
    ZonedDateTime now1 = ZonedDateTime.now(Clock.systemUTC());
    System.out.println("now1 = " + now1);

    //now() :使用计算机的默认的时区,创建日期时间
    ZonedDateTime now2 = ZonedDateTime.now();
    System.out.println("now2 = " + now2);

    //使用指定的时区创建日期时间
    ZonedDateTime now3 = ZonedDateTime.now(ZoneId.of("America/Hermosillo"));
    System.out.println("now3 = " + now3);

    //修改时区
    //withZoneSameInstant 即修改时区,也修改时间
    ZonedDateTime now4 = now2.withZoneSameInstant(ZoneId.of("America/Hermosillo"));
    System.out.println("now4 = " + now4);

    //withZoneSameLocal 只修改时区,不修改时间
    ZonedDateTime now5 = now2.withZoneSameLocal(ZoneId.of("America/Hermosillo"));
    System.out.println("now5 = " + now5);
}

小结

详细学习了新的日期是时间相关类,

LocalDate表示日期,包含年月日,

LocalTime表示时间,包含时分 秒,

LocalDateTime = LocalDate + LocalTime,

时间的格式化和解析,通过DateTimeFormatter类型进行.

学习了Instant类,方便操作秒和纳秒,一般是给程序使用的

学习Duration/Period计算日期或时间的距离,还使用时间调 整器方便的调整时间

学习了带时区的3个类ZoneDate/ZoneTime/ZoneDateTime

JDK 8新的日期和时间 API的优势:

  1. 新版的日期和时间API中,日期和时间对象是不可变的。操纵的日期不会影响老值,而是新生成一个实例。
  2. 新的API提供了两种不同的时间表示方式,有效地区分了人和机器的不同需求。
  3. TemporalAdjuster可以更精确的操纵日期,还可以自定义日期调整器。
  4. 是线程安全的

JDK 8重复注解与类型注解

自从Java 5中引入 注解 以来,注解开始变得非常流行,并在各个框架和项目中被广泛使用。不过注解有一个很大的限 制是:在同一个地方不能多次使用同一个注解。JDK 8引入了重复注解的概念,允许在同一个地方多次使用同一个注 解。在JDK 8中使用**@Repeatable**注解定义重复注解。

定义重复注解


package com.sl.java8;

import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * @author 86178
 * @version 1.0.0
 * @ClassName Test02.java
 * @Description TODO
 * @createTime 2022年02月14日 21:11:00
 */
 
//3. 配置多个重复的注解
@Test02.Mytest("test01")
@Test02.Mytest("test02")
@Test02.Mytest("test01")
public class Test02 {

    //1、定义一个重复的注解容器
    @Retention(RetentionPolicy.RUNTIME)
    @interface MYTests{//重复注解容器
        Mytest[] value();
    }

    //2、定义一个可以重复的注解
    @Retention(RetentionPolicy.RUNTIME)
    @Repeatable(MYTests.class)
    @interface Mytest{
        String value();
    }

	//3. 配置多个重复的注解
    @Mytest("tm1")
    @Mytest("tm2")
    @Mytest("tm3")
    public void test(){

    }

    public static void main(String[] args) {

        //4、解析重复注解
        //获取类上的注解
        //getAnnotationsByType 新增的API用于获取重复的注解
        Mytest[] annotationsByType = Test02.class.getAnnotationsByType(Mytest.class);
        for (Mytest mytest : annotationsByType) {
            System.out.println("mytest = " + mytest);
        }

        //获取方法上的注解
        try {
            Mytest[] tests = Test02.class.getMethod("test").getAnnotationsByType(Mytest.class);
            for (Mytest test : tests) {
                System.out.println("test = " + test);
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }

    }
}

类型注解的使用

JDK 8为@Target元注解新增了两种类型: TYPE_PARAMETER , TYPE_USE 。

TYPE_PARAMETER :表示该注解能写在类型参数的声明语句中。 类型参数声明如:

TYPE_USE :表示注解可以再任何用到类型的地方使用。

TYPE_PARAMETER的使用

public class Test03<@TyptParam t> {

    public @NotNull int a = 0;

    public <@TyptParam E extends Integer> void test01(){

    }

}

@Target(ElementType.TYPE_PARAMETER)
@interface TyptParam {

}

@Target(ElementType.TYPE_USE)
@interface NotNull {

}

小结

通过@Repeatable元注解可以定义可重复注解, TYPE_PARAMETER 可以让注解放在泛型上, TYPE_USE 可以让注解放 在类型的前面

你可能感兴趣的:(java,java,java8,Lambda)