I have to filter a Collection of Objects by a Map, which holds key value pairs of the Objects field names and field values. I am trying to apply all filters by stream().filt
It has nothing to do with filter. Actually the filter never work as per your code. Look at
//Does not apply the result
filterMap.forEach((key, value) -> list.stream()
.filter(testObject -> testObject.getProperty(key) == value)
.collect(Collectors.toList())
);
List has been filtered but nothing is changed here. No element has been deleted and No object address has been changed either. Try removeIf
// Does not apply the result
filterMap.forEach((key, value) -> list.removeIf(testObject -> testObject.getProperty(key) != value));
output is
1 2 3
A more general approach is to create a multi filter (Predicate
) which is concatenated using Predicate.and(...)
or Predicate.or(...)
. This is applicable to anything using Predicate
- first of all Stream
and Optional
.
Since the result is a Predicate
itself one can continue with Predicate.and(...)
or Predicate.or(...)
or with building more complex predicates using MultiPredicate
again.
class MultiPredicate {
public static <T> Predicate<T> matchingAll(Collection<Predicate<T>> predicates) {
Predicate<T> multiPredicate = to -> true;
for (Predicate<T> predicate : predicates) {
multiPredicate = multiPredicate.and(predicate);
}
return multiPredicate;
}
@SafeVarargs
public static <T> Predicate<T> matchingAll(Predicate<T> first, Predicate<T>... other) {
if (other == null || other.length == 0) {
return first;
}
Predicate<T> multiPredicate = first;
for (Predicate<T> predicate : other) {
multiPredicate = multiPredicate.and(predicate);
}
return multiPredicate;
}
public static <T> Predicate<T> matchingAny(Collection<Predicate<T>> predicates) {
Predicate<T> multiPredicate = to -> false;
for (Predicate<T> predicate : predicates) {
multiPredicate = multiPredicate.or(predicate);
}
return multiPredicate;
}
@SafeVarargs
public static <T> Predicate<T> matchingAny(Predicate<T> first, Predicate<T>... other) {
if (other == null || other.length == 0) {
return first;
}
Predicate<T> multiPredicate = first;
for (Predicate<T> predicate : other) {
multiPredicate = multiPredicate.or(predicate);
}
return multiPredicate;
}
}
Applying to the question:
public static void main(String... args) {
List<TestObject> list = new ArrayList<>();
Map<Integer, Integer> filterMap = new HashMap<>();
list.add(new TestObject(1, 2, 3));
list.add(new TestObject(1, 2, 4));
list.add(new TestObject(1, 4, 3));
filterMap.put(3, 3); // Filter property3 == 3
filterMap.put(2, 2); // Filter property2 == 2
List<Predicate<TestObject>> filters = filterMap.entrySet().stream()
.map(filterMapEntry -> mapToFilter(filterMapEntry))
.collect(Collectors.toList());
Predicate<TestObject> multiFilter = MultiPredicate.matchingAll(filters);
List<TestObject> filtered = list.stream()
.filter(multiFilter)
.collect(Collectors.toList());
for (TestObject to : filtered) {
System.out.println("(" + to.getProperty(1) + "|" + to.getProperty(2) + "|" + to.getProperty(3) + ")");
}
}
private static Predicate<TestObject> mapToFilter(Entry<Integer,Integer> filterMapEntry) {
return to -> to.getProperty(filterMapEntry.getKey()) == filterMapEntry.getValue();
}
In this case all filters have to match. The result is:
(1|2|3)
If we use MultiPredicate.matchingAny(...)
the result is:
(1|2|3)
(1|2|4)
(1|4|3)
I suppose you want to keep all the TestObjects
that satisfy all the conditions specified by the map?
This will do the job:
List<TestObject> newList = list.stream()
.filter(x ->
filterMap.entrySet().stream()
.allMatch(y ->
x.getProperty(y.getKey()) == y.getValue()
)
)
.collect(Collectors.toList());
Translated into "English",
filter
the listlist
by keeping all the elementsx
that:
- all of the key value pairs
y
offilterMap
must satisfy:
x.getProperty(y.getKey()) == y.getValue()
(I don't think I did a good job at making this human readable...) If you want a more readable solution, I recommend Jeroen Steenbeeke's answer.
To apply a variable number of filter steps to a stream (that only become known at runtime), you could use a loop to add filter steps.
Stream<TestObject> stream = list.stream();
for (Predicate<TestObject> predicate: allPredicates) {
stream = stream.filter(predicate);
}
list = stream.collect(Collectors.toList());