Stream,集合操作利器,让你好用到飞起来

作者:xcbeyond
疯狂源自梦想,技术成就辉煌!微信公众号:《程序猿技术大咖》号主,专注后端开发多年,拥有丰富的研发经验,乐于技术输出、分享,现阶段从事微服务架构项目的研发工作,涉及架构设计、技术选型、业务研发等工作。对于Java、微服务、数据库、Docker有深入了解,并有大量的调优经验。


集合是Java中使用最多的API,几乎每个程序员天天都会和它打招呼,它可以让你把相同、相似、有关联的数据整合在一起,便于使用、提取以及运算等操作。在实际Java程序中,集合的使用往往随着业务需求、复杂度而变得更加复杂,在这其中将可能会涉及到更多的运算,如:求和、平均值、分组、过滤、排序等等。如何这些操作混合出现,又该如何实现?难道遍历、再遍历、再运算么?抛开性能因素,这些操作已经严重影响了代码的整洁,这种代码也没有几个人愿意来读。

那么,有没有什么好的办法来解决这种现状呢?毕竟集合最为最常用的操作,难道Java语言的设计者没有意识到这一点吗?如何能够帮助你节约宝贵的时间,让程序员活得更轻松一点呢?

你可能已经猜到了,答案就是流—Stream。

本文将从JDK1.8中Stream API讲起,让你觉得集合操作原来可以这么轻松使用。

(在学习本节之前,必须先学习Lambda表达式相关知识,不清楚的可以翻看前几篇文章JDK1.8新特性(三):Lambda表达式,让你爱不释手、JDK1.8新特性(四):函数式接口)

一、Stream是什么
Stream是Java API中的新成员,它允许你以声明的方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现),你可以把它看成是遍历数据集的高级迭代器。此外,Stream还可以透明地并行处理,而无需写任何多线程代码了。

我们先简单的对比使用下Stream的好处吧。下面两段代码都是实现筛选出名字中包含“xc”字符串的人,并按照其年龄进行排序。

传统方式(JDK1.8之前,非Stream流):

List<People> peoples = new ArrayList<>();
// 遍历 + 判断
for (People people : allPeoples) {
    if (people.getName().contains("xc")) {
        peoples.add(people);
    }
}
// 对年龄排序
Collections.sort(peoples, new Comparator<People>() {
    @Override
    public int compare(People p1, People p2) {
        return Integer.compare(p1.getAge(), p2.getAge());
    }
});

Stream方式:

List<People> peoples2 = allPeoples.stream()
                .filter(people -> people.getName().contains("xc"))
                .sorted(Comparator.comparing(People::getAge))
                .collect(Collectors.toList());

没有对比就没有伤害,效果显而易见。从开发角度来看,Stream方式有以下显而易见的好处:

代码以声明方式写的:说明想要完成什么(筛选出满足条件的数据)而不是说明如何实现一个操作(利用循环和if条件等控制流语句)。

多个基本操作链接起来:将多个基础操作链接起来,来表达复杂的数据处理流水线(如下图),同时体现了代码的清晰、可读性。
 

Stream API功能非常强大,类似上面Stream处理流水线方式应用场景很多,理论上可以生成一个具有无穷长的流水线的。更重要的是,在复杂业务中你用不着为了让某些数据处理任务并行而去操心线程和锁了,Stream API都替你做好了!

Stream,即:”流“,通过将集合转换为一种叫做”流“的元素序列,通过声明方式,对集合中的每个元素进行一系列并行或串行的流水线操作。

换句话说,你只需要告诉流你的要求,流便会在背后自行根据要求对元素进行处理,而你只需要 “坐享其成”。

二、Stream操作
整个流操作就是一条流水线,将元素放在流水线上一个个地进行处理,如下图所示。


 

其中,数据源是原始集合数据,然后将如 List<T>的集合转换为Stream<T>类型的流,并对流进行一系列的操作,比如过滤保留部分元素、对元素进行排序、类型转换等,最后再进行一个终止操作,可以把 Stream 转换回集合类型,也可以直接对其中的各个元素进行处理,比如打印、比如计算总数、计算最大值等。

很多流操作本身就会返回一个流,所以多个操作可以直接连接起来,就如上面举例中Stream方式的代码一样。
 

如果是以前,进行这么一系列操作,你需要做个迭代器或者 foreach 循环,然后遍历,一步步地亲力亲为地去完成这些操作。但是如果使用流,你便可以直接声明式地下指令,流会帮你完成这些操作。

通过上面Stream操作流水线、实例,Stream操作大体上分为两种:中间操作符和终止操作符。

1. 中间操作符
对于数据流来说,中间操作符在执行指定处理逻辑后,数据流依然可以传递给下一级的操作符。

中间操作符包含8种:

map(mapToInt,mapToLong,mapToDouble) 转换操作:把比如A->B,这里默认提供了转int,long,double的操作符。

flatmap(flatmapToInt,flatmapToLong,flatmapToDouble)拍平操作:比如把int[]{2,3,4}拍平变成 2,3,4,也就是从原来的一个数据变成了3个数据,这里默认提供了拍平成int,long,double的操作。

limit限流操作:比如数据流中有10个,我只要前3个就可以使用。

distinct去重操作:重复元素去重。

filter过滤操作:对集合数据进行过滤。

peek消费操作:如果想对数据进行某些操作,如:读取、编辑修改等。

skip跳过操作:跳过某些元素。

sorted排序操作:对元素排序,前提是实现Comparable接口,当然也可以自定义比较器。

(具体可参照源码java.util.stream.Stream)

2. 终止操作符
数据经过一系列的中间操作,就轮到终止操作符上场了。终止操作符就是用来对数据进行收集或者消费的,数据到了终止操作这里就不会向下流动了,终止操作符只能使用一次。

collect收集操作:将所有数据收集起来,这个操作非常重要,官方的提供的Collectors 提供了非常多收集器,可以说Stream的核心在于Collectors。

count统计操作:统计最终的数据个数。

findFirst、findAny查找操作:查找第一个、查找任何一个,返回的类型为Optional。

noneMatch、allMatch、anyMatch匹配操作:数据流中是否存在符合条件的元素,返回值为bool 值。

min、max最值操作:需要自定义比较器,返回数据流中最大、最小的值。

reduce规约操作:将整个数据流的值规约为一个值,count、min、max底层就是使用reduce。

forEach、forEachOrdered遍历操作:这里就是对最终的数据进行消费了。

toArray数组操作:将数据流的元素转换成数组。

说了这么多,心动不如行动,俗话说:实践出真理。那么,一起来实战吧。






三、实战演练
Stream的一系列操作,必须要使用终止操作符,否则整个数据流是不会执行起来的。

1. map
转换、映射操作,将元素转换成其他形式或提取一些信息。

比如,从People集合中获取所有人的年龄:

allPeoples.stream()
    .map(People::getAge)
    .forEach(System.out::println);

2. flatmap
将元素拍平拍扁 ,将拍扁的元素重新组成Stream,并将这些Stream 串行合并成一条Stream。

比如,带有-字符的字符串进行拆分,并输出:

Stream.of("x-c-b-e-y-o-n-d","a-b-c-d")
    .flatMap(m -> Stream.of(m.split("-")))
    .forEach(System.out::println);

输出:

x
c
b
e
y
o
n
d
a
b
c
d

3. limit
限流操作,限制集合元素的个数。

比如,集合中有10个元素,我只要前4个就可以使用:

Stream.of(1,2,3,4,5,6,7,8,9,10)
    .limit(4)
    .forEach(System.out::println);

输出:

1
2
3
4

4. distinct
去重操作,重复元素去重,类似数据库中的关键字distinct。

比如,集合中可能存在一些重复的数据,需要进行去重操作:

Stream.of("xcbeyond","Niki","Liky","xcbeyond")
    .distinct()
    .forEach(System.out::println);

输出:

xcbeyond
Niki
Liky

5. filter
过滤、筛选,对某些元素进行过滤,不符合筛选条件的将无法进入流的下游。

比如,筛选出一个数字集合中的所有偶数:

Stream.of(1,2,3,4,5,6,7,8,9,10)
    .filter(n -> 0 == n%2)
    .forEach(System.out::println);

输出:

2
4
6
8
10

6. peek
消费操作,如果想对数据进行某些操作,如:读取、编辑修改等。

比如,将People集合中name统一修改为name+age的形式:

allPeoples.stream()
    .peek(people -> people.setName(people.getName() + people.getAge()))
    .forEach(people -> System.out.println(people.getName()));

7. skip
跳过操作,跳过某些元素。

比如,一个数字集合,跳过前4个元素:

Stream.of(1,2,3,4,5,6,7,8,9,10)
    .skip(4)
    .forEach(System.out::println);

输出:

5
6
7
8
9
10

8. sorted
排序操作,对元素排序,前提是实现Comparable接口,当然也可以自定义比较器。

比如,将People集合按照年龄排序:

allPeoples.stream()
    .sorted(Comparator.comparing(People::getAge))
    .forEach(System.out::println);

9. collect
收集操作,终止操作符,用于将最终的数据收集到新的集合中,如,List、Set、Map等集合。

比如,将People集合按照年龄排序,并存放在一个新的集合中,供后续使用:

List<People> sortedPeople = allPeoples.stream()
    .sorted(Comparator.comparing(People::getAge))
    .collect(Collectors.toList());

10. count
统计操作,用于对集合元素个数的统计,返回类型是long。

比如,计算People集合中年龄大于30的人数:

long count = allPeoples.stream()
    .filter(people -> people.getAge() > 30)
    .count();

11. findFirst、findAny
查找操作,查找第一个、任何一个,返回的类型为Optional。常用于查询集中符合条件的元素,并结合Optional.isPresent()进行判断,防止出现未找到而强制获取数据元素的异常情况。

比如,查找People集合中名字为xcbeyond的人:

People xcbeyondPeople = null;
Optional<People> optional = allPeoples.stream()
    .filter(people -> "xcbeyond".equals(people.getName()))
    .findFirst();
if (optional.isPresent()) {
    xcbeyondPeople = optional.get();
}

12. noneMatch、allMatch、anyMatch
匹配操作,判断数据流中是否存在符合条件的元素,返回值为boolean值。

noneMatch:没有匹配条件的元素

allMatch、anyMatch:全匹配

比如,判断People集合中是否有名字为xcbeyond的人:

boolean bool = allPeoples.stream()
    .allMatch(people -> "xcbeyond".equals(people.getName()));

13. min、max
最值操作,根据自定义比较器,返回数据流中最大、最小的元素。

比如,找到People集合中最大年龄的人:

People maxAgePeople = null;
Optional<People> maxAgeOptional = allPeoples.stream()
    .max(Comparator.comparing(People::getAge));
if (maxAgeOptional.isPresent()) {    // 可能没有,则需要进行判断
    maxAgePeople = maxAgeOptional.get();
}

14. reduce
规约操作:将整个数据流的值规约为一个值,其中count、min、max底层就是使用reduce。

比如,对一个整数集合进行求和:

int sum = Stream.of(1,9,8,4,5,6,-1)
    .reduce(0,(e1,e2)->e1+e2);
System.out.println(sum);

输出:

32

四、总结
本文就Stream的基础使用层面进行了全面的介绍、实战,告诉你该怎么用每种操作符,只有掌握了这些基本的操作,在面对实际复杂处理逻辑时,需要进一步配合使用,就会知道它的妙处了。这也让你对集合的操作更上一步,为你省去了不少麻烦。关于Stream更深入的说明,如:并行处理、是否高效等,将会在之后的章节进行详尽的阐述、验证,以消除使用中的疑惑与担忧。