Reputation: 394
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.
Upvotes: 0
Views: 344
Reputation: 637
Here is one of the tricks you could play:
final ObjectA[] tmp= new ObjectA[1];
ObjectA match = listOfObjectA.stream()
.filter(oA -> oB.getProp1().equals(oA.getProp1()))
.peek(oA -> tmp[0] = tmp[0] == null ? oA : tmp[0])
.filter(oA -> oB.getProp2().equals(oA.getProp2()))
.peek(oA -> tmp[0] = oA)
.filter(oA -> oB.getProp3().equals(oA.getProp3()))
.findFirst().orElse(tmp[0]);
Or:
final ObjectA[] tmp= new ObjectA[2];
ObjectA match = listOfObjectA.stream()
.filter(oA -> oB.getProp1().equals(oA.getProp1()))
.peek(oA -> tmp[0] = oA)
.filter(oA -> oB.getProp2().equals(oA.getProp2()))
.peek(oA -> tmp[1] = oA)
.filter(oA -> oB.getProp3().equals(oA.getProp3()))
.findFirst().orElse(tmp[1] == null ? tmp[0] : tmp[1]);
Upvotes: 0
Reputation: 298213
As far as I understood your logic:
List<Predicate<ObjectA>> props = Arrays.asList(
oA -> oB.getProp1().equals(oA.getProp1()),
oA -> oB.getProp2().equals(oA.getProp2()),
oA -> oB.getProp3().equals(oA.getProp3()));
ObjectA previousChoice = null;
for(Predicate<ObjectA> p: props) {
listOfObjectA = listOfObjectA.stream().filter(p).collect(Collectors.toList());
if(listOfObjectA.isEmpty()) return previousChoice;
else {
previousChoice = listOfObjectA.get(0);
if(listOfObjectA.size() == 1) break;
}
}
return previousChoice;
or without streams:
listOfObjectA = new ArrayList<>(listOfObjectA);
ObjectA previousChoice = null;
for(Predicate<ObjectA> p: props) {
listOfObjectA.removeIf(p.negate());
if(listOfObjectA.isEmpty()) return previousChoice;
else {
previousChoice = listOfObjectA.get(0);
if(listOfObjectA.size() == 1) break;
}
}
return previousChoice;
This could also become generified to handle your two scenarios:
static ObjectB get(List<ObjectB> list, ObjectA oA) {
return get(list,
oB -> oA.getProp1().equals(oB.getProp1()),
oB -> oA.getProp2().equals(oB.getProp2()),
oB -> oA.getProp3().equals(oB.getProp3()));
}
static ObjectA get(List<ObjectA> list, ObjectB oB) {
return get(list,
oA -> oB.getProp1().equals(oA.getProp1()),
oA -> oB.getProp2().equals(oA.getProp2()),
oA -> oB.getProp3().equals(oA.getProp3()));
}
static <T> T get(List<T> listOfT, Predicate<T>... props) {
listOfT = new ArrayList<>(listOfT);
T previousChoice = null;
for(Predicate<T> p: props) {
listOfT.removeIf(p.negate());
if(listOfT.isEmpty()) return previousChoice;
else {
previousChoice = listOfT.get(0);
if(listOfT.size() == 1) break;
}
}
return previousChoice;
}
While the predicates look identical, they’re doing different things, assuming the ObjectA
and ObjectB
do not have a common base class defining these properties (otherwise it would be too simple). So this duplication is unavoidable. Trying to generify it even more using Function
delegates in the predicates, is unlikely to make the code simpler.
Upvotes: 4