Java Streams GroupingBy 和按计数过滤(类似于 SQL 的 HAVING)

kni*_*ttl 6 java group-by having java-stream collectors

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

GROUP BY id
HAVING COUNT(*) > 5
Run Code Online (Sandbox Code Playgroud)

我能想到的最接近的是:

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

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

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

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

Hol*_*ger 7

操作一般需要在分组后进行,因为需要将一个组完全收集起来,才能确定它是否满足条件。

您可以使用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;
            }));
Run Code Online (Sandbox Code Playgroud)

由于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;
    });
}
Run Code Online (Sandbox Code Playgroud)

为了获得更高的灵活性,这个收集器允许一个任意的映射生成收集器,但由于这不强制映射类型,它会在之后通过简单地使用复制构造函数来强制一个可变映射。在实践中,这不会发生,因为默认是使用HashMap. 当调用者显式请求 aLinkedHashMap来维护订单时,它也可以工作。我们甚至可以通过将行更改为

if(!(m instanceof HashMap || m instanceof TreeMap
  || m instanceof EnumMap || m instanceof ConcurrentMap)) {
    m = new HashMap<>(m);
}
Run Code Online (Sandbox Code Playgroud)

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

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

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