Reputation: 914
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().filter().
The Objects are actually JSON, therefore the Map holds the names of its variables as well as the value they have to contain in order to be accepted, but for simplicity reasons and because its not relevant to the question I wrote a simple Testclass for simulating the behaviour:
public class TestObject {
private int property1;
private int property2;
private int property3;
public TestObject(int property1, int property2, int property3) {
this.property1 = property1;
this.property2 = property2;
this.property3 = property3;
}
public int getProperty(int key) {
switch(key) {
case 1: return property1;
case 2: return property2;
default: return property3;
}
}
}
What I have tried so far:
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
//Does not apply the result
filterMap.forEach((key, value) -> list.stream()
.filter(testObject -> testObject.getProperty(key) == value)
.collect(Collectors.toList())
);
/* Gives error: boolean can not be converted to void
list = list.stream()
.filter(testObject -> filterMap.forEach((key, value) -> testObject.getProperty(key) == value))
.collect(Collectors.toList()
);
*/
//Printing result
list.forEach(obj -> System.out.println(obj.getProperty(1) + " " + obj.getProperty(2) + " " + obj.getProperty(3)));
}
I tried putting forEach of the Map first and the stream of the Collection first, but both solutions did not work as intended. The desired output of this example would be only to print the object with the values property1=1, property2=2 and property3=3.
How can I apply all filters correctly like when you would put them one after another in the code with a fixed amount of filters?
With a known amount of filters:
list.stream().filter(...).filter(...)
Edit:
Sweeper summed my question up very well in his answer, so just for clarification (and probably future readers) here again: I want to keep all Objects that satisfy all filters.
Upvotes: 16
Views: 22120
Reputation: 5173
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)
Upvotes: 3
Reputation: 61
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
Upvotes: 3
Reputation: 270860
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.
Upvotes: 16
Reputation: 4013
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());
Upvotes: 8