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);
}
性能结论(由高到低):
- 可变整数方案:无对象创建开销
- 原生数组方案:接近可变整数性能
- 并行流方案:大数据量时优势明显
- 普通Stream:包装类拖累性能
- 基础Map方案:性能最差
💡 简单粗暴的选择建议:
- 追求极致性能 → 使用可变整数
- 代码简洁优先 → 使用Stream API
- 超大数据量 → 并行流+可变整数
4. 总结
本文系统介绍了Java中词频统计的五种实现方案,从基础实现到性能优化逐步深入。核心结论:
- 避免频繁创建对象:可变整数方案性能最佳
- 合理使用Stream:代码简洁但注意包装类开销
- 并行化优化:大数据量场景下效果显著
- 根据场景选择:平衡性能与可读性
完整代码实现可在GitHub项目获取,基于Maven构建,可直接运行测试。