使用分组,计数和过滤操作收集流

ytt*_*rrr 10 java java-8 java-stream

我正在尝试收集丢弃很少使用的项目的流,如下例所示:

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

我觉得有可能做得更简单 - 我是对的吗?

Hol*_*ger 7

Map除非您希望接受非常高的CPU复杂性,否则无法创建.

但是,您可以删除第二个 collect操作:

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

请注意,在Java 8中,HashSet仍然包装a HashMap,所以当你想要a时,使用keySet()a HashMap,在Set当前实现的情况下不会浪费空间.

当然,Collector如果感觉更"流" ,你可以隐藏后处理:

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

  • 来自[文档](http://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html#groupingBy-java.util.function.Function-java.util.stream.收集器 - ):"*无法保证返回的Map的类型,可变性,可序列性或线程安全性.*".**当前实现**返回一个`HashMap`,但你*不能*依赖它.因此,如果您需要像我的解决方案中那样的可变地图,您应该指定像`HashMap :: new`这样的供应商. (4认同)

Tag*_*eev 4

不久前我为我的图书馆写了一个实验方法: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;
    });
}
Run Code Online (Sandbox Code Playgroud)

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

Set<String> commonlyUsed = StreamEx.of(allWords).distinct(3).toSet();
Run Code Online (Sandbox Code Playgroud)

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

Set<String> commonlyUsed = distinct(allWords.stream(), 3).collect(Collectors.toSet());
Run Code Online (Sandbox Code Playgroud)

更新(2015/05/31):我将该distinct(atLeast)方法添加到了 StreamEx 0.3.1 中。它是使用自定义 spliterator实现的。基准测试表明,对于顺序流,此实现比上述状态过滤要快得多,并且在许多情况下,它也比本主题中提出的其他解决方案更快。null如果在流中遇到它,它也能很好地工作(groupingBy收集器不支持null作为类,因此如果遇到groupingBy基于 - 的解决方案将失败)。null

  • @Holger:似乎 spliterator 更好:基准和结果在[此处](https://gist.github.com/amaembo/669ddaf83fadd2920e83)。在顺序测试中,spliterator (distinct2) 始终优于过滤器 (distinct),并且在大多数情况下优于此处提出的其他解决方案(双流和删除 if)。在并行测试中,distinct 和distinct2 显示出相似的性能,并且在大约一半的情况下比其他解决方案更快。因此我会继续使用分离器解决方案。定制的原始地图可能会更快,但对于我的库来说太多了(特别是考虑到并发情况)。 (3认同)