1. 概述

本文将探讨在Java中实现词频统计的多种方案。词频统计是文本处理中的常见需求,比如分析日志文件、处理用户输入等场景。我们将从基础实现到性能优化逐步展开,对比不同方案的优劣。

2. 计数器实现方案

先定义一个基础数据源,后续所有方案都将基于这个字符串数组进行统计:

static String[] COUNTRY_NAMES 
  = { "China", "Australia", "India", "USA", "USSR", "UK", "China", 
  "France", "Poland", "Austria", "India", "USA", "Egypt", "China" };

⚠️ 如果需要处理超大文件,建议参考大文件处理方案,避免内存溢出。

2.1. 使用Map+Integer计数

最直观的实现方式是使用Map存储单词及其出现次数:

Map<String, Integer> counterMap = new HashMap<>();

for (String country : COUNTRY_NAMES) { 
    counterMap.compute(country, (k, v) -> v == null ? 1 : v + 1); 
}

assertEquals(3, counterMap.get("China").intValue());
assertEquals(2, counterMap.get("India").intValue());

核心逻辑:

  • 利用Map.compute()方法自动处理键值初始化
  • 首次出现时初始化为1,已有则递增

踩坑提醒:这种方法效率较低,因为Integer是不可变对象,每次计数都会创建新对象,产生额外GC压力。

2.2. Stream API方案

利用Java 8的Stream API实现函数式风格的计数:

@Test
public void whenMapWithLambdaAndWrapperCounter_runsSuccessfully() {
    Map<String, Long> counterMap = new HashMap<>();
 
    Stream.of(COUNTRY_NAMES)
      .collect(Collectors.groupingBy(k -> k, ()-> counterMap,
        Collectors.counting());

    assertEquals(3, counterMap.get("China").intValue());
    assertEquals(2, counterMap.get("India").intValue());
}

并行流版本(适合大数据量):

@Test
public void whenParallelStreamWithWrapperCounter_runsSuccessfully() {
    Map<String, Long> counterMap = new HashMap<>();
 
    Stream.of(COUNTRY_NAMES).parallel()
      .collect(Collectors.groupingBy(k -> k, ()-> counterMap,
        Collectors.counting());

    assertEquals(3, counterMap.get("China").intValue());
    assertEquals(2, counterMap.get("India").intValue());
}

核心优势:

  • ✅ 代码简洁,一行搞定统计
  • ✅ 并行流自动利用多核CPU
  • ❌ 底层仍使用Long包装类,存在对象创建开销

2.3. 使用Map+原生数组

通过int[]数组替代包装类,减少对象创建:

@Test
public void whenMapWithPrimitiveArrayCounter_runsSuccessfully() {
    Map<String, int[]> counterMap = new HashMap<>();

    counterWithPrimitiveArray(counterMap);

    assertEquals(3, counterMap.get("China")[0]);
    assertEquals(2, counterMap.get("India")[0]);
}
 
private void counterWithPrimitiveArray(Map<String, int[]> counterMap) {
    for (String country : COUNTRY_NAMES) {
        counterMap.compute(country, (k, v) -> v == null ? 
          new int[] { 0 } : v)[0]++;
    }
}

关键优化点:

  • 使用int[]作为Map值类型
  • 通过数组索引直接操作计数器
  • ✅ 比包装类方案减少对象创建
  • ❌ 代码可读性稍差

2.4. 使用Map+可变整数

自定义可变整数类,彻底避免对象创建:

private static class MutableInteger {
    int count = 1;
    
    public void increment() {
        this.count++;
    }
    
    // getter和setter方法
}

使用方式:

@Test
public void whenMapWithMutableIntegerCounter_runsSuccessfully() {
    Map<String, MutableInteger> counterMap = new HashMap<>();

    mapWithMutableInteger(counterMap);

    assertEquals(3, counterMap.get("China").getCount());
    assertEquals(2, counterMap.get("India").getCount());
}
private void counterWithMutableInteger(
  Map<String, MutableInteger> counterMap) {
    for (String country : COUNTRY_NAMES) {
        counterMap.compute(country, (k, v) -> v == null 
          ? new MutableInteger(0) : v).increment();
    }
}

核心优势:

  • ✅ 完全复用计数器对象
  • ✅ 性能最优(参考下节基准测试)
  • ✅ Apache Commons Collections的HashMultiSet采用类似实现

3. 性能对比分析

使用JMH基准测试对比各方案性能(单位:ops/毫秒,数值越大越好):

词频计数器性能对比

测试代码:

Map<String, Integer> counterMap = new HashMap<>();
Map<String, MutableInteger> counterMutableIntMap = new HashMap<>();
Map<String, int[]> counterWithIntArrayMap = new HashMap<>();
Map<String, Long> counterWithLongWrapperMap = new HashMap<>();
 
@Benchmark
public void wrapperAsCounter() {
    counterWithWrapperObject(counterMap);
}

@Benchmark
public void lambdaExpressionWithWrapper() {
    counterWithLambdaAndWrapper(counterWithLongWrapperMap );
}

@Benchmark
public void parallelStreamWithWrapper() {
    counterWithParallelStreamAndWrapper(counterWithLongWrapperStreamMap);
}
    
@Benchmark
public void mutableIntegerAsCounter() {
    counterWithMutableInteger(counterMutableIntMap);
}
    
@Benchmark
public void mapWithPrimitiveArray() {
   counterWithPrimitiveArray(counterWithIntArrayMap);
}

性能结论(由高到低):

  1. 可变整数方案:无对象创建开销
  2. 原生数组方案:接近可变整数性能
  3. 并行流方案:大数据量时优势明显
  4. 普通Stream:包装类拖累性能
  5. 基础Map方案:性能最差

💡 简单粗暴的选择建议:

  • 追求极致性能 → 使用可变整数
  • 代码简洁优先 → 使用Stream API
  • 超大数据量 → 并行流+可变整数

4. 总结

本文系统介绍了Java中词频统计的五种实现方案,从基础实现到性能优化逐步深入。核心结论:

  1. 避免频繁创建对象:可变整数方案性能最佳
  2. 合理使用Stream:代码简洁但注意包装类开销
  3. 并行化优化:大数据量场景下效果显著
  4. 根据场景选择:平衡性能与可读性

完整代码实现可在GitHub项目获取,基于Maven构建,可直接运行测试。


原始标题:Efficient Word Frequency Calculator in Java | Baeldung