How do you group first and then apply filtering using Java streams?
Example: Consider this Employee
class:
I want to group by Departme
nullpointer’s answer shows the straight-forward way to go. If you can’t update to Java 9, no problem, this filtering
collector is no magic. Here is a Java 8 compatible version:
public static <T, A, R> Collector<T, ?, R> filtering(
Predicate<? super T> predicate, Collector<? super T, A, R> downstream) {
BiConsumer<A, ? super T> accumulator = downstream.accumulator();
return Collector.of(downstream.supplier(),
(r, t) -> { if(predicate.test(t)) accumulator.accept(r, t); },
downstream.combiner(), downstream.finisher(),
downstream.characteristics().toArray(new Collector.Characteristics[0]));
}
You can add it to your codebase and use it the same way as Java 9’s counterpart, so you don’t have to change the code in any way, if you’re using import static
.
Use Map#putIfAbsent(K,V) to fill in the gaps after filtering
Map<String, List<Employee>> map = list.stream()
.filter(e->e.getSalary() > 2000)
.collect(Collectors.groupingBy(Employee::getDepartment, HashMap::new, toList()));
list.forEach(e->map.putIfAbsent(e.getDepartment(), Collections.emptyList()));
Note: Since the map returned by groupingBy is not guaranteed to be mutable, you need to specify a Map Supplier to be sure (thanks to shmosel for pointing that out).
Another (not recommended) solution is using toMap instead of groupingBy
, which has the downside of creating a temporary list for every Employee. Also it looks a bit messy
Predicate<Employee> filter = e -> e.salary > 2000;
Map<String, List<Employee>> collect = list.stream().collect(
Collectors.toMap(
e-> e.department,
e-> new ArrayList<Employee>(filter.test(e) ? Collections.singleton(e) : Collections.<Employee>emptyList()) ,
(l1, l2)-> {l1.addAll(l2); return l1;}
)
);
Java 8 version: You can make grouping by Department and then stream the entry set and do the collect again with adding the predicate in filter:
Map<String, List<Employee>> collect = list.stream()
.collect(Collectors.groupingBy(Employee::getDepartment)).entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey,
entry -> entry.getValue()
.stream()
.filter(employee -> employee.getSalary() > 2000)
.collect(toList())
)
);
There is no cleaner way of doing this in Java 8: Holger has shown clear approach in java8 here Accepted the Answer.
This is how I have done it in java 8:
Step: 1 Group by Department
Step: 2 loop throw each element and check if department has an employee with salary >2000
Step: 3 update the map copy values in new map based on noneMatch
Map<String, List<Employee>> employeeMap = list.stream().collect(Collectors.groupingBy(Employee::getDepartment));
Map<String, List<Employee>> newMap = new HashMap<String,List<Employee>>();
employeeMap.forEach((k, v) -> {
if (v.stream().noneMatch(emp -> emp.getSalary() > 2000)) {
newMap.put(k, new ArrayList<>());
}else{
newMap.put(k, v);
}
});
Java 9 : Collectors.filtering
java 9 has added new collector Collectors.filtering
this group first and then applies filtering. filtering Collector is designed to be used along with grouping.
The Collectors.Filtering takes a function for filtering the input elements and a collector to collect the filtered elements:
list.stream().collect(Collectors.groupingBy(Employee::getDepartment),
Collectors.filtering(e->e.getSalary()>2000,toList());
You can make use of the Collectors.filtering API introduced since Java-9 for this :
Map<String, List<Employee>> output = list.stream()
.collect(Collectors.groupingBy(Employee::getDepartment,
Collectors.filtering(e -> e.getSalary() > 2000, Collectors.toList())));
Important from the API note :
The filtering() collectors are most useful when used in a multi-level reduction, such as downstream of a
groupingBy
orpartitioningBy
.A filtering collector differs from a stream's
filter()
operation.