【问题标题】:Special behavior of a stream if there are no elements没有元素时流的特殊行为
【发布时间】:2016-02-23 14:41:27
【问题描述】:

如何使用 java8 流式 API 来表达这一点?

我想为流的每个项目执行itemConsumer。如果有 没有我想执行的项目emptyAction。

当然可以这样写:

Consumer<Object> itemConsumer = System.out::println;
Runnable emptyAction = () -> {System.out.println("no elements");};

Stream<Object> stream = Stream.of("a","b"); // or Stream.empty()
List<Object> list = stream.collect(Collectors.toList());
if (list.isEmpty())
    emptyAction.run();
else
    list.stream().forEach(itemConsumer);

但我宁愿避免使用任何Lists。

我还考虑过在 peek 方法中设置一个标志 - 但该标志不是最终的,因此不允许。使用布尔容器似乎也是一种解决方法。

【问题讨论】:

标签: java java-stream


【解决方案1】:

您可以强制 reduce 执行此操作。逻辑是减少false,如果遇到任何有用的数据,将值设置为true。

reduce 的结果是false,然后没有遇到任何项目。如果遇到任何项目,则结果将是true:

boolean hasItems = stream.reduce(false, (o, i) -> {
    itemConsumer.accept(i);
    return true;
}, (l, r) -> l | r);
if (!hasItems) {
    emptyAction.run();
}

这应该适用于并行流,因为任何遇到项目的流都会将值设置为true。

不过,我不确定我是否喜欢这个,因为它对 reduce 操作的使用略显迟钝。

另一种方法是将AtomicBoolean 用作可变的boolean 容器:

final AtomicBoolean hasItems = new AtomicBoolean(false);
stream.forEach(i -> {
    itemConsumer.accept(i);
    hasItems.set(true);
});
if (!hasItems.get()) {
    emptyAction.run();
}

我不知道我是否更喜欢这个。

最后,你可以让你的itemConsumer记住状态:

class ItemConsumer implements Consumer<Object> {

    private volatile boolean hasConsumedAny;

    @Override
    public void accept(Object o) {
        hasConsumedAny = true;
        //magic magic
    }

    public boolean isHasConsumedAny() {
        return hasConsumedAny;
    }
}

final ItemConsumer itemConsumer = new ItemConsumer();
stream.forEach(itemConsumer::accept);
if (!itemConsumer.isHasConsumedAny()) {
    emptyAction.run();
}

这看起来有点整洁,但可能不实用。所以也许是装饰器模式 -

class ItemConsumer<T> implements Consumer<T> {

    private volatile boolean hasConsumedAny;
    private final Consumer<T> delegate;

    ItemConsumer(final Consumer<T> delegate) {
        this.delegate = delegate;
    }

    @Override
    public void accept(T t) {
        hasConsumedAny = true;
        delegate.accept(t);
    }

    public boolean isHasConsumedAny() {
        return hasConsumedAny;
    }
}

final ItemConsumer<Object> consumer = new ItemConsumer<Object>(() -> /** magic **/);

TL;DR: something 必须记住你在消费Stream 的过程中是否遇到过任何事情,是这样的:

  • Stream 本身在reduce 的情况下;
  • AtomicBoolean;或
  • 消费者

从逻辑的角度来看,我认为消费者可能处于最佳位置。

【讨论】:

  • 当使用AtomicBoolean 时,我更喜欢使用peek - 但这只是个人喜好。感谢您提出的所有解决方法 - 我为我当前的项目选择了第二个。谢谢!
  • @Boris 您是否有理由在 reduce 运算符中选择非短路 | 而不是 ||?我认为它的工作方式不会有所不同,但我很少使用|,并且想知道我错过的行为是否有所不同。
  • @Joshua 短路有什么用?
  • 正如我所指出的,我认为无论哪种方式的行为都是等效的,并且想要确定。
【解决方案2】:

没有任何附加变量的解决方案:

stream.peek(itemConsumer).reduce((a, b) -> a).orElseGet(() -> {
    emptyAction.run();
    return null;
});

请注意,如果流是并行的,则可以同时为不同线程中的不同元素调用itemConsumer(例如在forEach 中,而不是在forEachOrdered 中)。如果第一个流元素为空,此解决方案也将失败。

【讨论】:

  • 要修复 null 第一个元素问题,您可以将 reduce 更改为例如reduce((a, b) -&gt; true).
  • @DidierL,是的,适用于对象流。如果您有其他内容,您可能需要先执行 unsafe cast(或添加类似.map(Object.class::cast) 的内容)。
  • 现在想象一个足够聪明的实现可以识别.reduce((a, b) -&gt; a)在语义上与“获取第一个元素”相同并缩短操作......
  • @Holger,当 Java 变得足够聪明可以识别这样的事情时,我很乐意删除我的答案。不幸的是,在可观察的未来,我们不能希望如此......
  • 关键点是,peek 的执行是依赖于实现的流处理的产物,而 peek(…).count() 的问题仅在 Java 9 中 一个 示例依赖关于peek 的假设可能会在将来破坏您的代码。你可能是对的,这样的优化还很遥远,但这让我怀疑你是否会记住这个答案并删除它然后。我认为任何基于peek 的解决方案至少应该对其可能的短期性质作出免责声明。
【解决方案3】:

有一个简单直接的解决方案:

Spliterator<Object> sp=stream.spliterator();
if(!sp.tryAdvance(itemConsumer))
    emptyAction.run();
else
    sp.forEachRemaining(itemConsumer);

如果您愿意,您甚至可以在第一个元素之后保持对元素的并行支持:

Spliterator<Object> sp=stream.parallel().spliterator();
if(!sp.tryAdvance(itemConsumer))
    emptyAction.run();
else
    StreamSupport.stream(sp, true).forEach(itemConsumer);

在我看来,它更容易理解为基于reduce 的解决方案。

【讨论】:

  • 并行版这里有问题。在 Java-8 中,如果您有上游 flatMap 操作,则输出中可​​能有乱序元素(请参阅 my question)。如果不需要顺序可能没问题,但在 Java-9 中,trySplit() 在有任何上游中间操作时在tryAdvance() 之后完全禁用。见this discussion。
  • @Tagir Valeev:既然你提到了它,我记得那个问题,正如我在那里的回答中所说,我必须在我自己的实现中处理这个问题。不过,我的实现得到了一个直接的修复,现在返回正确的顺序而不是禁用拆分。好吧,我的flatMap 也很懒惰,所以似乎存在根本差异。无论如何,不​​应该将特定流实现的错误和缺陷归咎于上述解决方案。
【解决方案4】:

你可以这样做:

if(stream.peek(itemConsumer).count() == 0){
    emptyAction.run();
}

但如果count 知道 Java 9 中 Stream 的大小(请参阅here),它似乎可以更改为跳过 peek,所以如果你希望它在未来工作,你可以使用:

if(stream.peek(itemConsumer).mapToLong(e -> 1).sum() == 0){
    emptyAction.run();
}

【讨论】:

  • 这是所有基于peek 的解决方案滥用 API 的普遍问题。 peek 仅用于调试处理,如果有办法在没有处理元素的情况下解决任务,则可以跳过它们。所以理论上,一个实现可以足够聪明地优化.mapToLong(e -&gt; 1).sum() 来做与count() 相同的事情。甚至更聪明地认识到,与==0 的后续比较意味着您只需要知道是否至少存在一个元素。今天不存在这样的优化,但它们的缺失并不是一种保证行为。
【解决方案5】:

另一个使用reduce的尝试:

    Stream<Object> stream = Stream.of("a","b","c");
    //Stream<Object> stream = Stream.empty();


    Runnable defaultRunnable = () -> System.out.println("empty Stream");
    Consumer<Object> printConsumer = System.out::println;

    Runnable runnable = stream.map(x -> toRunnable(x, printConsumer)).reduce((a, b) -> () -> {
        a.run();
        b.run();
    }).orElse(defaultRunnable);

    runnable.run(); // prints a, b, c (or empty stream when it is empty)


    // for type inference
    static <T> Runnable toRunnable(T t, Consumer<T> cons){ 
        return ()->cons.accept(t);
    }

这种方法不使用peek() 哪个according to Javadoc "mainly exists to support debugging"

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-20
    • 1970-01-01
    • 2020-08-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多