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

目录

一: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 实用类提供了很多静态方法,可以方便地创建常见收集器实例,具体方法与实例如下表:

方法 返回类型 作用
toList Collector<T, ?, List<T>> 把流中元素收集到List
List<Employee> emps= list.stream().collect(Collectors.toList());
方法 返回类型 作用
toSet Collector<T, ?, Set<T>> 把流中元素收集到Set
Set<Employee> emps= list.stream().collect(Collectors.toSet());
方法 返回类型 作用
toCollection Collector<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

    }
}

猜你喜欢

转载自blog.csdn.net/m0_61933976/article/details/129040679