有没有办法检查 Stream 是否包含所有集合元素?

Is there a way to check if a Stream contains all collection elements?

例如,我需要这样的东西:

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

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

无论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() 操作。

这应该可以解决问题:

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)为空那么我们return true不处理stream
  3. 对于 stream 的每个条目,我们尝试将其从 set 中删除。如果 Set::remove 的结果是 true(因此它被 set 包含)并且 set 在删除后为空,我们可以得出结论 stream 包含初始 collection.
  4. 的所有元素
  5. 终端操作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());

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

Collection<String> 创建一个集合以加快搜索操作 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;