Java Stream后续来了,汇总一些项目开发中高频使用的 Stream操作

4,351 阅读11分钟

本文为掘金社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

前面一篇文章我们把 Stream 的两大类操作:流的中间操作、流的终结操作都有哪些方法给大家列举了一遍,让大家对 Stream 能完成的操作有了大致的印象。

前文回顾:

不过讲解这些操作时用的都是非常简单的例子,流操作的数据也都是简单类型的,主要的目的是让大家能更快速地理解 Stream 的各种操作应用在数据上后,都有什么效果。

在现实场景中实际做项目的时候,我们使用Stream操作的数据大多数情况下是各种业务对象 DO(Domain Object)的集合,比如用 Stream 在业务对象组成的List中筛选出符合规则的元素,按照业务对象的某个字段排序生成新的业务对象集合等等。

所以这里我们特地单独开一篇内容,汇总一下我在项目开发中曾高频使用过的几种 Stream 操作,这几个操作在实际做项目时,对操作业务对象流得到自己想要的结果非常有用,代码编写起来也非常简洁,编码体验很爽。

不过想要通过 Stream 操作拿到想要的结果,往往需要多个 Stream 操作的链式调用组合而成,再加上很多 Stream 操作步骤里都会用到 Lambda 表达式,新手乍一看到这些代码会有:“这一大坨代码都是干了啥?”的感觉,所以前面 Stream 各种基础操作以及 Lambda 表达式方面的知识大家一定要理解透了,打好基础后再来看这篇内容。

本文一共总结了四大类在项目开发中高频使用的 Stream 操作,相信大家看完,理解到 Stream 的精髓后,一定会在项目开发中举一反三,应用得得心应手。另外也欢迎大家把自己在开发项目中最常用的 Stream 操作,通过私信、评论等方式告诉我,让这篇文章能越来越完善,给更多人提供参考价值。

本文大纲如下:

求两个对象 List 的交集/差集

以对象的属性为判断,求两个 List 的交集或者差集的操作,在项目开发中挺常见的,用普通循环来实现的话,需要两层循环才能实现,如果是用 Steam 的话代码就会显得很清晰,没有那么多层级嵌套。 先说一下交集和差集的概念,其实以前中学的时候学过,这里再交代一下,防止有人搞混。 假设有两个集合,集合A中有这几个元素 [1, 2, 3, 4, 5],集合B中有 [3, 4, 5, 6, 7, 8] 这几个元素,那么两个集合的交集和差集分别是:

  • 交集:表示在两个集合中都存在的元素,所以 A、B的交集为:[3, 4, 5]
  • 差集:求差集需要看以谁为参照
    • 集合 A 与 B 的差集:在集合 A 中而不在集合 B 中的元素,所以差集是:[1, 2]
    • 集合 B 与 A 的差集:在集合 B 中而不在集合 A 中的元素,所以差集是:[6, 7, 8]

现在假设我们有这样一个 名为 Person 的业务对象的类。

package com.example.streampractice;
public class Person {
    String id;
    String nickName;

    public A(String id, String nickName) {
        this.id = id;
        this.nickName = nickName;
    }

    @Override
    public String toString() {
        return "A{" +
                "id='" + id + '\'' +
                ", nickName='" + nickName + '\'' +
                '}';
    }

    public String getId() {
        return id;
    }

    public String getNickName() {
        return nickName;
    }
}

现在给到两个 Person对象的 List,要求以 Person 对象的 Id 为标准,求两个 List 的交集和差集。

用Stream 求两个对象List的交集

根据元素 Person 对象的Id 为判断标准,求两个List的交集,用 Stream 的实现如下:

package com.example.streampractice;

public class GetObjectListIntersection {

    public static void main(String[] args) {
        List<Person> aList = new ArrayList<>(Arrays.asList(
                new Person("1", "张三"),
                new Person("2", "李四"),
                new Person("3", "王五")
        ));

        List<Person> bList = new ArrayList<>(Arrays.asList(
                new Person("2", "李四"),
                new Person("3", "王五"),
                new Person("4", "赵六")
        ));

        // aList 与 bList 的交集 (在两个集合中都存在的元素)
        List<Person> intersections = aList
                .stream() //获取第一个集合的Stream1
                .filter(  //取出Stream1中符合条件的元素组成新的Stream2,lambda表达式1返回值为true时为符合条件
                    a ->  //lambda表达式1,a为lambda表达式1的参数,是Stream1中的每个元素
                    bList.stream() //获取第二个集合的Stream3
                            .map(Person::getId) //将第二个集合每个元素的id属性取出来,映射成新的一个Stream4
                            .anyMatch( //返回值(boolean):Stream4中是否至少有一个元素使lambda表达式2返回值为true
                                id -> //lambda表达式2,id为lambda表达式2的参数,是Stream4中的每个元素
                                Objects.equals(a.getId(), id) //判断id的值是否相等
                        	)
                )
                .collect(Collectors.toList()); //将Stream2转换为List
        System.out.println("----------aList 与 bList 的交集为:");
        System.out.println(intersections);
    }
}

上面代码示例中的 Stream 调用因为缩进的问题显得挺吓人,不过这么缩进主要是为了写注释。在注释里,我为每个Stream步骤都做了解释,大家可以跟着注释走一遍,就不觉得这个 Stream 操作很难懂了。 运行例程会有下面输出,大家可以自己练习试一下:

----------aList 与 bList 的交集为:
[Person{id='2', nickName='李四'}, Person{id='3', nickName='王五'}]

用 Stream 求两个对象List的差集

接下来这个例子以 Person 对象的 Id 为判断标准,求 bList 对象列表与 aList 对象列表的差集,差集的意思是,上面我们已经解释过了

List<Person> differences = bList.
    stream().
    filter(
    	b -> 
    	aList.stream()
        	.map(Person::getId)
        	.noneMatch(
                id -> 
                Objects.equals(b.getId(), id)
            )
    ).collect(Collectors.toList());

System.out.println("----------bList 与 aList 的差集为:");
System.out.println(differences);

根据元素对象的ID求两个对象List的差集,只要把上面求交集的 Stream 操作步骤里的 anyMatch 换成 noneMatch 就可以了。

用 Stream 求集合的差集(高效版)

上面这个 Stream 操作的执行效率不高,因为 bList 中的每个元素都要在 noneMatch 里判断在 aList 里有没有跟它 Id 重复的对象,相当于整个筛选是O(N²)的复杂度, 所以如果列表的数据足够多,还是挺耗费性能的。为了减少遍历操作,我们可以先把 aList 转化成以 Person 对象 Id为 key 的 Map 容器,这样 noneMatch 里的操作只需要判断一次key存不存在即可,整个筛选变成了O(N)的复杂度。

Map<String, A> aMap = aList.stream().collect(Collectors.toMap(A::getId, Function.identity())) ;
List<A> diffEffective = bList.stream().filter(b -> !aMap.containsKey(b.getId())).collect(Collectors.toList());
System.out.println("----------bList 与 aList 的差集为:");
System.out.println(diffEffective);

用 Stream 求交集的例子也能按同样思路优化, 这两个例子一开始选择低效版本的给大家演示主要是把求交集和差集的 Stream 步骤剥开给大家分析一下,实际开发的时候考虑性能记得要使用高效版本的。

用 Stream 中排序对象集合

接下来我们再说第二种在开发中会高频使用 Stream 完成的操作--排序对象集合,常见的是以业务对象的某个字段为标准,对一组业务对象进行排序。

比如有这样一个 Person类,有一组 Person 类的对象,放在 List 中。

public class Person {
    private Long personId;
    private String name;
    private Integer age;
    private Double salary;

    public Long getPersonId() {
        return personId;
    }

    public void setPersonId(Long personId) {
        this.personId = personId;
    }

    public Person(Long personId, String name, Integer age, Double salary) {
        this.personId = personId;
        this.name = name;
        this.age = age;
        this.salary = salary;
    }

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

要按 Person 对象的 personId 为标准,对这组对象进行排序。这个操作同样能够通过 Stream 操作完成。

按对象字段正序排序对象集合

Stream API 里有一个 sorted 方法,通过该方法我们就能Stream 元素按照对象的指定的字段进行排序。

public static void main(String[] args) {
    List<Person> personList = Arrays.asList(new Person(1000L, "First", 25, 30000D),
            new Person(2000L, "Second", 30, 45000D),
            new Person(3000L, "Third", 35, 25000D));

    // 使用字段包装类型的 compareTo 进行排序, 这个方式可以简化成下面的使用 Comparator.comparing 的例子
    personList.stream().sorted(
        (p1, p2) -> 
        (p1.getPersonId().compareTo(p2.getPersonId()))
    )
    .forEach(person -> System.out.println(person.getName()));
}

Stream 的 sorted 方法内部,这个例子使用的是Person对象personId字段的包装类型实现的compareTo方法进行的比较。 这个方式可以进一步简化成下面的使用Comparator.comparing方法的方式:

personList.stream()
    .sorted(
        Comparator.comparing(Person::getPersonId)
    ).forEach(person -> System.out.println(person.getName()));

Comparator.comparing()该方法是一个泛型方法,会根据参数的类型,内部调用相应类型实现的CompareTo方法进行两个参数的比较。

上面两个例子是把Stream里的元素按照Person对象personId的正序进行排序,运行例程后会看到下面的输出,按照personId的正序输出了每个Person对象的名字。

First
Second
Third

用对象字段倒序排列对象集合

如果需要按照倒序对Stream中的元素进行排列,可以在Comparator.comparing()后面追加reversed()调用

personList.stream().sorted(
    Comparator.comparing(
        Person::getPersonId).reversed()
    ).forEach(person -> System.out.println(person.getName()));

这两个例子都是用对象的单个字段,对对象集合进行排序,其实还可以让集合按照多字段排序规则进行排序。

用对象多个字段排序对象集合

Streamsorted步骤里使用Comparator.comparing()的好处是,除了倒序排序非常方便,我们还可以根据多字段进行排--比如先按Person对象的personId字段,如果再按Person对象的年龄age字段进行排序。这个时候可以在Comparator.comparaing调用后,再接上thenComparing调用,实现元素按照多字段进行排序的功能。

public static void main(String[] args) {
    List<Person> personList = Arrays.asList(new Person(1000L, "First", 25, 30000D),
                new Person(1000L, "Second", 22, 45000D),
                new Person(3000L, "Third", 35, 25000D));
    // 先按personId 排序,再按 age 排序
    personList.stream().sorted(
        Comparator.comparing(Person::getPersonId)
        .thenComparing(Person::getAge)
    ).forEach(person -> System.out.println(person.getName()));

为了测试排序效果, 我们把List中的头两个Person对象,设置成了personId一样,年龄不同,执行下例程会看到元素排序后,打印出的Person对象Name值为:

Second
First
Third

符合我们的预期,元素先按照 Person 对象的 personId 进行了排序,因为 personList 中前两个对象的 PersonId 相等,就继续按照 age 字段进行了排序,第二个对象的年龄更小,所以程序的输出的姓名里 Second 会排在第一位。

在Stream 迭代中使用元素索引

在 Stream 操作中没办法像 For 循环那样拿到当前迭代的元素的自然索引,虽然 Java 11 里有办法可以当前迭代元素的自然索引,因为国内大部分公司的 JDK 版本还是 8 ,所以只能另外用其他办法。

有另外一个办法,程序可以借助AtomicInteger的获取并自增方法getAndIncrement方法实现在 Stream 迭代中拿到当前迭代元素的自然索引的效果,看下面这个示例程序。

import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;

// Java8 里没办法直接访问 stream 里的元素,不过可以用下面这种方式实现
public class AtomicIntegerAsStreamIndices {
    public static void main(String[] args)
    {
        String[] array = { "A", "B", "C", "D"  };

        AtomicInteger index = new AtomicInteger();
        Arrays.stream(array)
                .map(str -> index.getAndIncrement() + " -> " + str)
                .forEach(System.out::println);
    }
}

该例程会输出:

0 -> A
1 -> B
2 -> C
3 -> D

把对象 List 转换为对象 Map

最后这个 Stream 操作,在开发中使用的也非常多,一般为了提高程序效率的时候,我们会把对象 List 转换成以对象 Id 为 Key 的对象 Map。用 Stream 我们能简单便捷的把 List 转换成 Map ,免去了在程序里写两层循环的窘境。 假设我们有这样一个 Animal 类:

public class Animal {
    private int id;
    private String name;

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

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

接下来,我们使用 Stream 操作把 Animal 对象的 List 转换成以对象 id 为 Key 的对象 Map。

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

public class ListStreamToMap {


    public static void main(String[] args) {
        List<Animal> aList = new ArrayList<>();
        aList.add(new Animal(1, "Elephant"));
        aList.add(new Animal(2, "Bear"));

        Map<Integer, Animal> map = aList.stream()
                .collect(Collectors.toMap(Animal::getId, Function.identity()));

        map.forEach((integer, animal) -> {
            System.out.println(animal.getName());
        });

    }
}

总结

本文一共总结了四大类在项目开发中高频使用的 Stream 操作,相信大家看完,理解到 Stream 的精髓后,一定会在项目开发中举一反三,应用得得心应手。另外也欢迎大家把自己在开发项目中最常用的 Stream 操作,通过私信、评论等方式告诉我,让这篇文章能越来越完善,给更多人提供参考价值。