Issue
I am trying to use Java 8 Stream
s 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 User
s 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
- Collecting our objects in a
List
with theCollectors.toList()
collector. - Applying an extra finisher at the end, that returns the single element — or throws an
IllegalStateException
iflist.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);
Answered By - skiwi