通过分组、计数和过滤操作收集流

Collect stream with grouping, counting and filtering operations

我正在尝试收集丢弃很少使用的物品的流,如本例所示:

import java.util.*;
import java.util.function.Function;
import static java.util.stream.Collectors.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import org.junit.Test;

@Test
public void shouldFilterCommonlyUsedWords() {
    // given
    List<String> allWords = Arrays.asList(
       "call", "feel", "call", "very", "call", "very", "feel", "very", "any");

    // when
    Set<String> commonlyUsed = allWords.stream()
            .collect(groupingBy(Function.identity(), counting()))
            .entrySet().stream().filter(e -> e.getValue() > 2)
            .map(Map.Entry::getKey).collect(toSet());

    // then
    assertThat(commonlyUsed, containsInAnyOrder("call", "very"));
}

我觉得可以做得更简单 - 对吗?

没有办法绕过创建 Map,除非您想接受非常高的 CPU 复杂度。

不过,你可以去掉secondcollect操作:

Map<String,Long> map = allWords.stream()
    .collect(groupingBy(Function.identity(), HashMap::new, counting()));
map.values().removeIf(l -> l<=2);
Set<String> commonlyUsed=map.keySet();

请注意,在 Java 8 中,HashSet 仍包含 HashMap,因此当您需要 HashMap 时,请使用 keySet() =18=] 首先,考虑到当前的实施,不会浪费 space。

当然,如果感觉更“流畅”,您可以将 post 处理隐藏在 Collector 中:

Set<String> commonlyUsed = allWords.stream()
    .collect(collectingAndThen(
        groupingBy(Function.identity(), HashMap::new, counting()),
        map-> { map.values().removeIf(l -> l<=2); return map.keySet(); }));

不久前,我 wrote 我的图书馆的一个实验性 distinct(atLeast) 方法:

public StreamEx<T> distinct(long atLeast) {
    if (atLeast <= 1)
        return distinct();
    AtomicLong nullCount = new AtomicLong();
    ConcurrentHashMap<T, Long> map = new ConcurrentHashMap<>();
    return filter(t -> {
        if (t == null) {
            return nullCount.incrementAndGet() == atLeast;
        }
        return map.merge(t, 1L, (u, v) -> (u + v)) == atLeast;
    });
}

所以我的想法是像这样使用它:

Set<String> commonlyUsed = StreamEx.of(allWords).distinct(3).toSet();

这执行了状态过滤,看起来有点难看。我怀疑这个功能是否有用,所以我没有将它合并到 master 分支中。尽管如此,它还是在单流通道中完成了工作。也许我应该复活它。同时你可以将这段代码复制到静态方法中并像这样使用它:

Set<String> commonlyUsed = distinct(allWords.stream(), 3).collect(Collectors.toSet());

更新(2015/05/31):我添加了distinct(atLeast) method to the StreamEx 0.3.1. It's implemented using custom spliterator。基准测试表明,对于顺序流,此实现比上述有状态过滤快得多,而且在许多情况下,它也比本主题中提出的其他解决方案快。如果在流中遇到 null,它也能很好地工作(groupingBy 收集器不支持 null 作为 class,因此基于 groupingBy 的解决方案将失败如果遇到null)。

我个人更喜欢 Holger 的解决方案 (+1),但是,我不会从 groupingBy 映射中删除元素,而是 filter 它的 entrySet 和 map finalizer 中的 Set 的结果(对我来说感觉更 streamy

    Set<String> commonlyUsed = allWords.stream().collect(
            collectingAndThen(
                groupingBy(identity(), counting()), 
                (map) -> map.entrySet().stream().
                            filter(e -> e.getValue() > 2).
                            map(e -> e.getKey()).
                            collect(Collectors.toSet())));