How to apply Filtering on groupBy in java streams

谁都会走 提交于 2019-11-27 20:54:04

问题


How do you group first and then apply filtering using Java streams?

Example: Consider this Employee class: I want to group by Department with a list of an employee having a salary greater than 2000.

public class Employee {
    private String department;
    private Integer salary;
    private String name;

    //getter and setter

    public Employee(String department, Integer salary, String name) {
        this.department = department;
        this.salary = salary;
        this.name = name;
    }
}   

This is how I can do this

List<Employee> list   = new ArrayList<>();
list.add(new Employee("A", 5000, "A1"));
list.add(new Employee("B", 1000, "B1"));
list.add(new Employee("C", 6000, "C1"));
list.add(new Employee("C", 7000, "C2"));

Map<String, List<Employee>> collect = list.stream()
    .filter(e -> e.getSalary() > 2000)
    .collect(Collectors.groupingBy(Employee::getDepartment));  

Output

{A=[Employee [department=A, salary=5000, name=A1]],
 C=[Employee [department=C, salary=6000, name=C1], Employee [department=C, salary=7000, name=C2]]}

As there are no employees in Department B with a salary greater than 2000. So there is no key for Department B: But actually, I want to have that key with empty list –

Expected output

{A=[Employee [department=A, salary=5000, name=A1]],
 B=[],
 C=[Employee [department=C, salary=6000, name=C1], Employee [department=C, salary=7000, name=C2]]}

How can we do this?


回答1:


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.




回答2:


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 or partitioningBy.

  • A filtering collector differs from a stream's filter() operation.




回答3:


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;}
        )
);



回答4:


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());


来源:https://stackoverflow.com/questions/48273090/how-to-apply-filtering-on-groupby-in-java-streams

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!