问题背景
在排查一个应用启动缓慢的问题时,我先怀疑是要加载的数据量太大。可实际看了下数据库,只有 6 万条左右。更奇怪的是,单独写一个读取这 6 万条数据的方法,跑完竟然不到 10 秒。说明瓶颈不在数据库,而是在后面的处理逻辑里。
继续往下看,问题出在一段去重代码上:系统会把记录里某几个字段拼成一个唯一键,如果这个键已经出现过,就认为这条数据是重复的,直接跳过。实现方式很直白:先用一个 List 存这些唯一键,再通过 contains 判断是否已经存在。
List<String> uniqueKeyList = new ArrayList<String>();
// ...
if (uniqueKeyList.contains(uniqueKey)) {
continue;
}
为什么会慢
ArrayList.contains() 的底层并不是'快速查找',它最终会走到 indexOf(),本质上就是一次线性遍历:
public int indexOf(Object elem) {
if (elem == null) {
for (int i = 0; i < size; i++)
if (elementData[i] == null)
return i;
} else {
for (int i = 0; i < size; i++)
if (elem.equals(elementData[i]))
return i;
}
return -1;
}
这意味着,列表越长,contains() 越慢。这里有 6 万条数据,去重过程中每次都要扫一遍 List,累计下来就是很可观的时间开销。启动阶段又特别敏感,几秒钟都可能被放大成明显的卡顿。
改成 HashSet 之后
这类'判断是否已存在'的场景,更合适的容器其实是 Set。把原来的 List 换成 HashSet 后,代码结构几乎不需要大改:
Set<String> uniqueKeySet = new HashSet<String>();
(uniqueKeySet.contains(uniqueKey)) {
;
}

