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;
}

⚠️ 注意:computeremappingFunction 中,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


原始标题:How to Count Duplicate Elements in Arraylist | Baeldung