lambda - Filter Java Stream to 1 and only 1 element


Translate

I am trying to use Java 8 Streams to find elements in a LinkedList. I want to guarantee, however, that there is one and only one match to the filter criteria.

Take this code:

public static void main(String[] args) {

    LinkedList<User> users = new LinkedList<>();
    users.add(new User(1, "User1"));
    users.add(new User(2, "User2"));
    users.add(new User(3, "User3"));

    User match = users.stream().filter((user) -> user.getId() == 1).findAny().get();
    System.out.println(match.toString());
}

static class User {

    @Override
    public String toString() {
        return id + " - " + username;
    }

    int id;
    String username;

    public User() {
    }

    public User(int id, String username) {
        this.id = id;
        this.username = username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public int getId() {
        return id;
    }
}

This code finds a User based on their ID. But there are no guarantees how many Users matched the filter.

Changing the filter line to:

User match = users.stream().filter((user) -> user.getId() < 0).findAny().get();

Will throw a NoSuchElementException (good!)

I would like it to throw an error if there are multiple matches, though. Is there a way to do this?


All Answers
  • Translate

    Create a custom Collector

    public static <T> Collector<T, ?, T> toSingleton() {
        return Collectors.collectingAndThen(
                Collectors.toList(),
                list -> {
                    if (list.size() != 1) {
                        throw new IllegalStateException();
                    }
                    return list.get(0);
                }
        );
    }
    

    We use Collectors.collectingAndThen to construct our desired Collector by

    1. Collecting our objects in a List with the Collectors.toList() collector.
    2. Applying an extra finisher at the end, that returns the single element — or throws an IllegalStateException if list.size != 1.

    Used as:

    User resultUser = users.stream()
            .filter(user -> user.getId() > 0)
            .collect(toSingleton());
    

    You can then customize this Collector as much as you want, for example give the exception as argument in the constructor, tweak it to allow two values, and more.

    An alternative — arguably less elegant — solution:

    You can use a 'workaround' that involves peek() and an AtomicInteger, but really you shouldn't be using that.

    What you could do istead is just collecting it in a List, like this:

    LinkedList<User> users = new LinkedList<>();
    users.add(new User(1, "User1"));
    users.add(new User(2, "User2"));
    users.add(new User(3, "User3"));
    List<User> resultUserList = users.stream()
            .filter(user -> user.getId() == 1)
            .collect(Collectors.toList());
    if (resultUserList.size() != 1) {
        throw new IllegalStateException();
    }
    User resultUser = resultUserList.get(0);
    

  • Translate

    For the sake of completeness, here is the ‘one-liner’ corresponding to @prunge’s excellent answer:

    User user1 = users.stream()
            .filter(user -> user.getId() == 1)
            .reduce((a, b) -> {
                throw new IllegalStateException("Multiple elements: " + a + ", " + b);
            })
            .get();
    

    This obtains the sole matching element from the stream, throwing

    • NoSuchElementException in case the stream is empty, or
    • IllegalStateException in case the stream contains more than one matching element.

    A variation of this approach avoids throwing an exception early and instead represents the result as an Optional containing either the sole element, or nothing (empty) if there are zero or multiple elements:

    Optional<User> user1 = users.stream()
            .filter(user -> user.getId() == 1)
            .collect(Collectors.reducing((a, b) -> null));
    

  • Translate

    The other answers that involve writing a custom Collector are probably more efficient (such as Louis Wasserman's, +1), but if you want brevity, I'd suggest the following:

    List<User> result = users.stream()
        .filter(user -> user.getId() == 1)
        .limit(2)
        .collect(Collectors.toList());
    

    Then verify the size of the result list.

    if (result.size() != 1) {
      throw new IllegalStateException("Expected exactly one user but got " + result);
    User user = result.get(0);
    }
    

  • Translate

    Guava provides MoreCollectors.onlyElement() which does the right thing here. But if you have to do it yourself, you could roll your own Collector for this:

    <E> Collector<E, ?, Optional<E>> getOnly() {
      return Collector.of(
        AtomicReference::new,
        (ref, e) -> {
          if (!ref.compareAndSet(null, e)) {
             throw new IllegalArgumentException("Multiple values");
          }
        },
        (ref1, ref2) -> {
          if (ref1.get() == null) {
            return ref2;
          } else if (ref2.get() != null) {
            throw new IllegalArgumentException("Multiple values");
          } else {
            return ref1;
          }
        },
        ref -> Optional.ofNullable(ref.get()),
        Collector.Characteristics.UNORDERED);
    }
    

    ...or using your own Holder type instead of AtomicReference. You can reuse that Collector as much as you like.


  • Translate

    Use Guava's MoreCollectors.onlyElement() (JavaDoc).

    It does what you want and throws an IllegalArgumentException if the stream consists of two or more elements, and a NoSuchElementException if the stream is empty.

    Usage:

    import static com.google.common.collect.MoreCollectors.onlyElement;
    
    User match =
        users.stream().filter((user) -> user.getId() < 0).collect(onlyElement());
    

  • Translate

    The "escape hatch" operation that lets you do weird things that are not otherwise supported by streams is to ask for an Iterator:

    Iterator<T> it = users.stream().filter((user) -> user.getId() < 0).iterator();
    if (!it.hasNext()) 
        throw new NoSuchElementException();
    else {
        result = it.next();
        if (it.hasNext())
            throw new TooManyElementsException();
    }
    

    Guava has a convenience method to take an Iterator and get the only element, throwing if there are zero or multiple elements, which could replace the bottom n-1 lines here.


  • Translate

    Update

    Nice suggestion in comment from @Holger:

    Optional<User> match = users.stream()
                  .filter((user) -> user.getId() > 1)
                  .reduce((u, v) -> { throw new IllegalStateException("More than one ID found") });
    

    Original answer

    The exception is thrown by Optional#get, but if you have more than one element that won't help. You could collect the users in a collection that only accepts one item, for example:

    User match = users.stream().filter((user) -> user.getId() > 1)
                      .collect(toCollection(() -> new ArrayBlockingQueue<User>(1)))
                      .poll();
    

    which throws a java.lang.IllegalStateException: Queue full, but that feels too hacky.

    Or you could use a reduction combined with an optional:

    User match = Optional.ofNullable(users.stream().filter((user) -> user.getId() > 1)
                    .reduce(null, (u, v) -> {
                        if (u != null && v != null)
                            throw new IllegalStateException("More than one ID found");
                        else return u == null ? v : u;
                    })).get();
    

    The reduction essentially returns:

    • null if no user is found
    • the user if only one is found
    • throws an exception if more than one is found

    The result is then wrapped in an optional.

    But the simplest solution would probably be to just collect to a collection, check that its size is 1 and get the only element.


  • Translate

    An alternative is to use reduction: (this example uses strings but could easily apply to any object type including User)

    List<String> list = ImmutableList.of("one", "two", "three", "four", "five", "two");
    String match = list.stream().filter("two"::equals).reduce(thereCanBeOnlyOne()).get();
    //throws NoSuchElementException if there are no matching elements - "zero"
    //throws RuntimeException if duplicates are found - "two"
    //otherwise returns the match - "one"
    ...
    
    //Reduction operator that throws RuntimeException if there are duplicates
    private static <T> BinaryOperator<T> thereCanBeOnlyOne()
    {
        return (a, b) -> {throw new RuntimeException("Duplicate elements found: " + a + " and " + b);};
    }
    

    So for the case with User you would have:

    User match = users.stream().filter((user) -> user.getId() < 0).reduce(thereCanBeOnlyOne()).get();
    

  • Translate

    Using a Collector:

    public static <T> Collector<T, ?, Optional<T>> toSingleton() {
        return Collectors.collectingAndThen(
                Collectors.toList(),
                list -> list.size() == 1 ? Optional.of(list.get(0)) : Optional.empty()
        );
    }
    

    Usage:

    Optional<User> result = users.stream()
            .filter((user) -> user.getId() < 0)
            .collect(toSingleton());
    

    We return an Optional, since we usually can't assume the Collection to contain exactly one element. If you already know this is the case, call:

    User user = result.orElseThrow();
    

    This puts the burden of handeling the error on the caller - as it should.


  • Translate

    Guava has a Collector for this called MoreCollectors.onlyElement().


  • Translate

    Using reduce

    This is the simpler and flexible way I found (based on @prunge answer)

    Optional<User> user = users.stream()
            .filter(user -> user.getId() == 1)
            .reduce((a, b) -> {
                throw new IllegalStateException("Multiple elements: " + a + ", " + b);
            })
    

    This way you obtain:

    • the Optional - as always with your object or Optional.empty() if not present
    • the Exception (with eventually YOUR custom type/message) if there's more than one element

  • Translate

    We can use RxJava (very powerful reactive extension library)

    LinkedList<User> users = new LinkedList<>();
    users.add(new User(1, "User1"));
    users.add(new User(2, "User2"));
    users.add(new User(3, "User3"));
    
    User userFound =  Observable.from(users)
                      .filter((user) -> user.getId() == 1)
                      .single().toBlocking().first();
    

    The single operator throws an exception if no user or more then one user is found.


  • Translate

    As Collectors.toMap(keyMapper, valueMapper) uses a throwing merger to handle multiple entries with the same key it is easy:

    List<User> users = new LinkedList<>();
    users.add(new User(1, "User1"));
    users.add(new User(2, "User2"));
    users.add(new User(3, "User3"));
    
    int id = 1;
    User match = Optional.ofNullable(users.stream()
      .filter(user -> user.getId() == id)
      .collect(Collectors.toMap(User::getId, Function.identity()))
      .get(id)).get();
    

    You will get a IllegalStateException for duplicate keys. But at the end I am not sure if the code would not be even more readable using an if.


  • Translate

    I am using those two collectors:

    public static <T> Collector<T, ?, Optional<T>> zeroOrOne() {
        return Collectors.reducing((a, b) -> {
            throw new IllegalStateException("More than one value was returned");
        });
    }
    
    public static <T> Collector<T, ?, T> onlyOne() {
        return Collectors.collectingAndThen(zeroOrOne(), Optional::get);
    }
    

  • Translate

    If you don't mind using a 3rd party library, SequenceM from cyclops-streams (and LazyFutureStream from simple-react) both a have single & singleOptional operators.

    singleOptional() throws an exception if there are 0 or more than 1 elements in the Stream, otherwise it returns the single value.

    String result = SequenceM.of("x")
                              .single();
    
    SequenceM.of().single(); // NoSuchElementException
    
    SequenceM.of(1, 2, 3).single(); // NoSuchElementException
    
    String result = LazyFutureStream.fromStream(Stream.of("x"))
                              .single();
    

    singleOptional() returns Optional.empty() if there are no values or more than one value in the Stream.

    Optional<String> result = SequenceM.fromStream(Stream.of("x"))
                              .singleOptional(); 
    //Optional["x"]
    
    Optional<String> result = SequenceM.of().singleOptional(); 
    // Optional.empty
    
    Optional<String> result =  SequenceM.of(1, 2, 3).singleOptional(); 
    // Optional.empty
    

    Disclosure - I am the author of both libraries.


  • Translate

    I think this way is more simple:

    User resultUser = users.stream()
        .filter(user -> user.getId() > 0)
        .findFirst().get();
    

  • Translate

    I went with the direct-approach and just implemented the thing:

    public class CollectSingle<T> implements Collector<T, T, T>, BiConsumer<T, T>, Function<T, T>, Supplier<T> {
    T value;
    
    @Override
    public Supplier<T> supplier() {
        return this;
    }
    
    @Override
    public BiConsumer<T, T> accumulator() {
        return this;
    }
    
    @Override
    public BinaryOperator<T> combiner() {
        return null;
    }
    
    @Override
    public Function<T, T> finisher() {
        return this;
    }
    
    @Override
    public Set<Characteristics> characteristics() {
        return Collections.emptySet();
    }
    
    @Override //accumulator
    public void accept(T ignore, T nvalue) {
        if (value != null) {
            throw new UnsupportedOperationException("Collect single only supports single element, "
                    + value + " and " + nvalue + " found.");
        }
        value = nvalue;
    }
    
    @Override //supplier
    public T get() {
        value = null; //reset for reuse
        return value;
    }
    
    @Override //finisher
    public T apply(T t) {
        return value;
    }
    
    
    } 
    

    with the JUnit test:

    public class CollectSingleTest {
    
    @Test
    public void collectOne( ) {
        List<Integer> lst = new ArrayList<>();
        lst.add(7);
        Integer o = lst.stream().collect( new CollectSingle<>());
        System.out.println(o);
    }
    
    @Test(expected = UnsupportedOperationException.class)
    public void failOnTwo( ) {
        List<Integer> lst = new ArrayList<>();
        lst.add(7);
        lst.add(8);
        Integer o = lst.stream().collect( new CollectSingle<>());
    }
    
    }
    

    This implementation not threadsafe.


  • Translate

    Have you tried this

    long c = users.stream().filter((user) -> user.getId() == 1).count();
    if(c > 1){
        throw new IllegalStateException();
    }
    

    long count()
    Returns the count of elements in this stream. This is a special case of a reduction and is equivalent to:
    
         return mapToLong(e -> 1L).sum();
    
    This is a terminal operation.
    

    Source: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html