I am trying to perform a "best match" on a list of objects. I thought to implement a cascading filter, with the goal to end up with only one object that eventually becomes the "best match". I have a list of ObjectA, and a single ObjectB which I am comparing the properties of. Is there a way to optionally apply a filter to a stream if there is more than one element?
Currently I have implemented it like this:
List<ObjectA> listOfObjectA;
ObjectB oB;
List<ObjectA> matchedByProp1 = listOfObjectA.stream()
.filter(oA -> oB.getProp1().equals(oA.getProp1())).collect(Collectors.toList());
if (matchedByProp1.isEmpty()) {
// If no objects match, then return null
return null;
} else if (matchedByProp1.size() == 1) {
// If one object matches prop1, this is easy
return matchedByProp1.stream().findFirst().orElse(null);
}
// If more than one object is left, filter further by prop2
List<ObjectA> matchedByProp2 = matchedByProp1.stream()
.filter(oA -> oB.getProp2().equals(oA.getProp2()))
.collect(Collectors.toList());
if (matchedByProp2.isEmpty()) {
// If further filtering is not successful, return one from the previous set
return matchedByProp1.stream().findFirst().orElse(null);
} else if (matchedByProp2.size() == 1) {
// If one object matches prop2, this is easy
return matchedByProp2.stream().findFirst().orElse(null);
}
// If more than one object is left, filter further by prop3
List<ObjectA> matchedByProp3 = matchedByProp2.stream()
.filter(oA -> oB.getProp3().equals(oA.getProp3()))
.collect(Collectors.toList());
if (matchedByProp3.isEmpty()) {
// If further filtering is not successful, return one from the previous set
return matchedByProp2.stream().findFirst().orElse(null);
} else if (matchedByProp3.size() == 1) {
// If one object matches prop3, this is easy
return matchedByProp3.stream().findFirst().orElse(null);
}
// We still have too many options, just choose one
return matchedByProp3.stream().findFirst().orElse(null);
This works for this scenario, but it seems like a lot of repeated code. Moreover, ObjectA and ObjectB can switch, so I have had to repeat this code twice, once for a list of ObjectA, and once for a list of ObjectB. What I would like to do is something more like this:
ObjectA match = listOfObjectA.stream()
.filter(oA -> oB.getProp1().equals(oA.getProp1()))
.optionallyFilter(oA -> oB.getProp2().equals(oA.getProp2()))
.optionallyFilter(oA -> oB.getProp3().equals(oA.getProp3()))
.getFirst().orElse(null);
I've tried implementing this approach as follows, but ran into an issue where I'm trying to consume the stream twice.
private class Matcher<T, U> {
private final U u;
private final Stream<T> stream;
public Matcher(U u) {
this.u = u;
stream = Stream.empty();
}
public Matcher(U u, Stream<T> stream) {
this.u = u;
this.stream = stream;
}
public Matcher<T, U> from(Stream<T> stream) {
return new Matcher<>(u, stream);
}
public Matcher<T, U> mustMatch(Function<T, Object> tProp, Function<U, Object> uProp) {
return new Matcher<>(u, stream.filter(t -> tProp.apply(t).equals(uProp.apply(u))));
}
public Matcher<T, U> shouldMatch(Function<T, Object> tProp, Function<U, Object> uProp) {
if (stream.filter(t -> tProp.apply(t).equals(uProp.apply(u))).count() > 0) {
return new Matcher<>(stream.filter(t -> tProp.apply(t).equals(uProp.apply(u))));
}
return this;
}
public Optional<T> get() {
return stream.findFirst();
}
}
ObjectA match = new Matcher<ObjectA, ObjectB>(oB, listOfObjectA.stream())
.mustMatch(ObjectA::getProp1, ObjectB::getProp1)
.shouldMatch(ObjectA::getProp2, ObjectB::getProp2)
.shouldMatch(ObjectA::getProp3, ObjectB::getProp3)
.get().orElse(null);
Now I could use a list collector in my Matcher class like I'm doing currently, but it seems like for just a simple condition collecting the stream into a list and re-streaming it seems unnecessary. Is there a better way of doing this? Note that in different uses of this there may be a different # of properties.