JDK8 新特性

此笔记只为了自学&复习,同时也为能够帮助同样努力奋斗的小伙伴们。

笔记配套视频是波哥讲解的2021最新版JDK8新特性详解,笔记内容来自波哥,也自写了一些,如果觉得侵权请联系我。很感谢波哥大佬,受益颇多。

一、Java发展历史

1. Java的发展历史

Sun公司在 1991 年成立了一个称为绿色计划( Green Project )的项目,由James Gosling(高斯林)博土领导,绿色计划的目的是开发一种能够在各种消费性电子产品(机顶盒、冰箱、收音机等)上运行的程序架构。这个项目的产品就是Java语言的前身: Oak(橡树)。Oak当时在消费品市场上并不算成功,但随着 1995 年互联网潮流的兴起,Oak迅速找到了最适合自己发展的市场定位。

  • JDK Beta - 1995

  • JDK 1.0 - 1996年 1 月 (真正第一个稳定的版本JDK 1.0.2,被称作 Java 1 )

  • JDK 1.1 - 1997年 2 月

  • J2SE 1.2 - 1998年 12 月

  • J2ME(Java 2 Micro Edition,Java 2平台的微型版),应用于移动、无线及有限资源的环境。

  • J2SE(Java 2 Standard Edition,Java 2平台的标准版),应用于桌面环境。

  • J2EE(Java 2 Enterprise Edition,Java 2平台的企业版),应用于基于Java的应用服务器。

  • J2SE 1.3 - 2000年 5 月

  • J2SE 1.4 - 2002年 2 月

  • J2SE 5.0 - 2004年 9 月(取得巨大进步)

  • Java SE 6 - 2006年 12 月

  • Java SE 7 - 2011年 7 月

  • Java SE 8 (LTS) - 2014年 3 月

  • Java SE 9 - 2017年 9 月

  • Java SE 10(18.3) - 2018年 3 月

  • Java SE 11(18.9 LTS) - 2018年 9 月

  • Java SE 12(19.3) - 2019年 3 月

  • Java SE 13(19.9) - 2019年 9 月

  • Java SE 14(20.3) - 2020年 3 月

  • Java SE 15(20.9) - 2020年 9 月

  • Java SE 16 - 2021年 3 月

  • Java SE 17(LTS)- 2021年 9 月

我们可以看到Java SE的主要版本大约每两年发布一次,直到Java SE 6到Java SE 7开始花了五年时间,之后又花了三年时间到达Java SE 8。

2.OpenJDK和OracleJDK

2.1 Open JDK来源

Java 由 Sun 公司发明,Open JDK是Sun在 2006 年末把Java开源而形成的项目。也就是说Open JDK是Java SE平台版的开源和免费实现,它由 SUN 和 Java 社区提供支持, 2009 年 Oracle 收购了 Sun 公司,自此 Java 的维护方之一的 SUN 也变成了 Oracle。

2.2 Open JDK 和 Oracle JDK的关系

大多数 JDK 都是在 Open JDK 的基础上进一步编写实现的,比如 IBM J9, Oracle JDK 和 Azul Zing。

Oracle JDK完全由 Oracle 公司开发,Oracle JDK是基于Open JDK源代码的商业版本。此外,它包含闭源组件。

Oracle JDK根据二进制代码许可协议获得许可,在没有商业许可的情况下,在 2019 年1月之后发布的Oracle Java SE 8的公开更新将无法用于商业或生产用途。但是 Open JDK是完全开源的,可以自由使用。

image-20211026170502468

2.3 Open JDK 官网介绍

Open JDK 官网: http://openjdk.java.net/

JDK Enhancement Proposals(JDK增强建议)。通俗的讲JEP就是JDK的新特性

Oracle JDK是基于Open JDK源代码的商业版本。我们要学习Java新技术可以去Open JDK 官网学习。

二、Lambda表达式

1. 需求分析

创建一个新的线程,指定线程要执行的任务

public class DemoLambda01 {
    public static void main(String[] args) {
        //开启一个线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程1: " + Thread.currentThread().getName());
            }
        }).start();
        System.out.println("主线程中的代码: " + Thread.currentThread().getName());
    }
}

代码分析:

  • Thread类需要一个Runnable接口作为参数,其中的抽象方法run方法是用来指定线程任务内容的核心

  • 为了指定run方法体,不得不需要Runnable的实现类

  • 为了省去定义一个Runnable 的实现类,不得不使用匿名内部类

  • 必须覆盖重写抽象的run方法,所有的方法名称,方法参数,方法返回值不得不都重写一遍,而且不能出错

  • 实际上,我们只在乎方法体中的代码

2.Lambda表达式初体验

new Thread(()-> {
   System.out.println("lambda写法的线程2: " + Thread.currentThread().getName());
}).start();

Lambda表达式是一个匿名函数,可以理解为一段可以传递的代码

Lambda表达式的优点:简化了匿名内部类的使用,语法更加简单

匿名内部类语法冗余,体验了Lambda表达式后,发现Lambda表达式是简化匿名内部类的一种方式

3. Lambda的语法规则

Lambda省去了面向对象的条条框框,Lambda的标准格式由 3 个部分组成:

(参数类型 参数名称) -> {
    代码体;
}

格式说明:

  • (参数类型 参数名称):参数列表

  • {代码体;}:方法体

  • ->:箭头,分割参数列表和方法体

3.1 Lambda练习 1

无参无返回值的Lambda

//定义一个接口
public interface userService {
    void show();
}
//创建主方法使用
public class DemoLambda02 {
    public static void main(String[] args) {
        //匿名内部类的写法
        goShow(new UserService() {
            @Override
            public void show() {
                System.out.println("show方法执行了");
            }
        });
        System.out.println("----------------------------------");

        goShow(()-> {
            System.out.println("Lambda的show方法执行了");
        });

    }

    public static void goShow(UserService userService) {
        userService.show();
    }
}

输出:

show方法执行了
----------------------------------
Lambda的show方法执行了

3.2 Lambda练习 2

有参且有返回值得Lambda

// 创建一个Person对象
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
    private String name;
    private Integer age;
    private Integer height;
}
// 然后我们在List集合中保存多个Person对象,然后对这些对象做根据age排序操作
public class DemoLambda03 {
    public static void main(String[] args) {
        List<Person> list = new ArrayList<>();
        list.add(new Person("张三",12,170));
        list.add(new Person("李四",18,175));
        list.add(new Person("王五",32,167));

//        Collections.sort(list, new Comparator<Person>() {
//            @Override
//            public int compare(Person o1, Person o2) {
//                return o1.getAge() - o2.getAge();
//            }
//        });

        for (Person person : list) {
            System.out.println(person);
        }
        System.out.println("--------------------------------");

        // 我们发现在sort方法的第二个参数是一个Comparator接口的匿名内部类
        // 且执行的方法有参数和返回值,那么我们可以改写为Lambda表达式
        Collections.sort(list, (Person o1, Person o2) -> {
            return o1.getAge() - o2.getAge();
        });
    }
}

输出:

Person(name=张三, age=12, height=170)
Person(name=李四, age=18, height=175)
Person(name=王五, age=32, height=167)
--------------------------------

4.@FunctionalInterface注解

如果UserService接口中的方法不唯一,那么在DemoLambda2中的lambda报错如下

image-20211028075208702

@FunctionalInterface 这是一个标志注解,被该注解修饰的接口只能声明一个抽象方法

image-20211028075937947

5. Lambda表达式的原理

匿名内部类的本质是在编译时生成一个Class 文件:xxx$1.class

image-20211028081847955

还可以通过反编译工具(Java Decompiler)来查看生成的代码

image-20211028084210124

写的有Lambda表达式的class文件,我们可以通过JDK自带的一个工具查看:javap 对字节码进行反汇编操作。

# -c:表示对代码进行反汇编 
# -p:显示所有的类和成员
javap -c -p 文件名.class

反汇编的结果:

image-20211028090845275

在这个反编译的源码中我们看到了一个静态方法lambda$main$​0(),为了更加直观的理解这个内容,我们可以在运行的时候添加 -Djdk.internal.lambda.dumpProxyClasses, 加上这个参数命令会将内部class码输出到一个文件中

命令执行

java -Djdk.internal.lambda.dumpProxyClasses 要运行的包名.类名

反编译后的内容:

public class Demo03Lambda {
	public static void main(String[] args) {
            goShow(new UserService() {
                @Override
                public void show() {
                    Demo03Lambda.lambda$main$0();
                }
            }
		);
		System.out.println("----------");
	}
	public static void goShow(UserService userService){
		userService.show();
	}
	private static void lambda$main$0();
		System.out.println("Lambda show 方法执行了...");
	}
}

可以看到这个匿名的内部类实现了UserService接口,并重写了show()方法。在show方法中调用了Demo03Lambda.lambda$main$0(),也就是调用了Lambda中的内容

小结:

匿名内部类在编译的时候会产生一个class文件。

Lambda表达式在程序运行的时候会形成一个类。

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

6.Lambda表达式的省略写法

在lambda表达式的标准写法基础上,可以使用省略写法的规则为:

  • 小括号内的参数类型可以省略

  • 如果小括号内有且仅有一个参数,则小括号可以省略

  • 如果大括号内有且仅有一个语句,可以同时省略大括号,return 关键字及语句分号。

public class DemoLambda04 {
    public static void main(String[] args) {
        goStudent((String name,Integer age)->{
            return name+age+"666...";
        });

        //省略写法:小括号参数类型和大括号,分号,return都省略
        goStudent((name,age)->name+age+"666...");

        System.out.println("------------------------------------");

        goOrder((String name)->{
            System.out.println("--->"+ name);
            return 666;
        });
        
        //省略写法:小括号,小括号内的参数类型
        goOrder(name -> {
            System.out.println("--->"+ name);
            return 666; 
        });
    }

    public static void goStudent(StudentService studentService) {
        studentService.show("张三",12);
    }

    public static void goOrder(OrderService orderService) {
        orderService.show("李四");
    }
}

7.Lambda表达式的使用前提

Lambda表达式的语法是非常简洁的,但是Lambda表达式不是随便使用的,使用时有几个条件要特别注意

  • 方法的参数或局部变量类型必须为接口才能使用Lambda
  • 接口中有且仅有一个抽象方法(@FunctionalInterface)

8.Lambda和匿名内部类的对比

Lambda和匿名内部类的对比

  • 所需类型不一样

    • 匿名内部类的类型可以是 类,抽象类,接口
    • Lambda表达式需要的类型必须是接口
  • 抽象方法的数量不一样

    • 匿名内部类所需的接口中的抽象方法的数量是随意的
    • Lambda表达式所需的接口中只能有一个抽象方法
  • 实现原理不一样

    • 匿名内部类是在编译后形成一个class
    • Lambda表达式是在程序运行的时候动态生成class

三、接口中新增的方法

1. JDK8中接口的新增

在JDK8中针对接口有做增强,在JDK8之前

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

JDK8之后对接口做了增强,接口中可以有默认方法和静态方法

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

2.默认方法

2.1 为什么要增加默认方法

在JDK8以前接口中只能有抽象方法和静态常量,会存在以下的问题:

如果接口中新增抽象方法,那么实现类都必须要抽象这个抽象方法,非常不利于接口的扩展的

image-20211028114214354

2.2 接口默认方法的格式

接口中默认方法的语法格式是

interface 接口名{
    修饰符 default 返回值类型 方法名{
    	方法体;
    }
}
public class Demo01Interface {
    public static void main(String[] args) {
        a a = new b();
        a a1 = new c();
        //这里执行的是实现类b中的重写的默认方法test3
        a.test3();
        //因为实现类c中没有重写test3方法,所以这里执行的是接口a中写的默认方法test3
        a1.test3();
    }
}

interface a{
    void test1();
    void test2();

    /**
     * 接口中定义的默认方法
     */
    default String test3() {
        System.out.println("接口中定义的默认方法");
        return "hello";
    }
}

class b implements a {
    @Override
    public void test1() {
    }

    @Override
    public void test2() {
    }

    @Override
    public String test3() {
        System.out.println("在b实现类中重写了接口a中的默认方法test3");
        return "ok...";
    }
}
class c implements a {
    @Override
    public void test1() {
    }

    @Override
    public void test2() {
    }
}

2.3 接口中默认方法的使用

接口中的默认方法有两种使用方式

  • 实现类直接调用接口的默认方法

  • 实现类重写接口的默认方法

3. 静态方法

JDK8中为接口新增了静态方法,作用也是为了接口的扩展

3.1 语法规则

interface 接口名{
    修饰符 static 返回值类型 方法名{
    	方法体;
    }
}

image-20211028121143411

3.2 静态方法的使用

接口中的静态方法在实现类中是不能被重写的,调用的话只能通过接口类型来实现: 接口名.静态方法名();

4. 两者的区别介绍

  • 默认方法通过实例调用,静态方法通过接口名调用

  • 默认方法可以被实现类继承,实现类可以直接调用接口默认方法,也可以重写接口默认方法

  • 静态方法不能被实现类继承,实现类不能重写接口的静态方法,只能使用接口名调用

四、函数式接口

1. 函数式接口的由来

我们知道使用Lambda表达式的前提是需要有函数式接口,而Lambda表达式使用时不关心接口名,抽象方法名。只关心抽象方法的参数列表和返回值类型。因此为了让我们使用Lambda表达式更佳的方法,在JDK中提供了大量常用的函数式接口

public class Demo01Fun {
    public static void main(String[] args) {
        fun1((arr) -> {
            int sum =0;
            for (int i : arr) {
                sum += i;
            }
            return sum;
        });
    }

    public static void fun1(Operator operator) {
        int[] arr = {1,2,3,4};
        int sum = operator.getSum(arr);
        System.out.println("sum = " + sum);
    }
}

/**
 * 函数式接口
 */
@FunctionalInterface
interface Operator {
    int getSum(int[] arr);
}

2. 函数式接口介绍

在JDK中帮我们提供的有函数式接口,主要是在 java.util.function 包中。

2.1 Supplier

无参有返回值的接口,对于的Lambda表达式需要提供一个返回数据的类型。

/**
* 函数式接口
*/
@FunctionalInterface
public interface Supplier<T> {

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

使用:

public class SupplierTest {
    public static void main(String[] args) {
        fun1(()->{
            int[] arr = {22,31,213,11};
            //计算出数组中的最大值
            Arrays.sort(arr);
            return arr[arr.length-1];
        });
    }

    private static void fun1(Supplier<Integer> supplier) {
        //get() 是一个无参有返回值的抽象方法
        Integer max = supplier.get();
        System.out.println("max = " + max);
    }
}

2.2 Consumer

有参无返回值得接口,前面介绍的Supplier接口是用来生产数据的,而Consumer接口是用来消费数据的,使用的时候需要指定一个泛型来定义参数类型

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
}

使用:将输入的数据统一转换为小写输出

public class ConsumerTest {
    public static void main(String[] args) {
        test((msg)-> System.out.println(msg + " 全部转换为小写: " + msg.toLowerCase()));
    }

    public static void test(Consumer<String> consumer) {
        consumer.accept("Hello World");
    }
}

默认方法:andThen

如果一个方法的参数和返回值全部是Consumer类型,那么就可以实现效果,消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合,而这个方法就是Consumer接口中的default方法andThen方法

使用:

public class ConsumerAndThenTest {
    public static void main(String[] args) {
        test2(msg1 -> System.out.println(msg1.toLowerCase()),
                msg2-> System.out.println(msg2.toUpperCase()));
    }

    public static void test2(Consumer<String> c1, Consumer<String> c2) {
        String str = "Hello World";
        //转小写
//        c1.accept(str);
        //转大写
//        c2.accept(str);

        //先转小写,再转大写
        c1.andThen(c2).accept(str);
    }
}

2.3 Function

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

使用:传递进入一个字符串返回一个数字

public class FunctionTest {
    public static void main(String[] args) {
        test(msg -> Integer.parseInt(msg));
    }

    public static void test(Function<String,Integer> function) {
        Integer apply = function.apply("666");
        System.out.println("apply = " + apply);
    }
}

默认方法:andThen,也是用来进行组合操作。

默认的compose方法的作用顺序和andThen方法刚好相反,而静态方法identity则是输入什么参数就返回什么参数。

2.4 Predicate

有参且返回值为Boolean的接口,在Predicate中的默认方法也提供了逻辑关系操作 and or negate isEquals方法

public class PredicateTest {
    public static void main(String[] args) {
        test(msg -> msg.length()<3,"hello");
    }

    public static void test(Predicate<String> predicate,String msg) {
        boolean b = predicate.test(msg);
        System.out.println("b: " + b);
    }
}

五、方法引用

1. 为什么要用方法引用

1.1 lambda表达式冗余

在使用Lambda表达式的时候,也会出现代码冗余的情况,比如:用Lambda表达式求一个数组的和

public class FunctionRefTest01 {
    public static void main(String[] args) {
        //lambda和getTotal方法冗余了
        printMax(a -> {
            int sum = 0;
            for (int i : a) {
                sum += i;
            }
            System.out.println("数组之和:" + sum);
        });
    }

    /**
     * 求数组中所有元素的和
     * @param a
     */
    public void getTotal(int[] a) {
        int sum = 0;
        for (int i : a) {
            sum += i;
        }
        System.out.println("数组之和:" + sum);
    }

    private static void printMax(Consumer<int[]> consumer) {
        int[] a = {10, 20, 30, 40, 50};
        consumer.accept(a);
    }
}

1.2 解决方案

因为在Lambda表达式中要执行的代码和我们另一个方法中的代码是一样的,这时就没有必要重写一份逻辑了,这时我们就可以“引用”重复代码

方法引用(双冒号)也是JDK8中的新的语法

2. 方法引用的格式

符号表示:::

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

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

方法引用在JDK8中使用是相当灵活的,有以下几种常见的引用方式:

  • instanceName::methodName 对象::方法名

  • ClassName::staticMethodName 类名::静态方法

  • ClassName::methodName 类名::普通方法

  • ClassName::new 类名::new 调用的构造器

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

2.1 对象名::方法名

这是最常见的一种用法。如果一个类中的已经存在了一个成员方法,则可以通过对象名引用成员方法

public class FunctionRefTest02 {
    public static void main(String[] args) {
        Date date = new Date();
        //普通方法
        System.out.println(date.getTime());

        //使用lambda方式
        Supplier<Long> supplier = ()->date.getTime();
        System.out.println(supplier.get());
        //然后使用方法引用的方式处理
        Supplier<Long> supplier1 = date::getTime;
        System.out.println(supplier1.get());
    }
}

方法引用的注意事项:

  • 被引用的方法,参数要和接口中的抽象方法的参数一样

  • 当接口抽象方法有返回值时,被引用的方法也必须有返回值

2.2 类名::静态方法

也是比较常用的方式

public class FunctionRefTest03 {
    public static void main(String[] args) {
        Supplier<Long> supplier = ()-> System.currentTimeMillis();
        System.out.println(supplier.get());

        //因为System.currentTimeMillis();是一个静态方法,使用方法调用
        Supplier<Long> supplier1 = System::currentTimeMillis;
        System.out.println(supplier1.get());
    }
}

2.3 类名::引用实例方法

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

public class FunctionRefTest04 {
    public static void main(String[] args) {
        Function<String, Integer> function = (s)->s.length();
        System.out.println(function.apply("hello"));

        //通过方法引用来实现
        Function<String, Integer> function1 = String::length;
        System.out.println(function1.apply("hello"));

        BiFunction<String,Integer,String> function2 = String::substring;
        System.out.println(function2.apply("hello",2));
    }
}

2.4 类名::构造器

由于构造器的名称和类名完全一致,所以构造器引用使用::new的格式使用

public class FunctionRefTest05 {
    public static void main(String[] args) {
        Supplier<Person> sup = () -> new Person();
        System.out.println(sup.get());

        //通过方法引用实现
        Supplier<Person> sup1 = Person::new;
        System.out.println(sup1.get());
        BiFunction<String,Integer,Person> function = Person::new;
        System.out.println(function.apply("张三",22));
    }
}

2.5 数组::构造器

数组是怎么构造出来的呢?

public class FunctionRefTest06 {
    public static void main(String[] args) {
        Function<Integer, String[]> function = (len)->new String[len];
        System.out.println("数组的长度是:" + function.apply(5).length);

        //方法引用的方法
        Function<Integer, String[]> function1 = String[]::new;
        System.out.println("数组的长度是:" + function1.apply(3).length);
    }
}

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

六、Stream API

1.集合处理数据的弊端

当我们在需要对集合中的元素进行操作的时候,除了必需的添加,删除,获取外,最典型的操作就是集合遍历

public class StreamTest01 {
    public static void main(String[] args) {
        //定义一个list集合
        List<String> list = Arrays.asList("张三", "张三丰","李四", "王五", "赵六");
        //1.获取所有姓张的信息
        List<String> list1 = new ArrayList<>();
        for (String s : list) {
            if (s.startsWith("张")) {
                list1.add(s);
            }
        }

        //2.获取名称姓张且长度为3的用户
        List<String> list2 = new ArrayList<>();
        for (String s : list1) {
            if (s.length() == 3) {
                list2.add(s);
            }
        }

        //3.输出名称姓张且长度为3的所有的用户信息
        for (String s : list2) {
            System.out.println(s);
        }
    }
}

上面的代码针对与我们不同的需求总是一次次的循环循环,这时我们希望有更加高效的处理方式:通过JDK8中提供的Stream API来解决这个问题。

Stream更加优雅的解决方案:

public class StreamTest02 {    
	public static void main(String[] args) {        
	//定义一个list集合        
		List<String> list = Arrays.asList("张三", "张三丰","李四", "王五", "赵六");        
		//1.获取所有姓张的信息        
		//2.获取名称姓张且长度为3的用户        
		//3.输出名称姓张且长度为3的所有的用户信息        
		list.stream()
			.filter(s -> s.startsWith("张"))                
			.filter(s -> s.length() == 3)                
			.forEach(System.out::println);    
	}}

上面的SteamAPI代码的含义:获取流,过滤张,过滤长度,逐一打印。代码相比于上面的案例更加的简洁直观。

2. Steam流式思想概述

注意:Stream和IO流(InputStream/OutputStream)没有任何关系,请暂时忘记对传统IO流的固有印象!

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

image-20211029074238611

image-20211029074617106

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

3. Stream流的获取方式

3.1 根据Collection获取

首先,java.util.Collection 接口中加入了default方法 stream,也就是说Collection接口下的所有的实现都可以通过steam方法来获取Stream流。

public static void main(String[] args) {
	List<String> list = new ArrayList<>();
	list.stream();
	Set<String> set = new HashSet<>();
	set.stream();
	Vector vector = new Vector();
    vector.stream();
}

但是Map接口别没有实现Collection接口,那这时怎么办呢?这时我们可以根据Map获取对应的key-value的集合。

public static void main(String[] args) {
    Map<String,Object> map = new HashMap<>();
    Stream<String> stream = map.keySet().stream(); // key
    Stream<Object> stream1 = map.values().stream(); // value
    Stream<Map.Entry<String, Object>> stream2 = map.entrySet().stream(); // entry
}

3.1 通过Stream的of方法

在实际开发中我们不可避免的还是会操作到数组中的数据,由于数组对象不可能添加默认方法,所有Stream接口中提供了静态方法of

public class StreamTest03 {
    public static void main(String[] args) {

        Stream<String> arr0 = Stream.of("a", "b", "c");
        arr0.forEach(System.out::println);
        System.out.println("---------------------------------");
        
        String[] arr1 = {"aa", "bb", "cc"};
        Stream<String> arr11 = Stream.of(arr1);
        arr11.forEach(System.out::println);
        System.out.println("---------------------------------");
        
        Integer[] arr2 = {1, 2, 3, 4};
        Stream<Integer> arr21 = Stream.of(arr2);
        arr21.forEach(System.out::println);
        System.out.println("----------------------------------");

        //基本数据类型的数组是不行的
        int[] arr3 = {1,2,3,4};
        //输出: [I@53d8d10a 因此可以看出基本数据类型的数组是不行的
        Stream.of(arr3).forEach(System.out::println);

    }
}

4.Stream常用方法介绍

Stream常用方法

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

image-20211029091814195

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

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

Stream 注意事项(重要)

  • Stream只能操作一次

  • Stream方法返回的是新的流

  • Stream不调用终结方法,中间的操作不会执行

4.1 forEach

forEach用来遍历流中的数据的

void forEach(Consumer<? super T> action);

该方法接受一个Consumer接口,会将每一个流元素交给函数处理

public class StreamTest04 {
    public static void main(String[] args) {
        Stream.of("a","b","c").forEach(System.out::println);
    }
}

4.2 count

Stream流中的count方法用来统计其中的元素个数

long count();

该方法返回一个long值,代表元素的个数。

public class StreamTest05 {
    public static void main(String[] args) {
        long count = Stream.of("a", "b").count();
        System.out.println(count);
    }
}

4.3 filter

filter方法的作用是用来过滤数据的。返回符合条件的数据

image-20211029102730311

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

Stream<T> filter(Predicate<? super T> predicate);

该接口接收一个Predicate函数式接口参数作为筛选条件

public class StreamTest06 {    
	public static void main(String[] args) {        
		Stream.of("a1", "s1", "a3", "cc", "hd")                
			.filter(s -> s.contains("a"))                
			.forEach(System.out::println);    
	}
}

输出:

a1
a3

4.4 limit

image-20211029153236365

limit方法可以对流进行截取处理,只取前n个数据

Stream<T> limit(long maxSize);

参数是一个long类型的数值,如果集合当前长度大于参数就进行截取,否则不操作

操作:

public class StreamTest07 {    
	public static void main(String[] args) {        
		Stream.of("a1", "s1", "a3", "cc", "hd")                
			.limit(2)                
			.forEach(System.out::println);    
	}
}

4.5 skip

image-20211029154900817

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

Stream<T> skip(long n);

操作:

public class StreamTest08 {    
	public static void main(String[] args) {        
		Stream.of("a1", "s1", "a3", "cc", "hd")                
			.skip(1)                
			.forEach(System.out::println);    
	}
}

输出:

s1
a3
cc
hd

4.6 map

image-20211029155230059

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

<R> Stream<R> map(Function<? super T,? extends R> mapper);

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

public class StreamTest09 {    
	public static void main(String[] args) {        
		Stream.of("1", "2", "3", "4", "5")
			//.map(msg->Integer.parseInt(msg))                
			.map(Integer::parseInt)                
			.forEach(System.out::println);    
	}
}

4.7 sorted

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

Stream<T> sorted();

在使用的时候可以根据自然规则排序,也可以通过比较器来指定对应的排序规则

public class StreamTest11 {    
	public static void main(String[] args) {        
		Stream.of("1", "3", "7", "4", "5")                
			.map(Integer::parseInt)                
			//根据数据的自然顺序排序
			// .sorted()                
			//根据比较器指定排序规则                
			.sorted((o1, o2) -> o2 - o1)                
			.forEach(System.err::println);    
	}
}

4.8 distinct

如果要去掉重复数据,可以使用distinct方法:

Stream<T> distinct();

image-20211029180327615

使用:

public class StreamTest12 {
    public static void main(String[] args) {
        Stream.of("1", "3", "7", "4", "5","2","7")
                .map(Integer::parseInt)
                .sorted((o1,o2)->o2-o1)
                .distinct() //去掉重复的记录
                .forEach(System.out::println);

        System.out.println("-------------------------");
        Stream.of(
                new Person("张三",19)
                ,new Person("张三",14) // name只一样,distinct是不会去掉的,必须完全一致才可以
                ,new Person("张三",19)
                ,new Person("李四",22)
        ).distinct()
                .forEach(System.out::println);
    }
}

Stream流中的distinct方法对于基本数据类型是可以直接去重的,但是对于自定义类型,我们是需要重写hashCode和equals方法来移除重复元素。

因为使用了lombok注解所以也就不需要手动重写hashcode和equals,当没有使用lombok时,在stream流中使用distinct方法时需要手动重写hashcode和equals方法

4.9 match

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

boolean anyMatch(Predicate<? super T> predicate); // 元素是否有任意一个满足条件
boolean allMatch(Predicate<? super T> predicate); // 元素是否都满足条件
boolean noneMatch(Predicate<? super T> predicate); // 元素是否都不满足条件

使用:

public class StreamTest13 {    
	public static void main(String[] args) {        
		boolean b = Stream.of("1", "2", "3", "3", "1", "7")                
			.map(Integer::parseInt)
			//.allMatch(s -> s > 0) 
			//true,元素是否都大于0
			//.anyMatch(s -> s > 0); 
			//true,是否存在任意一个元素大于0                
			.noneMatch(s -> s > 0);//false,元素是否都不满足条件        
		System.out.println(b);    
	}
}

注意match是一个终结方法

4.10 find

如果我们需要找到某些数据,可以使用find方法来实现

Optional<T> findFirst();Optional<T> findAny();

image-20211029201110898

使用:

public class StreamTest14 {    
	public static void main(String[] args) {        
		Optional<Integer> any = Stream.of("1", "2", "3", "3", "1", "7")                
			.map(Integer::parseInt)                
			// findAny()操作,返回的元素是不确定的,                
			// 对于同一个列表多次调用findAny()有可能会返回不同的值。使用findAny()是为了更高效的性能。                			   // 如果是数据较少,串行地情况下,一般会返回第一个结果,如果是并行的情况,那就不能确保是第一个                			.findAny();        
		System.out.println(any.get());        
		System.out.println("----------------------------------------------");        
		Optional<Integer> first = Stream.of("1", "2", "3", "3", "1", "7")                
			.map(Integer::parseInt)                
			//  返回列表中的第一个元素                
			.findFirst();        
		System.out.println(first.get());    
	}
}

4.11 max和min

image-20211029201949592

如果我们想要获取最大值和最小值,那么可以使用max和min方法

Optional<T> min(Comparator<? super T> comparator);
Optional<T> max(Comparator<? super T> comparator);

使用:

public class StreamTest15 {    
	public static void main(String[] args) {        
		Optional<Integer> max = Stream.of("1", "2", "3", "3", "1", "7")                
			.map(Integer::parseInt)                
			.max((Comparator.comparingInt(o -> o)));        
		System.out.println(max.get());        
		System.out.println("----------------------------------------------");        
		Optional<Integer> min = Stream.of("1", "2", "3", "3", "1", "7")                
			.map(Integer::parseInt)                
			.min((Comparator.comparingInt(o -> o)));        
		System.out.println(min.get());    
	}
}

4.12 reduce方法

image-20211029202726628

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

T reduce(T identity, BinaryOperator<T> accumulator);

使用:

public class StreamTest16 {    
	public static void main(String[] args) {        
		Integer sum = Stream.of(4, 5, 6, 3, 7)                
			//identity默认值                
			//第一次的时候会将默认值赋值给x                
			//之后每次会将上一次的操作结果赋值给x,y就是每次从数据中获取的元素                
			.reduce(0, (x, y) -> {                    
				System.out.println("x = " + x + " , y = " + y);                    
				return x + y;                
			});        
		System.out.println(sum);        
		//获取最大值        
		Integer max = Stream.of(4, 9, 1, 2, 30)                
			.reduce(0, (x, y) -> x > y ? x : y);        
		System.out.println(max);    
	}
}

4.13 map和reduce的组合

在实际开发中我们经常会将map和reduce一块来使用

public class StreamTest17 {    
	public static void main(String[] args) {        
		//求出年龄之和        
		Integer sumAge = Stream.of(new Person("张三", 12),                
								   new Person("李四", 22),                
								   new Person("王五", 32))                
			.map(Person::getAge) //实现数据类型的转换                
			.reduce(0, Integer::sum);        
		System.out.println(sumAge);        
		//求出所有年龄中的最大值       
		Integer maxAge = Stream.of(new Person("张三", 12),                
								   new Person("李四", 22),              
								   new Person("王五", 32))              
			.map(Person::getAge) //实现数据类型的转换,符合reduce对数据的要求               
			.reduce(0, Integer::max); //reduce实现数据的处理        
		System.out.println(maxAge);       
		//统计字符a出现的次数        
		Integer count = Stream.of("2", "32", "a", "44", "a", "b", "c", "2", "a")               
			.map(ch -> "2".equals(ch) ? 1 : 0)            
			.reduce(0, Integer::sum);      
		System.out.println(count);    
	}
}

输出:

66322

4.14 mapToInt

如果需要将Stream中的Integer类型转换成int类型,可以使用mapToInt方法来实现

image-20211031194733509

使用:

public class StreamTest18 {
    public static void main(String[] args) {
        //Integer占用的内存比int多很多,在Stream流操作中会自动装箱和拆箱操作
        Integer arr[] = {1, 2, 0, -2, 3, 4, 5};
        Stream.of(arr)
                .filter(i -> i > 0)
                .forEach(System.out::println);

        //为了提高程序代码的效率,我们可以先讲流中的Integer数据转换为int数据,然后再操作
        Stream.of(arr)
                .mapToInt(Integer::intValue)  //IntStream
                .filter(i -> i > 0)
                .forEach(System.out::println);
    }
}

4.15 concat

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

public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T > b) {
	Objects.requireNonNull(a);
	Objects.requireNonNull(b);
	
    @SuppressWarnings("unchecked")
	Spliterator<T> split = new Streams.ConcatSpliterator.OfRef<>(
	(Spliterator<T>) a.spliterator(), (Spliterator<T>) b.spliterator());
	Stream<T> stream = StreamSupport.stream(split, a.isParallel() || b.isParallel());
	return stream.onClose(Streams.composedClose(a, b));
}

使用:

public class StreamTest19 {    
	public static void main(String[] args) {       
		Stream<String> stream1 = Stream.of("a,b,c");   
		Stream<String> stream2 = Stream.of("x,y,z");      
		//通过concat将两个stream流合并为一个新的流        
		Stream.concat(stream1,stream2).forEach(System.out::println);  
	}
}

4.16 综合案例

定义两个集合,然后在集合中存储多个用户名称,然后完成如下的操作:

  • 第一个队伍只保留姓名长度为 3 的成员

  • 第一个队伍筛选之后只要前 3 个人

  • 第二个队伍只要姓张的成员

  • 第二个队伍筛选之后不要前两个人

  • 将两个队伍合并为一个队伍

  • 根据姓名创建Person对象

  • 打印整个队伍的Person信息

public class StreamTest20 {   
	public static void main(String[] args) {      
		List<String> list1 = Arrays.asList("迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子", "洪七 公");       		List<String> list2 = Arrays.asList("古力娜扎", "张无忌", "张三丰", "赵丽颖", "张二狗", "张天爱", "张三");         Stream<String> stream1 = list1.stream().filter(s -> s.length() == 3).limit(3);     
		Stream<String> stream2 = list2.stream().filter(s -> s.startsWith("张")).skip(2);        
		Stream.concat(stream1, stream2)               
			.map(Person2::new)              
			.forEach(System.out::println);    
	}
}

输出:

// 注意:"洪七 公"间有个空格,所以不包含这个
	Person2(name=宋远桥)
	Person2(name=苏星河)
	Person2(name=张二狗)
	Person2(name=张天爱)
	Person2(name=张三)

5.Stream结果收集

5.1 结果收集到集合中

public class  ResultCollect{    
	/**     
	* Stream流结果收集    
	* 收集到集合中    
	*/    @Test 
	public void test01() {     
		//收集到list中    
		List<String> list = Stream.of("aa", "bb", "cc", "aa").collect(Collectors.toList());       
		System.out.println(list);      
		//收集到set中      
		Set<String> set = Stream.of("aa", "bb", "cc", "aa").collect(Collectors.toSet());       
		System.out.println(set);      
		//如果需要获取的类型为具体的实现,比如:ArrayList HashSet    
		ArrayList<String> arrayList = Stream.of("aa", "bb", "cc","aa").collect(Collectors.toCollection(ArrayList::new));       
		System.out.println(arrayList);       
		HashSet<String> hashSet = Stream.of("aa", "bb", "cc","aa").collect(Collectors.toCollection(HashSet::new));        
		System.out.println(hashSet);    
	}
}

输出:

[aa, bb, cc, aa]
[aa, bb, cc]
[aa, bb, cc, aa]
[aa, bb, cc]

5.2 结果收集到数组中

Stream中提供了toArray方法来将结果放到一个数组中,返回值类型是Object[],如果我们要指定返回的类型,那么可以使用另一个重载的toArray(IntFunction f)方法

/**
  * Stream流结果收集
  * 收集到数组中
  */
@Test
public void test02() {
    //返回的类型是Object类型的
    Object[] objects = Stream.of("aa", "bb", "cc", "aa").toArray();
    System.out.println(Arrays.toString(objects));

    //如果需要指定返回的数组中的元素类型
    String[] strings = Stream.of("aa", "bb", "cc", "aa").toArray(String[]::new);
    System.out.println(Arrays.toString(strings));

}

5.3 对流中的数据做聚合计算

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

/**
  * Stream流中的聚合计算
  */
@Test
public void test03() {
    //获取年龄的最大值的person数据
    Optional<Person> maxAge = Stream.of(
        new Person("张三", 19),
        new Person("李四", 23),
        new Person("王五", 33),
        new Person("李四", 23)
    ).collect(Collectors.maxBy(Comparator.comparingInt(Person::getAge)));
    System.out.println(maxAge);

    //获取年龄的最小值的Person数据
    Optional<Person> minAge = Stream.of(
        new Person("张三", 19),
        new Person("李四", 23),
        new Person("王五", 33),
        new Person("李四", 23)
    ).collect(Collectors.minBy(Comparator.comparing(Person::getAge)));
    System.out.println(minAge);

    //求所有人的年龄之和
    Integer sumAge = Stream.of(
        new Person("张三", 19),
        new Person("李四", 23),
        new Person("王五", 33),
        new Person("李四", 23)
    ).collect(Collectors.summingInt(Person::getAge));
    System.out.println(sumAge);

    //求所有年龄的平均值
    Double avgAge = Stream.of(
        new Person("张三", 19),
        new Person("李四", 23),
        new Person("王五", 33),
        new Person("李四", 23)
    ).collect(Collectors.averagingInt(Person::getAge));
    System.out.println(avgAge);

    //统计年龄大于23的人数
    Long countAge = Stream.of(
        new Person("张三", 19),
        new Person("李四", 23),
        new Person("王五", 33),
        new Person("李四", 23)
    ).filter(p -> p.getAge() > 23)
        .collect(Collectors.counting());
    System.out.println(countAge);
}

输出:

Optional[Person(name=王五, age=33)]
Optional[Person(name=张三, age=19)]
9824.51

5.4 对流中数据做分组操作

当我们使用Stream流处理数据后,可以根据某个属性将数据分组

public class StreamGroup {    
	/**    
	* 分组计算  
	*/    @Test    
	public void test01() {     
		//根据姓名对数据进行分组      
		Map<String, List<Person3>> map1 = Stream.of(       
			new Person3("张三", 18, 175),             
			new Person3("李四", 22, 177),         
			new Person3("张三", 14, 165),            
			new Person3("李四", 15, 166),             
			new Person3("张三", 19, 182)   
		).collect(Collectors.groupingBy(Person3::getName));    
		map1.forEach((k,v)->System.out.println("k=" + k + "\t" + "v=" + v));       
		System.out.println("---------------------------------------------");     
		//根据年龄分组,如果大于18 成年,否则未成年       
		Map<String, List<Person3>> map2 = Stream.of(      
			new Person3("张三", 18, 175),       
			new Person3("李四", 22, 177),         
			new Person3("张三", 14, 165),            
			new Person3("李四", 15, 166),          
			new Person3("张三", 19, 182)       
		).collect(Collectors.groupingBy(p -> p.getAge() > 18 ? "成年" : "未成年"));        
		map2.forEach((k,v)->System.out.println("k=" + k + "\t" + "v=" + v));   
	}
}

输出:

image-20211106182611672

多级分组: 先根据name分组然后根据年龄分组

/**  
* 分组计算——多级分组 
*/
@Testpublic void test02() { 
	//先根据name分组,然后根据age(成年和未成年)分组 
	Map<String, Map<String, List<Person3>>> map = Stream.of(      
		new Person3("张三", 18, 175),     
		new Person3("李四", 22, 177),       
		new Person3("张三", 14, 165),      
		new Person3("李四", 15, 166),      
		
		new Person3("张三", 19, 182)  
	).collect(Collectors.groupingBy(       
		Person3::getName,      
		Collectors.groupingBy(p -> p.getAge() >= 18 ? "成年" : "未成年")    ));
	map.forEach((k, v) -> {    
		System.out.println(k);      
		v.forEach((k1, v1) -> {          
			System.out.println("\t\t" + k1 + "=" + v1);    
		});   
	});
}

输出结果:

image-20211106192151454

5.5 对流中的数据做分区操作

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

image-20211106195008895

public class StreamPartition {
    /**
     * 流分区操作
     */
    @Test
    public void test01() {
        Map<Boolean, List<Person3>> map = Stream.of(
            new Person3("张三", 18, 175),
            new Person3("李四", 22, 177),
            new Person3("张三", 14, 165),
            new Person3("李四", 15, 166),
            new Person3("张三", 19, 182)
        ).collect(Collectors.partitioningBy(p -> p.getAge() > 18));
        map.forEach((k,v)-> System.out.println("k=" + k + "\t" + "v=" + v));
    }
}

输出结果:

image-20211106200546911

5.6 对流中的数据做拼接

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

public class StreamDataJoin {
    /**
     * 流数据拼接
     */
    @Test
    public void test01() {
        String s1 = Stream.of(
                new Person3("张三", 18, 175),
                new Person3("李四", 22, 177),
                new Person3("张三", 14, 165),
                new Person3("李四", 15, 166),
                new Person3("张三", 19, 182)
        ).map(Person3::getName).collect(Collectors.joining());
        //s1输出结果:张三李四张三李四张三
        System.out.println(s1);

        String s2 = Stream.of(
                new Person3("张三", 18, 175),
                new Person3("李四", 22, 177),
                new Person3("张三", 14, 165),
                new Person3("李四", 15, 166),
                new Person3("张三", 19, 182)
        ).map(Person3::getName).collect(Collectors.joining("_"));
        //s2输出结果:张三_李四_张三_李四_张三
        System.out.println(s2);

        String s3 = Stream.of(
                new Person3("张三", 18, 175),
                new Person3("李四", 22, 177),
                new Person3("张三", 14, 165),
                new Person3("李四", 15, 166),
                new Person3("张三", 19, 182)
        ).map(Person3::getName).collect(Collectors.joining("_","###","$$$"));
        //s3输出结果:###张三_李四_张三_李四_张三$$$
        System.out.println(s3);
    }
}

输出结果:

张三李四张三李四张三
张三_李四_张三_李四_张三
###张三_李四_张三_李四_张三$$$

6. 并行的Stream流

6.1 串行的Stream流

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

public class StreamSerial {    
	/**     
	* 串行流    
	*/   
	@Test    
	public void test01() {      
		Stream.of(5,3,21,541,1)          
			.filter(s->{              
				System.out.println(Thread.currentThread() + " " + s);        
				return s > 3;         
			}).count(); 
	}
}

输出:

Thread[main,5,main] 5
Thread[main,5,main] 3
Thread[main,5,main] 21
Thread[main,5,main] 541
Thread[main,5,main] 1

6.2 并行流

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

6.2.1 获取并行流

我们可以通过两种方式来获取并行流。

  • 通过List接口中的parallelStream方法来获取

  • 通过已有的串行流转换为并行流(parallel

实现:

/**
  * 获取并行流的两种方式
  */
@Test
public void test01() {
    List<Integer> list = new ArrayList<>();
    // 通过List接口,直接获取并行流
    Stream<Integer> integerStream = list.parallelStream();
    // 将已有的串行流转换为并行流
    Stream<Integer> parallel = Stream.of(1, 2, 3).parallel();
}

6.2.2 并行流操作

@Test
public void test02() {
    Stream.of(5,3,21,541,1)
        //将流转为并发流,Stream处理的时候就会通过多线程
        .parallel()
        .filter(s->{
            System.out.println(Thread.currentThread() + " " + s);
            return s > 2;
        }).count();
}

结果:

Thread[main,5,main] 21
Thread[main,5,main] 541
Thread[ForkJoinPool.commonPool-worker-3,5,main] 1
Thread[ForkJoinPool.commonPool-worker-2,5,main] 5
Thread[ForkJoinPool.commonPool-worker-1,5,main] 3

6.3 并行流和串行流对比

我们通过for循环,串行Stream流,并行Stream流来对 500000000 亿个数字求和。来看消耗时间

/**
 * 并行流和串行流对比
 */
public class SerialCompareParallel {
    private static long times = 500000000;
    private long start;

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

    /**
     * 普通for循环 消耗时间: 258
     */
    @Test
    public void test01() {
        System.out.println("普通for循环:");
        long res = 0;
        for (int i = 0; i < times; i++) {
            res += i;
        }
    }

    /**
     * 串行流:serialStream
     * 消耗时间:288
     */
    @Test
    public void test02() {
        System.out.println("串行流:serialStream");
        LongStream.rangeClosed(0,times).reduce(0,Long::sum);
    }

    /**
     * 并行流:parallel
     * 消耗时间:162
     */
    @Test
    public void test03() {
        System.out.println("并行流:parallel");
        LongStream.rangeClosed(0,times).parallel().reduce(0,Long::sum);
    }
}

通过案例我们可以看到parallelStream的效率是最高的。

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

6.4 线程安全问题

在多线程的处理下,肯定会出现数据安全问题。如下:

public class StreamThreadSafe {
    /**
     * Stream流多线程安全问题
     */
    @Test
    public void test01() {
        List<Integer> list1 = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            list1.add(i);
        }
        // 输出1000
        System.out.println(list1.size());

        List<Integer> list2 = new ArrayList<>();
        //使用并行流来向集合中添加数据
        list1.parallelStream().forEach(list2::add);
        // 可能会报数组下标越界异常:ArrayIndexOutOfBoundsException,也可能会随机出现数值
        System.out.println(list2.size());
    }
}

运行效果:

1000
981

或者直接抛异常

image-20211107090210192

针对这个问题,我们的解决方案有哪些呢?

  • 加同步锁

  • 使用线程安全的容器

  • 通过Stream中的toArray/collect操作

实现:

/**
 * 加同步锁
 */
@Test
public void test02() {
    List<Object> list = new ArrayList<>();
    Object obj = new Object();
    IntStream.rangeClosed(1,1000)
        .parallel()
        .forEach(i->{
            synchronized (obj) {
                list.add(i);
            }
        });
    // 输出:1000
    System.out.println(list.size());
}

/**
 * 使用线程安全的容器,比如:Vector
 */
@Test
public void test03() {
    Vector<Integer> vector = new Vector<>();
    IntStream.rangeClosed(1,1000)
        .parallel()
        .forEach(vector::add);
    // 输出:1000
    System.out.println(vector.size());
}

/**
 * 将线程不安全的容器转换为线程安全的容器再遍历
 */
@Test
public void test04() {
    List<Integer> list = new ArrayList<>();
    //开始线程转换
    List<Integer> synchronizedList = Collections.synchronizedList(list);
    IntStream.rangeClosed(1,1000)
        .parallel()
        .forEach(synchronizedList::add);
    // 输出:1000
    System.out.println(synchronizedList.size());
}

/**
 * 还可以通过Stream中的toArray或者collect方法来操作,达到满足线程安全的要求
 */
@Test
public void test05() {
    List<Integer> list = IntStream.rangeClosed(1, 1000)
        .parallel()
        .boxed()
        .collect(Collectors.toList());
    // 输出:1000
    System.out.println(list.size());
}

7.Fork/Join框架

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

  • 线程池:ForkJoinPool
  • 任务对象:ForkJoinTask
  • 执行任务的线程:ForkJoinWorkerThread

image-20211107113446188

7.1Fork/Join原理——分治法

image-20211107114351278

7.2Fork/Join原理——工作窃取算法

image-20211107120951779

image-20211107121028509

7.3 Fork/Join案例

需求:使用Fork/Join计算1-10000的和,当一个任务的计算数量大于3000的时候拆分任务,数量小于3000的时候就计算

image-20211107121522303

案例的实现:

public class ForkJoin {

    /**
     * 需求:使用Fork/Join计算1-10000的和,
     * 当一个任务的计算数量大于3000的时候拆分任务,
     * 数量小于1000的时候就计算
     */
    @Test
    public void test01() {
        long start = System.currentTimeMillis();

        ForkJoinPool pool = new ForkJoinPool();
        SumRecursiveTask task = new SumRecursiveTask(1, 10000L);
        Long result = pool.invoke(task);
        System.out.println("result:" + result);

        long end = System.currentTimeMillis();
        System.out.println("总耗时:" + (end-start));
    }

}

@Data
class SumRecursiveTask extends RecursiveTask<Long> {

    //定义一个拆分的临界值
    private static final long THRESHOLD = 3000L;
    private final long start;
    private final long 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也写成了left,所以程序会一直运行,不终止,一开始还以为是电脑配置问题,所以任务数值由10000减小到10也无用,仔细查看,原来是这里的问题
            right.fork();
            return left.join() + right.join();
        }
    }
}

输出:

拆分:左边1----->5000,右边5001---->10000
拆分:左边5001----->7500,右边7501---->10000
拆分:左边1----->2500,右边2501---->5000
计算:5001----->7500,的结果为:15618750
计算:1----->2500,的结果为:3123750
计算:7501----->10000,的结果为:21866250
计算:2501----->5000,的结果为:9371250
result:49980000
总耗时:4

七、Optional类

这个Optional类注意是解决空指针的问题

1. 以前对null 的处理

/**
 * 以前对null的处理
 * <p>
 * 刚开始命名文件名为Optional,然后启动报错:类型com.liang.Optional不带有参数,
 * 因为在别的类中有用到这个Optional,所以那个类首先来找的是这个自定义文件,
 * 而不是jdk中java.util包下的Optional.
 */
@Test
public void test01() {
    //String userName = "张三";
    String userName = null;
    if (userName != null) {
        System.out.println("字符串的长度:" + userName.length());
    } else {
        System.out.println("字符串为空");
    }
}

2. Optional类

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

image-20211107132626842

3. Optional的基本使用

Optional对象的创建方式

/**
 * Optional对象的创建方式
 */
@Test
public void test02() {
    //第一种方式:通过of方法 of方法是不支持null的
    Optional<String> op1 = Optional.of("zhangsan");
    Optional<String> op2 = Optional.of(null);

    //第二种方法:通过ofNullable方法,支持null
    Optional<String> op3 = Optional.ofNullable("lisi");
    Optional<String> op4 = Optional.ofNullable(null);

    //第三种方式:通过empty方法直接创建一个空的Optional对象
    Optional<Object> op5 = Optional.empty();

    // 输出:Optional[zhangsan]
    System.out.println(op1);
    //这个会报空指针异常
    System.out.println(op2);
    // 输出:Optional[lisi]
    System.out.println(op3);
    // 输出:Optional.empty
    System.out.println(op4);
    // 输出:Optional.empty
    System.out.println(op5);
}

4. Optional的常用方法

/**
 * Optional中的常用方法介绍
 * get(): 如果Optional有值则返回,否则抛出NoSuchElementException异常
 * get()通常和isPresent方法一块使用
 * isPresent():判断是否包含值,包含值返回true,不包含值返回false
 * orElse(T t):如果调用对象包含值,就返回该值,否则返回t
 * orElseGet(Supplier s):如果调用对象包含值,就返回该值,否则返回 Lambda表达式的返回值
 */
@Test
public void test03() {
    Optional<String> op1 = Optional.of("张三");
    Optional<String> op2 = Optional.empty();

    // 获取Optional中的值
    if (op1.isPresent()) {
        String s1 = op1.get();
        // 输出: 用户名称:张三
        System.out.println("用户名称:" + s1);
    }

    // 输出: op2是一个空Optional对象
    if (op2.isPresent()) {
        System.out.println(op2.get());
    } else {
        System.out.println("op2是一个空Optional对象");
    }

    // 输出:张三
    String s3 = op1.orElse("李四");
    System.out.println(s3);
    // 输出:李四
    String s4 = op2.orElse("李四");
    System.out.println(s4);

    // 输出:hello
    String s5 = op2.orElseGet(() -> {
        return "hello";
    });
    System.out.println(s5);
}

ifPresent方法:如果存在值 就做什么

@Test
public void test04() {
    // 如果存在值 就做什么
    Optional<String> op1 = Optional.of("zhangsan");
    Optional<String> op2 = Optional.empty();
    // 输出: 有值:zhangsan
    op1.ifPresent(s-> System.out.println("有值:" + s));
    // 输出:zhangsan
    op1.ifPresent(System.out::println);
}

案例:将Person对象的名称由小写字母改为大写字母

/**
 * 自定义一个方法,将Person对象中的name转换为大写,并返回
 */
@Test
public void test05() {
    Person p = new Person("zhangsan", 18);
    Optional<Person> op = Optional.of(p);
    String name = getNameForOptional(op);
    // 输出: name:ZHANGSAN
    System.out.println("name:" + name);
}

/**
 * 根据Person对象,将name转换为大写并返回
 * 通过Optional方法实现
 * @param op
 * @return
 */
private String getNameForOptional(Optional<Person> op) {
    if (op.isPresent()) {
        String msg = op.map(Person::getName).map(String::toUpperCase).orElse("空值");
        return msg;
    }
    return null;
}

/**
 * 根据Person对象,将name转换为大写并返回 (采用普通的写法)
 * @param person
 * @return
 */
public String getName4UpperCase(Person person) {
    if (person != null) {
        String name = person.getName();
        if (name != null) {
            return name.toUpperCase();
        } else {
            return null;
        }
    } else {
        return null;
    }
}

八、新时间日期API

1.旧版日期时间的问题

在旧版本中JDK对于日期和时间这块的时间是非常差的:

  • 设计不合理,在java.utiljava.sql的包中都有日期类,java.util.Date同时包含日期和时间的,而java.sql.Date仅仅包含日期,此外用于格式化和解析的类在java.text包下。

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

  • 时区处理麻烦,日期类并不提供国际化,没有时区支持。

    /**
     * 旧版本日期时间
     */
    @Test
    public void test01() {
        // 1.设计不合理
        Date date = new Date(2021,11,07);
        System.out.println(date);
    
        // 2.时间格式化和解析操作是线程不安全的
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        for (int i = 0; i < 50; i++) {
            new Thread(()->{
                //                System.out.println(sdf.format(date));
                try {
                    System.out.println(sdf.parse("2021-11-07"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
    

2. 新日期时间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:伊斯兰历

2.1 日期时间的常见操作

LocalDate,LocalTime以及LocalDateTime的操作。

/**
 * JDK8 日期和时间操作
 */
@Test
public void test02() {
    // 创建指定的日期
    LocalDate date1 = LocalDate.of(2021, 11, 07);
    System.out.println("date1:" + date1);

    // 2.得到当前的日期
    LocalDate now = LocalDate.now();
    System.out.println("now:" + now);

    // 3.根据LocalDate对象获取对应的日期信息
    System.out.println("年:" + now.getYear());
    System.out.println("月:" + now.getMonth().getValue());
    System.out.println("日:" + now.getDayOfMonth());
    System.out.println("星期:" + now.getDayOfWeek().getValue());
}

/**
 * 时间操作
 */
@Test
public void test03() {
    // 1.得到指定的时间
    LocalTime time = LocalTime.of(5, 23, 30, 12233);
    System.out.println(time);

    // 2.截取当前的时间
    LocalTime now = LocalTime.now();
    System.out.println(now);

    // 3.获取时间信息
    System.out.println(now.getHour());
    System.out.println(now.getMinute());
    System.out.println(now.getSecond());
    System.out.println(now.getNano());
}

/**
 * 日期时间类型 LocalDateTime
 */
@Test
public void test04() {
    LocalDateTime dateTime = LocalDateTime.of(2020, 06, 01,
                                              12, 12, 33, 213);
    System.out.println(dateTime);
    //获取当前的日期时间
    LocalDateTime now = LocalDateTime.now();
    System.out.println(now);
    //获取日期时间信息
    System.out.println(now.getYear());
    System.out.println(now.getMonth().getValue());
    System.out.println(now.getDayOfMonth());
    System.out.println(now.getDayOfWeek().getValue());
    System.out.println(now.getHour());
    System.out.println(now.getMinute());
    System.out.println(now.getSecond());
    System.out.println(now.getNano());
}

2.2 日期时间的修改和比较

/**
 * 日期时间的修改
 */
@Test
public void test05() {
    LocalDateTime now = LocalDateTime.now();
    System.out.println("now:" + now);

    // 修改日期时间,对已存在的LocalDate对象,创建了它模板并不会修改原来的信息
    LocalDateTime dateTime = now.withYear(1998);
    System.out.println("修改后的:" + dateTime);
    System.out.println("now:" + now);

    System.out.println("月份:" + now.withMonth(10));
    System.out.println("天:" + now.withDayOfMonth(4));
    System.out.println("小时:" + now.withHour(12));
    System.out.println("分钟:" + now.withMinute(30));

    // 在当前日期时间的基础上,加上或者减去指定的时间
    System.out.println("两天后:" + now.plusDays(2));
    System.out.println("10年后:" + now.plusYears(10));
    System.out.println("6个月后:" + now.plusMonths(6));
    System.out.println("10年前:" + now.minusYears(10));
    System.out.println("半年前:" + now.minusMonths(6));
    System.out.println("一周前:" + now.minusWeeks(1));
    System.out.println("一周前:" + now.minusDays(7));
}

/**
 * 日期时间的比较
 */
@Test
public void test06() {
    LocalDate now = LocalDate.now();
    LocalDate date = LocalDate.of(2021, 11, 11);
    // 在JDK8中要实现日期的比较 isAfter isBefore isEqual 通过这几个方法来直接比较
    // false
    System.out.println(now.isAfter(date));
    // true
    System.out.println(now.isBefore(date));
    // false
    System.out.println(now.isEqual(date));
}

注意:在进行日期时间修改的时候,原来的LocalDate对象是不会被修改,每次操作都是返回了一个新的LocalDate对象,所以在多线程场景下是数据安全的。

2.3 格式化和解析操作

在JDK8中我们可以通过java.time.format.DateTimeFormatter类可以进行日期的解析和格式化操作

/**
 * 日期格式化
 */
@Test
public void test07() {
    LocalDateTime now = LocalDateTime.now();
    // 指定格式 使用系统默认格式
    DateTimeFormatter isoLocalDateTime = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
    // 将日期时间转换为字符串
    String format = now.format(isoLocalDateTime);
    // format:2021-11-07T18:32:08.663
    System.out.println("format:" + format);

    // 通过 ofPattern 方法指定特定的格式
    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    String format1 = now.format(dateTimeFormatter);
    // format1:2021-11-07 18:32:08
    System.out.println("format1:" + format1);

    // 将字符串解析为一个日期时间类型
    LocalDateTime parse = LocalDateTime.parse("2021-11-07 18:30:53", dateTimeFormatter);
    // parse:2021-11-07T18:30:53
    System.out.println("parse:" + parse);
}

2.4 Instant类

在JDK8中给我们新增一个Instant类(时间戳/时间线),内部保存了从 1970 年 1 月 1 日 00:00:00以来的秒和纳秒

/**
 * Instant 时间戳,可以用来统计时间消耗
 * @throws InterruptedException
 */
@Test
public void test08() throws InterruptedException {
    Instant now = Instant.now();
    System.out.println("now:" + now);

    // 获取从1970年1月1日 00:00:00 到现在的纳秒
    System.out.println(now.getNano());
    Thread.sleep(5);
    Instant now1 = Instant.now();
    System.out.println("耗时:" + (now1.getNano() - now.getNano()));
}

2.5 计算日期时间差

JDK8中提供了两个工具类Duration/Period:计算日期时间差

  • Duration:用来计算两个时间差(LocalTime)

  • Period:用来计算两个日期差(LocalDate)

/**
 * 计算日期时间差
 */
@Test
public void test09() {
    // 计算时间差
    LocalTime now = LocalTime.now();
    LocalTime time = LocalTime.of(22, 30, 12);
    System.out.println("now:" + now); // now:18:53:41.490
    // 通过Duration来计算时间差
    Duration duration = Duration.between(now, time);
    System.out.println(duration.toDays()); // 0
    System.out.println(duration.toHours()); // 3
    System.out.println(duration.toMillis()); // 12990510
    System.out.println(duration.toMillis()); // 12990510

    // 计算日期差
    LocalDate nowDate = LocalDate.now();
    LocalDate date = LocalDate.of(1997, 12, 12);
    Period period = Period.between(date, nowDate);
    System.out.println(period.getYears()); // 23
    System.out.println(period.getMonths()); // 10
    System.out.println(period.getDays()); // 26
}

2.6 时间校正器

有时候我们可以需要如下调整:将日期调整到"下个月的第一天"等操作。这时我们通过时间校正器效果可能会更好。

  • TemporalAdjuster:时间校正器

  • TemporalAdjusters:通过该类静态方法提供了大量的常用TemporalAdjuster的实现。

/**
 * 时间校正器
 */
@Test
public void test10() {
    LocalDateTime now = LocalDateTime.now();
    // 将当前的日期调整到下个月的一号
    TemporalAdjuster adjuster = (temporal) -> {
        LocalDateTime dateTime = (LocalDateTime) temporal;
        LocalDateTime nextMonth = dateTime.plusMonths(1).withDayOfMonth(1);
        System.out.println("nextMonth1:" + nextMonth);
        return nextMonth;
    };

    // 我们可以通过TemporalAdjusters 来实现
    LocalDateTime nextMonth = now.with(TemporalAdjusters.firstDayOfNextMonth());
    System.out.println("nextMonth2:" + nextMonth);
}

2.7 日期时间的时区

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

其中每个时区都对应着 ID,ID的格式为 “区域/城市” 。例如 :Asia/Shanghai 等。ZoneId:该类中包含了所有的时区信息

/**
 * 时区操作
 */
@Test
public void test11() {
    // 获取所有时区的id
    // ZoneId.getAvailableZoneIds().forEach(System.out::println);

    // 获取当前时间,中国使用的,东八区时区,比标准时间早8个小时
    LocalDateTime now = LocalDateTime.now();
    // now:2021-11-07T19:25:29.423
    System.out.println("now:" + now);

    // 获取标准时间
    ZonedDateTime bz = ZonedDateTime.now(Clock.systemUTC());
    // bz:2021-11-07T11:25:29.424Z
    System.out.println("bz:" + bz);

    // 使用计算机默认的时区,创建日期时间
    ZonedDateTime now1 = ZonedDateTime.now();
    // now1:2021-11-07T19:25:29.424+08:00[Asia/Shanghai]
    System.out.println("now1:" + now1);

    // 使用指定的时区创建日期时间
    ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("America/Marigot"));
    // now2:2021-11-07T07:25:29.425-04:00[America/Marigot]
    System.out.println("now2:" + now2);
}

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

  • 新版日期时间API中,日期和时间对象是不可变,操作日期不会影响原来的值,而是生成一个新的实例
  • 提供不同的两种方式,有效的区分了人和机器的操作
  • TemporalAdjuster可以更精确的操作日期,还可以自定义日期调整期
  • 线程安全

九、其他新特性

1.重复注解

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

(1)定义一个重复注解的容器

/**
 * 定义一个重复注解的容器
 */
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotations {
    MyAnnotation[] value();
}

(2) 定义一个可以重复的注解

/**
 * 定义一个可以重复的注解
 */
@Repeatable(MyAnnotations.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    String value();
}

(3) 配置多个重复的注解

/**
 * 配置多个重复的注解
 */
@MyAnnotation("test1")
@MyAnnotation("test2")
@MyAnnotation("test3")
public class AnnoTest01 {

    @MyAnnotation("fun1")
    @MyAnnotation("fun2")
    public void test01() {

    }
}

(4)解析得到指定的注解

/**
 * 解析重复注解
 * @param args
 */
public static void main(String[] args) throws NoSuchMethodException {
    // 获取类中标注的重复注解
    MyAnnotation[] annotationsByType = AnnoTest01.class.getAnnotationsByType(MyAnnotation.class);
    for (MyAnnotation myann: annotationsByType) {
        //            System.out.println(myann.value());
    }

    //获取方法上标注的重复注解
    MyAnnotation[] test01s = AnnoTest01.class.getMethod("test01").getAnnotationsByType(MyAnnotation.class);
    for (MyAnnotation test01 : test01s) {
        System.out.println(test01.value());
    }
}

2.类型注解

JDK 8为@Target元注解新增了两种类型: TYPE_PARAMETERTYPE_USE

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

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

TYPE_PARAMETE

@Target(ElementType.TYPE_PARAMETER)
public @interface TypeParam {
}

使用:

public class TypeDemo01 <@TypeParam T> {
	public <@TypeParam K extends Object> K test01(){
		return null;
	}
}

TYPE_USE

@Target(ElementType.TYPE_USE)
public @interface NotNull {
}

使用:

public class TypeUseDemo01 {   
	public @NotNull Integer age = 10;
	public Integer sum(@NotNull Integer a,@NotNull Integer b){
		return a + b;
	}
}
posted @ 2021-11-07 20:44  intTom  阅读(57)  评论(0编辑  收藏  举报