Java 8 中,流有一个非常大的(也可能是最大的)局限性,使用时,对它操作一次仅能得到一个处理结果。实际操作中,如果你试图多次遍历同一个流,结果只有一个,那就是遭遇下面这样的异常:
java.lang.IllegalStateException: stream has already been operated upon or closed
虽然流的设计就是如此,但我们在处理流时经常希望能同时获取多个结果。
本篇利用一个通用API,即Spliterator,尤其是它的延迟绑定能力,结合BlockingQueues和Futures来实现这一大有裨益的特性。
1.复制流
要达到在一个流上并发地执行多个操作的效果,你需要做的第一件事就是创建一个StreamForker,这个StreamForker会对原始的流进行封装,在此基础之上你可以继续定义你希望执行的各种操作。我们看看下面这段代码。
public class StreamForker<T> {
private final Stream<T> stream;
private final Map<Object, Function<Stream<T>, ?>> forks = new HashMap<>();
public StreamForker(Stream<T> stream) {
this.stream = stream;
}
/**
* 这里的fork方法接受两个参数。
* Function参数,它对流进行处理,将流转变为代表这些操作结果的任何类型。
* key参数,通过它你可以取得操作的结果,并将这些键/函数对累积到一个内部的Map中。
*
* @param key
* @param f
* @return
*/
public StreamForker<T> fork(Object key, Function<Stream<T>, ?> f) {
forks.put(key, f);
return this; //返回this从而保证多次流畅地调用fork方法
}
public Results getResults() {
ForkingStreamConsumer<T> consumer = build();
try {
stream.sequential().forEach(consumer);
} finally {
consumer.finish();
}
return consumer;
}
}
所有由fork方法添加的操作的执行都是通过getResults方法的调用触发的,该方法返回一个Results接口的实现,具体的定义如下:
public interface Results {
<R> R get(Object key);
}
1.1 使用 ForkingStreamConsumer 实现 Results 接口
你可以用下面的方式实现getResults方法:
public Results getResults() {
ForkingStreamConsumer<T> consumer = build();
try {
stream.sequential().forEach(consumer);
} finally {
consumer.finish();
}
return consumer;
}
ForkingStreamConsumer同时实现了前面定义的Results接口和Consumer接口。随着我们进一步剖析它的实现细节,你会看到它主要的任务就是处理流中的元素,将它们分发到多个BlockingQueues中处理,BlockingQueues的数量和通过fork方法提交的操作数是一致的。注意,我们很明确地知道流是顺序处理的,不过,如果你在一个并发流上执行forEach方法,它的元素可能就不是顺序地被插入到队列中了。finish方法会在队列的末尾插入特殊元素表明该队列已经没有更多需要处理的元素了。build方法主要用于创建ForkingStreamConsumer。
private ForkingStreamConsumer<T> build() {
//创建由队列组成的列表,每一个队列对应一个操作
List<BlockingQueue<T>> queues = new ArrayList<>();
//建立用于标识操作的键与包含操作结果的Future之间的映射关系
HashMap<Object, Future<?>> actions = forks.entrySet().stream().reduce(
new HashMap<>(),
(map, e) -> {
map.put(e.getKey(), getOperationResult(queues, e.getValue()));
return map;
},
(m1, m2) -> {
m1.putAll