史上最全Java8新特性详解

JDK8新特性

文章目录

前言

Java 是第一大编程语言和开发平台。它有助于企业降低成本、缩短开发周期、推动创新以及改善应用服务。如今全球有数百万开发人员运行着超过 51 亿个 Java 虚拟机,Java 仍是企业和开发人员的首选开发平台

本文课程大纲介绍:

  1. 了解Java发展史
  2. Lambda表达式
  3. 接口的增强
  4. 函数式接口
  5. 方法引用
  6. Stream API
  7. Optional
  8. 新时间日期API
  9. 其他新特性

一、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的主要版本大约每两年发布一次,直到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 Zulu, Azul Zing。
  • Oracle JDK完全由 Oracle 公司开发,Oracle JDK是基于Open JDK源代码的商业版本。此外,它包含闭源组件。
  • Oracle JDK根据二进制代码许可协议获得许可,在没有商业许可的情况下,在2019年1月之后发布的Oracle Java SE 8的公开更新将无法用于商业或生产用途。但是 Open JDK是完全开源的,可以自由使用。

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、需求分析

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

代码分析:

package cn.wujinagbo.test;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-03 17:13 星期三
 */
public class Test001 {
    public static void main(String[] args){
      new Thread(new Runnable() {
          @Override
          public void run() {
              System.out.println("新线程=" + Thread.currentThread().getName());
          }
      }).start();
      System.out.println("主线程=" + Thread.currentThread().getName());
    }
}

执行结果:

主线程=main
新线程=Thread-0

对上面代码我们做如下分析:

  1. Thread类需要一个Runnable接口作为参数传进去,其中run方法是主要的线程体,执行业务代码到地方
  2. 为了指定run方法体,不得不需要Runnable的实现类
  3. 为了省去定义一个Runnable的实现类,我们不得不使用匿名内部类
  4. 必须覆盖重写抽象的run方法,所有的方法名称,方法参数,方法返回值不得不都重写一遍,而且不能出错
  5. 但其实,我们只在乎run方法体中的代码

那么有没有简单点的写法呢?

是有点,那就是采用Lambda表达式写

2、Lambda表达式初体验

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

package cn.wujinagbo.test;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-03 17:13 星期三
 */
public class Test001 {
    public static void main(String[] args){
      new Thread(() -> {
          System.out.println("新线程=" + Thread.currentThread().getName());
      }).start();
      System.out.println("主线程=" + Thread.currentThread().getName());
    }
}

运行效果都一样,打印结果也一样,但是代码简化了不少

  • Lambda表达式的优点:简化了匿名内部类的使用,语法更加简单
  • 匿名内部类语法冗余,体验了Lambda表达式后,发现Lambda表达式是简化匿名内部类的一种方式

3、Lambda的语法规则

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

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

格式说明:

  • (参数类型 参数名称):参数列表
  • {代码体;} :方法体
  • -> :箭头,分割参数列表和方法体

3.1、Lambda练习1

我们先练习无参无返回值的Lambda

先定义一个接口:

package cn.wujinagbo.test;

public interface OrderService {

    //下单接口
    void addOrder();
}

然后创建主方法使用

package cn.wujinagbo.test;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-03 18:25 星期三
 */
public class LambdaDemo001 {

    public static void main(String[] args){
        //第一种方式:匿名内部类
        addOrderTest(new OrderService() {
            @Override
            public void addOrder() {
                System.out.println("匿名内部类:新增一个订单");
            }
        });

        //第二种方式:Lambda写法
        addOrderTest(() -> {
            System.out.println("Lambda:新增一个订单");
        });
    }

    public static void addOrderTest(OrderService orderService){
        orderService.addOrder();
    }
}

打印结果:

匿名内部类:新增一个订单
Lambda:新增一个订单

3.2、Lambda练习2

接下来我们一起完成一个有参且有返回值的案例

需求:

创建一个Student对象,然后我们在List集合中保存多个Student对象,然后对这些对象做根据age排序操作

我们先定义一个Student对象:

package cn.wujinagbo.test.dto;

import lombok.Data;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-03 18:34 星期三
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {

    private Long id;
    private String name;
    private Integer age;
}

然后开始写代码完成需求:

package cn.wujinagbo.test;

import cn.wujinagbo.test.dto.Student;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-03 18:25 星期三
 */
public class LambdaDemo002 {

    public static void main(String[] args){
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student(1L, "张三", 20));
        studentList.add(new Student(2L, "李四", 22));
        studentList.add(new Student(3L, "王五", 28));
        studentList.add(new Student(4L, "赵六", 25));

        //排序:升序
        Collections.sort(studentList, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o1.getAge() - o2.getAge();
            }
        });

        //打印List集合值
        for(int i=0; i<studentList.size(); i++){
            System.out.println(studentList.get(i));
        }
    }
}

打印结果:

Student(id=1, name=张三, age=20)
Student(id=2, name=李四, age=22)
Student(id=4, name=赵六, age=25)
Student(id=3, name=王五, age=28)

从结果我们分析:

我们发现在sort方法的第二个参数是一个Comparator接口的匿名内部类,且执行的方法有参数和返回值

那么我们可以将上面代码改写为Lambda表达式,代码如下:

package cn.wujinagbo.test;

import cn.wujinagbo.test.dto.Student;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-03 18:25 星期三
 */
public class LambdaDemo002 {

    public static void main(String[] args){
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student(1L, "张三", 20));
        studentList.add(new Student(2L, "李四", 22));
        studentList.add(new Student(3L, "王五", 28));
        studentList.add(new Student(4L, "赵六", 25));

        //排序:升序
        Collections.sort(studentList, (Student s1, Student s2) -> {
            return s1.getAge() - s2.getAge();
        });

        //打印List集合值
        for(int i=0; i<studentList.size(); i++){
            System.out.println(studentList.get(i));
        }
    }
}

该写完之后,执行结果还是一样的

4、@FunctionalInterface注解

  1. @FunctionalInterface:是一个信息性注解类型,用于指示接口类型符合 Java 语言规范定义的函数式接口要求
  2. 函数式接口只有一个抽象方法,其他方法都有默认的实现
  3. 如果接口声明了一个覆盖 java.lang.Object 的公共方法之一的抽象方法,这也不会进入抽象方法计数,因为接口的任何实现都具有来自 java.lang.Object 或其他地方的实现

看下面案例:

在这里插入图片描述

在这里插入图片描述

5、Lambda表达式的原理

首先给出结论:匿名内部类的本质是在编译时生成一个Class文件:XXXXX$1.class

就来下面代码为例:

package cn.wujinagbo.test;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-03 17:13 星期三
 */
public class Test001 {
    public static void main(String[] args){
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("新线程=" + Thread.currentThread().getName());
            }
        }).start();
        System.out.println("主线程=" + Thread.currentThread().getName());
    }
}

我们打开target目录查看一下,如下:

在这里插入图片描述

还可以通过反编译工具来查看生成的代码 XJad 工具来查看内容:

在这里插入图片描述

那么Lambda表达式的原理是什么呢?

我们代码变成这样了:

package cn.wujinagbo.test;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-03 17:13 星期三
 */
public class Test001 {
    public static void main(String[] args){
      new Thread(() -> {
          System.out.println("新线程=" + Thread.currentThread().getName());
      }).start();
      System.out.println("主线程=" + Thread.currentThread().getName());
    }
}

target下面同样会生成一个Test001$1.class文件

我们也通过反编译工具来查看:

在这里插入图片描述

结果发现,写的有Lambda表达式的class文件,通过XJad查看时,会报错,无法查看

那这时,我们只能通过JDK自带的一个工具:javap

javap可以对字节码进行反汇编操作,语法格式如下:

javap -c -p 文件名.class

-c:表示对代码进行反汇编
-p:显示所有类和成员

好,我们对以下代码使用javap看一下:

package cn.wujinagbo.test;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-03 18:25 星期三
 */
public class LambdaDemo001 {

    public static void main(String[] args){
        //第一种方式:匿名内部类
        //addOrderTest(new OrderService() {
        //    @Override
        //    public void addOrder() {
        //        System.out.println("匿名内部类:新增一个订单");
        //    }
        //});

        //第二种方式:Lambda写法
        addOrderTest(() -> {
            System.out.println("Lambda:新增一个订单");
        });
    }

    public static void addOrderTest(OrderService orderService){
        orderService.addOrder();
    }
}

在这里插入图片描述

在这个反编译的源码中我们看到了一个方法 addOrder(),这个方法里面做了什么事情呢?

为了更加直观的理解这个内容,我们可以在运行的时候添加 -Djdk.internal.lambda.dumpProxyClasses, 加上这个参数会将内部class码输出到一个文件中

命令执行:

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

也就是运行:
java -Djdk.internal.lambda.dumpProxyClasses cn.wujinagbo.test.LambdaDemo001.class

小结一下:

  1. 匿名内部类在编译的时候会产生一个class文件
  2. Lambda表达式在程序运行的时候会形成一个类
    • 在类中新增了一个方法,这个方法的方法体就是Lambda表达式中的代码
    • 还会形成一个匿名内部类,实现接口,重写抽象方法
    • 在接口中重写方法会调用新生成的方法

6、Lambda表达式的省略写法

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

规则:

  1. 如果Lambda表达式的方法体代码只有一行代码。可以省略大括号不写,同时要省略分号

    Thread t2 = new Thread(() -> {
        System.out.println(Thread.currentThread().getName()+"执行");
    });
    
    //简写方式:
    Thread t3 = new Thread(() ->
        System.out.println(Thread.currentThread().getName()+"执行")
    );
    
  2. 如果Lambda表达式的方法体代码只有一行代码。可以省略大括号不写

    Collections.sort(ls,(Student o3, Student o4) ->{
        return o3.getAge()-o4.getAge();
    });
    
    //简写方式:
    Collections.sort(ls,(Student o3, Student o4) ->o3.getAge()-o4.getAge());
    
  3. 参数类型可以省略不写

    Collections.sort(ls,( o3,  o4) ->o3.getAge()-o4.getAge());
    
  4. 如果只有一个参数,参数类型可以省略,同时()也可以省略

7、Lambda表达式的使用前提

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

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

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

Lambda和匿名内部类的对比

1、所需类型不一样

  • 匿名内部类的类型可以是 类,抽象类,接口
  • Lambda表达式需要的类型必须是接口

2、抽象方法的数量不一样

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

3、实现原理不一样

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

三、JDK8接口中新增的方法

1、JDK8中接口的新增

在JDK8中针对接口有做增强,在JDK8之前是这样的:

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

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

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

案例代码:

package cn.wujinagbo.test;

public interface TestInterface {

    //常量:默认是 static final 的
    String name = "hello";

    //抽象方法,默认是public的
    void test1();

    //默认方法
    default void test02(){
        System.out.println("我是默认方法");
    }

    //静态方法
    static void test03(){
        System.out.println("我是静态方法");
    }
}

上面代码是不会报错的,但是在JDK1.7中就会报错了

2、默认方法

2.1 为什么要增加默认方法

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

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

package cn.wujinagbo.test.test001;

public interface DemoA {

    void testDemo001();
    //接口中新增抽象方法,所有实现类都需要重写这个方法,不利于接口的扩展
    void testDemo002();

}
package cn.wujinagbo.test.test001;

public class B implements DemoA {

    @Override
    public void testDemo001() {
    }

    @Override
    public void testDemo002() {
    }
}
package cn.wujinagbo.test.test001;

public class C implements DemoA{
    @Override
    public void testDemo001() {
    }

    @Override
    public void testDemo002() {
    }
}
package cn.wujinagbo.test.test001;

public class DemoTest {

    public static void main(String[] args){
      DemoA b = new B();
      DemoA c = new C();
    }
}

2.2 接口默认方法的格式

现在我将DemoA改成下面这样:

package cn.wujinagbo.test.test001;

public interface DemoA {

    void testDemo001();
    //接口中新增抽象方法,所有实现类都需要重写这个方法,不利于接口的扩展
    void testDemo002();

    //定义默认方法
    default String testDemo003(){
        System.out.println("我是默认方法,执行啦.......");
        return "我是接口中的默认方法";
    }

}

然后B类重写默认方法testDemo003,而C类不重写testDemo003方法:

package cn.wujinagbo.test.test001;

public class B implements DemoA {

    @Override
    public void testDemo001() {
    }

    @Override
    public void testDemo002() {
    }

    @Override
    public String testDemo003() {
        System.out.println("我是B类中的testDemo003");
        return "我是B类中的testDemo003";
    }
}
package cn.wujinagbo.test.test001;

public class C implements DemoA{
    @Override
    public void testDemo001() {
    }

    @Override
    public void testDemo002() {
    }
}

打印结果:

我是B类中的testDemo003
我是默认方法,执行啦.......

小结:

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

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

3、静态方法

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

3.1、实例代码

package cn.wujinagbo.test.test001;

public interface DemoA {

    void testDemo001();
    //接口中新增抽象方法,所有实现类都需要重写这个方法,不利于接口的扩展
    void testDemo002();

    //定义默认方法
    default String testDemo003(){
        System.out.println("我是默认方法,执行啦.......");
        return "我是接口中的默认方法";
    }

    //静态方法
    static String testDemo004(){
        System.out.println("我是静态方法,执行啦.......");
        return "我是接口中的静态方法";
    }

}
package cn.wujinagbo.test.test001;

public class B implements DemoA {

    @Override
    public void testDemo001() {
    }

    @Override
    public void testDemo002() {
    }

    @Override
    public String testDemo003() {
        System.out.println("我是B类中的testDemo003");
        return "我是B类中的testDemo003";
    }
}
package cn.wujinagbo.test.test001;

public class C implements DemoA{
    @Override
    public void testDemo001() {
    }

    @Override
    public void testDemo002() {
    }
}
package cn.wujinagbo.test.test001;

public class DemoTest {

    public static void main(String[] args){
      DemoA b = new B();
      b.testDemo003();
      DemoA c = new C();
      c.testDemo003();

      DemoA.testDemo004();
    }
}

运行结果:

我是B类中的testDemo003
我是默认方法,执行啦.......
我是静态方法,执行啦.......

3.2、静态方法的使用

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

在这里插入图片描述

4、两者的区别介绍

  1. 默认方法通过实例调用,静态方法通过接口名调用
  2. 默认方法可以被继承,实现类可以直接调用接口默认方法,也可以重写接口默认方法
  3. 静态方法不能被继承,实现类不能重写接口的静态方法,只能使用接口名调用

四、函数式接口

1、函数式接口的由来

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

package cn.wujinagbo.demo;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 08:37 星期一
 */
public class Demo001Fun {

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

    public static void function001(Test test){
        int[] param = {1,2,3,4,5,6,7};
        int sum = test.getSum(param);
        System.out.println("sum=" + sum);
    }
}

//函数式接口
@FunctionalInterface
interface Test{
    int getSum(int param[]);
}

打印结果:

sum=28

2、函数式接口介绍

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

2.1、Supplier

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

package cn.wujinagbo.demo;

@FunctionalInterface
public interface Supplier<T>{

    //获取一个结果
    T get();
}

使用:

package cn.wujinagbo.demo;

import java.util.Arrays;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 08:42 星期一
 */
public class SupplierTest {

    public static void main(String[] args){
        function001(() -> {
            int param[] = {1,56,2,45,34,12,901};
            //升序排序
            Arrays.sort(param);
            //返回数组中最大的值
            return param[param.length - 1];
        });
    }

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

2.2、Consumer

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

package cn.wujinagbo.demo;

@FunctionalInterface
public interface Consumer<T> {

    //对给定参数执行此操作
    void receive(T t);
}

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

package cn.wujinagbo.demo;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 08:50 星期一
 */
public class ConsumerTest {

    public static void main(String[] args){
        functtion001(msg -> {
            System.out.println(msg + "转换成小写:" + msg.toLowerCase());
        });
    }

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

打印结果:

Hello World转换成小写:hello world

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

现在我们将上面的Consumer接口改成如下这样,加上andThen默认方法:

package cn.wujinagbo.demo;

import java.util.Objects;

@FunctionalInterface
public interface Consumer<T> {

    //对给定参数执行此操作
    void receive(T t);

    default Consumer<T> andThen(Consumer<? super T> after){
        Objects.requireNonNull(after);
        return (T t) -> {receive(t); after.receive(t);};
    }
}

然后再测试一下:

package cn.wujinagbo.demo;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 08:50 星期一
 */
public class ConsumerTest {

    public static void main(String[] args){
        functtion002(msg1 -> {
            System.out.println(msg1 + "转换成小写:" + msg1.toLowerCase());
        }, msg2 -> {
            System.out.println(msg2 + "转换成大写:" + msg2.toUpperCase());
        });
    }

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

    public static void functtion002(Consumer<String> c1, Consumer<String> c2){
        String str = "Hello World";
        c1.andThen(c2).receive(str);
    }
}

打印结果:

Hello World转换成小写:hello world
Hello World转换成大写:HELLO WORLD

从结果可以看出,c1执行完成后再执行c2,如果这样写:

c2.andThen(c1).receive(str);

那么执行结果就会反过来了:

Hello World转换成大写:HELLO WORLD
Hello World转换成小写:hello world

2.3、Function

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

package cn.wujinagbo.demo;

@FunctionalInterface
public interface Function<T, R> {

    //将此函数应用于给定参数
    R apply(T t);
}

下面我们来使用:传递进入一个字符串返回一个数字

package cn.wujinagbo.demo;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 09:15 星期一
 */
public class FunctionTest {

    public static void main(String[] args){
      function1(msg -> {
          return Integer.parseInt(msg);
      });
    }

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

执行结果:

apply=123456

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

package cn.wujinagbo.demo;

import java.util.Objects;

@FunctionalInterface
public interface Function<T, R> {

    //将此函数应用于给定参数
    R apply(T t);

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after){
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }
}

测试代码:

package cn.wujinagbo.demo;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 09:15 星期一
 */
public class FunctionTest {

    public static void main(String[] args){
      function1(msg1 -> {
          return Integer.parseInt(msg1);
      }, msg2 -> {
          return msg2 + 4;
      });
    }

    public static void function1(Function<String, Integer> f1, Function<Integer, Integer> f2){
        Integer apply = f1.andThen(f2).apply("123456");
        System.out.println("apply=" + apply);
    }
}

打印结果:

apply=123460

从结果可以看出,先对123456字符串进行转换,转成Integer类型后,再加4操作

2.4、Predicate

有参且返回值为Boolean的接口

package cn.wujinagbo.demo;

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);
}

使用:

package cn.wujinagbo.demo;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 09:27 星期一
 */
public class PredicateTest {

    public static void main(String[] args){
        function1(msg -> {
            return msg.length() > 3;
        }, "HelloWorld");
    }

    private static void function1(Predicate<String> predicate, String msg){
        boolean result = predicate.test(msg);
        System.out.println("result=" + result);
    }
}

运行结果:

result=true

从结果可以看出,计算字符串【HelloWorld】的长度大于3,所以返回了true

五、方法引用

1、为什么要用方法引用

1.1、lambda表达式冗余

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

package cn.wujinagbo.demo;

import java.util.Objects;

@FunctionalInterface
public interface Consumer<T> {

    //对给定参数执行此操作
    void receive(T t);
}
package cn.wujinagbo.demo;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 09:32 星期一
 */
public class FunctionRefTest01 {

    public static void main(String[] args){
        printSum(a -> {
            //Lambda表达式中的代码和getTotal中的代码冗余了
            int sum = 0;
            for(int i : a){
                sum += i;
            }
            System.out.println("数组之和=" + sum);
        });
    }

    public void getTotal(int a[]){
        int sum = 0;
        for(int i : a){
            sum += i;
        }
        System.out.println("数组之和=" + sum);
    }

    private static void printSum(Consumer<int[]> consumer){
        int[] a = {20,30,40,50,60,70,90};
        consumer.receive(a);
    }
}

打印结果:

数组之和=360

1.2、解决方案

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

package cn.wujinagbo;

import cn.wujinagbo.demo.Consumer;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 09:38 星期一
 */
public class FunctionRefTest02 {

    public static void main(String[] args){
      // :: 方法引用 也是JDK8中的新的语法
      printSum(FunctionRefTest02 :: getTotal);
    }

    //计算入参数组中的和
    public static void getTotal(int a[]){
        int sum = 0;
        for(int i : a){
            sum += i;
        }
        System.out.println("数组之和=" + sum);
    }

    private static void printSum(Consumer<int[]> consumer){
        int[] a = {20,30,40,50,60,70,90};
        consumer.receive(a);
    }
}

打印结果还是:

数组之和=360

:: 方法引用,也是JDK8中的新的语法

2、方法引用的格式

符号表示: ::

符号说明:双冒号为方法引用运算符,而它所在的表达式被称为方法引用
应用场景:如果Lambda表达式所要实现的方案,已经有其他方法存在相同的方案,那么则可以使用方法引用

常见的引用方式:

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

  1. instanceName::methodName 对象::方法名
  2. ClassName::staticMethodName 类名::静态方法
  3. ClassName::methodName 类名::普通方法
  4. ClassName::new 类名::new 调用的构造器
  5. TypeName[]::new String[]::new 调用数组的构造器

2.1、对象名::方法名

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

package cn.wujinagbo.demo;

@FunctionalInterface
public interface Supplier<T>{

    //获取一个结果
    T get();
}
public static void main(String[] args){
    Date now = new Date();
    Supplier<Long> supplier1 = () -> {return now.getTime();};
    System.out.println(supplier1.get());

    //然后我们通过 方法引用 的方法来处理
    Supplier<Long> supplier2 = now :: getTime;
    System.out.println(supplier2.get());
}

运行结果是一样的

方法引用的注意事项:

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

2.2、类名::静态方法名

也是比较常用的方式:

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

    //通过 方法引用 来实现
    Supplier<Long> supplier2 = System :: currentTimeMillis;
    System.out.println(supplier2.get());
}

打印结果也是一样的

2.3、类名::引用实例方法

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

package cn.wujinagbo.test;

import java.util.function.BiFunction;
import java.util.function.Function;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 11:10 星期一
 */
public class FunctionRefTest05 {

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

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

        BiFunction<String, Integer, String> function3 = String :: substring;
        String msg = function3.apply("HelloWorld", 5);
        System.out.println(msg);
    }
}

2.4、类名::构造器

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

package cn.wujinagbo.test;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 11:17 星期一
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    private String name;
    private Integer age;
}
package cn.wujinagbo.test;

import cn.wujinagbo.demo.Supplier;
import java.util.function.BiFunction;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 11:18 星期一
 */
public class FunctionRefTest06 {

    public static void main(String[] args){
        Supplier<Student> student1 = () -> {
            return new Student("张三", 23);
        };
        System.out.println(student1.get());

        //通过方法引用来实现
        Supplier<Student> student2 = Student :: new;
        System.out.println(student2.get());
        BiFunction<String, Integer, Student> student3 = Student :: new;
        System.out.println(student3.apply("李四", 29));

    }
}

运行结果:

Student(name=张三, age=23)
Student(name=null, age=null)
Student(name=李四, age=29)

2.5、数组::构造器

package cn.wujinagbo.test;

import java.util.function.Function;

/**
* @auth: wujiangbo(QQ1134135987)
* @date: 2022-08-15 11:22 星期一
*/
public class FunctionRefTest07 {

   public static void main(String[] args){
       Function<Integer, String[]> fun1 = (length) -> {
           return new String[length];
       };
       String[] a1 = fun1.apply(5);
       System.out.println("数组a1的长度=" + a1.length);

       //方法引用的方式来调用数组的构造器
       Function<Integer, String[]> fun2 = String[] :: new;
       String[] a2 = fun2.apply(10);
       System.out.println("数组a2的长度=" + a2.length);
   }
}

打印结果:

数组a1的长度=5
数组a2的长度=10

2.6、小结

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

六、Stream API

1、集合处理数据的弊端

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

package cn.wujinagbo.test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 11:30 星期一
 */
public class StreamTest01 {

    public static void main(String[] args) {
        //定义一个集合
        List<String> list = Arrays.asList("张三", "张无忌", "李四", "王五", "赵六", "陈军", "刘德华");

        //需求1:查询出所有姓张的信息
        List<String> list1 = new ArrayList<>();
        for (String s : list) {
            if (s.startsWith("张")) {
                list1.add(s);
            }
        }
        //输出结果
        for (String s : list1) {
            System.out.println(s);
        }
        System.out.println("=============================");

        //需求2:查询出所有长度为3的信息
        List<String> list2 = new ArrayList<>();
        for (String s : list) {
            if (s.length() == 3) {
                list2.add(s);
            }
        }
        //输出结果
        for (String s : list2) {
            System.out.println(s);
        }
    }
}

运行结果:

张三
张无忌
=============================
张无忌
刘德华

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

package cn.wujinagbo.test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 11:30 星期一
 */
public class StreamTest02 {

    public static void main(String[] args) {
        //定义一个集合
        List<String> list = Arrays.asList("张三", "张无忌", "李四", "王五", "赵六", "陈军", "刘德华");

        //1、打印出长度为3且姓张的信息
        list.stream()
                .filter(s -> s.startsWith("张"))
                .filter(s -> s.length() == 3)
                .forEach(s -> {
                    System.out.println(s);
                });

        System.out.println("===================");
        //第二张打印方式
        list.stream()
                .filter(s -> s.startsWith("张"))
                .filter(s -> s.length() == 3)
                .forEach(System.out :: println);
    }
}

运行结果:

张无忌
===================
张无忌

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

2、Steam流式思想概述

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

  1. Stream流式思想类似于工厂车间的“生产流水线”,Stream流不是一种数据结构,不保存数据,而是对数据进行加工处理
  2. Stream可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品
  3. Stream API能让我们快速完成许多复杂的操作,如筛选、切片、映射、查找、去除重复,统计,匹配和归约

3、Stream流的获取方式

3.1、根据Collection获取

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

package cn.wujinagbo.test;

import java.util.*;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 11:57 星期一
 */
public class StreamTest03 {

    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的集合。

package cn.wujinagbo.test;

import java.util.*;
import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 11:57 星期一
 */
public class StreamTest03 {

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

3.2、通过Stream的of方法

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

package cn.wujinagbo.test;

import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 11:57 星期一
 */
public class StreamTest03 {

    public static void main(String[] args){
        Stream<String> a1 = Stream.of("s1", "s2", "s3");
        a1.forEach(System.out :: println);
        System.out.println("--------------");

        String[] arr1 = {"a1", "a2", "a3"};
        Stream<String> arr11 = Stream.of(arr1);
        arr11.forEach(System.out :: println);
        System.out.println("--------------");

        Integer[] arr2 = {1,2,3,4};
        Stream<Integer> arr22 = Stream.of(arr2);
        arr22.forEach(System.out :: println);
        System.out.println("--------------");

        //注意:基本数据类型的数组是不行的
        int[] arr3 = {1,2,3,4};
        Stream.of(arr3).forEach(System.out :: println);
    }
}

打印结果:

s1
s2
s3
--------------
a1
a2
a3
--------------
1
2
3
4
--------------
[I@69d0a921

4、Stream常用方法介绍

Stream流模型的操作很丰富,这里介绍一些常用的API。

在这里插入图片描述

这些方法可以被分成两种:

  1. 终结方法:返回值类型不再是 Stream 类型的方法,不再支持链式调用。本小节中,终结方法包括 【count 】和 【forEach 】方法。
  2. 非终结方法:返回值类型仍然是 Stream 类型的方法,支持链式调用。(除了终结方法外,其余方法均为非终结方法。)

Stream注意事项(重要):

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

4.1、forEach

forEach用来遍历流中的数据的

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

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

public static void main(String[] args){
    Stream.of("a1","a2","a3").forEach(System.out :: println);
}

4.2、count

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

long count();

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

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

4.3、filter

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

在这里插入图片描述

如上图所示,按照原型进行过滤

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

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

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

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

运行结果:

a1
a2
a3
aa

4.4、limit

在这里插入图片描述

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

Stream<T> limit(long maxSize);

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

public static void main(String[] args){
    Stream.of("a1", "a2", "a3", "zz", "cc", "bb", "dd")
        .limit(4)
        .forEach(System.out :: println);
}

运行结果:

a1
a2
a3
zz

4.5、skip

在这里插入图片描述

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

Stream<T> skip(long n);

操作:

public static void main(String[] args){
    Stream.of("a1", "a2", "a3", "zz", "cc", "bb", "dd")
        .skip(2)
        .forEach(System.out :: println);
}

运行结果:

a3
zz
cc
bb
dd

4.6、map

在这里插入图片描述

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

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

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

package cn.wujinagbo.test;

import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 11:57 星期一
 */
public class StreamTest03 {

    public static void main(String[] args){
        Stream.of("1", "2", "3", "5", "25", "7", "9", "11", "29")
                .map(Integer :: parseInt)
                .forEach(System.out :: println);
    }
}

运行结果:

1
2
3
5
25
7
9
11
29

4.7、sorted

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

Stream<T> sorted();

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

package cn.wujinagbo.test;

import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 11:57 星期一
 */
public class StreamTest03 {

    public static void main(String[] args){
        Stream.of("1", "2", "3", "5", "25", "7", "9", "11", "29")
                .map(Integer :: parseInt)
                .sorted() //升序
                //.sorted((o1, o2) -> o2 - o1) //降序
                .forEach(System.out :: println);
    }
}

运行结果:

1
2
3
5
7
9
11
25
29

4.8、distinct

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

Stream<T> distinct();

在这里插入图片描述

使用:

package cn.wujinagbo.test;

import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 11:57 星期一
 */
public class StreamTest03 {

    public static void main(String[] args){
        Stream.of("1", "3", "3", "5", "6", "7", "1")
                .map(Integer :: parseInt)
                .sorted((o1, o2) -> o2 - o1) //降序
                .distinct() //去掉重复记录
                .forEach(System.out :: println);
        System.out.println("------------------");
        Stream.of(
                new Student("张三", 25),
                new Student("李四", 29),
                new Student("张三", 25),
                new Student("王五", 19)
        ).distinct()
                .forEach(System.out :: println);
    }
}

运行结果;

7
6
5
3
1
------------------
Student(name=张三, age=25)
Student(name=李四, age=29)
Student(name=王五, age=19)

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);//元素是否都不满足条件

测试代码:

package cn.wujinagbo.test;

import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 11:57 星期一
 */
public class StreamTest03 {

    public static void main(String[] args){
        boolean result = Stream.of("1", "3", "3", "5", "6", "7", "1")
                .map(Integer :: parseInt)
                .allMatch(s -> s > 0)//返回true
                //.anyMatch(s -> s > 4) //返回true
                //.noneMatch(s -> s > 4)//返回false
                ;
        System.out.println(result);
    }
}

注意:match是一个终结方法

4.10、find

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

Optional<T> findFirst();//查找Stream中的第一个元素。 当您特别需要序列中的第一个元素时,可以使用此方法

Optional<T> findAny();//顾名思义,findAny()方法允许您从Stream中查找任何元素。 在寻找元素时使用时不会关注元素顺序

在这里插入图片描述

实例代码:

package cn.wujinagbo.test;

import java.util.Optional;
import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 11:57 星期一
 */
public class StreamTest03 {

    public static void main(String[] args){
        Optional<String> first = Stream.of("1", "3", "4", "5", "6", "1", "7").findFirst();
        System.out.println(first.get());

        Optional<String> any = Stream.of("1", "3", "4", "5", "6", "1", "7").findAny();
        System.out.println(any.get());
    }
}

运行结果:

1
1

4.11、max和min

在这里插入图片描述

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

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

示例代码:

package cn.wujinagbo.test;

import java.util.Optional;
import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 11:57 星期一
 */
public class StreamTest03 {

    public static void main(String[] args){
        Optional<Integer> max = Stream.of("1", "3", "4", "5", "6", "1", "7")
                .map(Integer :: parseInt)
                .max((o1, o2) -> o1 - o2);
        System.out.println(max.get());

        System.out.println("-----------------------");
        Optional<Integer> min = Stream.of("1", "3", "4", "5", "6", "1", "7")
                .map(Integer :: parseInt)
                .min((o1, o2) -> o1 - o2);
        System.out.println(min.get());
    }
}

运行结果:

7
-----------------------
1

4.12、reduce方法

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

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

示例代码:

package cn.wujinagbo.test;

import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 11:57 星期一
 */
public class StreamTest04 {

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

运行结果:

x=0,y=4
x=4,y=5
x=9,y=6
x=15,y=9
x=24,y=2
x=26,y=3
29
------------
9

4.13、map和reduce的组合

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

package cn.wujinagbo.test;

import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 11:57 星期一
 */
public class StreamTest04 {

    public static void main(String[] args){
        //1、计算所有人的年龄之和
        Integer sumAge = Stream.of(
                new Student("刘德华", 24),
                new Student("谢霆锋", 22),
                new Student("梁朝伟", 25),
                new Student("吴彦祖", 23)
        ).map(Student::getAge)
                .reduce(0, Integer::sum);
        System.out.println("sumAge=" + sumAge);
        System.out.println("-----------------------");

        //2、计算所有年龄中的最大值
        Integer maxAge = Stream.of(
                new Student("刘德华", 24),
                new Student("谢霆锋", 22),
                new Student("梁朝伟", 25),
                new Student("吴彦祖", 23)
        ).map(Student::getAge)
                .reduce(0, Math::max);
        System.out.println("maxAge=" + maxAge);
        System.out.println("-----------------------");

        //3、统计字符【刘】出现的次数
        Integer count = Stream.of("张", "李", "刘", "王", "刘"
        ).map(ch -> "刘".equals(ch) ? 1 : 0)
                .reduce(0, Integer :: sum);
        System.out.println("count=" + count);
    }
}

运行结果:

sumAge=94
-----------------------
maxAge=25
-----------------------
count=2

4.14、mapToInt

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

在这里插入图片描述

示例代码:

package cn.wujinagbo.test;

import java.util.stream.IntStream;
import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 11:57 星期一
 */
public class StreamTest05 {

    public static void main(String[] args){
        //Integer 占用的内存比 int 多很多,再 Strenm 流操作中会进行【自动装箱】和【自动拆箱】操作
        Integer arr[] = {1,2,3,4,5,7,8,9};
        Stream.of(arr)
                .filter(i -> i>3)
                .forEach(System.out :: println);
        System.out.println("----------------------");

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

运行结果:

4
5
7
8
9
----------------------
4
5
7
8
9

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));
}

示例代码:

package cn.wujinagbo.test;

import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 星期一
 */
public class StreamTest05 {

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

运行结果:

a
b
c
x
y
z

4.16、综合案例

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

  1. 第一个队伍只保留姓名长度为3的成员
  2. 第一个队伍筛选之后只要前3个人
  3. 第二个队伍只要姓张的成员
  4. 第二个队伍筛选之后不要前两个人
  5. 将两个队伍合并为一个队伍
  6. 根据姓名创建Person对象
  7. 打印整个队伍的Person信息

代码:

package cn.wujinagbo.test;

import lombok.Data;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 星期一
 */
@Data
public class Student {
    private String name;
    private Integer age;

    public Student() {
    }

    public Student(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public Student(String name) {
        this.name = name;
    }
}
package cn.wujinagbo.test;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 星期一
 */
public class StreamTest06 {

    /**
     * 1.	第一个队伍只保留姓名长度为3的成员
     * 2.	第一个队伍筛选之后只要前3个人
     * 3.	第二个队伍只要姓张的成员
     * 4.	第二个队伍筛选之后不要前两个人
     * 5.	将两个队伍合并为一个队伍
     * 6.	根据姓名创建 Student 对象
     * 7.	打印整个队伍的 Student 信息
     */
    public static void main(String[] args){
        List<String> list1 = Arrays.asList("迪丽热巴", "孙俪", "刘亦菲", "佟丽娅", "关之琳", "王天霸", "张天工");
        List<String> list2 = Arrays.asList("赵丽颖", "邓超", "刘德华", "张三丰", "张家辉", "张靓颖", "李四", "王麻子", "张天爱");
        //1、第一个队伍只保留姓名长度为3的成员
        //2、第一个队伍筛选之后只要前3个人
        Stream<String> s1 = list1.stream().filter(s -> s.length() == 3).limit(3);


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

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

运行结果:

Student(name=刘亦菲, age=null)
Student(name=佟丽娅, age=null)
Student(name=关之琳, age=null)
Student(name=张靓颖, age=null)
Student(name=张天爱, age=null)

5、Stream结果收集

5.1、结果收集到集合中

代码:

package cn.wujinagbo.test;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 星期一
 */
public class StreamTest07 {
    public static void main(String[] args){
        //收集到 List 集合中
        List<String> list = Stream.of("aa", "cc", "aa", "bb").collect(Collectors.toList());
        System.out.println(list);
        System.out.println("----------------------");

        //收集到 Set 集合中
        Set<String> set = Stream.of("aa", "cc", "aa", "bb").collect(Collectors.toSet());
        System.out.println(set);
        System.out.println("----------------------");

        //如果需要获取的类型为具体的实现,比如:ArrayList/HashSet
        ArrayList<String> arrayList = Stream.of("aa", "cc", "aa", "bb").collect(Collectors.toCollection(ArrayList::new));
        System.out.println(arrayList);
        System.out.println("----------------------");

        HashSet<String> hashSet = Stream.of("aa", "cc", "aa", "bb").collect(Collectors.toCollection(HashSet::new));
        System.out.println(hashSet);
    }
}

打印结果:

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

5.2、结果收集到数组中

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

代码:

package cn.wujinagbo.test;

import java.util.Arrays;
import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 星期一
 */
public class StreamTest08 {

    public static void main(String[] args){
        //返回的数组中的元素是 Object 类型
        Object[] objects = Stream.of("aa", "cc", "bb", "aa").toArray();
        System.out.println(Arrays.toString(objects));
        System.out.println("-----------------------");

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

运行结果:

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

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

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

示例代码:

package cn.wujinagbo.test;

import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 星期一
 */
public class StreamTest08 {

    public static void main(String[] args) {
        // 获取年龄的最大值
        Optional<Student> maxAge = Stream.of(new Student("张三", 18)
                , new Student("李四", 22)
                , new Student("张三", 13)
                , new Student("王五", 15)
                , new Student("张三", 19)
        ).collect(Collectors.maxBy((p1, p2) -> p1.getAge() - p2.getAge()));
        System.out.println("最大年龄:" + maxAge.get());
        System.out.println("------------------");
        // 获取年龄的最小值
        Optional<Student> minAge = Stream.of(new Student("张三", 18)
                , new Student("李四", 22)
                , new Student("张三", 13)
                , new Student("王五", 15)
                , new Student("张三", 19)
        ).collect(Collectors.minBy((p1, p2) -> p1.getAge() - p2.getAge()));
        System.out.println("最新年龄:" + minAge.get());
        System.out.println("------------------");
        // 求所有人的年龄之和
        Integer sumAge = Stream.of(new Student("张三", 18)
                , new Student("李四", 22)
                , new Student("张三", 13)
                , new Student("王五", 15)
                , new Student("张三", 19)
        )
                .collect(Collectors.summingInt(Student::getAge));
        System.out.println("年龄总和:" + sumAge);
        System.out.println("------------------");
        // 年龄的平均值
        Double avgAge = Stream.of(
                new Student("张三", 18)

                , new Student("李四", 22)
                , new Student("张三", 13)
                , new Student("王五", 15)
                , new Student("张三", 19)
        ).collect(Collectors.averagingInt(Student::getAge));
        System.out.println("年龄的平均值:" + avgAge);
        System.out.println("------------------");
        // 统计数量
        Long count = Stream.of(
                new Student("张三", 18)

                , new Student("李四", 22)
                , new Student("张三", 13)
                , new Student("王五", 15)
                , new Student("张三", 19)
        ).filter(p -> p.getAge() > 18)
                .collect(Collectors.counting());
        System.out.println("满足条件的记录数:" + count);
    }
}

运行结果:

最大年龄:Student(name=李四, age=22)
------------------
最新年龄:Student(name=张三, age=13)
------------------
年龄总和:87
------------------
年龄的平均值:17.4
------------------
满足条件的记录数:2

5.4、对流中数据做分组操作

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

示例代码:

package cn.wujinagbo.test;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 星期一
 */
public class StreamTest09 {

    public static void main(String[] args) {
        // 根据账号对数据进行分组
        Map<String, List<Student>> map1 = Stream.of(new Student("张三", 18)
                , new Student("李四", 22)
                , new Student("张三", 13)
                , new Student("王五", 15)
                , new Student("张三", 19)
                , new Student("张三", 11)
        ).collect(Collectors.groupingBy(Student::getName));
        map1.forEach((k, v) -> System.out.println("key=" + k + "\t" + "value=" + v));
        System.out.println("------------------");

        //根据年龄分组,小于18则是未成年
        Map<String, List<Student>> map2 = Stream.of(new Student("张三", 18)
                , new Student("李四", 22)
                , new Student("张三", 13)
                , new Student("王五", 15)
                , new Student("张三", 19)
                , new Student("张三", 11)
        ).collect(Collectors.groupingBy(s -> s.getAge() >= 18 ? "成年" : "未成年"));
        map2.forEach((k, v) -> System.out.println("key=" + k + "\t" + "value=" + v));
    }
}

运行结果:

key=李四	value=[Student(name=李四, age=22)]
key=张三	value=[Student(name=张三, age=18), Student(name=张三, age=13), Student(name=张三, age=19), Student(name=张三, age=11)]
key=王五	value=[Student(name=王五, age=15)]
------------------
key=未成年	value=[Student(name=张三, age=13), Student(name=王五, age=15), Student(name=张三, age=11)]
key=成年	value=[Student(name=张三, age=18), Student(name=李四, age=22), Student(name=张三, age=19)]

多级分组,根据name分组后再根据年龄分组

代码:

package cn.wujinagbo.test;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 星期一
 */
public class StreamTest10 {

    public static void main(String[] args) {
		// 先根据name分组,然后根据age分组为:成年人和未成年人
        Map<String, Map<Object, List<Student>>> map1 = Stream.of(new Student("张三", 18)
                , new Student("李四", 22)
                , new Student("张三", 13)
                , new Student("王五", 15)
                , new Student("张三", 19)
                , new Student("张三", 11)
        ).collect(Collectors.groupingBy(Student::getName, Collectors.groupingBy(s -> s.getAge() >= 18 ? "成年" : "未成年")));
        map1.forEach((k, v) -> {
            System.out.println(k);
            v.forEach((k1, v1) -> {
                System.out.println("\t" + k1 + "=" + v1);
            });
        });
    }
}

运行结果:

李四
	成年=[Student(name=李四, age=22)]
张三
	未成年=[Student(name=张三, age=13), Student(name=张三, age=11)]
	成年=[Student(name=张三, age=18), Student(name=张三, age=19)]
王五
	未成年=[Student(name=王五, age=15)]

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

在这里插入图片描述

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

示例代码:

package cn.wujinagbo.test;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 星期一
 */
public class StreamTest11 {

    public static void main(String[] args) {
        Map<Boolean, List<Student>> map = Stream.of(
                new Student("张三", 18)
                , new Student("李四", 22)
                , new Student("张三", 13)
                , new Student("王五", 15)
                , new Student("张三", 19)
                , new Student("张三", 11)
        ).collect(Collectors.partitioningBy(s -> s.getAge() > 18));
        map.forEach((k, v) -> System.out.println(k + "\t" + v));
    }
}

运行结果:

false	[Student(name=张三, age=18), Student(name=张三, age=13), Student(name=王五, age=15), Student(name=张三, age=11)]
true	[Student(name=李四, age=22), Student(name=张三, age=19)]

5.6、对流中的数据做拼接

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

代码:

package cn.wujinagbo.test;

import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 星期一
 */
public class StreamTest12 {

    public static void main(String[] args) {
        String s1 = Stream.of(
                new Student("张三", 18)
                ,new Student("李四", 22)
                ,new Student("张三", 14)
                ,new Student("李四", 15)
                ,new Student("张三", 19)
        ).map(Student::getName).collect(Collectors.joining());
        System.out.println("s1=" + s1);
        System.out.println("-------------------");

        String s2 = Stream.of(
                new Student("张三", 18)
                ,new Student("李四", 22)
                ,new Student("张三", 14)
                ,new Student("李四", 15)
                ,new Student("张三", 19)
        ).map(Student::getName).collect(Collectors.joining("_"));
        System.out.println("s2=" + s2);
        System.out.println("-------------------");

        String s3 = Stream.of(
                new Student("张三", 18)
                ,new Student("李四", 22)
                ,new Student("张三", 14)
                ,new Student("李四", 15)
                ,new Student("张三", 19)
        ).map(Student::getName).collect(Collectors.joining("_", "###", "***"));
        System.out.println("s3=" + s3);
    }
}

运行结果:

s1=张三李四张三李四张三
-------------------
s2=张三_李四_张三_李四_张三
-------------------
s3=###张三_李四_张三_李四_张三***

6、并行的Stream流

6.1、串行的Stream流

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

示例代码:

package cn.wujinagbo.test;

import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 星期一
 */
public class StreamTest13 {

    public static void main(String[] args) {
        Stream.of(5,6,8,3,1,6)
        .filter(s -> {
            System.out.println(Thread.currentThread() + "=" + s);
            return s > 3;
        }).count();
    }
}

运行结果:

Thread[main,5,main]=5
Thread[main,5,main]=6
Thread[main,5,main]=8
Thread[main,5,main]=3
Thread[main,5,main]=1
Thread[main,5,main]=6

6.2、并行流

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

6.2.1、获取并行流

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

  1. 通过List接口中的parallelStream方法来获取
  2. 通过已有的串行流转换为并行流(parallel)

示例代码:

package cn.wujinagbo.test;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 星期一
 */
public class StreamTest14 {

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        //通过List接口直接获取并行流
        Stream<Integer> integerStream = list.parallelStream();
        //将已有点串行流转换为并行流
        Stream<Integer> parallel = Stream.of(1, 2, 3).parallel();
    }
}
6.2.2、并行流操作

示例代码:

package cn.wujinagbo.test;

import java.util.stream.Stream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 星期一
 */
public class StreamTest15 {

    public static void main(String[] args) {
        Stream.of(1,4,2,6,1,5,9)
        .parallel() //将流转换为并发流 ,Stream处理的时候就会通过多线程处理
        .filter(s -> {
            System.out.println(Thread.currentThread() + "  s=" + s);
            return s > 2;
        }).count();
    }
}

运行结果:

Thread[ForkJoinPool.commonPool-worker-3,5,main]  s=1
Thread[ForkJoinPool.commonPool-worker-5,5,main]  s=2
Thread[ForkJoinPool.commonPool-worker-3,5,main]  s=6
Thread[ForkJoinPool.commonPool-worker-4,5,main]  s=5
Thread[main,5,main]  s=1
Thread[ForkJoinPool.commonPool-worker-1,5,main]  s=4
Thread[ForkJoinPool.commonPool-worker-2,5,main]  s=9

6.3、并行流和串行流对比

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

示例代码:

package cn.wujinagbo.test;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.stream.LongStream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 星期一
 */
public class StreamTest16 {

    private static long times = 600000000;//6亿

    private long start;

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

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

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

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

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

运行结果:耗时已经写明在方法的注释上了

pom测试包依赖:

<!--测试包-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>

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

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

6.4、线程安全问题

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

package cn.wujinagbo.test;

import java.util.ArrayList;
import java.util.List;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 星期一
 */
public class StreamTest17 {

    public static void main(String[] args){
        List<Integer> list = new ArrayList<>();
        for(int i=0; i<1000; i++){
            list.add(i);
        }
        System.out.println(list.size());

        List<Integer> listNew = new ArrayList<>();
        //使用并行流来向集合中添加数据
        list.parallelStream().forEach(listNew :: add);
        System.out.println(listNew.size());
    }
}

运行效果:

1000
934

或者直接抛异常:

在这里插入图片描述

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

  1. 加同步锁
  2. 使用线程安全的容器
  3. 通过Stream中的toArray/collect操作

代码实现:

package cn.wujinagbo.test;

import org.junit.Test;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-15 星期一
 */
public class StreamTest18 {

    /**
     * 加同步锁
     * 运行结果:1000
     */
    @Test
    public void test001(){
        List<Integer> listNew = new ArrayList<>();
        Object obj = new Object();
        IntStream.rangeClosed(1, 1000)
                .parallel()
                .forEach(i -> {
                    synchronized (obj){
                        listNew.add(i);
                    }
                });
        System.out.println(listNew.size());
    }

    /**
     * 使用线程安全的容器
     * 运行结果:1000
     */
    @Test
    public void test002(){
        Vector v = new Vector();
        Object obj = new Object();
        IntStream.rangeClosed(1, 1000)
        .parallel()
        .forEach( i-> {
            synchronized (obj){
                v.add(i);
            }
        });
        System.out.println(v.size());
    }

    /**
     * 将线程不安全的容器转换为线程安全的容器
     * 运行结果:1000
     */
    @Test
    public void test003(){
        List<Integer> listNew = new ArrayList<>();
        //将线程不安全的容器包装为线程安全的容器
        List<Integer> synchronizedList = Collections.synchronizedList(listNew);
        IntStream.rangeClosed(1, 1000)
        .parallel()
        .forEach(i -> {
            synchronizedList.add(i);
        });
        System.out.println(synchronizedList.size());
    }

    /**
     * 还可以通过Stream中的 toArray 方法或者 collect 方法来操作
     * 就是满足线程安全的要求
     * 运行结果:1000
     */
    @Test
    public void test004(){
        List<Integer> listNew = new ArrayList<>();
        Object obj = new Object();
        //将线程不安全的容器包装为线程安全的容器
        List<Integer> list = IntStream.rangeClosed(1, 1000)
            .parallel()
            .boxed()
            .collect(Collectors.toList());
        System.out.println(list.size());
    }
}

七、Optional类

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

1、以前对null 的处理

以前是这样对null值进行处理的,如下:

package cn.wujinagbo.demo;

import org.junit.Test;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-16 星期二
 */
public class OptionalTest01 {

    @Test
    public void test001(){
        //String userName = "王天霸";
        String userName = null;
        if(userName != null){
            System.out.println("字符串长度=" + userName.length());
        }else{
            System.out.println("字符串为空");
        }
    }
}

2、Optional类

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

3、Optional的基本使用

Optional对象的创建方式如下:

/**
     * Optional 对象的创建方式
     */
@Test
public void test002(){
    //第一种方式:通过 of 方法,但 of 方法是不支持null的
    Optional<String> op1 = Optional.of("王天霸");
    //Optional<String> op2 = Optional.of(null);//这样写会报NullPointerException

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

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

4、Optional的常用方法

@Test
public void test04(){
    Optional<String> op1 = Optional.of("zhangsan");
    Optional<String> op2 = Optional.empty();

    //获取 Optional 中的值
    if(op1.isPresent()){
        String s1 = op1.get();
        System.out.println("s1=" + s1);
    }

    if(op2.isPresent()){
        System.out.println("s2=" + op2.get());
    }else{
        System.out.println("op2是一个空Optional对象");
    }

    String s3 = op1.orElse("李四");
    System.out.println("s3=" + s3);
    String s4 = op1.orElse("王麻子");
    System.out.println("s4=" + s4);

    String s5 = op2.orElseGet(() -> {
        return "Hello";
    });
    System.out.println("s5=" + s5);

    //如果存在值就做什么
    op1.ifPresent(s -> System.out.println("有值啊=" + s));
    op1.ifPresent(System.out :: println);
}

运行结果:

s1=zhangsan
op2是一个空Optional对象
s3=zhangsan
s4=zhangsan
s5=Hello
有值啊=zhangsan
zhangsan
/**
     * 需求:
     * 自定义一个方法 getNameForOptional,将 Student 对象中的 name 转换为大写,并返回
     */
@Test
public void test03(){
    //Student p = new Student(null,18);//打印:name=空值
    Student p = new Student("zhangsan",18);//打印:name=ZHANGSAN
    Optional<Student> op = Optional.of(p);
    String name = getNameForOptional(op);
    System.out.println("name=" + name);
}

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

八、新时间日期API

1、旧版日期时间的问题

在旧版本中JDK对于日期和时间这块的体验是非常差的,我们来看下示例代码:

package cn.wujinagbo.demo;

import org.junit.Test;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-16 星期二
 */
public class DateTimeTest01 {

    @Test
    public void test01(){
        //设计不合理
        Date date = new Date(2022, 8, 16);
        System.out.println("date=" + date);

        //时间格式化和解析操作时线程不安全的
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        for(int i=0; i<50; i++){
            new Thread(() -> {
                try {
                    System.out.println(sdf.parse("2022-08-16"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

运行结果:

在这里插入图片描述

有如下缺陷:

  1. 设计不合理,在java.util和java.sql的包中都有日期类,java.util.Date同时包含日期和时间的,而java.sql.Date仅仅包含日期,此外用于格式化和解析的类在java.text包下
  2. 非线程安全,java.util.Date是非线程安全的,所有的日期类都是可变的,这是java日期类最大的问题之一
  3. 时区处理麻烦,日期类并不提供国际化,没有时区支持

2、新日期时间API介绍

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

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

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

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

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

2.1、日期时间的常见操作

LocalDate,LocalTime以及LocalDateTime的常见操作方式

LocalDate示例代码:

package cn.wujinagbo.demo;

import org.junit.Test;

import java.time.LocalDate;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-16 星期二
 */
public class DateTimeTest02 {

    //JDK8日期操作
    @Test
    public void test01(){
        //创建指定的日期
        LocalDate date1 = LocalDate.of(2022, 8, 16);
        System.out.println("date1=" + date1);

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

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

打印结果:

date1=2022-08-16
now=2022-08-16
年:2022
月:8
日:16
星期:2

LocalTime示例代码:

//JDK8时间操作
@Test
public void test02(){
    //得到指定的时间
    LocalTime time = LocalTime.of(9, 33, 33, 23145);
    System.out.println("time=" + time);
    //得到当前时间
    LocalTime now = LocalTime.now();
    System.out.println("now=" + now);
    //获取时间信息
    System.out.println("小时:" + now.getHour());
    System.out.println("分钟:" + now.getMinute());
    System.out.println("秒:" + now.getSecond());
    System.out.println("纳米秒:" + now.getNano());
}

打印结果:

time=09:33:33.000023145
now=09:38:24.610
小时:9
分钟:38
秒:24
纳米秒:610000000

LocalDateTime示例代码:

//日期时间类型 LocalDateTime
@Test
public void test03(){
    //得到指定的时间
    LocalDateTime dateTime = LocalDateTime.of(2022, 8, 16, 9, 35, 25, 14154);
    System.out.println("dateTime=" + dateTime);
    //得到当前的日期时间
    LocalDateTime now = LocalDateTime.now();
    System.out.println("now=" + 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());
}

打印结果:

dateTime=2022-08-16T09:35:25.000014154
now=2022-08-16T09:42:37.852
年:2022
月:8
日:16
星期:2
小时:9
分钟:42
秒:37
纳米秒:852000000

2.2、日期时间的修改和比较

示例代码:

//日期时间的修改
@Test
public void test01(){
    LocalDateTime now = LocalDateTime.now();

    //修改日期时间
    LocalDateTime localDateTime  = now.withYear(1991);
    System.out.println("now=" + now);
    System.out.println("修改后的now:");
    System.out.println("now=" + localDateTime);
    System.out.println("月:" + now.withMonth(12));
    System.out.println("天:" + now.withDayOfMonth(5));
    System.out.println("小时:" + now.withHour(16));
    System.out.println("分钟:" + now.withMinute(12));

    //在当前日期时间的基础上,加上或者减去指定的时间
    System.out.println("3天后" + now.plusDays(3));
    System.out.println("10年后" + now.plusYears(10));
    System.out.println("6个月后" + now.plusMonths(6));

    System.out.println("5年前" + now.minusYears(5));
    System.out.println("半年前" + now.minusMonths(6));
    System.out.println("两周前" + now.minusDays(14));
}

运行结果:

now=2022-08-16T09:53:12.558
修改后的now:
now=1991-08-16T09:53:12.558
月:2022-12-16T09:53:12.558
天:2022-08-05T09:53:12.558
小时:2022-08-16T16:53:12.558
分钟:2022-08-16T09:12:12.558
3天后2022-08-19T09:53:12.558
10年后2032-08-16T09:53:12.558
6个月后2023-02-16T09:53:12.558
5年前2017-08-16T09:53:12.558
半年前2022-02-16T09:53:12.558
两周前2022-08-02T09:53:12.558

比较案例:

//日期时间的比较
@Test
public void test02(){
    LocalDate now = LocalDate.now();//今天是2022.8.16
    LocalDate date = LocalDate.of(2020, 1, 15);
    //JDK8中实现日期的比较,用如下三个方法:
    System.out.println(now.isAfter(date));//打印:true
    System.out.println(now.isBefore(date));//打印:false
    System.out.println(now.isEqual(date));//打印:false
}

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

2.3、格式化和解析操作

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

package cn.wujinagbo.demo;

import org.junit.Test;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-16 星期二
 */
public class DateTimeTest04 {

    //日期格式化
    @Test
    public void test01(){
        LocalDateTime now = LocalDateTime.now();
        //指定格式:使用系统默认的格式
        DateTimeFormatter isoLocalDateTime = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
        //将日期时间转换为字符串
        String format1 = now.format(isoLocalDateTime);
        System.out.println("format1=" + format1);

        //通过 ofPattern 方法来指定特定的格式
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String format2 = now.format(dateTimeFormatter);
        System.out.println("format2=" + format2);

        //将字符串解析为一个 日期时间类型
        LocalDateTime parse = LocalDateTime.parse("1991-05-13 22:22:22", dateTimeFormatter);
        System.out.println("parse=" + parse);
    }
}

运行结果:

format1=2022-08-16T10:29:01.799
format2=2022-08-16 10:29:01
parse=1991-05-13T22:22:22

2.4、Instant类

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

示例代码:

package cn.wujinagbo.demo;

import org.junit.Test;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-16 星期二
 */
public class DateTimeTest05 {

    @Test
    public void test01() throws InterruptedException {
        Instant now1 = Instant.now();

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

运行结果:

now1=768000000
now2=773000000
耗时:5000000

2.5、计算日期时间差

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

  1. Duration:用来计算两个时间差(LocalTime)
  2. Period:用来计算两个日期差(LocalDate)

示例代码:

package cn.wujinagbo.demo;

import org.junit.Test;
import java.time.*;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-16 星期二
 */
public class DateTimeTest06 {

    @Test
    public void test01() throws InterruptedException {
        //计算时间差
        LocalTime now = LocalTime.now();
        LocalTime time = LocalTime.of(12, 18,30);
        System.out.println("now=" + now);//打印结果:10:39:06.054
        //通过 Duration 来计算时间差
        Duration duration = Duration.between(now, time);
        System.out.println(duration.toDays());//打印结果:0
        System.out.println(duration.toHours());//打印结果:1
        System.out.println(duration.toMinutes());//打印结果:99
        System.out.println(duration.toMillis());//打印结果:5963946

        //计算日期差
        LocalDate nowDate = LocalDate.now();
        System.out.println("nowDate=" + nowDate);//打印结果:nowDate=2022-08-16
        LocalDate date = LocalDate.of(1991, 12, 12);
        Period period = Period.between(date, nowDate);
        System.out.println(period.getYears());//打印结果:30
        System.out.println(period.getMonths());//打印结果:8
        System.out.println(period.getDays());//打印结果:4
    }
}

2.6、时间校正器

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

  • TemporalAdjuster:时间校正器
  • TemporalAdjusters:通过该类静态方法提供了大量的常用 TemporalAdjuster 的实现

示例代码:

package cn.wujinagbo.demo;

import org.junit.Test;
import java.time.*;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAdjusters;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-16 星期二
 */
public class DateTimeTest07 {

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

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

2.7、日期时间的时区

  • Java8 中加入了对时区的支持,LocalDate、LocalTime、LocalDateTime是不带时区的,带时区的日期时间类分别为:ZonedDate、ZonedTime、ZonedDateTime
  • 其中每个时区都对应着 ID,ID的格式为 “区域/城市” 。例如 :Asia/Shanghai 等
  • ZoneId:该类中包含了所有的时区信息

示例代码:

package cn.wujinagbo.demo;

import org.junit.Test;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-16 星期二
 */
public class DateTimeTest08 {

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

        //获取当前时间,中国使用的时区是东八区,比标准时间早8个小时
        LocalDateTime now = LocalDateTime.now();
        System.out.println("now=" + now);//打印结果:now=2022-08-16T11:11:41.521

        //获取标准时间
        ZonedDateTime zdt = ZonedDateTime.now(Clock.systemUTC());
        System.out.println("zdt=" + zdt);//打印结果:zdt=2022-08-16T03:11:41.521Z

        //使用计算机默认的时区,创建日期时间
        ZonedDateTime now1 = ZonedDateTime.now();
        System.out.println("now1=" + now1);//打印结果:now1=2022-08-16T11:11:41.521+08:00[Asia/Shanghai]

        //使用指定的时区创建日期时间
        ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("America/Marigot"));
        System.out.println("now2=" + now2);//打印结果:now2=2022-08-15T23:11:41.522-04:00[America/Marigot]
    }
}

2.8、小结

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

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

九、其他新特性

1、重复注解

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

1.1、定义一个重复注解的容器

package cn.wujinagbo.demo;

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

@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotations{

    MyAnnotation[] value();
}

1.2、定义一个可以重复的注解

package cn.wujinagbo.demo;

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

@Repeatable(MyAnnotations.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    String value();
}

1.3、配置多个重复的注解

package cn.wujinagbo.demo;

@MyAnnotation("test1")
@MyAnnotation("test2")
@MyAnnotation("test3")
public class AnnoTest01{
    
    @MyAnnotation("func1")
    @MyAnnotation("func2")
    public void test01(){
        //...
    }
}

1.4、解析得到指定的注解

package cn.wujinagbo.demo;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-16 11:34 星期二
 */
public class AnnotationTest01 {

    //解析重复注解
    public static void main(String[] args) throws NoSuchMethodException {
        MyAnnotation[] annotationsByType = AnnotationTest01.class.getAnnotationsByType(MyAnnotation.class);
        for(MyAnnotation annotation : annotationsByType){
            System.out.println(annotation.value());
        }

        //获取方法上标注的重复注解
        MyAnnotation[] annotations = AnnoTest01.class.getMethod("test01").getAnnotationsByType(MyAnnotation.class);
        for(MyAnnotation annotation : annotations){
            System.out.println(annotation.value());//打印结果:func1和func2
        }
    }
}

2、类型注解

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

  • TYPE_PARAMETER :表示该注解能写在类型参数的声明语句中
  • TYPE_USE :表示注解可以再任何用到类型的地方使用

TYPE_PARAMETER 示例:

package cn.wujinagbo.demo;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

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

使用:

package cn.wujinagbo.demo;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-16 11:51 星期二
 */
public class TypeDemo01<@TypeParam T> {

    public <@TypeParam k extends Object> k test001(){
        return null;
    }
}

TYPE_USE 示例:

package cn.wujinagbo.demo;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

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

使用:

package cn.wujinagbo.demo;

/**
 * @auth: wujiangbo(QQ1134135987)
 * @date: 2022-08-16 11:53 星期二
 */
public class TypeDemo02 {

    public @NotNUll Integer age = 20;
    
    public Integer sum(@NotNUll Integer i1, @NotNUll Integer i2){
        return i1 + i2;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小学生波波

感谢您的厚爱与支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值