Java 8 单词频率统计

79

如何在Java 8中统计列表中各单词的出现频率?

List <String> wordsList = Lists.newArrayList("hello", "bye", "ciao", "bye", "ciao");

结果必须是:

{ciao=2, hello=1, bye=2}
12个回答

118

我希望分享我找到的解决方案,因为一开始我预计要使用map和reduce方法,但实际上略有不同。

Map<String,Long> collect = wordsList.stream()
    .collect( Collectors.groupingBy( Function.identity(), Collectors.counting() ));

或者对于整数值:

Map<String,Integer> collect = wordsList.stream()
     .collect( Collectors.groupingBy( Function.identity(), Collectors.summingInt(e -> 1) ));

编辑

我添加了如何按值对地图排序:

LinkedHashMap<String, Long> countByWordSorted = collect.entrySet()
            .stream()
            .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
            .collect(Collectors.toMap(
                    Map.Entry::getKey,
                    Map.Entry::getValue,
                    (v1, v2) -> {
                        throw new IllegalStateException();
                    },
                    LinkedHashMap::new
            ));

38

(注意:请参见下面的编辑内容)

作为Mounas答案的替代方案,这里提供了一种并行计算单词数的方法:

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class ParallelWordCount
{
    public static void main(String[] args)
    {
        List<String> list = Arrays.asList(
            "hello", "bye", "ciao", "bye", "ciao");
        Map<String, Integer> counts = list.parallelStream().
            collect(Collectors.toConcurrentMap(
                w -> w, w -> 1, Integer::sum));
        System.out.println(counts);
    }
}

编辑:根据评论,我使用JMH进行了小型测试,比较了toConcurrentMapgroupingByConcurrent方法,使用不同大小的输入列表和不同长度的随机单词。这个测试表明,toConcurrentMap方法更快。考虑到这两种方法在“内部”有多大的不同,很难预测这样的结果。

进一步扩展,基于更多的评论,我将测试扩展到涵盖了toMapgroupingBy、串行和并行四种组合。

结果仍然是toMap方法更快,但出乎意料(至少对我来说),在两种情况下,“concurrent”版本都比串行版本更慢...

             (method)  (count) (wordLength)  Mode  Cnt     Score    Error  Units
      toConcurrentMap     1000            2  avgt   50   146,636 ±  0,880  us/op
      toConcurrentMap     1000            5  avgt   50   272,762 ±  1,232  us/op
      toConcurrentMap     1000           10  avgt   50   271,121 ±  1,125  us/op
                toMap     1000            2  avgt   50    44,396 ±  0,541  us/op
                toMap     1000            5  avgt   50    46,938 ±  0,872  us/op
                toMap     1000           10  avgt   50    46,180 ±  0,557  us/op
           groupingBy     1000            2  avgt   50    46,797 ±  1,181  us/op
           groupingBy     1000            5  avgt   50    68,992 ±  1,537  us/op
           groupingBy     1000           10  avgt   50    68,636 ±  1,349  us/op
 groupingByConcurrent     1000            2  avgt   50   231,458 ±  0,658  us/op
 groupingByConcurrent     1000            5  avgt   50   438,975 ±  1,591  us/op
 groupingByConcurrent     1000           10  avgt   50   437,765 ±  1,139  us/op
      toConcurrentMap    10000            2  avgt   50   712,113 ±  6,340  us/op
      toConcurrentMap    10000            5  avgt   50  1809,356 ±  9,344  us/op
      toConcurrentMap    10000           10  avgt   50  1813,814 ± 16,190  us/op
                toMap    10000            2  avgt   50   341,004 ± 16,074  us/op
                toMap    10000            5  avgt   50   535,122 ± 24,674  us/op
                toMap    10000           10  avgt   50   511,186 ±  3,444  us/op
           groupingBy    10000            2  avgt   50   340,984 ±  6,235  us/op
           groupingBy    10000            5  avgt   50   708,553 ±  6,369  us/op
           groupingBy    10000           10  avgt   50   712,858 ± 10,248  us/op
 groupingByConcurrent    10000            2  avgt   50   901,842 ±  8,685  us/op
 groupingByConcurrent    10000            5  avgt   50  3762,478 ± 21,408  us/op
 groupingByConcurrent    10000           10  avgt   50  3795,530 ± 32,096  us/op

我在JMH方面经验不太丰富,也许我在这里做错了什么——欢迎提出建议和更正:

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.infra.Blackhole;

@State(Scope.Thread)
public class ParallelWordCount
{

    @Param({"toConcurrentMap", "toMap", "groupingBy", "groupingByConcurrent"})
    public String method;

    @Param({"2", "5", "10"})
    public int wordLength;

    @Param({"1000", "10000" })
    public int count;

    private List<String> list;

    @Setup
    public void initList()
    {
         list = createRandomStrings(count, wordLength, new Random(0));
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    public void testMethod(Blackhole bh)
    {

        if (method.equals("toMap"))
        {
            Map<String, Integer> counts =
                list.stream().collect(
                    Collectors.toMap(
                        w -> w, w -> 1, Integer::sum));
            bh.consume(counts);
        }
        else if (method.equals("toConcurrentMap"))
        {
            Map<String, Integer> counts =
                list.parallelStream().collect(
                    Collectors.toConcurrentMap(
                        w -> w, w -> 1, Integer::sum));
            bh.consume(counts);
        }
        else if (method.equals("groupingBy"))
        {
            Map<String, Long> counts =
                list.stream().collect(
                    Collectors.groupingBy(
                        Function.identity(), Collectors.<String>counting()));
            bh.consume(counts);
        }
        else if (method.equals("groupingByConcurrent"))
        {
            Map<String, Long> counts =
                list.parallelStream().collect(
                    Collectors.groupingByConcurrent(
                        Function.identity(), Collectors.<String> counting()));
            bh.consume(counts);
        }
    }

    private static String createRandomString(int length, Random random)
    {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < length; i++)
        {
            int c = random.nextInt(26);
            sb.append((char) (c + 'a'));
        }
        return sb.toString();
    }

    private static List<String> createRandomStrings(
        int count, int length, Random random)
    {
        List<String> list = new ArrayList<String>(count);
        for (int i = 0; i < count; i++)
        {
            list.add(createRandomString(length, random));
        }
        return list;
    }
}

在拥有10000个元素和2个字母的列表的串行情况下,时间相似。

值得检查的是,在更大的列表大小下,并发版本是否最终会胜过串行版本,但目前没有时间对所有这些配置进行另一个详细的基准测试运行。


我猜 Collectors.groupingByConcurrent(w->w, Collectors.counting()) 会更有效率。 - Holger
@Holger 当然,这就是我试图用一个包含10000个长度为2的随机单词的列表来尽可能地覆盖的内容:相同单词出现的次数在6到32之间(平均值约为15)。使用100000次“aa”的快速测试看起来时间更加相似,但这个测试并不能告诉我们在实际应用中的性能表现如何。 - Marco13
如果您能发布您的基准测试代码,那就太好了。 - Holger
@Holger 我不想在回答中加入一些与实际问题无关的内容,但我会在今天稍后整理并发布代码。 - Marco13
1
可理解,但讨论替代方案时,进行比较会很有用。毕竟,即使是 OP 自己的 groupingBy 方法也可以并行工作,扩展基准测试以查看所有变体(groupingBy vs toMap)×(普通 vs. Concurrent)之间的差异将是很棒的... - Holger
显示剩余2条评论

11

使用泛型查找集合中出现频率最高的项:

private <V> V findMostFrequentItem(final Collection<V> items)
{
  return items.stream()
      .filter(Objects::nonNull)
      .collect(Collectors.groupingBy(Functions.identity(), Collectors.counting()))
      .entrySet()
      .stream()
      .max(Comparator.comparing(Entry::getValue))
      .map(Entry::getKey)
      .orElse(null);
}

计算项目频率:

private <V> Map<V, Long> findFrequencies(final Collection<V> items)
{
  return items.stream()
      .filter(Objects::nonNull)
      .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
}

4
如果您使用Eclipse Collections,您只需将List转换为Bag即可。
Bag<String> words = 
    Lists.mutable.with("hello", "bye", "ciao", "bye", "ciao").toBag();

Assert.assertEquals(2, words.occurrencesOf("ciao"));
Assert.assertEquals(1, words.occurrencesOf("hello"));
Assert.assertEquals(2, words.occurrencesOf("bye"));

您还可以使用Bags工厂类直接创建一个Bag
Bag<String> words = 
    Bags.mutable.with("hello", "bye", "ciao", "bye", "ciao");

这段代码适用于Java 5或以上版本。

注意: 我是Eclipse Collections的贡献者。


3
我将呈现我所做的解决方案(有分组的那个更好:))。请见以下内容:

static private void test0(List<String> input) {
    Set<String> set = input.stream()
            .collect(Collectors.toSet());
    set.stream()
            .collect(Collectors.toMap(Function.identity(),
                    str -> Collections.frequency(input, str)));
}

我只是提供一些意见。


3
这里有一种使用映射函数创建频率地图的方法。
List<String> words = Stream.of("hello", "bye", "ciao", "bye", "ciao").collect(toList());
Map<String, Integer> frequencyMap = new HashMap<>();

words.forEach(word ->
        frequencyMap.merge(word, 1, (v, newV) -> v + newV)
);

System.out.println(frequencyMap); // {ciao=2, hello=1, bye=2}

或者

words.forEach(word ->
       frequencyMap.compute(word, (k, v) -> v != null ? v + 1 : 1)
);

我喜欢map.merge,但是不确定为什么你要做那么多工作来创建List<String> words,为什么不直接这样做:List list = Arrays.asList("hello", "bye", "ciao", "bye", "ciao") - mancocapac
1
@mancocapac 谢谢!关于列表创建代码,我会避免使用Arrays.asList 因为它返回java.util.Arrays$ArrayList而不是普通的ArrayList。如果数据最终会在你的应用程序中进行序列化,我更喜欢不使用它,因为Arrays.asList会创建不想要的序列化结构。 - Piyush

2

您可以使用Java 8 Streams来实现此功能。

    Arrays.asList(s).stream()
          .collect(Collectors.groupingBy(Function.<String>identity(), 
          Collectors.<String>counting()));

0

我再提供两分钱的意见,给定一个数组:

import static java.util.stream.Collectors.*;

String[] str = {"hello", "bye", "ciao", "bye", "ciao"};    
Map<String, Integer> collected 
= Arrays.stream(str)
        .collect(groupingBy(Function.identity(), 
                    collectingAndThen(counting(), Long::intValue)));

0
  public static void main(String[] args) {
    String str = "Hi Hello Hi";
    List<String> s = Arrays.asList(str.split(" "));
    Map<String, Long> hm = 
              s.stream().collect(Collectors.groupingBy(Function.identity(), 
              Collectors.counting()));

              hm.entrySet().forEach(entry -> {

             System.out.println(entry.getKey() + " " + entry.getValue());
              });

}

0

我认为有一种更易读的方式:

var words = List.of("my", "more", "more", "more", "simple", "way");
var count = words.stream().map(x -> Map.entry(x, 1))
                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, Integer::sum));

类似于map-reduce方法,首先将每个单词w映射到(w, 1)。然后聚合(reduce部分)所有键(单词w)相似的对的计数(Map.Entry::getValue),(Map.Entry::getKey)并计算总和(Integer::sum)。

最终的终端操作将返回一个HashMap<String, Integer>

{more=3, simple=1, my=1, way=1}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接