Java 流分组和按计数过滤(类似于 SQL 的 HAVING)

Java Streams GroupingBy and filtering by count (similar to SQL's HAVING)

Java (9+) 流是否支持类似于 SQL 的 HAVING 子句?用例:分组,然后删除具有特定计数的所有组。是否可以将以下 SQL 子句编写为 Java 流?

GROUP BY id
HAVING COUNT(*) > 5

我能想到的最接近的是:

input.stream()
        .collect(groupingBy(x -> x.id()))
        .entrySet()
        .stream()
        .filter(entry -> entry.getValue().size() > 5)
        .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));

但是提取分组结果的 entrySet 收集两次感觉很奇怪,尤其是终端 collect 调用基本上是将映射映射到自身。

我看到有 collectingAndThenfiltering 收集器,但我不知道他们是否能解决我的问题(或者更确切地说如何正确应用它们)。

上面是否有更好(更惯用)的版本,或者我是否坚持收集到中间地图,过滤它然后收集到最终地图?

如果您想要更易读的代码,您也可以(作为重新流式传输的替代方案)使用 Guava filterValues 函数。

它允许转换地图,有时提供比 Java 流更短、更易读的语法。

Map<A,B> unfiltered = java stream groupingby
return Maps.filterValues(unfiltered, value -> value.size() > 5);

我知道的唯一方法是在 finisher 函数中使用具有相同实现的 Collectors.collectingAndThen

Map<Integer, List<Item>> a = input.stream().collect(Collectors.collectingAndThen(
        Collectors.groupingBy(Item::id),
        map -> map.entrySet().stream()
                             .filter(e -> e.getValue().size() > 5)
                             .collect(Collectors.toMap(Entry::getKey, Entry::getValue))));

一般要在分组之后进行操作,因为需要将一个分组全部收集起来才能判断是否符合条件。

您可以使用 removeIf 从结果图中删除不匹配的组,并将此整理操作注入收集器,而不是将地图收集到另一个类似的地图中:

Map<KeyType, List<ElementType>> result =
    input.stream()
        .collect(collectingAndThen(groupingBy(x -> x.id(), HashMap::new, toList()),
            m -> {
                m.values().removeIf(l -> l.size() <= 5);
                return m;
            }));

由于 groupingBy(Function) 收集器不保证创建的映射的可变性,我们需要为可变映射指定供应商,这需要我们明确下游收集器,因为没有重载 groupingBy 仅指定功能和地图供应商。

如果这是一个周期性任务,我们可以制作一个自定义收集器来改进使用它的代码:

public static <T,K,V> Collector<T,?,Map<K,V>> having(
                      Collector<T,?,? extends Map<K,V>> c, BiPredicate<K,V> p) {
    return collectingAndThen(c, in -> {
        Map<K,V> m = in;
        if(!(m instanceof HashMap)) m = new HashMap<>(m);
        m.entrySet().removeIf(e -> !p.test(e.getKey(), e.getValue()));
        return m;
    });
}

为了获得更高的灵活性,此收集器允许生成任意映射的收集器,但由于这不强制执行映射类型,因此它将在之后通过简单地使用复制构造函数强制执行可变映射。实际上,这不会发生,因为默认是使用 HashMap。当调用者明确请求 LinkedHashMap 来维护顺序时,它也有效。我们甚至可以通过将行更改为

来支持更多案例
if(!(m instanceof HashMap || m instanceof TreeMap
  || m instanceof EnumMap || m instanceof ConcurrentMap)) {
    m = new HashMap<>(m);
}

遗憾的是,没有标准的方法来确定地图是否可变。

自定义收集器现在可以很好地用作

Map<KeyType, List<ElementType>> result =
    input.stream()
        .collect(having(groupingBy(x -> x.id()), (key,list) -> list.size() > 5));