javalambdajava-8java-stream

Filter Java Stream to 1 and only 1 element


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?


Solution

  • 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 instead 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);