在 Java 中,HashSet
是一个常用的数据结构,用于存储不重复的元素。因为它使用了哈希表作为底层实现,所以具有高效的插入和查找性能。在这篇博客中,我们将详细剖析 HashSet
如何检查重复元素,并结合源码进行深入解析。
基本原理
当你把对象加入 HashSet
时,HashSet
会先计算对象的 hashcode
值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode
值作比较。如果没有相符的 hashcode
,HashSet
会假设对象没有重复出现。但是如果发现有相同 hashcode
值的对象,这时会调用 equals()
方法来检查 hashcode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让加入操作成功。
HashSet 的源码解析
在 JDK 1.8 中,HashSet
的 add()
方法只是简单地调用了 HashMap
的 put()
方法,并且判断了一下返回值以确保是否有重复元素。以下是 HashSet
中的源码:
java
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
}
可以看到,HashSet
内部使用了一个 HashMap
来存储元素。PRESENT
是一个静态的常量对象,用来作为 HashMap
的 value。
HashMap 的 putVal 方法解析
进一步来看 HashMap
的 putVal
方法,它是真正负责插入元素的地方:
java
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
该方法主要执行以下步骤:
- 计算哈希值并确定数组索引:通过
hash
值计算出元素在数组中的位置。 - 检查是否存在冲突:如果当前位置没有元素,直接插入。如果有冲突(即位置上已有元素),则通过链表或红黑树处理冲突。
- 检查重复元素:使用
equals
方法来比较键是否相等。如果相等,则认为是重复元素。
举例说明
假设我们有如下代码:
java
Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");
set.add("apple");
在上述代码中,HashSet
在添加第二个 "apple" 时,会先计算其 hashcode
,发现位置上已有元素(第一个 "apple"),然后调用 equals
方法判断两者是否相同,进而得出添加失败的结论。
性能对比
HashSet
的查找和插入操作在大多数情况下的时间复杂度为 O(1),这是因为哈希表具有快速的随机访问能力。然而在最坏情况下(哈希冲突严重,所有元素都在一个桶内),时间复杂度会退化为 O(n)。
我们可以通过以下代码来测试 HashSet
的性能:
java
public class HashSetPerformanceTest {
public static void main(String[] args) {
Set<Integer> hashSet = new HashSet<>();
long startTime = System.nanoTime();
for (int i = 0; i < 1000000; i++) {
hashSet.add(i);
}
long endTime = System.nanoTime();
System.out.println("Time taken to insert 1 million elements: " + (endTime - startTime) + " ns");
}
}
总结
通过对 HashSet
和 HashMap
源码的剖析,我们可以清晰地了解到 HashSet
如何检查重复元素。它通过计算 hashcode
并调用 equals
方法来确保集合中没有重复元素。