【问题标题】:Is there a way to check if a Stream contains all collection elements?有没有办法检查 Stream 是否包含所有集合元素?
【发布时间】:2020-02-04 18:05:31
【问题描述】:

例如,我需要类似的东西:

Collection<String> collection = /* ... */;
Stream<Object> stream = /* ... */;
boolean containsAll = stream.map(Object::toString).containsAll(collection);

当然,我可以使用collect() 方法和调用Collection.containsAll() 将流的所有元素累积到另一个Collection 中,但是如果流太大并且处理其所有元素效率低下怎么办?

【问题讨论】:

  • 我个人认为实际上最好选择Set&lt;String&gt; temp = source.stream().map(Object::toString).collect(toSet()); boolean containsAll = temp.containsAll(collection);
  • @OusmaneD。如果 流太大 如 OP 所假设的那样怎么办?想象一下stream 是延迟生成的并且并非所有元素都保存在内存中的情况。例如。使用Files::lines,您可以处理 huge 文件,即使它们无法放入您的记忆中。在这种情况下,收集到一个集合中会导致OutOfMemoryError

标签: java collections java-8 java-stream contains


【解决方案1】:

Collection&lt;String&gt;创建一个Set,让搜索操作更快O(1)

Set<String> set = new HashSet<>(collection);

然后使用allMatch检查流中的每个项目是否包含在集合中

boolean containsAll = stream.map(Object::toString)
                            .allMatch(s -> set.contains(s));

另一种方法是过滤不包含在集合中并使用limit(1)进行优化

boolean isContains = stream.map(Object::toString)
                           .filter(s -> !set.contains(s))
                           .limit(1)
                           .count() > 0;

【讨论】:

    【解决方案2】:

    这应该可以解决问题:

    Set<String> set = new HashSet<>(collection);
    boolean containsAll = set.isEmpty() || stream.map(Object::toString)
                                                 .anyMatch(s -> set.remove(s) && set.isEmpty());
    

    解决方案可能看起来令人困惑,但想法很简单:

    1. 为了防止对collection 进行多次迭代,我们将其包装成HashSet。 (如果您的 stream 是并行的,那么您将不得不使用并发哈希集。有关详细信息,请参阅 this post
    2. 如果collection(或set)为空,则返回true而不处理stream
    3. 对于stream 的每个条目,我们尝试将其从set 中删除。如果Set::remove的结果是true(因此它被set包含)并且set在删除后为空,我们可以得出结论stream包含初始collection的所有元素。
    4. 终端操作Stream::anyMatch是一种短路操作。因此,一旦set 为空,它将停止迭代stream。在最坏的情况下,我们将处理整个流。

    也许这是一种更易读的形式:

    Set<String> set = new HashSet<>(collection);
    boolean containsAll = set.isEmpty() || stream.map(Object::toString)
                                                 .filter(set::remove)
                                                 .anyMatch(__ -> set.isEmpty());
    

    如果collection 可以包含重复项,并且需要检查stream 是否包含所有重复项,那么我们将需要维护一个并发的计数器映射。

    Map<String, AtomicLong> map = new ConcurrentHashMap<>();
    collection.forEach(s -> map.computeIfAbsent(s, __ -> new AtomicLong()).incrementAndGet());
    boolean containsAll = map.isEmpty() || stream.map(Object::toString)
                                                 .filter(map::containsKey)
                                                 .filter(s -> map.get(s).decrementAndGet() == 0)
                                                 .filter(s -> map.remove(s) != null)
                                                 .anyMatch(__ -> map.isEmpty());
    

    代码略有改动,但思路是一样的。

    【讨论】:

    • 似乎您也可以将其包装在 List 中以支持检查流是否包含元素的多个实例。 List.remove 将返回 true,直到删除所有副本。
    • @SeanVanGorder 实际上,这是一个非常有趣的案例。为了简单起见,我在我的解决方案中忽略了这一点。但是使用List 我们将失去HashSet 提供的性能提升。
    • 在这种情况下,您可以检查原始集合是否为Set 并将其包装在HashSet 而不是ArrayList 中。这种地图计数预处理似乎是一种权衡,对于大型非Set 集合可能更快,但对于小型集合可能更糟。
    • 这是有道理的。编程就是权衡取舍。不同的数据结构适用于不同的情况。仍然由软件工程师来选择更适合他/她试图解决的任务的那个。
    • 这让我大吃一惊。我从没想过是否像这样使用流。非常感谢。来自大学生
    【解决方案3】:

    不管Stream 有多大,如果它不包含Collection 的所有元素,您都必须处理它的所有元素。

    如果Stream 的一个小前缀包含Collection 的所有元素,并且CollectionStream 小得多,则可以节省处理时间。

    boolean containsAll = 
        stream.map(Object::toString)
              .filter(s -> collection.contains(s)) // it would be wise to convert collection to a Set
              .limit(collection.size())
              .count() == collection.size();
    

    请注意,如果Stream 可能包含Collection 的同一元素的多个副本,您可能需要在filter() 之后添加.distinct() 操作。

    【讨论】:

    • 我真的不明白在这种情况下不使用collect 优于任何其他操作(count) 的好处。由于对完整元素的流处理无论如何都会发生(最坏的情况),是否有可能的优化来避免使用这么多内存?
    • @Naman 是否会处理所有元素,例如,如果流有 1000000 个元素但集合的所有元素都出现在流的前 50 个元素中?当然,如果不是 Collection 的所有元素都出现在 Stream 中(或者如果它们中的最后一个出现在 Stream 的末尾附近),则此解决方案将无济于事。
    • 如果流中有一些空条目怎么办?如果对流和集合进行排序,处理containsAll会更快吗?
    • @Eran By containsAll 我的意思是你的方法,查找集合是否包含流的所有元素。感谢您的澄清。
    • @Paul 我误解了你的问题。如果 Stream 和 Collection 被排序,即使 Stream 不包含 Collection 的所有元素,也应该可以做出有效的解决方案(不处理整个 Stream),因为我们可以识别 Stream 中的点之后不能再有属于该集合的元素。这可以通过takeWhile 完成。
    猜你喜欢
    • 1970-01-01
    • 2010-12-06
    • 2016-05-24
    • 1970-01-01
    • 2021-12-04
    • 1970-01-01
    • 2019-11-03
    • 2021-06-20
    • 2011-04-08
    相关资源
    最近更新 更多