在Java 8 Streams上实现自定义中间操作

时间:2019-06-14 05:55:17

标签: java java-stream

我正在尝试弄清楚如何在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+,因为出于各种不相关的原因我们还没有出现。

欢呼

4 个答案:

答案 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装饰前一个流状态的分隔符。

保留SIZEDSUBSIZED之外的特征,因为我们的操作导致不可预测的大小。原始大小仍会通过,现在将用作估计值。

此解决方案在操作期间存储传递给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将包含您感兴趣的值。