我正在尝试弄清楚如何在Java 8 Stream上实现自定义的中间操作。看来我被封锁了:(
具体来说,我想获取一个流并返回直到的所有条目,并包括第一个具有特定值的条目。而且我想在此之后停止生成任何内容-使其短路。
它正在对输入数据进行一系列验证检查。如果有一个错误,我想停在第一个错误上,但是我想在路上整理警告。而且由于这些验证检查可能很昂贵-例如涉及数据库查找-我只想运行所需的最小设置。
因此代码将类似于:
Optional<ValidationResult> result = validators.stream()
.map(validator -> validator.validate(data))
.takeUntil(result -> result.isError()) // This is the bit I can't do
.reduce(new ValidationResult(), ::mergeResults);
似乎我应该能够使用ReferencePipeline.StatefulOp做一些事情,除了它是所有包范围之外,因此我无法对其进行扩展。所以我想知道实现这一目标的正确方法是什么?还是有可能?
还请注意-这需要使用Java 8,而不是9+,因为出于各种不相关的原因我们还没有出现。
欢呼
答案 0 :(得分:1)
通常,自定义操作将需要处理Spliterator
接口。它通过添加特征和大小信息以及将元素的一部分作为另一个分隔符(因此得名)的功能来扩展Iterator
的概念。它也只需要一种方法就简化了迭代逻辑。
public static <T> Stream<T> takeWhile(Stream<T> s, Predicate<? super T> condition) {
boolean parallel = s.isParallel();
Spliterator<T> spliterator = s.spliterator();
return StreamSupport.stream(new Spliterators.AbstractSpliterator<T>(
spliterator.estimateSize(),
spliterator.characteristics()&~(Spliterator.SIZED|Spliterator.SUBSIZED)) {
boolean active = true;
Consumer<? super T> current;
Consumer<T> adapter = t -> {
if((active = condition.test(t))) current.accept(t);
};
@Override
public boolean tryAdvance(Consumer<? super T> action) {
if(!active) return false;
current = action;
try {
return spliterator.tryAdvance(adapter) && active;
}
finally {
current = null;
}
}
}, parallel).onClose(s::close);
}
要保留流的属性,我们首先查询并行状态,然后为新流重新建立并行状态。另外,我们注册了一个close操作,它将关闭原始流。
主要工作是实现一个Spliterator
装饰前一个流状态的分隔符。
保留SIZED
和SUBSIZED
之外的特征,因为我们的操作导致不可预测的大小。原始大小仍会通过,现在将用作估计值。
此解决方案在操作期间存储传递给Consumer
的{{1}},以便能够使用同一适配器使用者,而避免为每次迭代创建一个新的适配器使用者。之所以可行,是因为可以确保不会同时调用tryAdvance
。
并行化是通过拆分完成的,拆分是从tryAdvance
继承的。这种继承的实现将缓冲某些元素,这是合理的,因为为AbstractSpliterator
之类的操作实现更好的策略确实很复杂。
因此您可以像使用它
takeWhile
将打印
takeWhile(Stream.of("foo", "bar", "baz", "hello", "world"), s -> s.length() == 3)
.forEach(System.out::println);
或
foo
bar
baz
将打印
takeWhile(Stream.of("foo", "bar", "baz", "hello", "world")
.peek(s -> System.out.println("before takeWhile: "+s)), s -> s.length() == 3)
.peek(s -> System.out.println("after takeWhile: "+s))
.forEach(System.out::println);
表明它没有处理超出必要的部分。在before takeWhile: foo
after takeWhile: foo
foo
before takeWhile: bar
after takeWhile: bar
bar
before takeWhile: baz
after takeWhile: baz
baz
before takeWhile: hello
阶段之前,我们必须遇到第一个不匹配的元素,之后,我们只会遇到直到该元素为止的元素。
答案 1 :(得分:1)
我承认代码明智,霍尔格的答案要性感得多,但也许这在某种程度上更易于阅读:
public static <T> Stream<T> takeUntilIncluding(Stream<T> s, Predicate<? super T> condition) {
class Box implements Consumer<T> {
boolean stop = false;
T t;
@Override
public void accept(T t) {
this.t = t;
}
}
Box box = new Box();
Spliterator<T> original = s.spliterator();
return StreamSupport.stream(new AbstractSpliterator<>(
original.estimateSize(),
original.characteristics() & ~(Spliterator.SIZED | Spliterator.SUBSIZED)) {
@Override
public boolean tryAdvance(Consumer<? super T> action) {
if (!box.stop && original.tryAdvance(box) && condition.test(box.t)) {
action.accept(box.t);
return true;
}
box.stop = true;
return false;
}
}, s.isParallel());
}
答案 2 :(得分:0)
您可以使用以下结构;
AtomicBoolean gateKeeper = new AtomicBoolean(true);
Optional<Foo> result = validators.stream()
.filter(validator -> gateKeeper.get()
&& gateKeeper.compareAndSet(true, !validator.validate(data).isError())
&& gateKeeper.get())
.reduce(...) //have the first n non-error validators here
带有gateKeeper
的过滤器用作短路逻辑,并一直运行直到遇到第一个isError() == true
情况,然后拒绝它,然后关闭此后的其他validate()
调用上。它看起来有些疯狂,但是比其他 custom 实现要简单得多,并且如果满足您的要求,它可能会完美地工作。
不是100%不确定这是否有帮助,因为除了validator.validate(data)
的结果之外,我忽略了isError()
的结果以及它属于列表中任何validator
的事实。
答案 3 :(得分:-1)
您可以使用技巧:
List<ValidationResult> res = new ArrayList<>(); // Can modify it with your `mergeResults` instead of list
Optional<ValidationResult> result = validators.stream()
.map(validator -> validator.validate(data))
.map(v -> {
res.add(v);
return v;
})
.filter(result -> result.isError())
.findFirst();
List<ValidationResult> res
将包含您感兴趣的值。