1. 概述
在这个简短的教程中,我们将介绍一些不同的方法来计算ArrayList中的重复元素。
2. 循环使用Map.put()
我们的预期结果是一个Map对象,它包含输入列表中的所有元素作为键,每个元素的计数作为值。
实现此目的最直接的解决方案是遍历输入列表并针对每个元素:
- 如果resultMap包含该元素,我们将计数器加1
- 否则,我们将一个新的Map条目(element, 1)放入Map
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版本。
如果我们不需要Java 8之前的兼容性,我们可以进一步简化我们的方法:
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 = Lists.list(
"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中,方便的compute()方法已被引入到Map接口中。我们也可以使用这个方法:
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;
}
注意(k, v) -> v == null ? 1L : v + 1L是实现BiFunction<T, Long, Long>接口的重映射函数。对于给定的键,它要么返回其当前值加一(如果该键已经存在于Map中),要么返回默认值1。
为了使代码更具可读性,我们可以将重映射函数提取到它的变量中,甚至将其作为countByForEachLoopWithMapCompute的输入参数。
4. 使用Map.merge()循环
使用Map.compute()时,我们必须显式处理空值-例如,如果给定键的映射不存在。这就是我们在重映射函数中实现空检查的原因。然而,这看起来并不漂亮。
让我们在Map.merge()方法的帮助下进一步清理我们的代码:
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()是如何工作的。如果给定键的映射不存在,或者它的值为null,它会将键与提供的值相关联。否则,它会使用重映射函数计算一个新值并相应地更新Map。
请注意,这次我们使用Long::sum作为BiFunction<T, Long, Long>接口实现。
5. Stream API Collectors.toMap()
既然我们已经谈到了Java 8,那么就不能忘记强大的Stream API。感谢Stream API,我们可以用非常紧凑的方式解决问题。
toMap()收集器帮助我们将输入列表转换为Map:
public <T> Map<T, Long> countByStreamToMap(List<T> inputList) {
return inputList.stream().collect(Collectors.toMap(Function.identity(), v -> 1L, Long::sum));
}
toMap()是一个方便的收集器,它可以帮助我们将Stream转换为不同的Map实现。
6. Stream APICollectors.groupingBy()和Collectors.counting()
除了toMap(),我们的问题可以通过另外两个收集器groupingBy()和counting()来解决:
public <T> Map<T, Long> countByStreamGroupBy(List<T> inputList) {
return inputList.stream().collect(Collectors.groupingBy(k -> k, Collectors.counting()));
}
Java 8 Collectors的正确使用使我们的代码紧凑且易于阅读。
7. 总结
在这篇简短的文章中,我们说明了计算列表中重复元素计数的各种方法。
如果你想复习ArrayList本身,可以查看参考文章。
与往常一样,本教程的完整源代码可在GitHub上获得。