全栈开发那些事

全栈开发那些事

Lambda表达式与Stream API

77
2024-06-25
Lambda表达式与Stream API

Java8最具革命性的两个新特性是Lambda表达式和Stream API,它们都是基于函数式编程的思想,函数式编程给Java注入了新鲜的活力。

1、Lambda表达式

1.1 Lambda表达式语法

Lambda表达式是一个匿名函数,可以理解其为一段可以传递的代码。Lambda语法将代码像数据一样传递,可以代替大部分匿名内部类,使用它可以写出更简洁、更灵活的代码。Lambda表达式作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。

语法格式如下:

(参数列表)->{Lambda体}

案例需求:创建一个线程类对象,该线程可以完成打印“codeleader”。

面向对象编程方式,使用匿名内部类的语法实现。

public class TestRunnable {
    public static void main(String[] args) {
        new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("codeleader");
                    }
                }
        ).start();
    }
}

面向函数编程方式,使用Lambda语法实现。

public class TestLambda {
    public static void main(String[] args) {
        new Thread(()-> System.out.println("codeleader")).start();
    }
}

image-20221001205519063

上述代码可以简化,简化依据有如下两点:

  • Thread接口中要求的参数类型只能是Runnable类型,因此new Runnable(){}可以省略。
  • Runnable接口中只有一个抽象方法--run方法,因此run方法的声明语句可以省略,只保留run方法的核心语句:参数列表和方法体。

实际上,JDK8的核心思想就是力争去除形式化的东西,保留实质的内容。

Java8中引入了一种新的语法元素和操作符\to,该操作符称为Lambda操作符或箭头操作符,它将Lambda表达式分为一下两个部分。

  • 左侧:指定了Lambda参数列表,是函数的参数列表。
  • 右侧:指定了Lambda体,是函数的功能体,即Lambda表达式要执行的功能。

从上面的语法中可以看出,Lambda表达式代表的是一个函数,在Java中称为方法。在上面的案例中,Lambda表达式是作为Runnable接口的实例出现的,用于简化使用匿名内部类来实现接口的形式,因此Lambda表达式代表的函数就是所实现接口的抽象方法。也就是说,Lambda表达式的参数列表就是所实现接口的抽象方法的参数列表,Lambda体就是实现抽象方法的方法体。

1.2 案例:实现Comparator接口

案例需求:现有一个Integer[]数组,请使用Arrays.sort方法实现对数组中元素从大到小排序。

面向对象方式,使用匿名内部类的语法实现:

public class TestComparator {
    public static void main(String[] args) {
        Integer[] arr={1,3,4,6,2};
        Arrays.sort(arr, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return -Integer.compare(o1,o2);
            }
        });
        System.out.println(Arrays.toString(arr));
    }
}

image-20221001210714956

面向函数编程方式,使用Lambda表达式的语法实现:

public class TestComparatorLambda1 {
    public static void main(String[] args) {
        Integer[] arr={1,3,4,6,2};
        Arrays.sort(arr, (Integer o1,Integer o2) -> {return Integer.compare(o2,o1);});
        System.out.println(Arrays.toString(arr));
    }
}

1.3 类型推断

上述Lambda表达式中的参数类型都可以由编译器推断得出。Lambda表达式中无需指定类型,程序依然可以编译,这是因为javac根据程序的上下文,在后台推断出了参数的类型。Lambda表达式的类型依赖上下文环境,是由编译器推断出来的,这就是所谓的类型推断。

例如,JDK7在使用泛型时支持以下写法。

List<String> list=new ArrayList<>();//右侧<>中泛型类型可以不指定

示例代码:

public static void main(String[] args){
 method(new HashMap<>());//实参<>中泛型类型可以不指定。
}
public static void method(Map<String,Integer> map){
}

在JDK8中,java将类型推断思想应用得更加淋漓尽致。在JDK8中,类型推断不仅可以用于赋值语句,还可以根据代码中上下文中的信息推断出更多的信息,因此我们需要写的代码会更少。加强类型推断还有一个应用场景就是Lambda表达式。

1.4 Lambda类型推断

Lambda表达式在通常情况下还可以再简化,简化时主要基于以下几个原则。

  • 根据类型推断思想,左侧参数列表中的参数类型可以省略。
  • 左侧参数列表如果仅有一个参数且在参数数据类型省略的情况下,则左侧小括号可以省略。
  • 右侧Lambda体中如果仅有一句话,则右侧大括号与语句结束符;可以省略。如果仅有一句话为return语句,则return关键字也可以省略。

根据以上规则,1.2中的案例还可以简化为如下形式:

public class TestComparatorLambda2 {
    public static void main(String[] args) {
        Integer[] arr={1,3,4,6,2};
        Arrays.sort(arr, (o1,o2) -> Integer.compare(o2,o1));
        System.out.println(Arrays.toString(arr));
    }
}

image-20221001214743906

2、函数式接口

2.1 函数式接口的概念

前面两个案例中的Lambda表达式都是作为接口实现类的实例出现的,但并不是所有的接口实现都可以使用Lambda表达式。能使用Lambda表达式的接口要求只有一个抽象方法。我们把只含一个抽象方法的接口称为函数式接口

Java建议在一个函数式接口声明上方使用@FunctionalInterface注解,这样做可以明确它是一个函数式接口。

简单地说,Java8中Lambda表达式就是一个函数式接口的实例,这就是Lambda表达式和函数式接口的关系。也就是说,只要一个对象是函数式接口的实例,那么该对象就可以用Lambda表达式来表示,以前用匿名内部类表示的现在大多可以用Lambda表达式来写

Java8中java.lang.Runnable接口的声明上加了@FunctionalInterface注解,源码如下所示:

image-20221002111807966

Java8除了给之前满足函数式接口定义的接口加了@FunctionalInterface注解,并且给建议使用Lambda表达式进行赋值的接口也加了@FunctionalInterface注解,如java.lang.Runnable接口、java.util.Comparator<T>接口等。Java8还在java.util.function包下定义了更丰富的函数式接口供我们使用,java内置函数式接口如下表所示。

函数式接口参数类型返回类型用途
Consumer消费型接口Tvoid对类型为T的对象应用操作,包含方法void accept(T t)
Supplier供给型接口T返回类型为T的对象,包含方法T get()
Function<T,R>函数型接口TR对类型为T的对象应用操作,并返回结果,结果是R类型的对象,包含方法R apply(T t)
Predicate断定型接口Tboolean确定类型为T的对象是否满足某约束,并返回boolean值,包含方法boolean test(T t)
BiFunction<T,U,R>T,UR对类型为T,U参数应用操作,返回R类型的结果,包含方法R apply(T t,U u)
UnaryOperator(Finction子接口)TT对类型为T的对象进行一元运算,并返回T类型的结果,包含方法T apply(T t)
BinaryOperator(BiFunction子接口)T,TT对类型为T的对象进行二元运算,并返回T类型的结果,包含方法T apply(T t1,T t2)
BiConsumer<T,U>T,Uvoid对类型为T,U参数应用操作,包含方法void accept(T t,U u)
BiPredicate<T,U>T,Uboolean包含方法boolean test(T t,U u)
ToIntFunction
ToLongFunction
ToDoubleFunction
Tint
long
double
分别计算int、long、double值得函数
IntFunction
LongFunction
DoubleFunction
int
long
double
R参数分别为int、long、double类型的函数

表中前四个为核心的函数式接口,其余都是它们的变形。因此,java.util.function包下的函数式接口只要分为以下四大类:

(1)消费型接口:其抽象方法有参无返回值,用“有去无回”的纯消费行为比喻。

Consumer<Double> con=t->{
	if(t>1000){
		System.out.println("去看大海");
	}else if(t>500){
		System.out.println("去看小河");
	}else{
		System.out.println("来北京看雨");
	}
}

(2)供给型接口:其抽象方法无参有返回值,用“无私奉献”的行为比喻。

Supplier<String> sup=()->"hello";

(3)函数型接口:其抽象方法有参有返回值,参数类型与返回值类型可以不一致,也成为功能型接口。

Function<String,Character> fun=s->s.charAt(0);

(4)断定型接口:其抽象方法有参有返回值,但返回值类型是boolean,在Lambda体中

是对传入的参数做条件判断,并返回判断结果。

Predicate<Employee> pre=e->e.getGender() =='男;

在Java8中,原来java.util包中的集合API也得到了大量的改进,在很多接口中增加了静态方法和默认方法,并且这些静态方法和默认方法的形参类型也使用了函数式接口。

2.2 案例:消费型接口

之前遍历Collection系列的集合时,使用的是foreach遍历或Iterator<T>迭代器遍历,现在可以使用Java8中新增的forEach(Consumer<? super E> action)方法进行遍历。

案例需求:将一些字符串添加到ArrayList集合,并且要求使用forEach方法遍历显示它们。

public class ConsumerTest {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("codeleader");
        list.add("新一");
        list.add("ale");
        list.add("阿乐");
        list.add("LBL");
        list.add("LSBL");
        list.forEach(e-> System.out.println(e));
    }
}

image-20221002114541884

2.3 案例:断定型接口

Java8给Collection新增了removeIf(Predicate<? super E> filter),系统可以根据指定条件进行元素删除。

案例需求:将一些字符串添加到ArrayList集合,现在要删除它们当中包含数字字符的字符串。

public class PredicateTest {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("codeleader");
        list.add("java");
        list.add("bigdata");
        list.add("spring");
        list.add("springboot2");
        list.add("springcloud");
        list.removeIf(str->str.matches("^.*\\d.*$"));
        list.forEach(e-> System.out.println(e));
    }
}

image-20221002114746567

2.4 功能型接口

Java8给Map接口新增了方法:replaceAll(BiFunction<? super K,? super V,? extends V> function)forEach(BiConsumer<? super K,? super V> action)等。其中BiFunction是Function接口的扩展变形。

案例需求:将员工姓名和薪资作为键值对添加到Map。现在要查看所有员工的情况,如果他的薪资低于1000元,则给他涨薪20%。

public class FunctionTest {
    public static void main(String[] args) {
        HashMap<String, Double> map = new HashMap<>();
        map.put("张三",8000.0);
        map.put("李四",12000.0);
        map.put("王五",9655.5);
        map.replaceAll((Key,value)->value<10000.0?value*1.2:value);
        map.forEach((key,value)-> System.out.println(key+"->"+value));
    }
}

image-20221002120047838

2.5 案例:员工信息管理

案例需求:

针对员工的集合数据,当有如下的需求时,我们考虑应如何完成?

(1)获取所有员工信息。

(2)获取年龄大于30岁的员工信息。

(3)获取工资大于5000元的员工信息。

(4)获取所有男员工的信息。

(5)获取所有年龄超过20岁的女员工信息。

员工类代码:

public class Employee {
    private int id;
    private String name;
    private int age;
    private char gender;
    private double salary;

    public Employee(int id, String name, int age, char gender, double salary) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.gender = gender;
        this.salary = 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 char getGender() {
        return gender;
    }

    public void setGender(char gender) {
        this.gender = gender;
    }

    public double getSalary() {
        return salary;
    }

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

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

员工数据管理类代码:

public class EmployeeData {
    private static List<Employee> list = new ArrayList<>();
    static{
        list.add(new Employee(1,"段誉",29,'男',20000));
        list.add(new Employee(2,"乔峰",39,'男',80000));
        list.add(new Employee(3,"虚竹",29,'男',30000));
        list.add(new Employee(4,"王语嫣",19,'女',29000));
        list.add(new Employee(5,"阿朱",18,'女',25000));
        list.add(new Employee(6,"阿紫",17,'女',12000));
        list.add(new Employee(7,"阿碧",22,'女',10000));
        list.add(new Employee(4,"王语嫣",19,'女',29000));
        list.add(new Employee(5,"阿朱",18,'女',25000));
        list.add(new Employee(4,"王语嫣",19,'女',29000));
    }

    public static List<Employee> filter(Predicate<Employee> filter){
        ArrayList<Employee> datas = new ArrayList<>();//新集合用于保存过滤后的员工信息
        for (Employee employee : list) {
            if(filter.test(employee)){//如果满足条件,则添加到新集合
                datas.add(employee);
            }
        }
        return datas;
    }

    public static List<Employee> getEmployees(){
        return list;
    }
}

测试类代码:

public class EmployeeTest {
    public static void main(String[] args) {
        System.out.println("所有员工信息:");
        EmployeeData.getEmployees().forEach(e-> System.out.println(e));
        System.out.println("年龄大于30岁的员工信息:");
        EmployeeData.filter(e->e.getAge()>30).forEach(e-> System.out.println(e));

        System.out.println("工资大于5000元的员工信息:");
        EmployeeData.filter(e->e.getSalary()>15000).forEach(e-> System.out.println(e));

        System.out.println("所有男员工的信息:");
        EmployeeData.filter(e->e.getGender()=='男').forEach(e-> System.out.println(e));

        System.out.println("所有年龄超过20岁的女员工信息:");
        EmployeeData.filter(e->e.getGender()=='女'&&e.getAge()>20).forEach(e-> System.out.println(e));
    }
}

image-20221002120740886

3、Lambda表达式再简化

当Lambda表达式形式满足一些特殊情况时,还可以对Lambda表达式进行再次简化,这也可以体现Java8支持更简化语法的原则。

(1)能用Lambda表达式的地方,肯定能用匿名内部类。但能用匿名内部类的地方,不一定能用Lambda表达式,只有匿名内部类实现的接口才可以用Lambda表达式。

(2)能用方法引用、数组引用或构造器引用的地方,肯定能用Lambda表达式。但能用Lambda表达式的地方,不一定能用方法引用、数组引用或构造器引用,必须满足对应的要求。

3.1 方法引用

方法引用也是Lambda表达式,就是通过方法的名字来指向一个方法,可以认为它是Lambda表达式的一个语法糖。当要传递给Lambda体的操作是调用一个现有的方法来实现时,就可以使用方法引用。

方法引用的语法格式有如下三种:

对象::实例方法名
类::静态方法名
类::实例方法名

方法引用使用操作符"::"将类名/对象名与方法名区分开。

3.1.1 对象::实例方法名

案例需求:将1、3、4、8、9添加到List集合,并使用forEach方法遍历显示它们。

public class FunctionReferenceTest1 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 3, 4, 8, 9);
        //Lambda表达式基本形式
        list.forEach(e-> System.out.println(e));
        System.out.println("------------");
        //方法应用简化形式
        list.forEach(System.out::println);
    }
}

image-20221002121436419

3.1.2 类名::静态方法名

案例需求:将“张三、李四、王五、codeleader”添加到List集合,使用文本校对器按照中国语言的自然顺序对它们进行排序。

public class FunctionReferenceTest2 {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("张三", "李四", "王五", "codeleader");
        //Lambda表达式基本形式
        Collections.sort(list,((o1, o2) -> Collator.getInstance(Locale.CHINA).compare(o1,o2)));

        //方法引用简化形式
        Collections.sort(list,Collator.getInstance(Locale.CHINA)::compare);
        list.forEach(System.out::println);
    }
}

image-20221002121611244

3.1.3 类名::实例方法名

案例需求:将“codeleader、java、hello、html5”添加到List集合,并按照不分大小写的方式对它们进行升序排序。

public class FunctionReferenceTest3 {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("codeleader", "java", "hello", "html5");
        Collections.sort(list,String::compareToIgnoreCase);
        list.forEach(System.out::println);
    }
}

image-20221002121839474

从上面的示例代码可以看出,方法应用的实质是省略了参数。那为什么方法引用可以这样做呢?同样,这也是通过类型推断得出的,当然有个前提,就是Lambda体中调用的方法和实现的函数式接口抽象方法的参数列表一致。

总结,当Lambda表达式满足以下三个要求时,才能使用方法引用进行简化。

  • Lambda体中只有一句话。
  • Lambda体中只有这句话为方法调用。
  • 调用的方法参数列表和返回类型与接口中抽象方法的参数列表和返回类型完全一致。

如果是类名::普通方法,则需要满足调用方法的调用者必须是抽象方法的第一个参数。调用方法的参数列表和抽象方法的其他参数一致。

3.2 构造器引用

与方法引用类似,Lambda体中如果引用的是一个构造器,且参数列表和抽象方法的参数列表一致,则可以使用构造器引用。当Lambda表达式满足如下三个要求时,就可以使用构造器引用来进行简化。

  • Lambda体中只有一个语句。
  • 仅有的这个语句还是一个通过new 调用构造器的return语句。
  • 抽象方法的参数列表和调用的构造器参数列表完全一致,并且抽象方法返回的正好是通过构造器创建的对象。

构造器引用的语法格式如下所示:

类名::new

Java8在java.util包中增加了一个工具类Optional<T>,这个类中有一个方法:T orElseGet(Supplier<? extends T> other)。该方法的作用是返回Optional对象中包含的值,如果该值为null,则用Supplier的get方法返回值代替。

public class ConstructorReferenceTest1 {
    public static void main(String[] args) {
        Optional<String> opt = Optional.ofNullable("codeleader");
//        Optional<String> opt = Optional.ofNullable(null);

        String value = opt.orElseGet(String::new);
        System.out.println("value="+value);
    }
}

image-20221002122917489

工具类Optional<T>中还有一个方法:Optional<U> map(Function<? super T,? extends U> mapper)。该方法的作用是将原来的Optional<T>对象映射成Optional<U>对象。

public class ConstructorReferenceTest2 {
    public static void main(String[] args) {
        Optional<String> opt1 = Optional.of("123");
        Optional<Integer> opt2 = opt1.map(Integer::new);
        System.out.println("opt2="+opt2);
    }
}

image-20221002123225848

3.3 数组构造引用

与方法引用类似,Lambda体中如果是通过new关键字创建数组,且数组的长度正好是抽象方法的实参,抽象方法返回的正好是该新数组对象,则可以使用数组引用。当Lambda表达式满足如下三个要求时,就可以使用数组构造引用来进行简化。

(1)Lambda体中只有一句话。

(2)只有的这句话为创建一个数组。

(3)抽象方法的参数列表和新数组的长度一致,并且抽象方法的返回正好为该新数组对象。

数组引用的语法格式如下所示:

元素类型[] ::new 

案例需求:现在有一个创建长度为2^{n} 的数组的方法。

public class MyArrays {
    public static <R> R[] createArray(Function<Integer,R[]> fun,int length){
        int n=length-1;
        n|=n>>>1;
        n|=n>>>2;
        n|=n>>>4;
        n|=n>>>8;
        n|=n>>>16;
        length=n<0?1:n+1;
        return fun.apply(length);
    }

    public static void main(String[] args) {
        String[] array = MyArrays.createArray(String[]::new, 6);
        System.out.println(array.length);
    }
}

image-20221002123904484

4、强大的Stream API

Java8有两个最为重要的改变,一个是Lambda表达式,另一个是Stream API。

Stream API不是用来处理IO的,而是用于处理集合的。使用Stream API对集合进行操作,就类似于使用SQL执行的数据库查询。

Stream的使用步骤如下所示:

  • 开始操作,根据一个数据源,如集合、数组等,获取一个Stream流。
  • 中间操作,对Stream流中的数据进行处理。
  • 终止操作,获取或查看最终效果。

Stream的特点有如下几点:

(1)Stream讲究的是计算,可以处理数据,但不能更新数据。

(2)Stream更新后可以有零个或多个操作处理数据,每次处理都会返回一个新的Stream,这些操作称为中间操作。

(3)Stream属于惰性操作,必须等终止操作执行后,前面的中间操作或开始操作才会处理。

(4)Stream只能终结一次,一旦终结,就不能再次使用,除非重新创建Stream对象,因为终结操作后返回值就不再是Stream类型了。

(5)Stream相当于一个更强大的Iterator,可以处理更加复杂的数据,并且实现并行化,效率更高。

4.1 创建Stream对象

Java8引入了Stream之后,原来的集合、数组工具类等都增加了创建Stream对象的方法。

4.1.1 基于集合对象来创建Stream

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

  • default Stream<E> stream():返回一个顺序流,又称为串行流,即将所有内容从头到尾依次遍历,属于单线程操作。
  • default Stream<E> parallelStream():返回一个并行流,即把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。

Java8将并行进行了优化,便于我们对数据进行并行操作。Stream API可以声明性地通过parallel()sequential()在并行流与顺序流之间进行切换。

public class CreateStreamTest1 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("codeleader");
        list.add("新一");
        list.add("我只要一点火花就行");
        //通过集合创建stream
        Stream<String> stream = list.stream();
        stream.forEach(System.out::println);
    }
}

image-20221002173936009

4.1.2 基于数组创建Stream

java8在Arrays数组工具类中增加了stream(数组)方法来创建Stream。

  • static <T> Stream<T> stream(T[] array):返回一个Stream流。重载形式,能够处理对应基本数据类型的数组。
  • public static IntStream stream(int[] array):返回一个IntStream流。
  • public static LongStream stream(long[] array):返回一个LongStream流。
  • public static DoubleStream stream(double[] array):返回一个DoubleStream流。
public class CreateStreamTest2 {
    public static void main(String[] args) {
        String[] arr = new java.lang.String[]{"乔峰", "段誉", "虚竹"};
        Stream<String> stream = Arrays.stream(arr);
        stream.forEach(System.out::println);
    }
}

image-20221002174248571

4.1.3 直接通过Stream的of静态方法来创建Stream

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

public class CreateStreamTest3 {
    public static void main(String[] args) {
        Stream<Integer> stream = Stream.of(1, 3, 5, 7, 9);
        stream.forEach(System.out::println);
    }
}

image-20221002174359883

4.1.4 直接通过Stream的generate和iterate静态方法来创建Stream

  • public static<T> Stream<T> iterate(final T seed,final UnaryOperator<T> f):创建无限流。
  • public static<T> Stream<T> generate(Supplier<T> s):创建无限流。

generate方法示例代码:

public class CreateStreamTest4 {
    public static void main(String[] args) {
        Stream<Double> stream = Stream.generate(Math::random);
        stream.forEach(System.out::println);
    }
}

image-20221002174625875

iterate方法示例代码:

public class CreateStreamTest5 {
    public static void main(String[] args) {
        Stream<Integer> stream2 = Stream.iterate(1,t->t+2);
        stream2.forEach(System.out::println);
    }
}

image-20221002174657131

4.2 Stream中间操作

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

案例需求:

员工对象的管理类EmployeeData中有一个list,它存储了一组员工对象,该类有一个getEmployees方法可以获取该list中所有的员工对象。现要求使用Stream API对该集合中的数据进行各种处理。

员工类(Employee)代码:

public class Employee implements Comparable<Employee> {
    private int id;
    private String name;
    private int age;
    private char gender;
    private double salary;

    public Employee(int id, String name, int age, char gender, double salary) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.gender = gender;
        this.salary = 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 char getGender() {
        return gender;
    }

    public void setGender(char gender) {
        this.gender = gender;
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.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 &&
                gender == employee.gender &&
                Double.compare(employee.salary, salary) == 0 &&
                Objects.equals(name, employee.name);
    }

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

    @Override
    public int compareTo(Employee o) {
        return Integer.compare(id,o.id);
    }

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

员工对象的管理类示例代码:

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

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

    static {
        //包含员工编号、姓名、年龄、性别、薪资
        list.add(new Employee(1,"段誉",29,'男',20000));
        list.add(new Employee(2,"乔峰",39,'男',80000));
        list.add(new Employee(3,"虚竹",29,'男',30000));
        list.add(new Employee(4,"王语嫣",19,'女',29000));
        list.add(new Employee(5,"阿朱",18,'女',25000));
        list.add(new Employee(6,"阿紫",17,'女',12000));
        list.add(new Employee(7,"阿碧",22,'女',10000));
        list.add(new Employee(4,"王语嫣",19,'女',29000));
        list.add(new Employee(5,"阿朱",18,'女',25000));
        list.add(new Employee(4,"王语嫣",19,'女',29000));
    }
    public static List<Employee> getEmployees(){
        return list;
    }
}

4.2.1 筛选与切片

筛选与切片常见的方法如下表。

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

筛选出年龄大于20岁的员工信息的示例代码:

EmployeeData.getEmployees()
        .stream()
        .filter(t->t.getAge()>20)
        .forEach(System.out::println);

image-20221002175532936

获取去重后的员工信息:

EmployeeData.getEmployees()
        .stream()
        .distinct()
        .forEach(System.out::println);

image-20221002175613348

获取前5条员工数据:

EmployeeData.getEmployees()
        .stream()
        .limit(5)
        .forEach(System.out::println);

image-20221002175634463

获取从第11条之后的员工数据:

EmployeeData.getEmployees()
        .stream()
        .skip(10)
        .forEach(System.out::println);

image-20221002175659319

截取工资大于5000元的第3条到第6条员工记录:

EmployeeData.getEmployees()
        .stream()
        .filter(t->t.getSalary()>5000)
        .skip(2)
        .limit(4)
        .forEach(System.out::println);

image-20221002175815235

==提示:每个中间操作不会执行任何处理,而在终止操作时一次性全部处理,所以为了看到结果后面必须有终止Stream的方法,如forEach就是其中一个终止Stream的方法。==

4.2.2 映射

映射常见的方法如下:

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

(1)map方法:将每个元素映射成一个新类型的元素,一一对应。

Stream.of("codeleader","ale")
        .map(str->str.charAt(0))
        .forEach(System.out::println);

image-20221002180407644

(2)flatMap方法:将每个元素映射成一个流,然后把所有流连接成一个流。

Stream.of("codeleader","ale")
        .flatMap(str->Stream.of(str.split("|")))
        .forEach(System.out::println);

image-20221002180515505

4.2.3 排序

排序常见的方法如下:

方法排序
sorted()按自然排序产生一个新流,要求元素实现Comparator接口
sorted(Comparator com)按定制比较器(Comparator)排序产生一个新流
EmployeeData.getEmployees()
    .stream()
    .sorted()
    .forEach(System.out::println);

image-20221002180726253

EmployeeData.getEmployees()
    .stream()
    .sorted(Comparator.comparingDouble(Employee::getSalary))
    .forEach(System.out::println);

4.3 终止Stream操作

终止操作会从Stream的流水线操作生成最终结果,其结果可以是任何不是流的值,如List、Integer,甚至可以是void。Stream进行了终止操作后,不能再次使用。

4.3.1 统计和迭代

统计和迭代的常见方法如下:

方法描述
count()返回流中元素个数
max(Comparator c)返回流中的最大值
min(Comparator c)返回流中最小值
forEach(Consumer c)内部迭代(使用Collection接口需要用户去做迭代,称为外部迭代。Stream API使用内部迭代,它已经把迭代做了)

以下代码演示的list统一是该代码获取的list

List<Employee> list = EmployeeData.getEmployees();

image-20221002181847551

获取性别为男员工个数的代码:

//获取性别为男员工个数
long count = list.stream().filter(t -> t.getGender() == '男').count();
System.out.println("count="+count);

image-20221002181928962

获取年龄最大的员工对象:

//获取年龄最大的员工对象
Optional<Employee> max = list.stream().max((e1, e2) -> Integer.compare(e1.getAge(), e2.getAge()));
System.out.println("max="+max);

image-20221002182034408

获取工资最少的员工对象:

//获取工资最少的员工对象
Optional<Employee> min = list.stream().min((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()));
System.out.println("min="+min);

image-20221002182112063

遍历流中所有员工,并将其打印:

list.forEach(System.out::println);

image-20221002182210485

4.3.2 规约

规约需要将流中的每个元素串接起来。常见的方法如下:

方法描述
reduce(T iden,BinaryOperator b)可以将流中元素反复结合起来,得到一个值,返回T
reduce(BinaryOperator b)可以将流中元素反复结合起来,得到一个值,返回Optional<T>
System.out.println(Stream.of(1,2,3,4,5).reduce(0,(a,b)->a+b));
System.out.println(Stream.of(1,2,3,4,5).reduce((a,b)->a+b));

image-20221002182515509

4.3.3 收集

收集的常见方法如下:

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

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

Stream.of(1,2,3,4,5).filter(num->num%2==0)
        .collect(Collectors.toList()).forEach(System.out::println);

image-20221002182819721

到此,Lambda表达式与Strem API的基本用法就介绍完了,至于是否提高了开发效率,只有去生产环境中实践了。