zl程序教程

您现在的位置是:首页 >  后端

当前栏目

JDK8新特性 (Lambda表达式和Stream流式编程)

编程 特性 表达式 stream lambda JDK8 流式
2023-09-14 09:04:52 时间

目录

一:JDK8新特性

1. Java SE的发展历史

2. 了解Open JDK 和 Oracle JDK

3. JDK 8新特性

3.1 Lambda表达式(重点)

3.2 接口的增强

3.3 函数式接口

3.4 方法引用

3.5 集合之Stream流式操作(重点)

3.6 新的时间和日期 API


一:JDK8新特性

1. Java SE的发展历史

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

现在我们就介绍一下JDK的更新版本和命名:

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月

........................................................

2. 了解Open JDK 和 Oracle JDK

(1)Open JDK来源

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

(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是完全开源的,可以自由使用。

 (3)Open JDK 官网介绍

Open JDK 官网:https://openjdk.org/

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

3. JDK 8新特性

3.1 Lambda表达式(重点)

(1)匿名内部类存在的问题

我们先写一个线程的实现,通过匿名内部类的方式;由于面向对象的语法要求,首先创建一个 Runnable 接口的匿名内部类对象来指定线程要执行的任务内容,再将其交给一个线程来启动。

public class Test {
    public static void main(String[] args) {
        // 匿名内部类
        new Thread(new Runnable() {
            public void run() {
                System.out.println("Hello");
            }
        }).start();
    }
}

代码分析:对于 Runnable 的匿名内部类用法,可以分析出几点内容:

①Thread 类需要 Runnable 接口作为参数,其中的抽象 run 方法是用来指定线程任务内容的核心 为了指定 run 的方法体,不得不需要 Runnable 接口的实现类。

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

③必须覆盖重写抽象 run 方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;而实际上似乎只有方法体才是关键所在。

(2)Lambda表达式初体验

Lambda是一个匿名函数,可以理解为一段可以传递的代码;借助Java8的全新语法,上述 Runnable 接口的匿名内部类写法可以通过更简单的Lambda表达式达到相同的效果!

public class Test {
    public static void main(String[] args) {
        // Lambda表达式
        new Thread(() -> 
            System.out.println("Hello")).start();
    }
}

代码分析:这段代码和刚才的执行效果是完全一样的,可以在JDK8或更高的编译级别下通过。

①从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定;我们只需要将要执行的代码放到一个Lambda表达式中,不需要定义类,不需要创建对象

②Lambda的优点:简化匿名内部类的使用,语法更加简单;实际上Lambda是匿名内部类的简写。

(3)Lambda的标准格式

Lambda表达式是一个匿名函数,而函数相当于Java中的方法;Lambda省去面向对象的条条框框,Lambda的标准格式格式由3个部分组成:

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

格式说明:

①(参数类型 参数名称):参数列表;

②{代码体;}:方法体;

③-> :箭头,分隔参数列表和方法体,起到连接的作用;

Lambda与方法的实现对比

例如:public static void main(String[] args),Lambda就是这种形式(String[] args);可以省略修饰符列表、返回值、方法名:

匿名内部类

public void run() {
  System.out.println("aa");
}

Lambda表达式

() -> System.out.println("bb")

(4)无参数、无返回值的Lambda

定义一个接口,无参数方法返回void:

package com.zl;

public interface Swimmable {
    // 抽象方法,省略了public static
    void swimming();
}

测试类:

package com.zl;

public class SwimmableTest {
    public static void main(String[] args) {
        // 使用匿名内部类
        play(new Swimmable() {
            @Override
            public void swimming() {
                System.out.println("匿名内部类游泳!");
            }
        });

        // 使用Lambda表达式:对匿名内部类的简写
        play(()-> System.out.println("Lambda的游泳!"));
    }
    // 调用方法
    public static void play(Swimmable s){
        s.swimming();
    }
}

(5)有参数、有返回值的Lambda

定义一个接口,有参数方法返回int类型:

package com.zl;

public interface Love {
    int myLove(String name);
}

测试类

package com.zl;

public class LoveTest {
    public static void main(String[] args) {
        // 匿名内部类
        lovers(new Love() {
            @Override
            public int myLove(String name) {
                System.out.println("匿名内部类的方式");
                return 1;
            }
        });
        // Lambda表达式,多行代码需要一个大括号{}
        lovers((String name) ->{
            System.out.println("Lambda表达式的方式");
            return 1;
        });
    }

    // 调用方法
    public static void lovers(Love love){
        int count = love.myLove("小红");
        System.out.println("返回值是"+count);
    }
}

小总结:

①以后我们调用方法时,看到参数是接口就可以考虑使用Lambda表达式,Lambda表达式相当于是对接口中抽象方法的重写。

② 匿名内部类和Lambda表达式结果的对比:

匿名内部类:在编译后会形成一个新的类,叫做:类名$.class,可以直接打开。

Lambda表达式:在编译后不会生成新的类(运行的时候才会形成类)就是原来的类,但是此时原来的类打不开,并且反编译工具无法反编译;我们使用JDK自带的一个工具: javap ,对字节码进行反汇编,查看字节码指令:javap -c -p 文件名.class ,发现会在这个类当中生成一个私有的静态方法;实际上Lambda表达式中的代码就会放到这个新增的静态方法当中。

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

(6)Lambda省略格式

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

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

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

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

对于一个Lambda表达式:

(int a) -> { 
    return new Person(); 
} 

第一步省略:省略类型

(a) -> { 
    return new Person(); 
}

 第二步省略:只有一个参数时,小括号也可以省略

a -> { 
    return new Person(); 
}

第三步省略:发现括号内只有一条语句,省略大括号、return关键字、结束分号(必须同时省略)

a -> new Person()

例:在调用Collectons.sort()方法排序时,里面的参数只能是一个 List集合;对于自定义的类型进行排序还需要传一个比较器(比较器中编写比较的逻辑)

Person类:

package com.zl.mapper;

public class Person {
    private String name;
    private int age;
    public Person() {
    }
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

PersonSort类:把上面的Person放到List集合当中,然后进行排序

package com.zl.mapper;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class PersonSort {
    public static void main(String[] args) {
        // 创建一个ArrayList集合
        List<Person> pList = new ArrayList<>();
        // 准备数据
        Person p1 = new Person("张三", 20);
        Person p2 = new Person("李四", 19);
        Person p3 = new Person("王五", 22);
        // 把数据添加到集合当中
        pList.add(p1);
        pList.add(p2);
        pList.add(p3);
        // 调用Collections工具类的sort方法进行排序
        // 第一种方法:采用匿名内部类的方式
        Collections.sort(pList, new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge()-o2.getAge();
            }
        });
        // 第二种方式:采用Lambda表达式
        Collections.sort(pList,(Person o1,Person o2)->{
            return o1.getAge()-o2.getAge();
        });
        // 第三种方式:使用Lambda表达式的省略模式
        Collections.sort(pList,((o1, o2) -> o1.getAge()-o2.getAge()));

        // 打印
        for (Person person : pList) {
            System.out.println(person);
        }
        // 打印的第二种方式
        pList.forEach(person -> System.out.println(person));

    }
}

(7)Lambda的前提条件

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

①方法的参数或局部变量类型必须为接口才能使用Lambda表达式;

②接口中有且仅有一个抽象方法;

怎么判定在接口中只有一个抽象方法呢?使用函数式接口

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

②函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。

③使用FunctionalInterface注解,这个注解与 @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注解可用于一个接口的定义上

@FunctionalInterface 
public interface Operator { 
  void myMethod(); 
} 

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

(8)Lambda和匿名内部类对比

了解Lambda和匿名内部类在使用上的区别?

①所需的类型不一样

匿名内部类:需要的类型可以是:类、抽象类、接口。

Lambda表达式:需要的类型必须是接口

②抽象方法的数量不一样

匿名内部类:所需的接口中抽象方法的数量随意。

Lambda表达式:所需的接口中只能有一个抽象方法。

③实现原理不同:

匿名内部类:是在编译后会形成class。

Lambda表达式:在程序运行的时候动态生成class。

总结:一方面Lambda表达式作为接口的实现类的对象,另一方面Lambda表达式是一个匿名函数;当接口中只有一个抽象方法时,建议使用Lambda表达式;其他其他情况还是需要使用匿名内部类!

3.2 接口的增强

(1)JDK8中接口的新增

在JDK8中针对接口有做增强,在JDK8之前,接口中只能有常量和抽象方法

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

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

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

(2)默认方法

我们先创建一个接口,然后再创建一个类实现这个接口,并且必须重写接口中的方法!

Anmail接口

package com.zl.anmails;

public interface Anmails {
    // 抽象方法
    void fly();
}

Bird类实现接口,并重写里面的方法

package com.zl.anmails;

public class Bird implements Anmails{
    @Override
    public void fly() {
        System.out.println("小鸟起飞!");
    }
}

如果此时我们在接口中又增加了其它方法呢?那么实现类都必须要重写这个抽象方法,这样就不利于接口的扩展;所以我们就可以定义成默认方法,语法格式如下:

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

在接口中定义默认方法:

这个默认方法,实现类会默认继承过去,继承过去的是和父类一模一样的方法!

当然也可以进行重写,在方法体中编写自己的业务逻辑。

package com.zl.anmails;

public interface Anmails {
    // 抽象方法
    void fly();
    // 默认方法
    public default void eat(){
        System.out.println("小鸟爱吃虫子");
    }
}

(3)静态方法

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

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

接口中增加静态方法:

接口中的静态方法在实现类中是不能被重写的,换言之默认也没有被继承过去。

②调用的话只能通过接口类型来调用: 接口名.静态方法名();使用多态的形式创建对象,使用对象. 的方式进行访问不行,因为实现类中根本没有这个静态方法。

package com.zl.anmails;

public interface Anmails {
    // 抽象方法
    void fly();

    // 默认方法
    public default void eat() {
        System.out.println("小鸟爱吃虫子");
    }

    // 静态方法
    public static void tryCatch(){
        System.out.println("小鸟尝试抓虫子");
    }
}

默认方法和静态方法两者的区别:

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

②默认方法可以被继承,实现类可以直接调用接口默认方法,也可以重写接口默认方法;静态方法不能被继承,实现类不能重写接口的静态方法,只能使用接口名调用。

3.3 函数式接口

①如果接口只声明有一个抽象方法,则此接口就称为函数式接口。简单的说,在Java8中,Lambda表达式就是一个函数式接口的实例;这就是Lambda表达式和函数式接口的关系。

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

自定义函数式接口

package com.zl.anmails;

public class Computer {
    public static void main(String[] args) {
        // 调用fun方法
        fun((arr) -> {
            int sum = 0;
            for (int i : arr) {
                sum += i;
            }
            return sum;
        });
    }
    // 调用求和的方法
    public static void fun(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);
}

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

四大核心函数式接口

注:以下的接口是已经提供好的,和我们上面自定义的函数接口Operate作用是类似的!

函数式接口称谓参数类型用途
Consumer<T>消费型接口T对类型为T的对象应用操作,包含方法: void accept(T t)
Supplier<T>供给型接口返回类型为T的对象,包含方法:T get()
Function<T, R>函数型接口T对类型为T的对象应用操作,并返回结果。结果是R类型的对象。包含方法:R apply(T t)
Predicate<T>判断型接口T确定类型为T的对象是否满足某约束,并返回 boolean 值。包含方法:boolean test(T t)

(1)Supplier

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

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

例:求一组数据中的最大值,不自己定义接口和抽象方法了,使用Supplier函数式接口作为参数

package com.zl.fun;
import java.util.Arrays;
import java.util.function.Supplier;

public class SupplierFun {

    public static void main(String[] args) {
        // 使用Lambda表达式,相当于重写get()方法的逻辑
        fun(()->{
            int arr[] = {1,2,7,8,4,5,6};
            // 排序
            Arrays.sort(arr);
            // 找到最后一个元素就是最大值
            return arr[arr.length-1];

        });
    }

    // 调用接口中的方法
    public static void fun(Supplier<Integer> supplier){
        Integer max = supplier.get();
        System.out.println("max = "+max);
    }
}

(2)Consumer

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

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

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

package com.zl.fun;

import java.util.function.Consumer;

public class ConsumerFun {
    public static void main(String[] args) {
        fun((msg)->{
            // 小写转大写
            String s = msg.toLowerCase();
            System.out.println(msg+"对应的小写"+s); // Hello AAA对应的小写hello aaa
        });
    }

    // 调用接口中的方法
    public static void fun(Consumer<String> consumer){
        consumer.accept("Hello AAA");
    }
}

默认方法:andThen

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

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

具体的操作:

package com.zl.fun;

import java.util.Locale;
import java.util.function.Consumer;

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

    // 调用接口中的方法
    public static void fun(Consumer<String> c1,Consumer<String> c2){
        String str = "HELLO world";
        c1.accept(str);
        c2.accept(str);
        // 上面就等价于
        c1.andThen(c2).accept(str);
    }
}

(3)Function

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

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

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

package com.zl.fun;

import javax.print.DocFlavor;
import java.util.function.Function;

public class FuncationFun {
    public static void main(String[] args) {
        fun((num)->{
            Integer number = Integer.parseInt(num);
            return number;
        });
    }

    // 调用接口中的方法
    public static void fun(Function<String, Integer> function){
        Integer apply = function.apply("666");
        System.out.println("applay = "+apply);
    }
}

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

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

具体的操作:

package com.zl.fun;

import java.util.function.Function;

public class FuncationAndthenFun {

    public static void main(String[] args) {
        fun(num1->{
            return Integer.parseInt(num1);
        },num2->{
            return num2*10+6;
        });
    }
    // 调用接口中的方法
    public static void fun(Function<String, Integer> f1,Function<Integer,Integer> f2){
        String str = "666";
        Integer i1 = f1.apply(str);
        Integer i2 = f2.apply(i1);
        System.out.println("i2 = "+i2);
    }
}

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

(4)Predicate

Predicate是一个有参、且返回值为Boolean的接口。

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

例:求一个字符串的长度是否大于5

package com.zl.fun;

import java.util.function.Predicate;

public class PredicateFun {
    public static void main(String[] args) {
        fun(msg->{
            return msg.length() > 5;
        },"Hello World");
    }

    // 调用接口中的方法
    public static void fun(Predicate<String> p,String str){
        boolean b = p.test(str);
        System.out.println("b = "+b);
    }
}

在Predicate中的默认方法提供了逻辑关系操作 and、or、negate、isEquals方法

package com.zl.fun;

import java.util.function.Predicate;

public class PredicateTest {
    public static void main(String[] args) {
        fun(msg1->{
            return msg1.contains("H");
        },msg2->{
            return msg2.contains("W");
        });
    }

    // 调用接口中的方法
    public static void fun(Predicate<String> p1, Predicate<String> p2){
        // 1. 判断p1包含H,同时P2包含W
        boolean b1 = p1.and(p2).test("Hello World");
        System.out.println(b1);
        // 2. 判断p1包含H,或者P2包含W
        boolean b2 = p1.or(p2).test("Hello World");
        System.out.println(b2);
        // 3.结果取反,表示p1不包含H
        boolean b3 = p1.negate().test("HelloWorld");
        System.out.println(b3);
        // 4. 判断两个结果是否相等
        boolean bb1 = p1.test("Hello");
        boolean bb2 = p2.test("World");

    }

}

3.4 方法引用

①Lambda表达式是可以简化函数式接口的变量或形参赋值的语法;而方法引用和构造器引用是为了简化Lambda表达式的!

②当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用!方法引用可以看做是Lambda表达式深层次的表达。换句话说,方法引用就是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法,可以认为是Lambda表达式的一个语法糖。

方法引用的本质:方法引用作为了函数式接口的实例!

(1)为什么使用方法引用?

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

package com.zl.method;

import java.util.function.Consumer;

public class Test {
    public static void main(String[] args) {
        printSum(arr->{
            int sum = 0;
            for (int i : arr) {
                sum += i;
            }
            System.out.println("数组之和:" + sum);
        });

    }

    /**
     * 求数组的和,和上面功能代码相同
     * @param arr
     */
    public static void getSum(int arr[]){
        int sum = 0;
        for (int i : arr) {
            sum += i;
        }
        System.out.println("数组之和:" + sum);
    }

    public static void printSum(Consumer<int[]> consumer){
        int[] arr = {30,10,26,18,45,9};
        consumer.accept(arr);
    }
}

以上两个方法的逻辑业务明显相同,代码冗余,这样就可以使用方法的引用

package com.zl.method;

import java.util.function.Consumer;

public class Test {
    public static void main(String[] args) {
        // 引用下面相同功能的代码
        printSum(Test::getSum);
    }

    /**
     * 求数组的和,和上面功能代码相同
     * @param arr
     */
    public static void getSum(int arr[]){
        int sum = 0;
        for (int i : arr) {
            sum += i;
        }
        System.out.println("数组之和:" + sum);
    }

    public static void printSum(Consumer<int[]> consumer){
        int[] arr = {30,10,26,18,45,9};
        consumer.accept(arr);
    }
}

(2)方法引用的格式

符号表示: ::

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

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

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

①instanceName::methodName 对象::方法名
②ClassName::staticMethodName 类名::静态方法
③ClassName::methodName 类名::普通方法

(3)对象名::方法名

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

package com.zl.method;

import java.util.Date;
import java.util.function.Supplier;

public class MethodTest01 {
    public static void main(String[] args) {
        // 练习1:
        // 第一种方式:匿名内部类
        Consumer<String> c1 = new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        };
        c1.accept("张三");

        // 方法二:Lambda表达式
        Consumer<String> c2 = (msg)->{
            System.out.println(msg);
        };
        c2.accept("李四");

        // 方法三:方法引用
        Consumer<String> c3 = System.out::println;
        c3.accept("王五");

        // 练习2:
        Date date = new Date();
        Supplier<Long> supplier = ()->{ return date.getTime();};
        System.out.println(supplier.get());
        // 通过 方法引用 的方式处理
        Supplier<Long> supplier = date::getTime;
        System.out.println(supplier.get());
    }
}

总结:函数式接口中的抽象方法a与其内部实现时调用的对象的某个方法b的形参列表、返回值类型都相同(或一致:比如多态,自动拆箱装箱等),则我们可以使用方法b实现对方法a的重写、替换。 此方法b是非静态的方法,需要对象来调用!

(4)类名::静态方法名

也是一种比较常用的方式,通过类名调用静态方法!

原来的方式:

package com.zl.method;

import java.util.Date;
import java.util.function.Supplier;

public class MethodTest01 {
    public static void main(String[] args) {
        fun(()->{
            return System.currentTimeMillis();
        });
        
        // 采用方法引用的方式
       Supplier<Long> s = System::currentTimeMillis;
        System.out.println(s.get());
    }

    public static void fun(Supplier<Long> supplier){
        Long time = supplier.get();
        System.out.println(time);
        // 上面就等价于
        System.out.println(supplier.get());
    }
}

精简的方式:

public class FunctionRefTest04 {
    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());
   }
}

总结:函数式接口中的抽象方法a与其内部实现时调用的的某个方法b的形参列表、返回值类型都相同(或一致:比如多态,自动拆箱装箱等),则我们可以使用方法b实现对方法a的重写、替换。 此方法b是静态的方法,需要类来调用!

(5)类名::引用实例方法(难)

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

抽象方法有两个参数(n),内部实现的方法有一个参数(n-1),n个参数可以分为1和n-1,且第1个参数是方法的调用者!

例题1:

package com.zl.methods;

import java.util.Comparator;

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

        // 第一种:匿名内部类
        Comparator<String> com1 = new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) { // 两个参数
                return o1.compareTo(o2); // 一个参数
            }
        };
        System.out.println(com1.compare("abc", "abd"));
        // 第二种:Lambda
        Comparator<String> com2 = (s1,s2)->{
            return s1.compareTo(s2);
        };
        System.out.println(com2.compare("abc", "abd"));
        // 第三种:类::实例方法
        Comparator<String> com3 = String::compareTo;
        System.out.println(com3.compare("abc","abb"));

    }
}

 例题2:

package com.zl.method;

import java.util.function.Function;

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

        // 使用方法引用
        Function<String,Integer> function2 = String::length;
        System.out.println(function2.apply("hello"));
    }


}

总结:函数式接口中的抽象方法a与其内部实现时调用的对象的某个方法b的返回值类型都相同。同时,抽象方法a中有n个参数,方法b中有n-1个参数,且抽象方法a的第一个参数作为方法b的调用者,且抽象方法a的后n-1个参数与方法b的n-1个参数的类型相同(或一致:比如多态,自动拆箱装箱等)。则我们可以使用方法b实现对方法a的重写、替换。 此方法b是非静态的方法,需要对象来调用,但是形式上是写成对象a所属的类!

(6)类名::构造器

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

ClassName::new 类名::new

 Person类:提供有参和无参构造方法

package com.zl.method;

public class Person {
    private String name;
    private int age;

    public Person() {
    }

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

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

测试:

注:如果使用一个参数的构造方法,可以定义为Function;定义为两个参数的构造方法,使用BiFunction;最终返回的都是一个Person类型!

package com.zl.method;

import java.util.function.BiFunction;
import java.util.function.Supplier;

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

        // 通过 方法引用来实现
        Supplier<Person> supplier1 = Person::new;
        System.out.println(supplier1.get());
        // 也可以给属性赋值
        BiFunction<String ,Integer,Person> function = Person::new;
        System.out.println(function.apply("张三",22)); // 底层调用有参数构造方法

    }
}

总结:调用了类名对应的类中的某一个确定的构造器,具体调用的是类中的哪一个构造器?取决于函数式接口的抽象方法的形参列表!

(7)数组::构造器

当Lambda表达式是创建一个数组对象,并且满足Lambda表达式形参,正好是给创建这个数组对象的长度,就可以数组构造引用。

格式:数组类型名::new

TypeName[]::new 数组名[]::new 

 例1:传一个数字,返回一个对应长度的数组

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

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

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

3.5 集合之Stream流式操作(重点)

①Java8 中有两大最为重要的改变。第一个是 Lambda 表达式;另外一个则是 Stream API。

Stream API ( java.util.stream) 把真正的函数式编程风格引入到 Java 中。这是目前为止 对 Java 类库最好的补充,因为 Stream API 可以极大提供 Java 程序员的生产力,让程 序员写出高效率、干净、简洁的代码。

③Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。 使用 Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来 并行执行操作。简言之,Stream API 提供了一种高效且易于使用的处理数据的方式。

(1)什么是 Stream?

①实际开发中,项目中多数数据源都来自于MySQL、Oracle等。但现在数据源可以更多了,有MongDB,Radis等,而这些NoSQL的数据就需要Java层面去处理。

②Stream 是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。

③Stream 和 Collection 集合的区别:Collection 是一种静态的内存数据结构, 讲的是数据,而 Stream 是有关计算的,讲的是数据的计算(排序、查找、过滤、映射、遍历等)前者是主要面向内存, 存储在内存中,后者主要是面向 CPU,通过 CPU 实现计算。

注意:

①Stream 自己不会存储元素。

②Stream 不会改变源对象。相反,他们会返回一个持有结果的新 Stream。

③Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行;即 一旦执行终止操作,就执行中间操作链,并产生结果。

④ Stream 一旦执行了终止操作,就不能再调用其它中间操作或终止操作了。

(2)Stream的操作三个步骤?

1- 创建 Stream

Stream的实例化:一个数据源(如:集合、数组),获取一个流 !

2- 中间操作

每次处理都会返回一个持有结果的新Stream,即中间操作的方法返回值仍然是Stream类型的对象。因此中间操作可以是个操作链,可对数据源的数据进行n次处理,但是在终结操作前,并不会真正执行。

3- 终止操作(终端操作)

终止操作的方法返回值类型就不再是Stream了,因此一旦执行终止操作,就结束整个Stream操作了。一旦执行终止操作,就执行中间操作链,最终产生结果并结束Stream。

(3) 创建Stream实例

方式一:通过集合

 Java8 中的 Collection 接口被扩展,提供了两个获取流的方法:

default Stream<E> stream() : 返回一个顺序流
default Stream<E> parallelStream() : 返回一个并行流

例:

package com.zl.methods;

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

public class Test04 {
    public static void main(String[] args) {
        // 创建一个数组
        int[] arr = {1,2,3,4,5,6};
        // 把一个数组转换成一个集合
        List<int[]> list = Arrays.asList(arr);
        // 得到Stream实例(返回一个顺序流)
        Stream<int[]> stream = list.stream();
        // 得到Stream实例(返回一个并行流)
        Stream<int[]> stream1 = list.parallelStream();
    }
}

方式二:通过数组

Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:

static <T> Stream<T> stream(T[] array): 返回一个流
public static IntStream stream(int[] array)
public static LongStream stream(long[] array)
public static DoubleStream stream(double[] array)

例:

package com.zl.methods;

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


public class Test04 {
    public static void main(String[] args) {
        // 创建一个数组
        int[] arr = {1,2,3,4,5,6};
        // 得到Stream实例(返回一个顺序流)
        IntStream stream = Arrays.stream(arr);
        // 再例如
        Integer[] integers = {1,2,3,4,5};
        Stream<Integer> stream1 = Arrays.stream(integers);

    }
}

方式三:通过Stream的of()

可以调用Stream类静态方法 of(), 通过显示值创建一个流,它可以接收任意数量的参数。

public static<T> Stream<T> of(T... values) : 返回一个流

主要用在多个数据没有容器(数组、集合)进行存储

package com.zl.methods;

import java.util.stream.Stream;

public class Test04 {
    public static void main(String[] args) {
        // 通过静态方法进行调用
        Stream<Integer> stream = Stream.of(1, 2, 3, 4);
    }
}

(4)一系列中间操作

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”。

准备Employee类

package com.zl.data;

import java.util.Objects;

public class Employee {

    private int id;
    private String name;
    private int age;
    private double salary;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

    public Employee() {
        System.out.println("Employee().....");
    }

    public Employee(int id) {
        this.id = id;
    }

    public Employee(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public Employee(int id, String name, int age, double salary) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.salary = salary;
    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return id == employee.id && age == employee.age && Double.compare(employee.salary, salary) == 0 && Objects.equals(name, employee.name);
    }

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

准备数据

package com.zl.data;

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


public class EmployeeData {
    public static List<Employee> getEmployees(){
        List<Employee> list = new ArrayList<>();

        list.add(new Employee(1001, "马化腾", 34, 6000.38));
        list.add(new Employee(1002, "马云", 2, 19876.12));
        list.add(new Employee(1003, "刘强东", 33, 3000.82));
        list.add(new Employee(1004, "雷军", 26, 7657.37));
        list.add(new Employee(1005, "李彦宏", 65, 5555.32));
        list.add(new Employee(1006, "比尔盖茨", 42, 9500.43));
        list.add(new Employee(1007, "任正非", 26, 4333.32));
        list.add(new Employee(1008, "扎克伯格", 35, 2500.32));

        return list;
    }
}

1-筛选与切片

方 法描 述
filter(Predicate)接收 Lambda , 从流中排除某些元素
distinct()筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
limit(long maxSize)截断流,使其元素不超过给定数量
skip(long n)跳过元素,返回一个扔掉了前 n 个元素的流。 若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补

需求:查询员工表中薪资大于7000的员工

package com.zl.data;

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

public class Test01 {
    public static void main(String[] args) {
        List<Employee> list = EmployeeData.getEmployees();
        // 实例Stream
        Stream<Employee> stream = list.stream();
        // 进行筛选(参数是Predicate函数式接口)
        // stream.filter(emp -> emp.getSalary() > 7000);
        // 进行打印,实际上也就是终止条件
        // stream.filter(emp -> emp.getSalary() > 7000).forEach(emp-> System.out.println(emp));
        // 后面也可以使用方法引用
        stream.filter(emp -> emp.getSalary() > 7000).forEach(System.out::println);

    }
}

需求:截断流,使元素不超过给定数量

package com.zl.data;

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

public class Test01 {
    public static void main(String[] args) {
        List<Employee> list = EmployeeData.getEmployees();
        // 实例Stream
        Stream<Employee> stream = list.stream();
        // 截断流
        stream.limit(2).forEach(System.out::println);

    }
}

需求:跳过前2个,只打印后面的;实际上和limit(只取前几个)是一个互补的关系

package com.zl.data;

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

public class Test01 {
    public static void main(String[] args) {
        List<Employee> list = EmployeeData.getEmployees();
        // 实例Stream
        Stream<Employee> stream = list.stream();
        // 跳过前2个
        stream.skip(2).forEach(System.out::println);
    }
}

需求:去重,根据equals方法和hashCode方法进行去重

package com.zl.data;

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


public class Test01 {
    public static void main(String[] args) {
        List<Employee> list = EmployeeData.getEmployees();
        // 添加几个相同的元素
        list.add(new Employee(1008, "特朗布", 35, 2500.32));
        list.add(new Employee(1008, "特朗布", 35, 2500.32));
        list.add(new Employee(1008, "特朗布", 35, 2500.32));

        // 实例Stream
        Stream<Employee> stream = list.stream();
        // 进行去重
        stream.distinct().forEach(System.out::println);
    }
}

2-映 射

方法描述
map(Function f)接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
mapToDouble(ToDoubleFunction f)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream。
mapToInt(ToIntFunction f)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream。
mapToLong(ToLongFunction f)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream。
flatMap(Function f)接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流

需求:大写转小写

package com.zl.data;

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

public class Test01 {
    public static void main(String[] args) {
      // 把一个数组转化成集合
        List<String> list = Arrays.asList("aa", "bb", "cc");
        // 实例Stream
        Stream<String> stream = list.stream();
        // 大写转小写
        // stream.map(str->str.toUpperCase()).forEach(System.out::println);
        // 使用方法引用也可以
        stream.map(String::toUpperCase).forEach(System.out::println);
    }
}

需求:获取员工姓名长度大于3的员工姓名

package com.zl.data;

import java.util.List;

public class Test01 {
    public static void main(String[] args) {
        List<Employee> list = EmployeeData.getEmployees();
        // 获取员工姓名长度大于3的员工姓名
        // 方法一:先过滤到名字长度低于3的,在映射名字进行打印
        list.stream().filter(emp->emp.getName().length()>3).map(emp->emp.getName()).forEach(System.out::println);
        // 方式二:先映射名字
        list.stream().map(emp->emp.getName()).filter(name->name.length()>3).forEach(System.out::println);
        // 方式三:使用方法引用
        list.stream().map(Employee::getName).filter(name->name.length()>3).forEach(System.out::println);

    }
}

3-排序

方法描述
sorted()产生一个新流,其中按自然顺序排序
sorted(Comparator com)产生一个新流,其中按比较器顺序排序

需求:进行自然排序

package com.zl.data;

import java.util.Arrays;

public class Test01 {
    public static void main(String[] args) {
        // 定义一个数组
        int[] arr = new int[]{1,2,9,76,3,4,5};
        Arrays.stream(arr).sorted().forEach(System.out::println);
    }
}

需求:借用比较器进行排序

package com.zl.data;

import java.util.List;

public class Test01 {
    public static void main(String[] args) {
        List<Employee> list = EmployeeData.getEmployees();
        // 借用比较器进行排序,按照年级进行比较
        list.stream().sorted((o1,o2)-> o1.getAge() - o2.getAge()).forEach(System.out::println);
    }
}

(5)终止操作

①终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是 void 。

②流进行了终止操作后,不能再次使用。

1-匹配与查找

方法描述
allMatch(Predicate p)检查是否匹配所有元素
anyMatch(Predicate p) 检查是否至少匹配一个元素
noneMatch(Predicate p)检查是否没有匹配所有元素
findFirst()返回第一个元素
findAny()返回当前流中的任意元素
count()返回流中元素总数
max(Comparator c)返回流中最大值
min(Comparator c)返回流中最小值
forEach(Consumer c)内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。 相反,Stream API 使用内部迭代——它帮你把迭代做了)

需求:是否所有的员工的年龄都大于18

package com.zl.data;

import java.util.List;

public class Test01 {
    public static void main(String[] args) {
        List<Employee> list = EmployeeData.getEmployees();
        boolean b = list.stream().allMatch(emp -> emp.getAge() > 18);
        System.out.println(b); // false
    }
}

需求:是否存在员工的工资大于10000(至少有一个)

package com.zl.data;

import java.util.List;

public class Test01 {
    public static void main(String[] args) {
        List<Employee> list = EmployeeData.getEmployees();
        boolean b = list.stream().anyMatch(emp -> emp.getSalary() > 10000);
        System.out.println(b); // true
    }
}

 需求:返回第一个元素

package com.zl.data;

import java.util.List;
import java.util.Optional;

public class Test01 {
    public static void main(String[] args) {
        List<Employee> list = EmployeeData.getEmployees();
        // 返回的是一个Option
        Optional<Employee> optionalEmployee = list.stream().findFirst();
        System.out.println(optionalEmployee); // Optional[Employee{id=1001, name='马化腾', age=34, salary=6000.38}]
        // 在调用get方法就能拿到第一个值
        System.out.println(optionalEmployee.get()); // Employee{id=1001, name='马化腾', age=34, salary=6000.38}
    }
}

需求:返回流中薪资大于7000元素的总个数

package com.zl.data;

import java.util.List;

public class Test01 {
    public static void main(String[] args) {
        List<Employee> list = EmployeeData.getEmployees();
        long count = list.stream().filter(emp -> emp.getSalary() > 7000).count();
        System.out.println(count); // 3
    }
}

需求:返回最高的薪资

package com.zl.data;

import java.util.List;
import java.util.Optional;

public class Test01 {
    public static void main(String[] args) {
        List<Employee> list = EmployeeData.getEmployees();
        // 先获取返回最高工资的员工
        // 得到的是一个Option
        Optional<Employee> optionalEmployee = list.stream().max((o1, o2) -> Double.compare(o1.getSalary(), o2.getSalary()));
        System.out.println(optionalEmployee); // Optional[Employee{id=1002, name='马云', age=2, salary=19876.12}]

        // 方法1(根据上面在调用get方法,获取员工,在调用getSalary获取到薪资)
        System.out.println(list.stream().max((o1, o2) -> Double.compare(o1.getSalary(), o2.getSalary())).get().getSalary());
        // 方法2(映射)
        System.out.println(list.stream().map(emp -> emp.getSalary()).max((salgrade1, salgrade2) -> Double.compare(salgrade1, salgrade2)).get());
        // 使用方法引用
        System.out.println(list.stream().map(emp -> emp.getSalary()).max(Double::compare).get());

    }
}

注:在JDK8中增加了一个遍历集合的方法

package com.zl.data;

import java.util.List;

public class Test01 {
    public static void main(String[] args) {
        List<Employee> list = EmployeeData.getEmployees();
        // 进行遍历---使用lambda表达式
        list.forEach((emp)-> System.out.println(emp));
        // 使用方法的引用
        list.forEach(System.out::println);
    }
}

2-归约

方法描述
reduce(T identity, BinaryOperator b)可以将流中元素反复结合起来,得到一个值。返回 T
reduce(BinaryOperator b)可以将流中元素反复结合起来,得到一个值。返回 Optional<T>

需求:计算1-10的自然数的和

第一个参数是一个随机数的种子:相当于指定一个初始值,后续的累加的最终结果,都需要加上这个数值。

第二个参数BinaryOperator:实际上是继承了BiFunction类,两个参数返回一个值。

package com.zl.data;

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

public class Test01 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        // 进行求和
        // 第一种方法
        System.out.println(list.stream().reduce(0, (x1, x2) -> x1 + x2)); // 55
        // 第二种方法(调用Integer的sum方法进行累加)
        System.out.println(list.stream().reduce(0, (x1, x2) -> Integer.sum(x1,x2))); // 55
        // 第三种方法:在二的基础上就可以使用方法的引用
        System.out.println(list.stream().reduce(0, Integer::sum)); // 55
    }
}

需求:计算公司所员工的总和(先映射在规约)

package com.zl.data;

import java.util.List;

public class Test01 {
    public static void main(String[] args) {
        List<Employee> list = EmployeeData.getEmployees();
        // 先映射在规约(得到的是Option)
        // 在调用get方法得到值
        System.out.println(list.stream().map(emp -> emp.getSalary()).reduce((s1,s2)->Double.sum(s1,s2))); // Optional[58424.08]
        System.out.println(list.stream().map(emp -> emp.getSalary()).reduce(Double::sum).get()); // 58424.08

    }
}

备注:map 和 reduce 的连接通常称为 map-reduce 模式,因 Google 用它来进行网络搜索而出名。

3-收集

调用的sort方法进行排序,但是并不会影响到原来的数据,所以我们可以把排序过后的数据存储到一个集合当中;Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)。

方 法描 述
collect(Collector c)将流转换为其他形式。接收一个 Collector接口的实现, 用于给Stream中元素做汇总的方法

另外, Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例,具体方法与实例如下表:

方法返回类型作用
toListCollector<T, ?, List<T>>把流中元素收集到List
List<Employee> emps= list.stream().collect(Collectors.toList());
方法返回类型作用
toSetCollector<T, ?, Set<T>>把流中元素收集到Set
Set<Employee> emps= list.stream().collect(Collectors.toSet());
方法返回类型作用
toCollectionCollector<T, ?, C>把流中元素收集到创建的集合
Collection<Employee> emps =list.stream().collect(Collectors.toCollection(ArrayList::new));

需求:查找工资大于6000的员工,结果返回到一个List

package com.zl.data;

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


public class Test01 {
    public static void main(String[] args) {
        List<Employee> list = EmployeeData.getEmployees();
        // 进行过滤(并把数据存储到List集合)
        List<Employee> list1 = list.stream().filter(emp -> emp.getSalary() > 6000).collect(Collectors.toList());
        // 进行打印
        list1.forEach(System.out::println);
        
    }
}

3.6 新的时间和日期 API

JDK8之前:日期时间API

(1)java.lang.System类的currentTimeMills方法

①System类提供的public static long currentTimeMillis():用来返回当前时间与1970年1月1日0时0分0秒之间以毫秒为单位的时间差。

②此方法适于计算时间差。

package com.zl.data;

import java.text.SimpleDateFormat;
import java.util.Date;

public class Test01 {
    public static void main(String[] args) {
        // 获取当前时间的毫秒数
        long timeMillis = System.currentTimeMillis();
        System.out.println(timeMillis);
        // 获取昨天此时的时间
        Date date = new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000);
        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
    }
}

(2)java.util.Date

①构造器

Date():使用无参构造器创建的对象可以获取本地当前时间
Date(long 毫秒数):把该毫秒值换算成日期时间对象

②常用方法

getTime(): 返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数
toString(): 把此 Date 对象转换为以下形式的 String

例:

package com.zl.data;

import java.util.Date;

public class Test01 {
    public static void main(String[] args) {
        // 无参构造器
        Date date = new Date();
        System.out.println(date.toString()); // Mon Apr 10 19:46:50 CST 2023
        // 有参构造
        Date date1 = new Date(100L);
        System.out.println(date1.toString()); // Thu Jan 01 08:00:00 CST 1970
        // 调用getTime方法获取当前的毫秒数(和System.currentTimeMillis相同的效果)
        System.out.println(date.getTime());
        System.out.println(System.currentTimeMillis());

    }
}

③其子类java.sql.Date,对应着数据库中的Date类型,只有有参构造器

package com.zl.data;

import java.sql.Date;
import java.text.ParseException;

public class Test01 {
    public static void main(String[] args) throws ParseException {
        // java.sql.Date
        // 获取时间戳
        long timeMillis = System.currentTimeMillis(); // 1681181172832
        System.out.println(timeMillis);
        // 只有一个有参构造器
        java.sql.Date date = new Date(timeMillis);
        // 只打印年月日,但是其实也包含时分秒,只是没有显示,从下面调用getTime方法的结果与前面的时间戳相等也能体现
        System.out.println(date); // 2023-04-11
        System.out.println(date.getTime()); // 1681181172832


    }
}

(3)java.text.SimpleDateFormat

java.text.SimpleDateFormat类是一个不与语言环境有关的方式来格式化和解析日期的具体类

①可以进行格式化:日期 --> 文本

②可以进行解析:文本 --> 日期

构造器:

SimpleDateFormat() :默认的模式和语言环境创建对象
public SimpleDateFormat(String pattern):该构造方法可以用参数pattern指定的格式创建一个对象

格式化:

public String format(Date date):方法格式化时间对象date

解析:

public Date parse(String source):从给定字符串的开始解析文本,以生成一个日期

例:

package com.zl.data;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class Test01 {
    public static void main(String[] args) throws ParseException {
        // 无参构造器
        Date date = new Date();
        System.out.println(date); // Mon Apr 10 20:02:33 CST 2023
        // 指定格式化的格式
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        // 把日期格式化为字符串
        System.out.println(sdf.format(date)); // 2023-04-10 20:02:33
        // 把时间的字符串解析为日期
        System.out.println(sdf.parse("2022-4-10 6:10:10")); // Sun Apr 10 06:10:10 CST 2022

    }
}

(4)java.util.Calendar(日历)

①Date类的API大部分被废弃了,替换为Calendar(抽象类)。

②Calendar 类是一个抽象类,主用用于完成日期字段之间相互操作的功能。

获取Calendar实例

第一种方法:创建子类GregorianCalendar

第二种方法:调用Calendar的静态方法getInstance

package com.zl.data;


import java.util.Calendar;
import java.util.GregorianCalendar;

public class Test01 {
    public static void main(String[] args)  {
        // 第一种方法
        GregorianCalendar gregorianCalendar = new GregorianCalendar();
        System.out.println(gregorianCalendar);
        // 第二种方法(常用)实际上也是子类GregorianCalendar
        Calendar calendar = Calendar.getInstance();
        System.out.println(calendar.getClass()); // class java.util.GregorianCalendar
    }
}

常用的方法

public int get(int field):返回给定日历字段的值
public void set(int field,int value) :将给定的日历字段设置为指定的值
public void add(int field,int amount):根据日历的规则,为给定的日历字段添加或者减去指定的时间量
public final Date getTime():将Calendar转成Date对象
public final void setTime(Date date):使用指定的Date对象重置Calendar的时间

常用的field字段

package com.zl.data;


import java.util.Calendar;
import java.util.Date;

public class Test01 {
    public static void main(String[] args)  {
        Calendar calendar = Calendar.getInstance();
        // get方法
        System.out.println(calendar.get(Calendar.DAY_OF_WEEK)); // 3,今天是这个周的第几天
        System.out.println(calendar.get(Calendar.DAY_OF_YEAR)); // 101,今天是这个年的第几天
        // set方法
        calendar.set(Calendar.DAY_OF_WEEK,5); // 把今天是这个周的第几天设置为另一个值
        System.out.println(calendar.get(Calendar.DAY_OF_WEEK)); // 5,把今天设置为这个周的第5天
        // add方法
        calendar.add(Calendar.DAY_OF_WEEK,1);
        System.out.println(calendar.get(Calendar.DAY_OF_WEEK)); // 6,在原来的基础上增加一天
        calendar.add(Calendar.DAY_OF_WEEK,-2);
        System.out.println(calendar.get(Calendar.DAY_OF_WEEK)); // 4,在原来的基础上减少两天
        // getTime方法,从Calendar转换为Date
        System.out.println(calendar.getTime()); // Wed Apr 12 11:20:29 CST 2023
        // setTime方法,使用指定的Date重置Calendar
        Date date = new Date();
        calendar.setTime(date);
        // 其实再次获取今天是这周的第几天
        System.out.println(calendar.get(Calendar.DAY_OF_WEEK)); // 3,重置过后还是原来的第三天

    }
}

例题:

例:将一个java.util.Date的实例转换为java.sql.Date的实例

package com.zl.data;


import java.util.Date;

public class Test01 {
    public static void main(String[] args)  {
       // 很容易想到的思路,进行强转,会出现ClassCastException异常
        // 原因我们创建的就是父类java.util.Date,不能往下转为java.sql.Date(可以王当前的父类转,但是不能往当前的子类进行转)
        /*Date date = new Date();
        java.sql.Date date2 = (java.sql.Date) date; // err*/
        // 正确的做法---根据时间戳进行转换
        Date date = new Date();
        java.sql.Date date1 = new java.sql.Date(date.getTime());
    }
}

JDK8:新的日期时间API

如果我们可以跟别人说:“我们在1502643933071见面,别晚了!”那么就再简单不过了。但是我们希望时间与昼夜和四季有关,于是事情就变复杂了。JDK 1.0中包含了一个java.util.Date类,但是它的大多数方法已经在JDK 1.1引入Calendar类之后被弃用了。而Calendar并不比Date好多少。它们面临的问题是: 

①可变性:像日期和时间这样的类应该是不可变的。

②偏移性:Date中的年份是从1900开始的,而月份都从0开始。

③格式化:格式化只对Date有用,Calendar则不行。

④此外,它们也不是线程安全的;不能处理闰秒等。

Java 8 以一个新的开始为 Java 创建优秀的 API。新的日期时间API包含:

java.time – 包含值对象的基础包
java.time.chrono – 提供对不同的日历系统的访问。
java.time.format – 格式化和解析时间和日期
java.time.temporal – 包括底层框架和扩展特性
java.time.zone – 包含时区支持的类

(1)本地日期时间:LocalDate、LocalTime、LocalDateTime---类似于Calendar

获取实例对象

方法描述
now()/ now(ZoneId zone)静态方法,根据当前时间创建对象/指定时区的对象
of(xx,xx,xx,xx,xx,xxx)静态方法,根据指定日期/时间创建对象
package com.zl.data;


import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;


public class Test01 {
    public static void main(String[] args)  {
        // now():获取当前日期和时间对应的实例
        LocalDate localDate = LocalDate.now(); // 年月日
        LocalTime localTime = LocalTime.now(); // 时分秒
        LocalDateTime localDateTime = LocalDateTime.now(); // 年月日和时分秒
        System.out.println(localDate); // 2023-04-11
        System.out.println(localTime); // 14:41:45.596
        System.out.println(localDateTime); // 2023-04-11T14:41:45.596

        // of():获取指定的日期,时间对应的实例
        LocalDate localDate1 = LocalDate.of(2023, 4, 11);
        LocalDateTime localDateTime1 = LocalDateTime.of(2023, 4, 11, 14, 45, 30);
        System.out.println(localDate1); // 2023-04-11
        System.out.println(localDateTime1); // 2023-04-11T14:45:30
    }
}

常用方法:获取(get)、修改(with)、增加(plus)、减少(min)

方法描述
getDayOfMonth()/getDayOfYear()获得月份天数(1-31) /获得年份天数(1-366)
getDayOfWeek()获得星期几(返回一个 DayOfWeek 枚举值)
getMonth()获得月份, 返回一个 Month 枚举值
getMonthValue() / getYear()获得月份(1-12) /获得年份
getHours()/getMinute()/getSecond()获得当前对象对应的小时、分钟、秒
withDayOfMonth()/withDayOfYear()/withMonth()/withYear()将月份天数、年份天数、月份、年份修改为指定的值并返回新的对象
with(TemporalAdjuster t)将当前日期时间设置为校对器指定的日期时间
plusDays(), plusWeeks(), plusMonths(), plusYears(),plusHours()向当前对象添加几天、几周、几个月、几年、几小时
minusMonths() / minusWeeks()/minusDays()/minusYears()/minusHours()从当前对象减去几月、几周、几天、几年、几小时
plus(TemporalAmount t)/minus(TemporalAmount t)添加或减少一个 Duration 或 Period
isBefore()/isAfter()比较两个 LocalDate
isLeapYear()判断是否是闰年(在LocalDate类中声明)
format(DateTimeFormatter t)格式化本地日期、时间,返回一个字符串
parse(Charsequence text)将指定格式的字符串解析为日期、时间

package com.zl.data;


import java.time.LocalDateTime;

public class Test01 {
    public static void main(String[] args)  {
        LocalDateTime localDateTime = LocalDateTime.now();
        // getXxx()获取
        int dayOfMonth = localDateTime.getDayOfMonth();
        System.out.println(dayOfMonth); // 11,获取今天是这个月的第几天
        // withXxx()修改
        LocalDateTime localDateTime1 = localDateTime.withDayOfMonth(15); // 把今天修改为这个月的第15天
        System.out.println(localDateTime); // 2023-04-11T14:57:05.485,原来的并没有更改
        System.out.println(localDateTime1); // 2023-04-15T14:57:05.485,返回新的LocalDateTime对象才被更改,体现了不可变性
        // plusXxx()增加
        LocalDateTime localDateTime2 = localDateTime1.plusDays(5); // 又会生成一个新的LocalDateTime对象
        System.out.println(localDateTime2); // 2023-04-20T15:00:25.650,在原来的基础上增加了5天
        // minXxx()减少
        LocalDateTime localDateTime3 = localDateTime2.minusDays(4);
        System.out.println(localDateTime3); // 2023-04-16T15:02:58.050,在原来的基础上增加了4天
    }
}

(2)瞬时:Instant---类似于Date

①Instant:时间线上的一个瞬时点。 这可能被用来记录应用程序中的事件时间戳。 时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。

②java.time.Instant表示时间线上的一点,而不需要任何上下文信息,例如,时区。概念上讲,它只是简单的表示自1970年1月1日0时0分0秒(UTC)开始的秒数。

方法描述
now()静态方法,返回默认UTC时区的Instant类的对象
ofEpochMilli(long epochMilli)静态方法,返回在1970-01-01 00:00:00基础上加上指定毫秒数之后的Instant类的对象
atOffset(ZoneOffset offset)结合即时的偏移来创建一个 OffsetDateTime
toEpochMilli()返回1970-01-01 00:00:00到当前时间的毫秒数,即为时间戳
package com.zl.data;


import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;

public class Test01 {
    public static void main(String[] args)  {
        // 类似于Date的无参构造器
        Instant instant = Instant.now();
        System.out.println(instant); //2023-04-11T07:20:51.614Z与现在的时间差了8个小时
        OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.ofHours(8)); // 设置偏移的8个小时
        System.out.println(offsetDateTime); // 2023-04-11T15:20:51.614+08:00
        // 类似于Date的有参构造器
        Instant instant1 = Instant.ofEpochMilli(System.currentTimeMillis());
        System.out.println(instant1); // 2023-04-11T07:20:51.695Z
        // 获取当前的毫秒数
        long toEpochMilli = instant.toEpochMilli();
        System.out.println(toEpochMilli); // 1681197782827
    }
}

(3)日期时间格式化:DateTimeFormatter---类似于SimpleDateFormat

DateTimeFormatter用户格式化和解析:LocalDate、LocalTime、LocalDateTime!

自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)

ofPattern(String pattern)静态方法,返回一个指定字符串格式的DateTimeFormatter
format(TemporalAccessor t)格式化一个日期、时间,返回字符串
parse(CharSequence text)将指定格式的字符序列解析为一个日期、时间
package com.zl.data;


import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;

public class Test01 {
    public static void main(String[] args)  {
        LocalDateTime localDateTime = LocalDateTime.now();
        // 自定义格式
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        // 格式化
        String strDateTime = dtf.format(localDateTime);
        System.out.println(strDateTime); // 2023-04-11 15:44:14
        // 解析(得到的是一个TemporalAccessor接口,LocalDateTime实现了这个接口)
        TemporalAccessor temporalAccessor = dtf.parse("2023-10-10 15:30:30");
        // 在调用LocalDateTime.from方法转换为LocalDateTime
        LocalDateTime localDateTime1 = LocalDateTime.from(temporalAccessor);
        System.out.println(localDateTime1); // 2023-10-10T15:30:30

    }
}