HashTable 是一种基于哈希函数
的数据结构。它将每个键Key映射到一个唯一的索引Index,通过这个索引来快速访问数据。底层是一个数组
,数组中的每个元素称为桶(bucket)。
当我们需要访问某个元素时,首先会对键进行哈希函数
计算得到一个Hash值
,再将Hash值进行位运算
,最后和数组长度进行取模
,得到它的索引
。然后通过该索引找到相应的桶,最终在桶中找到对应的元素。
存在哈希碰撞
的问题,即不同的键经过哈希函数计算后可能得到相同的索引,导致数据被存储在同一个桶中,HashTable 默认使用链表
来解决哈希碰撞。
如何将Key映射成index的?
//先将key进行hash运算,得到一个hash值
int hash = key.hashCode();
//将hash值先进行位运算后,再与数组长度进行取模,得到索引index
int index = (hash & 0x7FFFFFFF) % tab.length;
源码分析(JDK1.8)
成员变量属性
/**
* 初始Entry数组.
*/
private transient Entry<?,?>[] table;
/**
* 记录table上Entry的个数
*/
private transient int count;
/**
* 数组的扩容容量阀值,当超过这个阀值时,就需要进行扩容操作
* (int)(容量*负载系数)
*/
private int threshold;
/**
* 负载因子,默认0.75.
*/
private float loadFactor;
/**
* 此哈希表在结构上被修改的次数
* 结构修改是指更改哈希表或以其他方式修改其内部结构(例如:rehash)
*/
private transient int modCount = 0;
构造函数
- 创建一个空的HashTable,默认初始化大小为11,负载因子为0.75
public Hashtable() {
this(11, 0.75f);
}
- 创建一个指定初始容量的HashTable,默认负载因子为0.75
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
- 创建一个指定初始容量和负载因子的HashTable
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
//创建一个初始容量的Entry[]数组
table = new Entry<?,?>[initialCapacity];
//计算扩容的阀值
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
- 创建一个包含指定Map中所有映射关系的新HashTable
public Hashtable(Map<? extends K, ? extends V> t) {
//创建一个指定容量大小,负载因子0.75的数组
//容量大小: 两倍的Map元素个数,与11相比,取大值
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
/**
* 循环将Map中的元素,复制到新的HashTable中
*/
public synchronized void putAll(Map<? extends K, ? extends V> t) {
for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
put(e.getKey(), e.getValue());
}
put()
HashTable的put()方法是往Hashtable中添加元素的方法
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode(); //先将key进行hash运算,得到一个hash值
//将hash值先进行位运算后,再与数组长度进行取模,得到索引index
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
//判断key是否已经存在,如果存在,则用新的value替换旧的value,并返回旧的value;
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
//key不存在,将键值对插入Hashtable中,返回null
addEntry(hash, key, value, index);
return null;
}
/**
* 添加Entry
*/
private void addEntry(int hash, K key, V value, int index) {
modCount++; //数组的结构变化次数 +1
Entry<?,?> tab[] = table;
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
//当数组上的元素个数已经达到阀值,则进行扩容操作
rehash();
tab = table;
hash = key.hashCode(); //重新计算hash值和index
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
//创建一个新Entry,并将Entry添加到数组指定的index位置
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++; //数组元素个数+1
}
/**
1. 扩容,新数组长度 = 旧数组长度 * 2 + 1
*/
protected void rehash() {
int oldCapacity = table.length; //记录旧数组的容量大小
Entry<?,?>[] oldMap = table;
// overflow-conscious code
//计算新数组的容量大小 = 旧数组的容量大小 * 2 + 1
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
//创建新的数组
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
//更新扩容阈值,为新数组大小的75%
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
// 遍历旧桶数组中的元素,将元素重新计算散列值放入新数组中
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
// 重新计算散列值 index
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
// 将元素插入到新数组中
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
remove()
HashTable的remove()方法用于从哈希表中删除指定键对应的元素,有两个删除方法
- 删除指定key
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) { //先匹配待删元素
modCount++;
if (prev != null) { //如果要删除的元素不是桶中第一个元素
//next重新指向即可
prev.next = e.next;
} else { //如果要删除的元素是桶中第一个元素
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue; // 返回删除元素的值
}
}
return null; // 没有找到要删除的元素,直接返回null
}
- 删除指定key和指定value
public synchronized boolean remove(Object key, Object value) {
Objects.requireNonNull(value); //校验指定value不能为null
Entry<?,?> tab[] = table;
int hash = key.hashCode(); //对指定key进行hash运算
int index = (hash & 0x7FFFFFFF) % tab.length; //再计算出数组的位置index
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index]; //获取数组上index位置出对应的元素
for (Entry<K,V> prev = null; e != null; prev = e, e = e.next) {
//匹配待删元素
if ((e.hash == hash) && e.key.equals(key) && e.value.equals(value)) {
modCount++;
if (prev != null) { //如果要删除的元素不是桶中第一个元素
//next重新指向即可
prev.next = e.next;
} else { //如果要删除的元素是桶中第一个元素
tab[index] = e.next;
}
count--; //数组个数-1
e.value = null; //设置待删元素的value为null
return true;
}
}
return false; //未匹配到待删元素,返回false
}
get()
HashTable的get()方法是用来获取给定键的值的方法。它可以根据传入的键值在哈希表中查找对应的节点,并返回该节点存储的值
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode(); //先将传入的键值通过哈希函数进行哈希计算,得到哈希值
int index = (hash & 0x7FFFFFFF) % tab.length; //根据哈希值找到对应的桶
//再在桶中查找对应的节点
//使用for循环匹配,此处可能是链表结构
//节点可能被放置在桶的链表中,需要遍历链表才能找到对应的节点
//因此,在桶中查找节点的时间复杂度为O(1)~O(n),其中n为桶中节点的数量。
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null; //未匹配上节点,直接返回null
}
总结: HashTable的优点?
- 快速查找:Hashtable中的键值对是通过哈希函数计算索引值得到的,因此可以快速地查找元素,
时间复杂度为 O(1)
。 - 高效插入和删除:由于哈希表的键值对是通过哈希函数计算索引值得到的,所以在插入或删除元素时,只需要计算一次哈希值即可定位元素,因此插入和删除操作都非常快速,
时间复杂度为 O(1)
。 - 空间利用率高:哈希表只需要存储键和值,不需要额外的空间来存储指向下一个元素的指针,因此空间利用率比较高。
- 可扩展性:由于哈希表的实现是基于数组的,因此哈希表的大小可以动态地扩展或收缩,使其可以适应不同的数据集大小。
- 灵活性高:哈希表的键和值可以是任何类型的数据,因此可以适用于各种不同的数据类型和数据结构