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
调用基本上是将映射映射到自身。
我看到有 collectingAndThen
和 filtering
收集器,但我不知道他们是否能解决我的问题(或者更确切地说如何正确应用它们)。
上面是否有更好(更惯用)的版本,或者我是否坚持收集到中间地图,过滤它然后收集到最终地图?
如果您想要更易读的代码,您也可以(作为重新流式传输的替代方案)使用 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));
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
调用基本上是将映射映射到自身。
我看到有 collectingAndThen
和 filtering
收集器,但我不知道他们是否能解决我的问题(或者更确切地说如何正确应用它们)。
上面是否有更好(更惯用)的版本,或者我是否坚持收集到中间地图,过滤它然后收集到最终地图?
如果您想要更易读的代码,您也可以(作为重新流式传输的替代方案)使用 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));