不幸的是,Stream API具有创建自己的短路操作的能力有限。不太干净的解决方案是抛出一个RuntimeException并捕获它。这是IntStream的实现,但它也可以推广用于其他流类型:
public static int reduceWithCancelEx(IntStream stream, int identity,
IntBinaryOperator combiner, IntPredicate cancelCondition) {
class CancelException extends RuntimeException {
private final int val;
CancelException(int val) {
this.val = val;
}
}
try {
return stream.reduce(identity, (a, b) -> {
int res = combiner.applyAsInt(a, b);
if(cancelCondition.test(res))
throw new CancelException(res);
return res;
});
} catch (CancelException e) {
return e.val;
}
}
用法示例:
int product = reduceWithCancelEx(
IntStream.of(2, 3, 4, 5, 0, 7, 8).peek(System.out::println),
1, (a, b) -> a * b, val -> val == 0);
System.out.println("Result: "+product);
输出:
2
3
4
5
0
Result: 0
请注意,即使它与并行流一起工作,一旦其中一个引发异常,就不能保证其他并行任务将完成。已经开始的子任务可能会运行到完成,因此您可以处理比预期更多的元素。
更新:替代解决方案更长,但更平行友好。它基于定制拼接器,它最多返回一个元素,这是所有底层元素的累积结果)。当您在顺序模式下使用它时,它会在单个tryAdvance调用中执行所有操作。当您拆分时,每个部分都会生成对应的单个部分结果,这些结果由Stream引擎使用组合器功能减少。这是通用版本,但是原始专业化也是可能的。
final static class CancellableReduceSpliterator implements Spliterator,
Consumer, Cloneable {
private Spliterator source;
private final BiFunction accumulator;
private final Predicate cancelPredicate;
private final AtomicBoolean cancelled = new AtomicBoolean();
private A acc;
CancellableReduceSpliterator(Spliterator source, A identity,
BiFunction accumulator, Predicate cancelPredicate) {
this.source = source;
this.acc = identity;
this.accumulator = accumulator;
this.cancelPredicate = cancelPredicate;
}
@Override
public boolean tryAdvance(Consumer super A> action) {
if (source == null || cancelled.get()) {
source = null;
return false;
}
while (!cancelled.get() && source.tryAdvance(this)) {
if (cancelPredicate.test(acc)) {
cancelled.set(true);
break;
}
}
source = null;
action.accept(acc);
return true;
}
@Override
public void forEachRemaining(Consumer super A> action) {
tryAdvance(action);
}
@Override
public Spliterator trySplit() {
if(source == null || cancelled.get()) {
source = null;
return null;
}
Spliterator prefix = source.trySplit();
if (prefix == null)
return null;
try {
@SuppressWarnings("unchecked")
CancellableReduceSpliterator result =
(CancellableReduceSpliterator) this.clone();
result.source = prefix;
return result;
} catch (CloneNotSupportedException e) {
throw new InternalError();
}
}
@Override
public long estimateSize() {
// let's pretend we have the same number of elements
// as the source, so the pipeline engine parallelize it in the same way
return source == null ? 0 : source.estimateSize();
}
@Override
public int characteristics() {
return source == null ? SIZED : source.characteristics() & ORDERED;
}
@Override
public void accept(T t) {
this.acc = accumulator.apply(this.acc, t);
}
}
public static U reduceWithCancel(Stream stream, U identity,
BiFunction accumulator, BinaryOperator combiner,
Predicate cancelPredicate) {
return StreamSupport
.stream(new CancellableReduceSpliterator<>(stream.spliterator(), identity,
accumulator, cancelPredicate), stream.isParallel()).reduce(combiner)
.orElse(identity);
}
public static T reduceWithCancel(Stream stream, T identity,
BinaryOperator combiner, Predicate cancelPredicate) {
return reduceWithCancel(stream, identity, combiner, combiner, cancelPredicate);
}
我们来测试这两个版本,并计算实际处理的元素数量。让我们把0接近尾声。例外版本:
AtomicInteger count = new AtomicInteger();
int product = reduceWithCancelEx(
IntStream.range(-1000000, 100).filter(x -> x == 0 || x % 2 != 0)
.parallel().peek(i -> count.incrementAndGet()), 1,
(a, b) -> a * b, x -> x == 0);
System.out.println("product: " + product + "/count: " + count);
Thread.sleep(1000);
System.out.println("product: " + product + "/count: " + count);
典型输出:
product: 0/count: 281721
product: 0/count: 500001
所以当只处理一些元素时返回结果,这些任务在后台继续工作,计数器仍在增加。这里是spliterator版本:
AtomicInteger count = new AtomicInteger();
int product = reduceWithCancel(
IntStream.range(-1000000, 100).filter(x -> x == 0 || x % 2 != 0)
.parallel().peek(i -> count.incrementAndGet()).boxed(),
1, (a, b) -> a * b, x -> x == 0);
System.out.println("product: " + product + "/count: " + count);
Thread.sleep(1000);
System.out.println("product: " + product + "/count: " + count);
典型输出:
product: 0/count: 281353
product: 0/count: 281353
当返回结果时,所有的任务实际上完成了。