Java 8-Lambda 与 Stream 入门

408 阅读25分钟

1. 为什么要使用 lambda

1.1 前置说明

以下示例全部采用如下演示

package com.omg;

import java.util.Objects;

public class Employe {

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

    public Employe() {
    }

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

    public Employe(String name, int age) {
        this.name = name;
        this.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;
    }

    public double getSalary() {
        return salary;
    }

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

    @Override
    public String toString() {
        return "Employe{" +
                "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;
        Employe employe = (Employe) o;
        return age == employe.age &&
                Double.compare(employe.salary, salary) == 0 &&
                Objects.equals(name, employe.name);
    }

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

使用上面的Employe Class生成列表:

    List<Employe> employees = Arrays.asList(
        new Employe("张三",18,9999.99),
        new Employe("李四",39,5555.55),
        new Employe("王五",50,666.99),
        new Employe("赵六",16,6666.66),
        new Employe("田七",8,8888.88),
        new Employe("田七",40,8888.88),
        new Employe("田七",60,8888.88)
);

1.2 对比

需求:过滤出所有工资大于5000的职员 和过滤年龄大于35的职员,一下通过多种方式实现:

  • 传统函数实现
  • 策略模式 + 继承
  • 策略模式 + 匿名内部类
  • 策略模式 + lambda
  • Stream 通过比较, Stream 方式代码实现最为简介高效
package com.omg;

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

/**
 * 前言:比较 传统方式,策略模式, 匿名内部类,lambda, stream 的差异
 *
 * 需求:过滤出所有工资大于5000的职员 和过滤年龄大于35的职员
 */
public class Main {

    List<Employe> employes = Arrays.asList(
            new Employe("张三", 18,9999.99),
            new Employe("李四", 38,5555.99),
            new Employe("王五", 50,6666.66),
            new Employe("赵六", 16,3333.33),
            new Employe("田七", 10,7777.77)
    );

    /*方式1: 通过函数过滤*/
    public void filterByFunction(){
        System.out.println("工资大于5000的员工:" + filterSalary(employes));
        System.out.println("年龄大于35的员工:" + filterAge(employes));
    }

    public static List<Employe> filterSalary(List<Employe> employes){
        if (null == employes || employes.isEmpty()){
            return null;
        }

        List<Employe> result =  new ArrayList<>();
        for (Employe employe:employes){
            if (employe.getSalary() > 5000){
                result.add(employe);
            }
        }

        return result;
    }

    public static List<Employe> filterAge(List<Employe> employes){
        if (null == employes || employes.isEmpty()){
            return null;
        }

        List<Employe> result =  new ArrayList<>();
        for (Employe employe:employes){
            if (employe.getAge() > 35){
                result.add(employe);
            }
        }

        return result;
    }

    /*************************策略模式,匿名内部类,lambda 公共模块  begin***********************************/

    interface MyPredicate<T> {
        public boolean test(T t);
    }

    public static List<Employe> filterEmploye(List<Employe> employes, MyPredicate<Employe> predicate){
        if (null == employes || employes.isEmpty()){
            return null;
        }

        List<Employe> result =  new ArrayList<>();
        for (Employe employe:employes){
            if (predicate.test(employe)){
                result.add(employe);
            }
        }
        return result;
    }
    /*************************策略模式,匿名内部类,lambda 公共模块  end***********************************/

    /**
     * 方式2: 使用策略模式
     */
    class filterEmployeBySalary implements MyPredicate<Employe>{
        @Override
        public boolean test(Employe employe) {
            return employe!= null && employe.getSalary() > 5000;
        }
    }

    class filterEmployeByAge implements MyPredicate<Employe>{
        @Override
        public boolean test(Employe employe) {
            return employe != null && employe.getAge() > 35;
        }
    }


    public void filterByStrategyPattern(){
        System.out.println("工资大于5000的员工:" + filterEmploye(employes, new filterEmployeBySalary()));
        System.out.println("年龄大于35的员工:" + filterEmploye(employes, new filterEmployeByAge()));
    }

    /**
     * 方式3: 通过匿名内部类实现
     */
    public  void filterByAnonymousClasses(){
        System.out.println("工资大于5000的员工:" + filterEmploye(employes, new MyPredicate<Employe>() {
            @Override
            public boolean test(Employe employe) {
                return employe!= null && employe.getSalary() > 5000;
            }
        }));

        System.out.println("年龄大于35的员工:" + filterEmploye(employes, new MyPredicate<Employe>() {
            @Override
            public boolean test(Employe employe) {
                return employe!= null && employe.getAge() > 35;
            }
        }));
    }

    /**
     * 方式4: 通过lambda表达式
     */
    public  void filterByLambda(){
        System.out.println("工资大于5000的员工:" + filterEmploye(employes, (employe)->employe!= null && employe.getSalary()>5000));
        System.out.println("年龄大于35的员工:" + filterEmploye(employes, (employe)->employe!= null && employe.getAge() > 35));
    }

    /**
     * 方式5: 通过stream实现
     * @param args
     */
    public  void filterByStream(){
        System.out.print("工资大于5000的员工:");
        employes.stream()
                .filter((employe)->employe!= null && employe.getSalary()>5000)
                .forEach(System.out::print);
        System.out.println();
        System.out.print("年龄大于35的员工:");
        employes.stream()
                .filter((employe)->employe!= null && employe.getAge()>35)
                .forEach(System.out::print);
        System.out.println();
    }

    public static void main(String[] args) {
        Main  main = new Main();
        System.out.println("-----------function------------");
	     main.filterByFunction();
        System.out.println("-----------Strategy Pattern------------");
         main.filterByStrategyPattern();
        System.out.println("------------Anonymous Classes-----------");
         main.filterByAnonymousClasses();
        System.out.println("-------------Lambda----------");
         main.filterByLambda();
        System.out.println("------------Stream-----------");
         main.filterByStream();
    }
}

2.Lambda

2.1 基本语法

Lambda 表达式的基础语法:Java8中引入了一个新的操作符 "->" 该操作符称为箭头操作符或 Lambda 操作符箭头操作符将 Lambda 表达式拆分成两部分:

  • 左侧:Lambda 表达式的参数列表
  • 右侧:Lambda 表达式中所需执行的功能,既 Lambda 体 基本语法规则为如下几条:
  1. 无参数,无返回值;
Runnable ri = ()-> System.out.println("lambda 语法格式一:无参数,无返回值");
  1. 有一个参数,并且无返回值;
Consumer<String> consumer = (x)-> System.out.println(x);
  1. 若只有一个参数,小括号可以省略不写(第二种基础上优化写法);
 Consumer<String> consumer1 = x-> System.out.println(x);
  1. 有两个以上参数,有返回值,并且 Lambda 体重有多条语句;
Comparator<Integer> com = (x,y)-> {
    System.out.println("函数式接口");
    return Integer.compare(x, y);
};

5.若 Lambda 体中只有一条语句,return 和大括号都可以省略不写

 Comparator<Integer> com = (x,y)-> Integer.compare(x, y);
  1. Lambda 表达式的参数列表的数据类型可以省略不写,因为JVM编译器通过上下文推断出,数据类型,既"类型推断"
Comparator<Integer> com1 = (Integer x,Integer y)-> Integer.compare(x, y);
//参数类型可以省略不写
Comparator<Integer> com2 = (x,y)-> Integer.compare(x, y);

完整代码示例如下:

package com.omg.lambda;

import jdk.jfr.StackTrace;

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

/**
 *   一、Lambda 表达式的基础语法:Java8中引入了一个新的操作符 "->" 该操作符称为箭头操作符或 Lambda 操作符
 *                             箭头操作符将 Lambda 表达式拆分成两部分:
 *
 *   左侧:Lambda 表达式的参数列表
 *   右侧:Lambda 表达式中所需执行的功能,既 Lambda 体
 *
 *   语法格式一:无参数,无返回值
 *       () -> System.out.println("Hello Lambda!");
 *
 *   语法格式二:有一个参数,并且无返回值
 *       (x) -> System.out.println(x)
 *
 *   语法格式三:若只有一个参数,小括号可以省略不谢
 *       x -> System.out.println(x)
 *
 *   语法格式四:有两个以上参数,有返回值,并且 Lambda 体重有多条语句
 *       Comparator<Integer> com = (x,y)-> {
 *           System.out.println("函数式接口");
 *           return Integer.compare(x, y);
 *       };
 *
 *   语法格式五:若 Lambda 体中只有一条语句,return 和大括号都可以省略不写
 *       Comparator<Integer> com = (x,y)-> Integer.compare(x, y);
 *
 *   语法格式六:Lambda 表达式的参数列表的数据类型可以省略不写,因为JVM编译器通过上下文推断出,数据类型,既"类型推断"
 *       (Integer x,Integer y)-> Integer.compare(x, y);
 *
 *   上联:左右遇一括号省
 *   下联:左侧推断类型省
 *   横批:能省则省
 */
public class Test1Lambda {

    public static void main(String[] args) {
        /* 语法格式一:无参数,无返回值 */
        Runnable ri = ()-> System.out.println("lambda 语法格式一:无参数,无返回值");
        ri.run();

        /*语法格式二:有一个参数,并且无返回值*/
        Consumer<String> consumer = (x)-> System.out.println(x);
        consumer.accept("lambda 语法格式二:有一个参数,并且无返回值 ");

        /*语法格式三:若只有一个参数,小括号可以省略不写*/
        Consumer<String> consumer1 = x-> System.out.println(x);
        consumer1.accept("lambda 语法格式三:若只有一个参数,小括号可以省略不写");

        /*语法格式四:有两个以上参数,有返回值,并且 Lambda 体重有多条语句*/
        BiFunction<String, String, String> function = (x,y)->{
            if (null == x || x.isEmpty()){
                x = "lambda ";
            }
            if (null == y || y.isEmpty()){
                y = "语法格式四:有两个以上参数,有返回值,并且 Lambda 体重有多条语句";
            }

            return x+y;
        };
        System.out.println(function.apply(null, null));

        /*语法格式五:若 Lambda 体中只有一条语句,return 和大括号都可以省略不写*/
        Supplier<String> supplier = ()->"lambda 语法格式五:若 Lambda 体中只有一条语句,return 和大括号都可以省略不写";
        System.out.println(supplier.get());

        /*语法格式六:Lambda 表达式的参数列表的数据类型可以省略不写,因为JVM编译器通过上下文推断出,数据类型,既"类型推断"
        (Integer x,Integer y)-> Integer.compare(x, y)*/
        BiFunction<String, String, String> function1 = (String x, String y)->x+y;
        // 参数类型不写也可以,JVM编译器可以推断出类型
        BiFunction<String, String, String> function2 = (x, y)->x+y;
    }
}

2.2 方法引用

2.2.1 方法引用

Lambda 表达式体中的内容已有方法实现,则我们可以使用“方法引用”

Lambda 表达实体中调用方法的参数列表、返回类型必须和函数式接口中抽象方法保持一致. 方法引用的基本语法如下:

  1. 对象 :: 实例方法

lambda表达式为: a -> x.function(a) 可以简化为 x::function, x为提前申请好的变量

    System.out.println("lambda表达式为: a -> x.function(a) 可以简化为 x::function, x为提前申请好的变量");
    PrintStream ps = System.out;
    Consumer<String> con1 = (s) -> ps.println(s);
    con1.accept("对象::实例方法,原始写法: (s) -> ps.println(s)");
    Consumer<String> con2 = ps::println;
    con2.accept("对象::实例方法,优化写法: ps::println");

2.类 :: 静态方法

lambda表达式为: (a,b) -> ClassName.staticMethod(a, b) 可以简化为 ClassName::function

    System.out.println("lambda表达式为: (a,b) -> ClassName.staticMethod(a, b) 可以简化为 ClassName::function");
    BinaryOperator<Integer> max = (x, y) -> Integer.max(x, y);
    BinaryOperator<Integer> max2 = Integer::max;
    System.out.println("类 :: 静态方法, 原始写法: BinaryOperator<Integer> max = (x, y) -> Integer.max(x, y);");
    System.out.println("类 :: 静态方法, 优化写法: BinaryOperator<Integer> max2 = Integer::max;");

3.类 :: 实例方法

lambda 为 (x)->x.method() 可以优化为 xClassName::method

lambda 为 (x, y)->x.method(y) 可以优化为 xClassName::method

    System.out.println("类 :: 实例方法优化1, lambda 为 (x)->x.method() 可以优化为 xClassName::method");
    Function<String, String> upper = x -> x.toUpperCase();
    Function<String, String> upper2 = String::toUpperCase;
    System.out.println("类 :: 实例方法, 原始写法: Function<String, String> upper = x -> x.toUpperCase();");
    System.out.println("类 :: 实例方法, 优化写法: Function<String, String> upper2 = String::toUpperCase;");

    System.out.println("");
    System.out.println("类 :: 实例方法优化2, lambda 为 (x, y)->x.method(y) 可以优化为 xClassName::method");
    BiFunction<String, String, Boolean> compare = (x,y) -> x.equals(y);
    BiFunction<String, String, Boolean> compare2 = String::equals;
    System.out.println("类 :: 实例方法, 原始写法: BiFunction<String, String, Boolean> compare = (x,y) -> x.equals(y);");
    System.out.println("类 :: 实例方法, 优化写法: BiFunction<String, String, Boolean> compare2 = String::equals;");

2.2.2 构造方法引用

需要调用的构造器的参数列表要与函数时接口中抽象方法的参数列表保持一致 基本语法

  1. 类::构造方法, 原始写法 (x)->new Class(x), 可以有华为 Class::new;
    System.out.println("类::构造方法, 原始写法 (x)->new Class(x), 可以有华为 Class::new;");
    // 调用 ArrayList 的无参构造函数
    Supplier<List> sup1 = () -> new ArrayList();
    Supplier<List> sup2 = ArrayList::new;
    System.out.println("类::构造方法, 原始写法 () -> new ArrayList()");
    System.out.println("类::构造方法, 优化写法 ArrayList::new");

    // 调用Employe 的构造方法为 public Employe(String name, int age) {}
    BiFunction<String, Integer, Employe> biFunction = (x,y) ->new Employe(x,y);
    BiFunction<String, Integer, Employe> biFunction1 = Employe::new;
    System.out.println("类::构造方法, 原始写法 (x,y) ->new Employe(x,y)");
    System.out.println("类::构造方法, 优化写法 Employe::new");

###1.2.3 数组引用

  1. 数组引用格式 Type :: new, lambda 表达式为 x -> new Type[x] 可以优化为 Type[]::new
    System.out.println("数组引用格式  Type :: new, lambda 表达式为 x -> new Type[x] 可以优化为 Type[]::new");
    Function<Integer,String[]> func1 = length -> new String[length];
    Function<Integer,String[]> func2 = String[] :: new;

完整代码逻辑如下:

package com.omg.lambda;

import com.omg.Employe;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.*;

/**
 *  Lambda 表达式体中的内容已有方法实现,则我们可以使用“方法引用”
 *  Lambda 表达实体中调用方法的参数列表、返回类型必须和函数式接口中抽象方法保持一致
 *  函数引用语法格式:
 *      对象 :: 实例方法
 *      类 :: 静态方法
 *      类 :: 实例方法
 * 构造器引用格式:
 *      ClassName :: new
 *
 * 数组引用格式:
 *      Type :: new;
 */
public class Test2FunctionReference {
    public static void main(String[] args) {
        System.out.println("lambda表达式为: a -> x.function(a) 可以简化为 x::function, x为提前申请好的变量");
        PrintStream ps = System.out;
        Consumer<String> con1 = (s) -> ps.println(s);
        con1.accept("对象::实例方法,原始写法: (s) -> ps.println(s)");
        Consumer<String> con2 = ps::println;
        con2.accept("对象::实例方法,优化写法: ps::println");

        System.out.println("");
        System.out.println("lambda表达式为: (a,b) -> ClassName.staticMethod(a, b) 可以简化为 ClassName::function");
        BinaryOperator<Integer> max = (x, y) -> Integer.max(x, y);
        BinaryOperator<Integer> max2 = Integer::max;
        System.out.println("类 :: 静态方法, 原始写法: BinaryOperator<Integer> max = (x, y) -> Integer.max(x, y);");
        System.out.println("类 :: 静态方法, 优化写法: BinaryOperator<Integer> max2 = Integer::max;");

        System.out.println("");
        System.out.println("类 :: 实例方法优化1, lambda 为 (x)->x.method() 可以优化为 xClassName::method");
        Function<String, String> upper = x -> x.toUpperCase();
        Function<String, String> upper2 = String::toUpperCase;
        System.out.println("类 :: 实例方法, 原始写法: Function<String, String> upper = x -> x.toUpperCase();");
        System.out.println("类 :: 实例方法, 优化写法: Function<String, String> upper2 = String::toUpperCase;");

        System.out.println("");
        System.out.println("类 :: 实例方法优化2, lambda 为 (x, y)->x.method(y) 可以优化为 xClassName::method");
        BiFunction<String, String, Boolean> compare = (x,y) -> x.equals(y);
        BiFunction<String, String, Boolean> compare2 = String::equals;
        System.out.println("类 :: 实例方法, 原始写法: BiFunction<String, String, Boolean> compare = (x,y) -> x.equals(y);");
        System.out.println("类 :: 实例方法, 优化写法: BiFunction<String, String, Boolean> compare2 = String::equals;");

        System.out.println("类::构造方法, 原始写法 (x)->new Class(x), 可以有华为 Class::new;");
        // 调用 ArrayList 的无参构造函数
        Supplier<List> sup1 = () -> new ArrayList();
        Supplier<List> sup2 = ArrayList::new;
        System.out.println("类::构造方法, 原始写法 () -> new ArrayList()");
        System.out.println("类::构造方法, 优化写法 ArrayList::new");

        // 调用Employe 的构造方法为 public Employe(String name, int age) {}
        BiFunction<String, Integer, Employe> biFunction = (x,y) ->new Employe(x,y);
        BiFunction<String, Integer, Employe> biFunction1 = Employe::new;
        System.out.println("类::构造方法, 原始写法 (x,y) ->new Employe(x,y)");
        System.out.println("类::构造方法, 优化写法 Employe::new");

        System.out.println("\n");
        System.out.println("数组引用格式  Type :: new, lambda 表达式为 x -> new Type[x] 可以优化为 Type[]::new");
        Function<Integer,String[]> func1 = length -> new String[length];
        Function<Integer,String[]> func2 = String[] :: new;
    }
}

2.3. 函数接口

2.3.1 函数接口定义

  • Lambda 表达式需要“函数式接口”的支持
  • 函数式接口:接口中只有一个抽象方法的接口,称为函数式接口。
  • 可以使用注解@FunctionalInterface 修饰可以检查是否是函数式接口
  1. 标准函数式接口,只有一个方法
/**
 * 1. 标准函数式接口,只有一个方法
 */
@FunctionalInterface
interface  IsFunctionalInterface{
    void test();
}

2.如果接口中有多个函数,那么不是一个函数式接口

/**
 * 2. 这不是一个函数式接口,函数式接口只能有一个方法,如果需要多个方法是用default修饰
 * 下面代码会报错: Multiple non-overriding abstract methods found in interface com.omg.lambda.NoFunctionalInterface
 */
@FunctionalInterface
interface  NoFunctionalInterface{
    void test();
    void test2();
}
  1. 如果希望函数式接口中有多个方法,那么需要将其他方法用default修饰,并实现方法默认体,只留一个未实现的方法
@FunctionalInterface
interface  IsFunctionalInterface2{
    void test();
    default void test2(){
        System.out.println("default 修饰的方法需要实现方法体");
    }
    default void test3(){
        System.out.println("default 修饰的方法需要实现方法体");
    }
}

2.3.2 Java8 内置的四大核心函数式接口

  • Consumer{void accept(T t)} :消费型接口 1 个参数无返回值
  • Supplier{T get()} :供给型接口 不需要参数,有返回值
  • Function <T, R> {R apply(T t)}:函数型接口 一个参数一个返回值
  • Predicate {boolean test(T t)}: 断言型接口 一个参数返回 true 或 false 使用示例:
package com.omg.lambda;

import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

/**
 * Java8 内置的四大核心函数式接口
 *
 *    Consumer<T> :消费型接口 1 个参数无返回值
 *         void accept(T t);
 *
 *    Supplier<T> :供给型接口 不需要参数,有返回值
 *         T get();
 *
 *    Function <T, R> :函数型接口 一个参数一个返回值
 *         R apply(T t);
 *
 *    Predicate<T> : 断言型接口 一个参数返回 true 或 false
 *         boolean test(T t);
 */
public class Test4DefaultFunctionInterface {
    public static void main(String[] args) {
        Test4DefaultFunctionInterface.TestDefaultFunctionInterface();
    }

    /**
     * 测试默认的函数式接口
     */
    public static void TestDefaultFunctionInterface(){
        /* 消费型函数式接口  Consumer<T>{void accept(T t)} 1 个参数无返回值*/
        Consumer<String> consumer = x-> System.out.println(x);
        consumer.accept("消费型接口  Consumer<T>{void accept(T t)}, 1 个参数无返回值");

        /* 供给型接口:  Supplier<T>{T get();} 不需要参数,有返回值*/
        Supplier<String> supplier = ()->"供给型接口:  Supplier<T>{T get();}, 不需要参数,有返回值";
        System.out.println(supplier.get());

        /*函数型接口: Function <T, R>{R apply(T t);} 一个参数一个返回值*/
        Function<String, String> function = x -> x;
        System.out.println(function.apply("函数型接口: Function <T, R>{R apply(T t);}, 一个参数一个返回值"));

        /*断言型接口: Predicate<T>{boolean test(T t);} 一个参数返回 true 或 false*/
        Predicate<String> predicate = x->{
            System.out.println(x);
            return x==null;
        };
        predicate.test("断言型接口: Predicate<T>{boolean test(T t);} 一个参数返回 true 或 false");
    }
}

2.3.3 默认函数式接口的子接口

Java 提供的4个默认的函数式接口存在一定的限制,所以又提供了一些扩展接口.

  1. Consumer 子接口:
    BiConsumer<T, U>{void accept(T var1, U var2);}
  1. Function <T, R> 子接口:
    BiFunction<T, U, R>{R apply(T var1, U var2);}
    UnaryOperator<T> extends Function<T, T>{<T> UnaryOperator<T> identity()}
    BinaryOperator<T> extends BiFunction<T, T, T>{}
  1. Predicate 子接口:
    BiPredicate<T, U> {boolean test(T var1, U var2);}
  1. other
    ToIntFunction<T> {int applyAsInt(T var1);}
    ToIntBiFunction<T, U>{int applyAsInt(T var1, U var2);}
    ToLongFunction<T> {long applyAsLong(T var1);}
    ToLongBiFunction<T, U> {long applyAsLong(T var1, U var2);}
    ToDoubleFunction<T> {double applyAsDouble(T var1);}
    ToDoubleBiFunction<T, U> {double applyAsDouble(T var1, U var2);}
 
    IntFunction<R> {R apply(int var1);}
    LongFunction<R> {R apply(long var1);}
    DoubleFunction<R> {R apply(double var1);

3. Stream

Stream 是数据的渠道,用于操作数据源(list, map)所生成的元素序列.集合讲的是数据的存取,流讲的是数据的计算. Stream有入下特性:

  • Stream 自己不会存储元素
  • Stream 不会改变原对象,而是会生成一个持有结果的新的Stream
  • Stream 是延迟计算的,意味着只有等到需要结果的时候才会执行 完整流程包括三个步骤:
  • 创建流
  • 中间操作
  • 终止操作

3.1 创建Stream

  1. 可以通过 Collection 系列集合提供的 stream() 或 parallelStream()(继承 Collection 的类实现了stream()/parallelStream()方法,可以使用该方法直接创建流)
    // 方式1: 继承 Collection 的类实现了stream()方法,可以使用该方法直接创建流
    List<String> list =  new ArrayList<>();
    list.stream(); // 创建串行流
    list.parallelStream() // 创建并行流
        .forEachOrdered(System.out::println); // 串行流会导致乱序,使用该方式按照顺序遍历
  1. 通过 Arrays 中的静态方法 stream() 获取数组流
    //方式2:通过 Arrays 中的静态方法 stream() 获取数组流
    int[] ints = new int[10];
    Arrays.stream(ints);
  1. 通过 Stream 类中的静态方法 Stream.of()
    //方式3:通过 Stream 类中的静态方法
    Stream<String> stream = Stream.of("aa", "bb", "cc");
  1. 创建无限流 迭代/生成
    //方式4:创建无限流 迭代/生成
    //迭代
    Stream<Integer> iStream = Stream.iterate(0, x->x+1);
    iStream.limit(10).forEach(System.out::println);
    //生成
    Stream<Double> dStream = Stream.generate(()-> Math.random());
    dStream.limit(10).forEach(System.out::println);

3.2 Stream 中间操作

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

3.2.1 筛选与切片

  1. filter(): 接收 Lambda,从流中排除某些元素
  2. distinct(): 筛选,通过流所生成元素的 hashcode() 和 equals()去除重复元素
  3. limit(): 截断流,使起元素不超过给定数量
  4. skip(long n):跳过元素,返回一个扔掉了前 n 个元素的流. 若流中元素不足 n 个,则返回一个空流
 public void test1(){
    // 过滤掉工资低于7000的员工
    System.out.println("filter(): 接收 Lambda,从流中排除某些元素");
    employees.stream()
            .filter((e)->e.getSalary()>=7000)
            .forEach(System.out::println);

    // 去重,需要重写 hashcode 和 equals 方法
    System.out.println("distinct(): 筛选,通过流所生成元素的 hashcode() 和 equals()去除重复元素");
    employees.stream()
            .distinct()
            .forEach(System.out::println);

    // limit 只要两个元素
    System.out.println("limit(): 截断流,使起元素不超过给定数量");
    employees.stream()
            .limit(2)
            .forEach(System.out::println);

    // skip 跳过前4个元素
    System.out.println("skip(long n):跳过元素,返回一个扔掉了前 n 个元素的流. 若流中元素不足 n 个,则返回一个空流");
    employees.stream()
            .skip(4)
            .forEach(System.out::println);
}

日志输出如下:

filter(): 接收 Lambda,从流中排除某些元素
Employe{name='张三', age=18, salary=9999.99}
Employe{name='田七', age=8, salary=8888.88}
Employe{name='田七', age=8, salary=8888.88}
Employe{name='田七', age=8, salary=8888.88}
distinct(): 筛选,通过流所生成元素的 hashcode() 和 equals()去除重复元素
Employe{name='张三', age=18, salary=9999.99}
Employe{name='李四', age=39, salary=5555.55}
Employe{name='王五', age=50, salary=666.99}
Employe{name='赵六', age=16, salary=6666.66}
Employe{name='田七', age=8, salary=8888.88}
limit(): 截断流,使起元素不超过给定数量
Employe{name='张三', age=18, salary=9999.99}
Employe{name='李四', age=39, salary=5555.55}
skip(long n):跳过元素,返回一个扔掉了前 n 个元素的流. 若流中元素不足 n 个,则返回一个空流
Employe{name='田七', age=8, salary=8888.88}
Employe{name='田七', age=8, salary=8888.88}
Employe{name='田七', age=8, salary=8888.88}

3.2.2 映射

  1. map(): 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素
  2. mapToDoubel(ToDoubleFunction f): 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream
  3. mapToInt(ToIntFunction f): 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream
  4. mapToLong(ToLongFunction f): 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream
  5. flatMap(Function f): 接收一个函数作为参数, 将流中的每个元素都换成另一个流,然后把所有流链接成一个流
 public void test2(){
    // 取出所有的名字形成一个新的流
    System.out.println("map(): 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素");
    employees.stream()
            .map((e)->e.getName())
            .forEach(System.out::println);

    // 取出所有个工资
    System.out.println("mapToDoubel(ToDoubleFunction f):  接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream");
    employees.stream()
            .mapToDouble(e->e.getSalary())
            .forEach(System.out::println);

    // 取出所有名字后将姓名按照自符拆分为一个个流然后输出
    System.out.println("flatMap(Function f): 接收一个函数作为参数, 将流中的每个元素都换成另一个流,然后把所有流链接成一个流");
    employees.stream()
            .map(e->e.getName())
            .flatMap(Test7StreamIntermediateOperations::filterCharacter)
            .forEach(System.out::println);
    System.out.println("不用flatmap的对比,输出是一个个的Stream, flatMap 将多个流合并为一个流");
    employees.stream()
            .map(e->e.getName())
            .map(Test7StreamIntermediateOperations::filterCharacter)
            .forEach(System.out::println);

}

public static Stream<Character> filterCharacter(String str){
    List<Character> list = new ArrayList<>();

    for (Character ch : str.toCharArray()) {
        list.add(ch);
    }

    return list.stream();
}

日志输出如下:

map(): 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素
张三
李四
王五
赵六
田七
田七
田七
mapToDoubel(ToDoubleFunction f):  接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream
9999.99
5555.55
666.99
6666.66
8888.88
8888.88
8888.88
flatMap(Function f): 接收一个函数作为参数, 将流中的每个元素都换成另一个流,然后把所有流链接成一个流
张
三
李
四
王
五
赵
六
田
七
田
七
田
七
不用flatmap的对比,输出是一个个的Stream, flatMap 将多个流合并为一个流
java.util.stream.ReferencePipeline$Head@9f70c54
java.util.stream.ReferencePipeline$Head@234bef66
java.util.stream.ReferencePipeline$Head@737996a0
java.util.stream.ReferencePipeline$Head@61dc03ce
java.util.stream.ReferencePipeline$Head@50f8360d
java.util.stream.ReferencePipeline$Head@2cb4c3ab
java.util.stream.ReferencePipeline$Head@13c78c0b

3.2.3 排序

  1. sorted(): 产生一个新流,其中按自然顺序排序
  2. sorted(Comparator comp): 产生一个新流,其中按比较器顺序排序
public void  test3(){
    // 工资排序
    System.out.println("sorted(): 产生一个新流,其中按自然顺序排序");
    employees.stream()
            .mapToDouble(Employe::getSalary)
            .sorted()
            .forEach(System.out::println);

    // 年龄排序,并输出 employees 对象,而不是只输出年龄
    System.out.println("sorted(Comparator comp): 产生一个新流,其中按比较器顺序排序");
    employees.stream()
            .sorted((x,y)->x.getAge()-y.getAge())
            .forEach(System.out::println);
}

日志输出如下:

sorted(): 产生一个新流,其中按自然顺序排序
666.99
5555.55
6666.66
8888.88
8888.88
8888.88
9999.99
sorted(Comparator comp): 产生一个新流,其中按比较器顺序排序
Employe{name='田七', age=8, salary=8888.88}
Employe{name='田七', age=8, salary=8888.88}
Employe{name='田七', age=8, salary=8888.88}
Employe{name='赵六', age=16, salary=6666.66}
Employe{name='张三', age=18, salary=9999.99}
Employe{name='李四', age=39, salary=5555.55}
Employe{name='王五', age=50, salary=666.99}

完整代码如下:

package com.omg.lambda;

import com.omg.Employe;

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

/**
 * Stream 中间操作:多个中间操作可以链接起来形成一个流水线,除非流水线上触发终止操作
 * 否则中间操作不会执行任何处理,而在终止操作时一次性全部处理,称为惰性求职
 *
 * 筛选与切片:
 *      filter(): 接收 Lambda,从流中排除某些元素
 *      distinct(): 筛选,通过流所生成元素的 hashcode() 和 equals()去除重复元素
 *      limit(): 截断流,使起元素不超过给定数量
 *      skip(long n):跳过元素,返回一个扔掉了前 n 个元素的流. 若流中元素不足 n 个,则返回一个空流
 *
 * 映射:
 *      map(): 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素
 *      mapToDoubel(ToDoubleFunction f):  接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream
 *      mapToInt(ToIntFunction f): 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream
 *      mapToLong(ToLongFunction f): 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream
 *      flatMap(Function f): 接收一个函数作为参数, 将流中的每个元素都换成另一个流,然后把所有流链接成一个流
 * 排序:
 *      sorted(): 产生一个新流,其中按自然顺序排序
 *      sorted(Comparator comp): 产生一个新流,其中按比较器顺序排序
 */
public class Test7StreamIntermediateOperations {
    List<Employe> employees = Arrays.asList(
            new Employe("张三",18,9999.99),
            new Employe("李四",39,5555.55),
            new Employe("王五",50,666.99),
            new Employe("赵六",16,6666.66),
            new Employe("田七",8,8888.88),
            new Employe("田七",8,8888.88),
            new Employe("田七",8,8888.88)
    );

    /**
     * 筛选与切片:
     *     filter(): 接收 Lambda,从流中排除某些元素
     *     distinct(): 筛选,通过流所生成元素的 hashcode() 和 equals()去除重复元素
     *     limit(): 截断流,使起元素不超过给定数量
     *     skip(long n):跳过元素,返回一个扔掉了前 n 个元素的流. 若流中元素不足 n 个,则返回一个空流
     */
    public void test1(){
        // 过滤掉工资低于7000的员工
        System.out.println("filter(): 接收 Lambda,从流中排除某些元素");
        employees.stream()
                .filter((e)->e.getSalary()>=7000)
                .forEach(System.out::println);

        // 去重,需要重写 hashcode 和 equals 方法
        System.out.println("distinct(): 筛选,通过流所生成元素的 hashcode() 和 equals()去除重复元素");
        employees.stream()
                .distinct()
                .forEach(System.out::println);

        // limit 只要两个元素
        System.out.println("limit(): 截断流,使起元素不超过给定数量");
        employees.stream()
                .limit(2)
                .forEach(System.out::println);

        // skip 跳过前4个元素
        System.out.println("skip(long n):跳过元素,返回一个扔掉了前 n 个元素的流. 若流中元素不足 n 个,则返回一个空流");
        employees.stream()
                .skip(4)
                .forEach(System.out::println);
    }

    /**
     *映射:
     *     map(): 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素
     *     mapToDoubel(ToDoubleFunction f):  接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream
     *     mapToInt(ToIntFunction f): 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream
     *     mapToLong(ToLongFunction f): 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream
     *     flatMap(Function f): 接收一个函数作为参数, 将流中的每个元素都换成另一个流,然后把所有流链接成一个流
     */
    public void test2(){
        // 取出所有的名字形成一个新的流
        System.out.println("map(): 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素");
        employees.stream()
                .map((e)->e.getName())
                .forEach(System.out::println);

        // 取出所有个工资
        System.out.println("mapToDoubel(ToDoubleFunction f):  接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream");
        employees.stream()
                .mapToDouble(e->e.getSalary())
                .forEach(System.out::println);

        // 取出所有名字后将姓名按照自符拆分为一个个流然后输出
        System.out.println("flatMap(Function f): 接收一个函数作为参数, 将流中的每个元素都换成另一个流,然后把所有流链接成一个流");
        employees.stream()
                .map(e->e.getName())
                .flatMap(Test7StreamIntermediateOperations::filterCharacter)
                .forEach(System.out::println);
        System.out.println("不用flatmap的对比,输出是一个个的Stream, flatMap 将多个流合并为一个流");
        employees.stream()
                .map(e->e.getName())
                .map(Test7StreamIntermediateOperations::filterCharacter)
                .forEach(System.out::println);

    }

    public static Stream<Character> filterCharacter(String str){//add(Object obj) addAll(Collection coll)
        List<Character> list = new ArrayList<>();

        for (Character ch : str.toCharArray()) {
            list.add(ch);
        }

        return list.stream();
    }

    /**
     * 排序:
     *     sorted(): 产生一个新流,其中按自然顺序排序
     *     sorted(Comparator comp): 产生一个新流,其中按比较器顺序排序
     */
    public void  test3(){
        // 工资排序
        System.out.println("sorted(): 产生一个新流,其中按自然顺序排序");
        employees.stream()
                .mapToDouble(Employe::getSalary)
                .sorted()
                .forEach(System.out::println);

        // 年龄排序,并输出 employees 对象,而不是只输出年龄
        System.out.println("sorted(Comparator comp): 产生一个新流,其中按比较器顺序排序");
        employees.stream()
                .sorted((x,y)->x.getAge()-y.getAge())
                .forEach(System.out::println);
    }

    public static void main(String[] args) {
        Test7StreamIntermediateOperations test7StreamIntermediateOperations = new Test7StreamIntermediateOperations();
        test7StreamIntermediateOperations.test1();
        test7StreamIntermediateOperations.test2();
        test7StreamIntermediateOperations.test3();
    }
}

3.3 终止操作

3.3.1 查找与匹配:

  1. allMatch--检查匹配所有元素
  2. anyMatch--检查是否至少匹配一个元素
  3. noneMatch--检查是否没有匹配所有元素
  4. findAny--返回当前流中的任意元素
  5. count--返回流中的元素的总个数
  6. max--返回流中的最大值
  7. min--返回流中的最小值
private void testMax() {
    Optional<Employe> min = employees.stream().max((x, y) -> x.getAge() - y.getAge());
    System.out.println("max 获取年龄最大的员工: " + min.get().toString());
}

private void testMin() {
    Optional<Employe> min = employees.stream().min((x, y) -> x.getAge() - y.getAge());
    System.out.println("min 获取年龄最小的员工: " + min.get().toString());
}

private void testCount() {
    long count = employees.stream().count();
    System.out.println("count 统计员工数量: " + count);
}

private void testFindAny() {
    Optional<Employe> any = employees.stream().findAny();
    System.out.println("findAny 随机获取一个员工信息: " + any.get().toString());
}

private void testNoneMatch() {
    boolean noneMatch = employees.stream().noneMatch(e->e.getSalary()>10000);
    System.out.println("noneMatch 判断工资是否全部小于10000: " + noneMatch);
}

private void testAnyMatch() {
    boolean anyMatch = employees.stream()
            .anyMatch(e->e.getSalary()>5000);
    System.out.println("anyMatch 判断工资是否有大于5000的: " + anyMatch);
}

private void testAllMatch() {
    boolean allMatch = employees.stream()
            .allMatch(e->e.getSalary()>5000);
    System.out.println("allMatch 判断工资是否全部大于5000: " + allMatch);

}

日志输出如下:

allMatch 判断工资是否全部大于5000: false
anyMatch 判断工资是否有大于5000的: true
noneMatch 判断工资是否全部小于10000: true
findAny 随机获取一个员工信息: Employe{name='张三', age=18, salary=9999.99}
count 统计员工数量: 7
max 获取年龄最大的员工: Employe{name='王五', age=50, salary=666.99}
min 获取年龄最小的员工: Employe{name='田七', age=8, salary=8888.88}

3.3.2 归约

可以将流中元素反复结合,得到一个值identity起始值BinaryOperator二元运算

  1. reduce(T identity, BinaryOperator)
  2. reduce(BinaryOperator)
private void testReduce() {
    String reduce = employees.stream().map(Employe::getName).reduce("名字统计:", (x, y) -> x + y);
    System.out.println("reduce 将所有员工的名称统计:" + reduce);
    Optional<Double> reduce1 = employees.stream().map(Employe::getSalary).reduce(Double::sum);
    System.out.println("reduce 统计所有员工的工资和: " + reduce1.get());
}

日志输入如下:

reduce 将所有员工的名称统计:名字统计:张三李四王五赵六田七田七田七
reduce 统计所有员工的工资和: 49555.829999999994

3.3.3 Collector接口

Collector接口中方法的实现决定了如何对流执行收集操作(如收集到 List、Set、Map)。 但是 Collectors 实用类提供了很多静态方法,可以方便地创建常见收集齐实例

package com.omg.lambda;

import com.omg.Employe;

import java.util.*;
import java.util.stream.Collectors;

/**
 * 终止操作
 *  Collector接口:
 *     Collector接口中方法的实现决定了如何对流执行收集操作(如收集到 List、Set、Map)。
 *     但是 Collectors 实用类提供了很多静态方法,可以方便地创建常见收集齐实例
 */
public class Test9StreamCollect {
    List<Employe> employees = Arrays.asList(
            new Employe("张三",18,9999.99),
            new Employe("李四",39,5555.55),
            new Employe("王五",50,666.99),
            new Employe("赵六",16,6666.66),
            new Employe("田七",8,8888.88),
            new Employe("田七",40,8888.88),
            new Employe("田七",60,8888.88)
    );

    private void testJoin() {
        String str = employees.stream()
                .map(Employe::getName)
                .collect(Collectors.joining(",","###","###"));
        System.out.println("Collections.joining() 将所有元素连接,并加前后缀'###',中间','分割: " + str);
    }

    //多级分组
    public void testGroupingBy2(){
        Map<String, Map<String, List<Employe>>> map = employees.stream()
                .collect(Collectors.groupingBy(Employe::getName, Collectors.groupingBy((e) -> {
                    if (e.getAge() <= 35) {
                        return "青年";
                    } else if (e.getAge() <= 50) {
                        return "中年";
                    } else {
                        return "老年";
                    }
                })));
        System.out.println("Collectors.groupingBy() 可以实现多级分组");
        System.out.println("按照元素名称和年龄分组多级分组: " + map);
    }

    //分组
    public void testGroupingBy(){
        Map<String, List<Employe>> map = employees.stream()
                .collect(Collectors.groupingBy(Employe::getName));
        System.out.println("Collectors.groupingBy() 将元素分组");
        System.out.println("按照元素名称分组: " + map);
    }

    public void testPartitioningBy(){
        Map<Boolean, List<Employe>> map = employees.stream()
                .collect(Collectors.partitioningBy((e) -> e.getSalary() > 8000));
        System.out.println("Collectors.partitioningBy() 将流中元素分为两组, 满足条件的一组和不满足条件的一组");
        System.out.println("所有员工按照工资8000为界分开: " + map);
    }

    public void testSummarizingDouble(){
        DoubleSummaryStatistics dss = employees.stream()
                .collect(Collectors.summarizingDouble(Employe::getSalary));
        System.out.println("Collectors.summarizingDouble 获取统计信息: ");
        System.out.println("员工工资总和: " + dss.getSum());
        System.out.println("员工平均工资: " + dss.getAverage());
        System.out.println("员工最高工资: " + dss.getMax());
    }

    public void testSummarizing(){
        // 统计元素数量
        Long count= employees.stream().collect(Collectors.counting());
        System.out.println("Collectors.counting() 统计元素数量, 员工数量为: " + count);

        //平均值
        Double avg= employees.stream().collect(Collectors.averagingDouble(Employe::getSalary));
        System.out.println("Collectors.averagingDouble() 统计平均值,员工工资平均值为: " + avg);

        //总和
        Double sum= employees.stream().collect(Collectors.summingDouble(Employe::getSalary));
        System.out.println("Collectors.summingDouble() 统计总和, 员工工资总和为: " + sum);

        //最大值d的员工
        Optional<Employe> max = employees.stream()
                .collect(Collectors.maxBy((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())));
        System.out.println("Collectors.maxBy() 获取最大值, 工资最高的员工为: " + max.get());

        //最小值
        Optional<Double> min = employees.stream().map(Employe::getSalary).collect(Collectors.minBy(Double::compare));
        System.out.println("Collectors.minBy() 获取最小值, 员工中的最低工资为: " + min.get());
    }

    public void testToCollect(){
        List<String> list = employees.stream().map(Employe::getName).collect(Collectors.toList());
        System.out.println("Collectors.toList() 将流转换为list: " + list);

        Set<String> set = employees.stream().map(Employe::getName).collect(Collectors.toSet());
        System.out.println("Collectors.toSet() 将流转换为set: " + set);

        HashSet<String> hashSet = employees.stream().map(Employe::getName).collect(Collectors.toCollection(HashSet::new));
        System.out.println("Collectors.toCollection() 指定将流转化为的 Collect 类型,本例转换为HashSet: " + hashSet);
    }

    public static void main(String[] args) {
        Test9StreamCollect test9StreamCollect = new Test9StreamCollect();
        test9StreamCollect.testJoin();
        test9StreamCollect.testSummarizingDouble();
        test9StreamCollect.testPartitioningBy();
        test9StreamCollect.testGroupingBy();
        test9StreamCollect.testGroupingBy2();
        test9StreamCollect.testSummarizing();
        test9StreamCollect.testToCollect();
    }
}

日志输出如下:

Collections.joining() 将所有元素连接,并加前后缀'###',中间','分割: ###张三,李四,王五,赵六,田七,田七,田七###
Collectors.summarizingDouble 获取统计信息: 
员工工资总和: 49555.83
员工平均工资: 7079.404285714286
员工最高工资: 9999.99
Collectors.partitioningBy() 将流中元素分为两组, 满足条件的一组和不满足条件的一组
所有员工按照工资8000为界分开: {false=[Employe{name='李四', age=39, salary=5555.55}, Employe{name='王五', age=50, salary=666.99}, Employe{name='赵六', age=16, salary=6666.66}], true=[Employe{name='张三', age=18, salary=9999.99}, Employe{name='田七', age=8, salary=8888.88}, Employe{name='田七', age=40, salary=8888.88}, Employe{name='田七', age=60, salary=8888.88}]}
Collectors.groupingBy() 将元素分组
按照元素名称分组: {李四=[Employe{name='李四', age=39, salary=5555.55}], 张三=[Employe{name='张三', age=18, salary=9999.99}], 王五=[Employe{name='王五', age=50, salary=666.99}], 赵六=[Employe{name='赵六', age=16, salary=6666.66}], 田七=[Employe{name='田七', age=8, salary=8888.88}, Employe{name='田七', age=40, salary=8888.88}, Employe{name='田七', age=60, salary=8888.88}]}
Collectors.groupingBy() 可以实现多级分组
按照元素名称和年龄分组多级分组: {李四={中年=[Employe{name='李四', age=39, salary=5555.55}]}, 张三={青年=[Employe{name='张三', age=18, salary=9999.99}]}, 王五={中年=[Employe{name='王五', age=50, salary=666.99}]}, 赵六={青年=[Employe{name='赵六', age=16, salary=6666.66}]}, 田七={青年=[Employe{name='田七', age=8, salary=8888.88}], 老年=[Employe{name='田七', age=60, salary=8888.88}], 中年=[Employe{name='田七', age=40, salary=8888.88}]}}
Collectors.counting() 统计元素数量, 员工数量为: 7
Collectors.averagingDouble() 统计平均值,员工工资平均值为: 7079.404285714286
Collectors.summingDouble() 统计总和, 员工工资总和为: 49555.83
Collectors.maxBy() 获取最大值, 工资最高的员工为: Employe{name='张三', age=18, salary=9999.99}
Collectors.minBy() 获取最小值, 员工中的最低工资为: 666.99
Collectors.toList() 将流转换为list: [张三, 李四, 王五, 赵六, 田七, 田七, 田七]
Collectors.toSet() 将流转换为set: [李四, 张三, 王五, 赵六, 田七]
Collectors.toCollection() 指定将流转化为的 Collect 类型,本例转换为HashSet: [李四, 张三, 王五, 赵六, 田七]