Search code examples
javacollectionsjava-8java-streamcontains

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


For example, I need something like:

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

Of course, I could accumulate all elements of the stream into another Collection using collect() method and the call Collection.containsAll(), but what if the stream is too big and it is inefficient to process all its elements?


Solution

  • This should do the trick:

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

    The solution might look confusing, but the idea is straightforward:

    1. In order to prevent multiple iterations over collection we wrap it into a HashSet. (In case your stream is a parallel one, then you will have to use a concurrent hash set. See this post for more details)
    2. If the collection (or set) is empty then we return true without processing the stream
    3. For each entry of stream we try to remove it from set. In case the result of Set::remove is true (hence it was contained by set) and the set is empty after removal, we can conclude that stream contained all the elements of initial collection.
    4. The terminal operation Stream::anyMatch is a short-circuiting one. So it will stop iterating over stream once the set is empty. In worst case we will process the entire stream.

    Perhaps this is a bit more readable form:

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

    If the collection can contain duplicates and there is a requirement to check if stream contains all of them, then we will need to maintain a concurrent map of counters.

    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());
    

    The code slightly changed but the idea is the same.