contains() 方法的效率高度依赖于其所在的集合(Collection)的具体实现类型,笼统地谈论 contains() 的效率是没有意义的,因为它在不同数据结构中的底层实现和性能表现天差地别。

下面我们将从最坏、最好和平均情况三个维度,分析几种常见集合中 contains() 的效率。
核心结论先行
| 集合类型 | 底层数据结构 | contains() 时间复杂度 |
简要说明 |
|---|---|---|---|
ArrayList |
动态数组 | O(n) | 需要遍历数组,逐个比较元素。 |
LinkedList |
双向链表 | O(n) | 需要从头或尾开始遍历链表,逐个比较。 |
HashSet |
哈希表(HashMap) | 平均 O(1),最坏 O(n) | 通过哈希码快速定位,理想情况下是常数时间。 |
TreeSet |
红黑树(二叉搜索树) | O(log n) | 基于元素的自然排序或比较器进行二分查找。 |
HashMap (Key Set) |
哈希表 | 平均 O(1),最坏 O(n) | 同 HashSet,containsKey() 的效率。 |
ArrayList 和 LinkedList (O(n) - 线性时间)
这两种列表的实现决定了它们的 contains() 方法效率较低。
ArrayList
- 底层结构: 一个动态数组,元素在内存中是连续存储的。
contains()实现: 遍历底层数组,从第一个元素开始,使用equals()方法逐个与目标元素进行比较,直到找到匹配的元素或遍历完整个数组。- 时间复杂度: O(n) (大O符号)。
- 最坏情况: 目标元素是最后一个,或者不存在,需要比较
n次。 - 最好情况: 目标元素是第一个,只需要比较 1 次。
- 平均情况: 需要比较大约
n/2次,时间复杂度仍为 O(n)。
- 最坏情况: 目标元素是最后一个,或者不存在,需要比较
- 示例代码:
List<String> names = new ArrayList<>(Arrays.asList("Alice", "Bob", "Charlie", "David")); boolean containsAlice = names.contains("Alice"); // 快,O(1) 在最佳情况下 boolean containsZoe = names.contains("Zoe"); // 慢,O(n) 在最坏情况下
LinkedList
- 底层结构: 一个双向链表,每个节点(Node)包含数据、指向前一个节点的指针和指向后一个节点的指针。
contains()实现: 同样需要遍历,但它不像ArrayList可以通过索引随机访问,它必须从头节点(或尾节点)开始,沿着指针逐个节点进行equals()比较。- 时间复杂度: O(n)。
- 性能和
ArrayList一样,都随着元素数量n的增加而线性下降。 LinkedList在随机访问上(get(i))比ArrayList慢很多,但在头部和尾部插入/删除元素时更快,不过对于contains()这种操作,两者性能相近且都不理想。
- 性能和
HashSet (平均 O(1) - 常数时间)
HashSet 是为了解决 ArrayList 和 LinkedList 在 contains() 效率低下的问题而设计的。
- 底层结构:
HashSet内部使用一个HashMap来存储元素,它将元素作为HashMap的key,一个固定的Object作为value。 contains()实现:- 计算哈希码: 对要查找的元素调用
hashCode()方法,得到一个哈希码。 - 定位桶: 根据这个哈希码,通过一个哈希函数计算出该元素在
HashMap数组(也叫“桶”或 bucket)中的存储位置(索引)。 - 比较元素:
- 如果该位置是空的,说明元素不存在。
- 如果该位置有元素,会先比较哈希码,如果哈希码相同,再调用
equals()方法进行深度比较。 - 哈希冲突: 如果多个元素的哈希码相同(哈希冲突),它们会以链表或红黑树的形式存储在同一个桶中,这时就需要在冲突的链表/树中遍历查找。
- 计算哈希码: 对要查找的元素调用
- 时间复杂度:
- 平均情况: O(1),理想情况下,哈希函数分布均匀,每个元素都能直接定位到自己的桶,无需比较或只需比较一次。
- 最坏情况: O(n),当所有元素的哈希码都相同时(一个糟糕的
hashCode()实现),所有元素都会被存入同一个桶中,contains()就退化成了在一个链表或数组中查找,效率变为 O(n)。
- 关键点:
HashSet的高效性依赖于两个因素:- 一个好的
hashCode()实现: 能让哈希码尽可能均匀分布。 - 正确的
equals()和hashCode()协约: 如果两个对象通过equals()比较返回true,那么它们的hashCode()必须相等,这能保证查找的正确性。
- 一个好的
TreeSet (O(log n) - 对数时间)
TreeSet 提供的是一种有序的集合,它的效率介于 ArrayList 和 HashSet 之间。

- 底层结构: 红黑树,一种自平衡的二叉搜索树。
contains()实现: 利用二叉搜索树的特性进行查找,每次比较后,都能排除掉一半的子树。- 从根节点开始比较。
- 如果目标元素小于当前节点,则在其左子树中继续查找。
- 如果目标元素大于当前节点,则在其右子树中继续查找。
- 如果相等,则找到。
- 如果到达叶子节点还没找到,则元素不存在。
- 时间复杂度: O(log n)。
- 查找次数与树的高度成正比,由于红黑树是平衡的,其高度始终保持在
log n级别。 - 性能非常稳定,不受元素插入顺序的影响。
- 查找次数与树的高度成正比,由于红黑树是平衡的,其高度始终保持在
- 示例代码:
Set<String> sortedNames = new TreeSet<>(Arrays.asList("David", "Alice", "Charlie", "Bob")); boolean containsBob = sortedNames.contains("Bob"); // 效率 O(log n)
性能对比总结与选择建议
| 场景 | 推荐集合 | 原因 |
|---|---|---|
| 需要频繁检查元素是否存在 | HashSet |
contains() 效率最高,平均 O(1)。 |
| 需要元素有序,并频繁检查存在性 | TreeSet |
contains() 效率 O(log n),同时保证有序。 |
| 需要保持元素的插入顺序,并检查存在性 | LinkedHashSet |
效率同 HashSet (平均 O(1)),同时维护插入顺序。 |
| 主要在列表的头部/尾部操作,很少检查存在性 | LinkedList |
头尾操作快,但 contains() 慢。 |
| 主要在列表中间进行随机访问,很少检查存在性 | ArrayList |
随机访问快,但 contains() 慢。 |
| 数据量小 (n < 100) | 任意一种 | 在数据量很小时,性能差异不明显,代码可读性可能更重要。 |
如何验证?一个简单的基准测试
我们可以用 JMH (Java Microbenchmark Harness) 来进行一个简单的性能对比。
import org.openjdk.jmh.annotations.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class ContainsPerformance {
private List<String> arrayList;
private List<String> linkedList;
private Set<String> hashSet;
private Set<String> treeSet;
private int size = 10000;
private String searchItem = "item_9999"; // 存在于集合中
private String nonExistentItem = "item_999999"; // 不存在于集合中
@Setup
public void setup() {
// 初始化集合
arrayList = new ArrayList<>(size);
linkedList = new LinkedList<>(arrayList);
hashSet = new HashSet<>(arrayList);
treeSet = new TreeSet<>(arrayList);
// 填充数据
for (int i = 0; i < size; i++) {
String item = "item_" + i;
arrayList.add(item);
linkedList.add(item);
hashSet.add(item);
treeSet.add(item);
}
}
@Benchmark
public boolean testArrayListContains() {
return arrayList.contains(searchItem);
}
@Benchmark
public boolean testLinkedListContains() {
return linkedList.contains(searchItem);
}
@Benchmark
public boolean testHashSetContains() {
return hashSet.contains(searchItem);
}
@Benchmark
public boolean testTreeSetContains() {
return treeSet.contains(searchItem);
}
// 测试不存在的元素
@Benchmark
public boolean testHashSetContainsNonExistent() {
return hashSet.contains(nonExistentItem);
}
}
预期结果分析:
testArrayListContains和testLinkedListContains的耗时会是微秒级别,并且会随着size增加而线性增长。testHashSetContains和testTreeSetContains的耗时会是纳秒级别。testHashSetContains的耗时将远低于testTreeSetContains。testHashSetContainsNonExistent的耗时会和testHashSetContains差不多,因为哈希查找的成本主要在于定位桶,而不在于equals()的调用次数。
- 没有银弹:
contains()的效率完全取决于你选择的集合实现。 HashSet是王者: 如果你需要频繁地、快速地判断一个元素是否存在,并且不关心元素的顺序,HashSet是不二之选,其基于哈希表的contains()方法提供了近乎常数时间的平均性能。TreeSet是平衡者: 如果你需要在保证元素有序的前提下进行高效的查找,TreeSet是最佳选择,其 O(log n) 的性能非常可靠。- 避免在
List中频繁使用contains(): 除非数据量非常小,否则尽量避免在ArrayList或LinkedList中频繁调用contains(),因为它会导致 O(n) 的性能开销,在数据量大时会成为性能瓶颈。 - 牢记
equals()和hashCode()协约: 当你使用自定义对象作为HashSet或HashMap的键时,必须正确实现这两个方法,否则contains()可能会返回错误的结果或性能急剧下降。

