最近工作后开始使用Stream,用起来比较顺手,可以说已经“沉浸于Stream无法自拔”,很少再用foreach循环了。
其中的Collectors.toMap 和 Collectors.groupingBy等操作好用到爆。
但是纠结于“Stream复用”问题。
看了一些文章如(https://blog.csdn.net/yiifaa/article/details/78118342)写得不是很清楚,
这里简单整理一下。
参考资料 :《Java 8 in Action: Lambdas, streams, and functional-style programming》
本文先对Stream作基本介绍,然后介绍如何“复用”stream。
Stream两种操作
[1] filter,map,和limit组合形成管道
[2] collect操作触发管道的执行和stream的关闭
前一种成为 中间操作(intermediate operations) ,后面称之为 终端操作(terminal operations)。
中间操作的特性:
中间操作是属于“懒性”的,直到终端操作才执行处理操作。因为中间操作经常被终端操作一次进行合并和处理。
流的“懒”特性是为了优化。
List menu = new ArrayList<>();
menu.add(new Dish("鱼香肉丝",500));
menu.add(new Dish("鱼香茄子",800));
menu.add(new Dish("红烧茄子",1000));
List names = menu.stream()
.filter(dish -> {
System.out.println("filtering"+ dish.getName());
return dish.getCalories()>100;
})
.map(dish -> {
System.out.println("mapping" + dish.getName());
return dish.getName();
})
.limit(2)
.collect(Collectors.toList());
输出结果:
可以看出
1 通过limit 只获取固定个数,不会整个遍历
2 filter和map 虽然是两个操作但是在同一个遍历中(循环合并)
我们看一下Stream的filter方法源码:
/**
* Returns a stream consisting of the elements of this stream that match
* the given predicate.
*
* This is an intermediate
* operation.
*
* @param predicate a non-interfering,
* stateless
* predicate to apply to each element to determine if it
* should be included
* @return the new stream
*/
Stream filter(Predicate predicate);
可以发现 中间操作的返回值都是Stream,而且根据注释可以清晰知道返回的是一个新的stream。
终端操作:
终端操作是为了产生结果,该结果是非stream的值,可以是List、Integer甚至也可以是void。
我们查看Stream的allMatch方法,发现返回值是boolean.
/**
* Returns whether all elements of this stream match the provided predicate.
* May not evaluate the predicate on all elements if not necessary for
* determining the result. If the stream is empty then {@code true} is
* returned and the predicate is not evaluated.
*
* This is a short-circuiting
* terminal operation.
*
* @apiNote
* This method evaluates the universal quantification of the
* predicate over the elements of the stream (for all x P(x)). If the
* stream is empty, the quantification is said to be vacuously
* satisfied and is always {@code true} (regardless of P(x)).
*
* @param predicate a non-interfering,
* stateless
* predicate to apply to elements of this stream
* @return {@code true} if either all elements of the stream match the
* provided predicate or the stream is empty, otherwise {@code false}
*/
boolean allMatch(Predicate predicate);
核心思想类似 建造者模式,在建造者模式中,有一系列的调用来构建配置(在stream中称之为中间操作),然后调用build方法(在stream中就是终端操作)。
一个简单的例子;
List menu = new ArrayList<>();
menu.add(new Dish("鱼香肉丝",500));
menu.add(new Dish("鱼香茄子",800));
menu.add(new Dish("红烧茄子",1000));
menu.add(new Dish("红烧鲍鱼",2000));
List dishes = menu.stream()
.filter(dish -> dish.getCalories()>300)
.limit(3)
.collect(Collectors.toList());
System.out.println(dishes);
图解:
中间操作就像是管道一样,数据从前面“流到”经过中间操作一步一步流到后面,最终通过终端操作获取结果并关闭流。
总结
1、一个stream就是从一个资源构建的的支持数据处理操作一系列元素。
2、Stream 可以使用内部迭代,迭代独立于filter/map/sorted等操作。
3、有两种类型stream操作:中间操作和终端操作。
4、中间操作如filter和map返回一个stream允许进行链式编程。中间操作用来构建操作的管道但不产生任何结果。
5、终端操作如forEach、count和collect返回一个非stream值或执行stream管道并返回一个值。
6、stream中的元素是按需计算的。
有的文章说“Stream执行终端操作后就被消费掉了,无法复用(这样说没错)”,给出一些曲折而且并非复用的方式,本质上还是重新创建Stream,如
https://blog.csdn.net/yiifaa/article/details/78118342
其实实现“复用”(对某个集合多次执行stream操作,请注意这并不是真正的复用),
但是我们要搞清楚自己的目的是啥,如果你的目的是对 集合多次使用stream执行终端操作,
最简单的做法就是将多次调用集合的.stream方法即可
List lists = new ArrayList<>();
lists.add(new User("张三",22));
lists.add(new User("张三",21));
lists.add(new User("李四",22));
lists.add(new User("张三",21));
List collect = lists.stream().filter(user -> user.getAge() < 22).collect(Collectors.toList());
List collect1 = lists.stream().filter(user -> user.getAge() > 50).collect(Collectors.toList());
我们看看stream方法的源码
/**
* Returns a sequential {@code Stream} with this collection as its source.
*
* This method should be overridden when the {@link #spliterator()}
* method cannot return a spliterator that is {@code IMMUTABLE},
* {@code CONCURRENT}, or late-binding. (See {@link #spliterator()}
* for details.)
*
* @implSpec
* The default implementation creates a sequential {@code Stream} from the
* collection's {@code Spliterator}.
*
* @return a sequential {@code Stream} over the elements in this collection
* @since 1.8
*/
default Stream stream() {
return StreamSupport.stream(spliterator(), false);
}
再进入 StreamSupport.stream方法
/**
* Creates a new sequential or parallel {@code Stream} from a
* {@code Spliterator}.
*
* The spliterator is only traversed, split, or queried for estimated
* size after the terminal operation of the stream pipeline commences.
*
* It is strongly recommended the spliterator report a characteristic of
* {@code IMMUTABLE} or {@code CONCURRENT}, or be
* late-binding. Otherwise,
* {@link #stream(java.util.function.Supplier, int, boolean)} should be used
* to reduce the scope of potential interference with the source. See
* Non-Interference for
* more details.
*
* @param the type of stream elements
* @param spliterator a {@code Spliterator} describing the stream elements
* @param parallel if {@code true} then the returned stream is a parallel
* stream; if {@code false} the returned stream is a sequential
* stream.
* @return a new sequential or parallel {@code Stream}
*/
public static Stream stream(Spliterator spliterator, boolean parallel) {
Objects.requireNonNull(spliterator);
return new ReferencePipeline.Head<>(spliterator,
StreamOpFlag.fromCharacteristics(spliterator),
parallel);
}
通过代码和注释我们可以清楚地发现,返回值是一个新的stream,因此可以实现Stream的“复用”(其实是再次创建一个新的 stream)。
其他更多详细内容参考:
1、《Java 8 in Action: Lambdas, streams, and functional-style programming》