什么是 HashSet?
在 Java 中,HashSet 是基于 HashMap 实现的一种集合类。HashSet 类继承自 AbstractSet 类,实现了 Set 接口。
HashSet 的本质是一个基于哈希表的 set 接口的实现。它不保证集合中元素的顺序,特别是它不保证元素的顺序随着时间的推移保持不变。HashSet 允许存储 null 元素,并且是非线程安全的。
Java 中的哈希表
哈希表是一种常见的数据结构,它基于哈希函数将键映射到表中的索引位置。哈希表的基本操作包括插入、删除和查找。在 Java 中,哈希表是由 Hashtable、HashMap、LinkedHashMap 和 ConcurrentHashMap 实现的。它们都是基于哈希函数来存储值的,但它们的实现稍有不同。
HashTable 是早期的 Java 哈希表实现,它是线程安全的,但它的效率较低。HashMap 是线程不安全的哈希表实现,并且比 HashTable 的效率更高。LinkedHashMap 通过链接维护了元素的顺序,它可以按照元素插入的顺序或者访问顺序维护元素的顺序。ConcurrentHashMap 是线程安全的哈希表实现,并且支持高并发。
自定义 HashSet
在本文中,我们将演示如何在 Java 中实现自定义的 HashSet。我们会使用 Hashtable 作为底层存储机制,并实现 HashSet 中常见的方法。
HashSet 类的架构设计
首先,我们将创建一个类名为 MyHashSet。该类应该实现 Set 接口。我们还应该考虑以下问题:
存储机制:如何存储元素
初始容量和负载因子:如何设置初始容量和负载因子
哈希冲突:如何处理哈希冲突
存储机制
我们将使用 Hashtable 作为底层数据结构来存储元素。Hashtable 是线程安全的,它可以自动扩容并根据负载因子来重新分配元素。
private Hashtable<Object, Object> table;
初始容量和负载因子
对于初始容量和负载因子,我们将使用以下常量值:
private static final int INITIAL_CAPACITY = 16;
private static final float LOAD_FACTOR = 0.75f;
INITIAL_CAPACITY 表示默认的初始容量大小,LOAD_FACTOR 表示默认的负载因子。当哈希表中元素的数量超过容量 * 负载因子时,哈希表会自动扩容。
哈希冲突
我们将使用链接法来解决键冲突问题。在链接法中,哈希表中的每一个元素都是一个链表,当多个键具有相同的哈希码时,它们会被存储在同一个链表中。
添加元素
接下来,我们来实现 add 方法。在添加元素时,我们将先计算元素的哈希码。
public boolean add(E e) {
if (e == null) {
return false;
}
int hash = e.hashCode();
int index = getIndex(hash);
LinkedList<Object> list = getList(index);
if (list.contains(e)) {
return false;
}
list.add(e);
size++;
if (size >= capacity * LOAD_FACTOR) {
resize();
}
return true;
}
在哈希表中,我们定义了以下几个方法:
getIndex:计算元素的索引
getList:获取索引处的链表
resize:重新分配哈希表的容量
计算索引
计算元素的索引需要使用元素的哈希码和哈希表的容量。下面是计算索引的方法:
private int getIndex(int hash) {
return hash % capacity;
}
使用哈希码对容量取模可以得到元素的索引。
获取链表
获取索引处的链表并添加元素到该链表。如果索引处没有链表,则新建一个链表。
private LinkedList<Object> getList(int index) {
LinkedList<Object> list = (LinkedList<Object>) table.get(index);
if (list == null) {
list = new LinkedList<>();
table.put(index, list);
}
return list;
}
重新分配容量
重新分配容量时,我们将创建一个新的哈希表,将所有元素插入到新的哈希表中。
private void resize() {
Hashtable<Object, Object> oldTable = table;
capacity = capacity << 1;
table = new Hashtable(capacity);
for (Object o : oldTable.values()) {
LinkedList<Object> list = (LinkedList<Object>) o;
for (Object e : list) {
int hash = e.hashCode();
int index = getIndex(hash);
LinkedList<Object> currentList = getList(index);
currentList.add(e);
}
}
}
在重新分配容量时,我们将容量翻倍。然后,我们创建一个新的哈希表,并将旧哈希表中的元素插入到新表中。
测试自定义 HashSet
在实现自定义 HashSet 后,我们需要测试其是否有效。下面是一个针对自定义 HashSet 的基本测试:
MyHashSet<Integer> set = new MyHashSet<>();
set.add(1);
set.add(2);
set.add(3);
set.add(4);
set.add(null);
assert set.contains(1);
assert set.contains(2);
assert set.contains(3);
assert set.contains(4);
assert set.contains(null);
assert !set.contains(5);
我们添加了几个整数和一个 null 值,测试了 contains 方法,如果有错误,将会抛出 AssertionError 异常。
总结
在本文中,我们介绍了 HashSet 的基本原理,深入探讨了哈希表和自定义 HashSet 的实现。我们使用了 Hashtable 作为底层存储机制,并实现了 add、contains 和 resize 方法。我们还测试了自定义 HashSet,确保其有效性。在使用自定义的 HashSet 时,请思考我们在此文中讨论的相关问题,并注意使用合适的容量和负载因子。