【问题标题】:Can sequential stream operations have side-effects?顺序流操作会产生副作用吗?
【发布时间】:2016-12-11 02:00:46
【问题描述】:

我正在尝试将有限数量的值流式传输到一个集合中,但我需要在应用限制之前验证它们是新元素。例如:

Set<Integer> destination = ... 
Set<Integer> source = ...
source.stream()
        .filter(i -> !destination.contains(i))
        .limit(10)
        .forEach(destination::add);

但是多余的contains() 检查让我很困扰,因为add() 可以添加元素 报告它是否是集合中的新元素。所以我想这样做:

source.stream()
        .filter(destination::add)
        .limit(10)
        .forEach(i -> {});    // no-op terminal operation to force evaluation

忽略hacky终端操作,使用带有副作用的过滤器操作存在问题,通常不鼓励这样做。我理解为什么使用map()filter() 对并行流有副作用是不安全的。我的问题是,在这种情况下,在顺序流上是否可以接受?如果没有,为什么不呢?

【问题讨论】:

    标签: java java-stream side-effects


    【解决方案1】:

    副作用和顺序流没有根本问题,但上面的第二个实现是无效的,因为流 API 不保证每个阶段都会在每个元素上依次执行.

    在第二个实现中,在应用限制之前,可能会向destination 添加超过 10 个元素。您的无操作 forEach 只会看到 10 个,但您最终可能会在集合中获得更多。

    除了流之外,java 还具有循环结构,如 forwhile,可以轻松表达此类内容。

    如果你必须使用流,你可以这样做:

    int maxSize = destination.size()+10;
    source.stream().allMatch(x -> destination.size()<maxsize && (destination.add(x)||true));
    

    只要谓词返回 false,allMatch 就会停止迭代。

    【讨论】:

    • 我认为你有点误解了我的情况。 source 是一个集合,所以它不包含重复的元素。我正在尝试确保我们正在添加来自source 的元素,而这些元素已经 存在于destination 中。所以我认为第一个解决方案仍然有效。但是您的总体观点很有趣,即流元素可能不会一次穿过整个管道。您是否有这方面的来源,或者您是否依赖于缺乏相反的文档?
    • 缺乏相反的文档,事实上,流上的许多优化都涉及块中的处理元素。您编写的代码今天似乎可以工作,但它依赖于在新 Java 版本或其他实现中可能会发生很大变化的事情。
    • 我确实误解了检查的目的。我相应地编辑了答案
    • 实际上documentation 确实明确表示某些有状态操作(如sorted())必须同时应用于多个元素,我想说某些实现可能选择对于无状态操作也是如此。除非我找到相反的证据,否则接受这个答案。
    • 如果你想要一个失败的例子:使用Stream.of(source).flatMap(Set::stream) .filter(destination::add).limit(10).forEach(i -&gt; {});,限制无效。 (当然forEach的动作最多被正确调用了十次)
    【解决方案2】:

    这对你有用吗:

    package be.objectsmith;
    
    import java.util.Arrays;
    import java.util.HashSet;
    import java.util.Set;
    import java.util.stream.Collectors;
    import java.util.stream.IntStream;
    
    public class Playground {
        public static void main(String[] args) {
            copy(
                IntStream.range(1, 20).boxed().collect(Collectors.toSet()),
                new HashSet<>(Arrays.asList(2, 5)));
            copy(
                IntStream.range(1, 5).boxed().collect(Collectors.toSet()),
                new HashSet<>(Arrays.asList(2, 5)));
        }
    
        private static void copy(Set<Integer> source, Set<Integer> destination) {
            source
                .stream()
                .map(destination::add)
                .filter(resultOfAdding -> resultOfAdding)
                .limit(10)
                .collect(Collectors.toList());  // Need a terminal operation
            System.out.println("source = " + source);
            System.out.println("destination = " + destination);
        }
    
    }
    

    运行此类将打印:

    source = [1, 2, 3, 4]
    destination = [1, 2, 3, 4, 5]
    source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
    destination = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
    

    如您所见,它只添加了 10 个元素。 第二个调用表明,如果要添加的元素少于 10 个,它也可以工作。

    【讨论】:

    • 这只是问题中第二个实现的更复杂的版本,将 filter(destination::add) 分解为两个操作。
    • @MattTimmermans 这是对第二个实现的修复,它完全符合 OP 的要求:利用 Set.add() 本身报告它是否是新元素的事实。然后它只需要计数并限制成功添加。我什至会用.count() 替换终端操作。总而言之,我仍然更喜欢第一个实现; add 报告成功是一种方便,它可以更优雅地表达某些情况,但是在这里我只看到增加的不便
    • @bowmore 它修复了什么?原版也适用于给定的测试用例,利用 Set::add 的返回,并且没有问题不在这个答案中。
    • @MattTimmermans 又看了一遍原版,你是对的
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-07-07
    • 2013-05-09
    • 2012-05-29
    • 1970-01-01
    • 1970-01-01
    • 2020-09-09
    • 1970-01-01
    相关资源
    最近更新 更多