Stream可不仅仅是个迭代器

139 阅读4分钟

写在前面

众所周知啊,java的Stream非常好用!其链式的代码结构,让一个变量起名困难症的资深患者(我)十分沉迷,就此抛弃了for
Ps. 就某些操作(异常处理,操作stream外变量等)来讲,还是写for更方便。
之前一直把Stream当做一个链式的迭代器来用,但Stream就仅仅是一个迭代器吗?
奥夫靠斯闹特! 记录一下自己走出盲目自信(不好好学)的误区。

起因

在一个略(lei)感(de)繁(yao)忙(si)的工作日,正在写一个非常简单的需求,找到List中第一个符合条件的元素,简单写一下for的实现

public static void main(final String[] args) {
    final List<Integer> numbers = List.of(1, 2, 3);
    System.out.println(findGreaterThanOne(numbers));
}

private static Integer findGreaterThanOne(final List<Integer> numbers) {
    for (final Integer number : numbers) {
        if (number > 1) {
            return number;
        }
    }
    return null;
}

非常的简单是不是,但是呢,作为一个Stream爱好者,非常的抗拒使用for,于是写了一个Stream版,写的时候发现了一些问题

private static Integer findGreaterThanOne(final List<Integer> numbers) {
    return numbers.stream() // 在我的认知里,这里返回[1,2,3]
            .filter(number -> number > 1) // 这里过滤后,剩下[2,3]
            .findFirst() // 找到第一个,返回2
            .orElse(null);
}

写完之后发现,相较于for,元素3多执行了一次判断。基于对Stream的执着,没有直接改成for,咨询了一下组里的大哥,大哥说加个peek(注①)看看执行过程。
结果不试不知道,一试吓一跳,直接颠覆了我对Stream的认知(学艺不精)。修改一下代码

private static Integer findGreaterThanOne(final List<Integer> numbers) {
    return numbers.stream()
            .filter(number -> number > 1)
            .peek(number -> System.out.println(number))
            .findFirst()
            .orElse(null);
}

执行前,想定的输出结果是

2
3

实际的输出结果却是

2

元素3并没有多执行了一次判断,这让我反思起来我对Stream的认知。

分析

先梳理一下,方便大家理解。
元素扩展为5个,filter增加到4个,看上去明显一些

public static void main(final String[] args) {
    final List<Integer> numbers = List.of(1, 2, 3, 4, 5);
    System.out.println(findGreaterThanOne(numbers));
}

private static Integer findGreaterThanOne(final List<Integer> numbers) {
    return numbers.stream()
            .filter(number -> number > 1)
            .filter(number -> number > 2)
            .filter(number -> number > 3)
            .filter(number -> number > 4)
            .findFirst()
            .orElse(null);
}

代码执行顺序如下(图片中处理步骤有一些错误,只看处理顺序即可) image.png

看图的话可以清晰的分辨两者的差别,是我把Stream想简单了。
整理一下我认知偏差的原因,按照常规的代码逻辑各个.filter()之间没有直接联系,会在执行完前一步后执行下一步操作。

举个例子,用js的数组写一下上述逻辑
image.png
可见常规的遍历符合我之前的认知。js的数组会在执行完上一句函数后,才能继续执行。
Stream却是收集所有处理后,按照元素遍历,依次执行。这和传统的遍历大不相同,有必要研究一下Stream的运行机制了。

思考

在调查Stream实现原理前,先将自己代入一下Stream开发者的视角,如果是要我完成这个集中处理需求,该怎么实现呢。不感兴趣的小伙伴们可以略过。

首先,将Stream的方法分为两种。一种返回Stream对象(mapfilterpeek等),简称为中间操作。一种不返回Stream对象(foreachfindFirstcolloct等),简称为终止操作。
这和Builder构造器的设计思想类似,Builder只有执行build方法时才实际执行。

Builder.setA()
    .setB()
    .setC()
    .build();

调用非截流方法时,将其保存在Stream内。调用截流方法时,执行相应处理。简单写一下伪代码(仅思路)

class Stream<T> {
    private List<T> items;
    private List<Predicate | Function ...> handlers;
    public map(Function function) {
        handlers.add(function);
    }
    public filter(Predicate predicate) {
        handlers.add(predicate);
    }
    public foreach(Consumer consumer) {
        for (item : items) {
            for (handler : handlers) {
                if (handler instanceof Function) {
                    item = handler.apply(item);
                }
                if (handler instanceof Predicate) {
                    if (handler.apply(item)) {
                        break;
                    }
                }
                consumer.apply(item);
            }
        }
    }
}

调查

其实这一部分写了一些之后,在翻看站内其他文章时,感觉自己写的简陋且生硬,还是推荐大家去搜索一下站内的优秀文章(详见注②),这里我简单说明一下,方便懒得看文章的小伙伴们理解。

核心功能:
1,Spliterator:分离器,负责遍历元素,还有一些并行处理的功能,在终止操作时调用
2,Pipeline+Sink:保存中间操作的链表,在执行终止操作时构建流水线
3,evaluate方法:终止操作,在不同的终止操作(foreachfindFirst等)实现各自的逻辑

到这里,相信大家已经明白了Stream的运行机制,有兴趣研究的话,可以进一步深入。

总结

总得来讲,Stream不仅仅只是链式操作的迭代器,其中包含的设计思路值得我们深入思考,以拓宽我们在其他方面的设计思路。

①,peek是一种不操作数据的流操作,多用于调试、log记录等
②,Java8 Stream源码精讲