06 | Stream如何提高遍历集合效率?

在这里插入图片描述
上一讲中,我在讲List集合类,那我想你一定也知道集合的顶端接口Collection,在Java8中,Collection新增了两个流方法,分别时stream()和parallealStream()。

	default Stream<E> stream() {
    
    
        return StreamSupport.stream(spliterator(), false);
    }
    default Stream<E> parallelStream() {
    
    
        return StreamSupport.stream(spliterator(), true);
    }

通过英文名不难猜测,这两个方法肯定和Stream有关,那进一步猜测,是不是和我们熟悉的InputStream和OutputStream也有关系呢?集合类中新增的两个Stream方法到底有什么作用?今天,我们就来深入了解下Stream。

什么是Stream?

现在很多大数据量系统中都存在分表分库的情况。
例如,电商系统中的订单标,常常使用用户ID的Hash值来实现分表分库,这样是为了减少单个表的数据量,优化用户查询订单的速度。
但在后台管理审核订单时,他们需要将各个数据源的数据查询到应用层之后进行合并操作。
例如,当我们需要查询出过滤条件下的所有订单,并按照订单的某个条件进行排序,单个数据查询出来的数据可以按照某个条件进行排序的,但多个数据源查询出来已经排序号的数据,并不代表合并后是正确的排序,所以我们需要在应用层对合并数据集合重新进行排序。
在Java8之前,我们通常是通过for循环或者Iteator迭代来重新排序合并数据,又或者通过重新定义Collections.sorts的Comparator方法来实现,这两种方式对于大数据量系统来说,效率并不是很理想。
Java8中添加了一个新的接口类Stream,他和我们之前接触的字节流概念不太一样,Java8集合中的Stream相当于高级版的Iterator,他可以通过Lambda表达式对集合进行各种非常便利、高效的聚合操作(Aggregate Operation),或者大批量数据操作(Bulk Data Operation)。
Stream的聚合操作与数据库SQL的聚合操作sorted、filter、map等类似。我们在应用层就可以高效地实现类似数据库SQL的聚合操作了,而在数据操作方面,Stream不仅可以通过串行的方式实现数据操作,还可以通过并行的方式处理大批量数据,提高数据的处理效率。
接下来我们就用一个简单的例子来体验下Stream的简洁与强大。
这个Demo的需求是过滤分组一所中学里身高在160cm以上的男女同学,我们先用传统的迭代方式来实现,代码如下:

    public static void main(String[] args) {
    
    
        //过滤分组一所中学里身高在 160cm 以上的男女同学
        Map<String, List<Student>> studentMap = new HashMap<String, List<Student>>();
        for (Student student : students) {
    
    
            if (student.getHeight() <= 160) {
    
    
                continue;
            }
            if (studentMap.containsKey(student.getSex())) {
    
    
                studentMap.get(student.getSex()).add(student);
            } else {
    
    
                studentMap.put(student.getSex(), new ArrayList<Student>() {
    
    {
    
    
                    add(student);
                }});
            }
        }
    }

我们再使用Java8中的Stream API进行实现:
1.串行实现

	public static void main(String[] args) {
    
    
        Map<String, List<Student>> result =
                students.stream().filter(s -> s.getHeight() > 160).collect(Collectors.groupingBy(s -> s.getSex()));
    }

2.并行实现

	public static void main(String[] args) {
    
    
        Map<String, List<Student>> result =
                students.parallelStream().filter(s -> s.getHeight() > 160).collect(Collectors.groupingBy(s -> s.getSex()));
    }

通过上面两个简单的例子,我们可以发现,Stream结合Lambda表达式实现遍历筛选功能非常的简洁和便捷。

Stream如何优化遍历?

上面我们初步了解了Java8中的Stream API,那Stream是如何做到优化迭代的呢?并行又是如何实现的?下面我们就透过Stream源码剖析Stream的实现原理。

1.Stream操作分类

在了解Stream的实现原理之前,我们先来了解下Stream的操作分类,因为他的操作分类其实是实现高效迭代大数据集合的重要原因之一。为什么这样说,分析完你就清楚了。
官方将Stream中的操作分为两大类:中间操作(Intermediate operations)和终结操作(Terminal operations)。中间操作只对操作进行了记录,即只会返回一个流,不会进行计算操作,而终结操作是实现了计算操作。
中间操作又可以分为无状态(Stateless)与有状态(Stateful)操作,前者是指元素的处理不受之前元素的影响,后者是指该操作只有拿到所有元素之后才能继续下去。
终结操作又可以分为短路(Short-circuiting)与非短路(Unshort-circuiting)操作,前者是指遇到某些符合条件的元素就可以得到最终结果,后者是指必须处理完所有元素才能得到最终结果。操作分类详情如下图所示:
Stream流操作
我们通过还会将中间操作称为懒操作,也正是由这种懒操作结合终结操作、数据源构成的处理管道(Pipeline),实现了Stream的高效。

2.Stream源码实现

在了解Stream如何工作之前,我们先来了解下Stream包是由哪些主要结构类组合而成的,各个类的职责是什么。如下图
牛逼不,自己画的
BaseStream和Stream为最顶端的接口类,BaseStream主要定义了流的基本接口方法,例如,spliterator\isParallel等;Stream则定义了一些流的常用操作方法,例如,map、filter 等。
ReferencePipeline 是一个结构类,他通过定义内部类组装了各种操作流。他定义了Head、StatelessOp、StatefulOp 三个内部类,实现了 BaseStream 与 Stream 的接口方
法。
Sink 接口是定义每个 Stream 操作之间关系的协议,他包含 begin()、end()、
cancellationRequested()、accept() 四个方法。ReferencePipeline 最终会将整个 Stream
流操作组装成一个调用链,而这条调用链上的各个 Stream 操作的上下关系就是通过 Sink
接口协议来定义实现的。

Stream操作叠加

我们知道,一个Stream的任何操作都是由管道组装,并统一完成数据处理的,在JDK中每次中断操作都会以使用阶段(State)命名。
管道结构通常是ReferencePipline实现的的,前面讲解 Stream 包结构时,我提到过Head 类主要用来定义数据源操作,在我们初次调用 names.stream() 方法时,会初次加载Head 对象,此时为加载数据源操作;接着加载的是中间操作,分别为无状态中间操作StatelessOp 对象和有状态操作 StatefulOp 对象,此时的 Stage 并没有执行,而是通过AbstractPipeline 生成了一个中间操作 Stage 链表;当我们调用终结操作时,会生成一个最终的 Stage,通过这个 Stage 触发之前的中间操作,从最后一个 Stage 开始,递归产生一个 Sink 链。如下图所示:
在这里插入图片描述
下面我们再通过一个例子来感受下 Stream 的操作分类是如何实现高效迭代大数据集合的。

		List<String> names= Arrays.asList("金库里","吴京","甄子丹","周杰伦","成龙","金晨");
        names.stream().filter(name->name.startsWith("金"))
                .mapToInt(String::length)
                .max().toString();

这个例子的需求是查找出一个长度最长,并且以张为姓氏的名字。从代码角度来看,你可能会认为是这样的操作流程:首先遍历一次集合,得到以“张”开头的所有名字;然后遍历一次 filter 得到的集合,将名字转换成数字长度;最后再从长度集合中找到最长的那个名字并且返回。
这里我要很明确地告诉你,实际情况并非如此。我们来逐步分析下这个方法里所有的操作是如何执行的。
首先 ,因为 names 是 ArrayList 集合,所以 names.stream() 方法将会调用集合类基础接口 Collection 的 Stream 方法:

	default Stream<E> stream() {
    
    
        return StreamSupport.stream(spliterator(), false);
    }

===未完待续=

猜你喜欢

转载自blog.csdn.net/lxn1023143182/article/details/113625165
06