Another approach is to make a mutable result container which collects the results, prioritizing any found id over any found name.
First, let's define such a result container:
public class OrderedKeyMatcher<T> {
private int predicateLevel;
private T result;
private final List<Predicate<T>> predicates;
public OrderedKeyMatcher(Predicate<T>... predicates) {
this.predicates = List.of(predicates);
this.predicateLevel = predicates.length;
}
public OrderedKeyMatcher<T> accept(T t) {
for (int i = 0; i < predicateLevel; i++) {
if (predicates.get(i).test(t)) {
predicateLevel = i;
result = t;
break;
}
}
return this;
}
public boolean isDefinitelyFound() {
return predicateLevel == 0;
}
public Optional<T> result() {
return Optional.ofNullable(result);
}
}
In short, this is how it works:
- In that order, the
predicates field holds the predicates to which the students are tested.
- The
result field holds any found student.
- The
predicateLevel holds an integer, which keeps track of what predicate matched the last result. This is because of the following: if we found a student with the given name, we don't need to search any further for students with the given name, because we already found one; however, we still want to try to find a student with the given id.
- Further, if a student is matched by the first predicate, we can tell the stream to stop altogether because we have definitely found a student matching or predicates. The
isDefinitelyFound() method can be used for this.
This is then what the stream looks like:
var matcher = new OrderedKeyMatcher<Student>(
s -> Objects.equals(s.id(), id),
s -> Objects.equals(s.name(), name)
);
Optional<Student> optStudent = studentDao.getStudent().stream()
.takeWhile(unused -> !matcher.isDefinitelyFound())
.reduce(matcher, OrderedKeyMatcher::accept, (a, b) -> a)
.result();
takeWhile is used to short-circuit the stream if a result is found. As noted above, if we find a student with a matching name, then we still need to inspect other elements to see if there's a student with a matching id, but once we find a student with a matching id, then we can tell the stream to terminate.
reduce collects the elements into our result container.1
- At last, with
result() we can collect the result as an Optional.
Example
Let's take a look at the following list:
List.of(
new Student("11", "John"),
new Student("13", "Jill"),
new Student("17", "Jack"),
new Student("17", "Anthony"),
new Student("23", "Bonnie"),
new Student("47", "Megan")
);
Suppose we are trying to match id = "23" and name = "Jack". The expected and actual result is that we'll find student Bonnie because she has id 23. Now let's say we'll remove Bonnie from the list, and search again. The result is that we find Jack.
1. Note that I used (a, b) -> a as a merge function, which is, of course, a dummy merge function, as it drops at least half of the results.