1. 概述
本文将介绍几种在 Java 中统计 ArrayList
中重复元素出现次数的方法。目标是生成一个 Map<T, Long>
,其中键为列表中的元素,值为该元素出现的次数。
这类需求在实际开发中并不少见,比如日志分析、数据去重、缓存统计等场景。虽然问题看似简单,但实现方式多种多样,各有优劣。我们一步步来看几种主流做法,帮你避开一些常见的“坑”。
2. 使用传统循环 + Map.put()
最朴素的做法:遍历列表,手动判断元素是否已存在于 Map
中。
✅ 优点:逻辑清晰,兼容所有 Java 版本
❌ 缺点:代码略显冗长
public <T> Map<T, Long> countByClassicalLoop(List<T> inputList) {
Map<T, Long> resultMap = new HashMap<>();
for (T element : inputList) {
if (resultMap.containsKey(element)) {
resultMap.put(element, resultMap.get(element) + 1L);
} else {
resultMap.put(element, 1L);
}
}
return resultMap;
}
如果你的项目使用的是 Java 8+,可以用 forEach
简化写法,同时借助 getOrDefault
避免显式判断 null
:
public <T> Map<T, Long> countByForEachLoopWithGetOrDefault(List<T> inputList) {
Map<T, Long> resultMap = new HashMap<>();
inputList.forEach(e -> resultMap.put(e, resultMap.getOrDefault(e, 0L) + 1L));
return resultMap;
}
测试数据与验证
我们使用如下输入数据进行测试:
private List<String> INPUT_LIST = Arrays.asList(
"expect1",
"expect2", "expect2",
"expect3", "expect3", "expect3",
"expect4", "expect4", "expect4", "expect4"
);
验证方法:
private void verifyResult(Map<String, Long> resultMap) {
assertThat(resultMap)
.isNotEmpty()
.hasSize(4)
.containsExactly(
entry("expect1", 1L),
entry("expect2", 2L),
entry("expect3", 3L),
entry("expect4", 4L)
);
}
后续所有方法都会复用这套测试逻辑。
3. 使用 Map.compute()
方法
Java 8 为 Map
接口引入了 compute()
方法,可以更优雅地处理“如果存在则更新,否则插入”的逻辑。
public <T> Map<T, Long> countByForEachLoopWithMapCompute(List<T> inputList) {
Map<T, Long> resultMap = new HashMap<>();
inputList.forEach(e -> resultMap.compute(e, (k, v) -> v == null ? 1L : v + 1L));
return resultMap;
}
⚠️ 注意:compute
的 remappingFunction
中,v
可能为 null
,所以必须做 null
判断。虽然功能没问题,但 v == null ? 1L : v + 1L
这种写法不够优雅。
建议:可以把这个函数提取成常量或作为参数传入,提升可读性和复用性。
4. 使用 Map.merge()
方法 ✅ 推荐
相比 compute()
,merge()
更适合这种“合并计数”的场景。它内置了对 null
的处理,代码更简洁。
public <T> Map<T, Long> countByForEachLoopWithMapMerge(List<T> inputList) {
Map<T, Long> resultMap = new HashMap<>();
inputList.forEach(e -> resultMap.merge(e, 1L, Long::sum));
return resultMap;
}
merge()
原理简析
- 如果 key 不存在或 value 为
null
:直接使用第二个参数1L
作为值 - 如果 key 已存在:使用第三个参数(
BiFunction
)计算新值,即oldValue + 1L
✅ Long::sum
作为 BiFunction
实现,简单粗暴,可读性极佳。
这是在不使用 Stream 的前提下,最推荐的写法。
5. 使用 Stream API + Collectors.toMap()
Java 8 的 Stream API 提供了更函数式的解决方案。Collectors.toMap()
可以将流转换为 Map
。
public <T> Map<T, Long> countByStreamToMap(List<T> inputList) {
return inputList.stream()
.collect(Collectors.toMap(
Function.identity(), // key: 元素本身
v -> 1L, // value: 初始计数为 1
Long::sum // 合并策略:累加
));
}
⚠️ 注意:toMap
的第三个参数是 mergeFunction
,用于处理 key 冲突,这里用 Long::sum
实现计数累加。
虽然写法紧凑,但不如 groupingBy
直观,且容易在合并函数上“踩坑”。
6. 使用 Stream API + Collectors.groupingBy()
和 counting()
✅ 最佳实践
这才是 Java 8 下最优雅的解法:
public <T> Map<T, Long> countByStreamGroupBy(List<T> inputList) {
return inputList.stream()
.collect(Collectors.groupingBy(
Function.identity(), // 按元素本身分组
Collectors.counting() // 每组计数
));
}
优势分析
- ✅ 语义清晰:“按元素分组并计数”,一看就懂
- ✅ 代码简洁,不易出错
- ✅ 是处理此类聚合问题的标准范式
Collectors.counting()
返回的是 Long
类型,正好满足我们的需求。
这也是官方示例中最常见的写法,建议在 Java 8+ 项目中优先使用。
7. 总结
方法 | Java 版本 | 推荐度 | 适用场景 |
---|---|---|---|
put() + containsKey() |
所有版本 | ⭐⭐ | 兼容老项目 |
getOrDefault() |
Java 8+ | ⭐⭐⭐ | 简单场景 |
Map.merge() |
Java 8+ | ⭐⭐⭐⭐ | 手动循环计数 |
Stream.toMap() |
Java 8+ | ⭐⭐⭐ | 了解即可 |
Stream.groupingBy() + counting() |
Java 8+ | ⭐⭐⭐⭐⭐ | 首选方案 |
📌 最终建议:
- 如果你在维护一个老项目,用
merge()
或getOrDefault()
都不错 - 如果是新项目,无脑上
groupingBy
+counting()
,代码最干净,也最不容易出错
完整源码已托管至 GitHub:https://github.com/baeldung/core-java-modules/tree/master/core-java-collections-list-3