Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Remove and collect elements with Java streams

Suppose I have a Collection, and a Predicate that matches elements I'd like to remove from the Collection. But I don't just want to discard them, I want to move the matched elements into a new collection. I'd do something like this in Java 7:

List<E> removed = new LinkedList<>();
for (Iterator<E> i = data.iterator(); i.hasNext();) {
    E e = i.next();
    if (predicate.test(e)) {
        removed.add(e);
        i.remove();
    }
}

I'm curious if there's a streams / Java 8 way to do it. Collections.removeIf() unfortunately simply returns a boolean (not even a count of the number of removed elements? Too bad.) I envision something like this (though of course .removeAndYield(Predicate) doesn't exist):

List<E> removed = data.removeAndYield(predicate).collect(Collectors.toList());

Note: this question was inspired by a similar question; this question is about the more general case of getting a stream over the items removed from a collection. As pointed out in the linked question, the imperative solution may be more readable, but I'm curious if this is even possible with streams.

Edit: Clearly we can split the task into two separate steps, and assuming the appropriate data structures it will be efficient. The question is can this be done on arbitrary collections (which may not have efficient .contains() etc.).

like image 343
dimo414 Avatar asked May 05 '15 00:05

dimo414


People also ask

How do I remove a particular element from a Stream list?

Using Streams, we can apply lambda functions known as Predicates. To read more about Streams and Predicates, we have another article here. Internally, removeIf uses an Iterator to iterate over the list and match the elements using the predicate. We can now remove any matching elements from the list.

How do you remove an element from a collection in Java?

An element can be removed from a Collection using the Iterator method remove(). This method removes the current element in the Collection. If the remove() method is not preceded by the next() method, then the exception IllegalStateException is thrown.

How do I remove all elements from a collection?

clear() deletes every element from the collection and removeAll() one only removes the elements matching those from another Collection.


3 Answers

If you don't mind, let me bend your requirements a little bit. :-)

One characteristic of the desired result is that the matching elements should end up in one collection, and the non-matching elements should end up in a different collection. In the pre-Java-8 mutative world, the easiest way to think about getting a collection of non-matching elements is to remove the matching elements from the original collection.

But is removal -- modification of the original list -- an intrinsic part of the requirement?

If it isn't, then the result can be achieved via a simple partitioning operation:

Map<Boolean, List<E>> map = data.stream().collect(partitioningBy(predicate));

The result map is essentially two lists, which contain the matching (key = true) and non-matching (key = false) elements.

The advantage is that this technique can be done in one pass and in parallel if necessary. Of course, this creates a duplicate list of non-matching elements compared to removing the matches from the original, but this is the price to pay for immutability. The tradeoffs might be worth it.

like image 198
Stuart Marks Avatar answered Oct 17 '22 14:10

Stuart Marks


I'd keep it simple:

Set<E> removed = set.stream()
    .filter(predicate)
    .collect(Collectors.toSet());

set.removeAll(removed);
like image 20
Misha Avatar answered Oct 17 '22 15:10

Misha


If you want a functional way to do this, you could write your own method.

static <E> Set<E> removeIf(Collection<? extends E> collection, Predicate<? super E> predicate) {
    Set<E> removed = new HashSet<>();
    for (Iterator<? extends E> i = collection.iterator(); i.hasNext();) {
        E e = i.next();
        if (predicate.test(e)) {
            removed.add(e);
            i.remove();
        }
    }
    return removed;
}

This could be used to remove all odd numbers from a List.

Set<Integer> set = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5, 6));
Set<Integer> removed = removeIf(set, i -> i % 2 != 0);
System.out.println(set);
System.out.println(removed);
like image 4
Paul Boddington Avatar answered Oct 17 '22 15:10

Paul Boddington