【问题标题】:How to apply Filtering on groupBy in java streams如何在 java 流中对 groupBy 应用过滤
【发布时间】:2018-01-16 02:00:42
【问题描述】:

如何先分组,然后使用 Java 流应用过滤?

示例:考虑这个Employee 类: 我想按部门分组,列出工资大于 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;
    }
}   

我可以这样做

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

输出

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

由于 B 部门没有工资大于 2000 的员工。因此没有 B 部门的密钥: 但实际上,我想要那个带有空列表的键 –

预期输出

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

我们该怎么做?

【问题讨论】:

  • 版本标签应用于特定于该版本的问题。如果这是关于跨多个版本的流,它不应该有任何标签 IMO。

标签: java java-8 java-stream java-9 collectors


【解决方案1】:

您可以使用自 Java-9 以来引入的 Collectors.filtering API:

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

API 说明中的重要内容:

  • filter() 收集器在用于多级归约时最有用,例如groupingBypartitioningBy 的下游。

  • 过滤收集器不同于流的filter() 操作。

【讨论】:

  • 有趣,我没想到这与直接过滤流的行为不同。
  • @shmosel 当您将filtering(…) 直接传递给collect 方法时,它的作用相同,例如filtering(…, groupingBy(…))。但是当你将它传递给groupingBy 作为下游收集器时,即groupingBy(…, filtering(…)),它将在创建组后接收元素。就这么简单,类似于mappingflatMapping 的工作方式。
【解决方案2】:

nullpointer’s answer 显示了直截了当的方法。如果你不能更新到 Java 9,没问题,这个 filtering 收集器没有魔法。这是一个 Java 8 兼容版本:

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

您可以将其添加到您的代码库中,并以与 Java 9 对应的相同方式使用它,因此如果您使用 import static,则无需以任何方式更改代码。

【讨论】:

    【解决方案3】:

    过滤后使用Map#putIfAbsent(K,V)填补空白

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

    注意:由于 groupingBy 返回的地图不能保证是可变的,因此您需要指定地图供应商以确保(感谢 shmosel 指出)。


    另一个(不推荐)的解决方案是使用toMap 而不是groupingBy,它的缺点是为每个员工创建一个临时列表。而且看起来有点乱

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

    【讨论】:

    • 您应该将函数更改为e-&gt; new ArrayList&lt;&gt;(filter.test(e)? Collections.singleton(e): Collections.emptyList()),以确保它始终返回(可变)ArrayList。否则,您可能会在合并函数中对emptyList() 的(不可变)结果调用addAll。或者您让函数始终创建一个不可变列表 e-&gt; filter.test(e)? Collections.singleton(e): Collections.emptyList() 并更改合并函数以创建一个新列表。
    • 当然,我的意思是,e-&gt; filter.test(e)? Collections.singletonList(e): Collections.emptyList() 在我最后的评论中。
    • 再次正确。改变了它。谢谢:-)
    • groupingBy() 返回的映射不保证是可变的。您需要使用 Supplier 重载。
    【解决方案4】:

    在 Java 8 中没有更简洁的方法: Holger 在 java8 中显示了清晰的方法 here 接受了答案。

    这就是我在 java 8 中的做法:

    步骤:1按部门分组

    步骤:2循环抛出每个元素并检查部门是否有薪水>2000的员工

    步骤:3 更新地图根据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 首先添加了新的收集器Collectors.filtering 这个组然后应用过滤。 过滤收集器旨在与分组一起使用。

    Collectors.Filtering 采用过滤输入元素的函数和收集过滤后的元素的收集器:

    list.stream().collect(Collectors.groupingBy(Employee::getDepartment),
     Collectors.filtering(e->e.getSalary()>2000,toList());
    

    【讨论】:

    • 在迭代地图时不能添加到地图。你会得到一个 ConcurrentModificationException。
    • 这里有一个简单的演示,如果你不相信我的话:ideone.com/SRtWiJ
    • 你的逻辑有问题。如果所有的薪水
    • 你为什么不用toCollection(ArrayList::new)然后employeeMap.values().forEach(list -&gt; list.removeIf(e -&gt; e.getSalary() &lt;= 2000));
    • 我不知道你在说什么。您当前的解决方案不会过滤掉个别员工。
    【解决方案5】:

    Java 8 版本:您可以按部门进行分组,然后流式传输条目集并通过在过滤器中添加谓词再次进行收集:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-08-23
      • 2017-01-20
      • 1970-01-01
      • 1970-01-01
      • 2023-03-08
      相关资源
      最近更新 更多