文章目录
写在前面的话
- 照旧,本文并没有完整的把所有 HashTable 的知识点和源码都解析完,只是挑了我个人觉得比较重要和有意思的点,如果解答了你的一个小困惑,记得给我点赞啊,你赞一下我可太爽了啊!
- HashTable 的这篇文章是我在学习完 HashMap 和 ConcurrentHashMap 之后才写的,我本来不打算看 HashTable 的源码的,因为在我实际的工作中根本不会用到 HashTable,但是无奈发现面试题都拿它来进行对比,所以才回过头去看了下。
因此,如果是学生党,我觉得可以仔细看看,有助于后续看 HashMap 和 ConcurrentHashMap。如果是急于准备面试的同行,我觉得可以快速过一下就好了。之前写的关于 HashMap 和 ConcurrentHashMap 的文章也贴在这里了:
源码理解
一些关键变量
/**
* The hash table data. */private transient Entry<?,?>[] table;
/**
* The total number of entries in the hash table. */
private transient int count;
/**
* The table is rehashed when its size exceeds this threshold. (The * value of this field is (int)(capacity * loadFactor).) * * @serial
*/
private int threshold;
/**
* The load factor for the hashtable. * * @serial
*/
private float loadFactor;
// 有 modCount 的话,是不是意味着快速失败呢?后文细讲。
private transient int modCount = 0;
private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
// 注意,HashTable 的 Entry 节点中没有使用 volatile 修饰
V value;
Entry<K,V> next;
}
HashTable 的基础属性不多,依旧是常规的 table 数组,就不展开讲解了。
构造方法
HashTable 的构造方法也很常规,不过有两个注意点
- 默认容量是 11 ,而不是如 HashMap 那样要求是 2 的幂次方,可以复习一下 HashMap 这么设置容量的原因。
- table 数组在构造方法中就会被初始化,HashMap 则是懒加载的,可以复习一下这么做的优劣。
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;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
public Hashtable() {
this(11, 0.75f);
}
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
常规方法概览
列举几个常见方法的定义,在方法维度直接使用了 synchronized 修饰,因此关于 HashTable 为什么线程安全的原因已经明确了,简单粗暴。
多提一句,我觉得不能看到 synchronized 就本能地觉得这个代码差,优劣都是相对的概念,实际上是一种性价比的抉择。在我们实际的开发中 synchronized 并不少见,在一些并发度要求没那么高的场景下使用 synchronized 对业务来说也几乎是无感的。
public synchronized int size() {
return count;
}
public synchronized boolean isEmpty() {
return count == 0;
}
public synchronized Enumeration<K> keys() {
return this.<K>getEnumeration(KEYS);
}
public synchronized V get(Object key) {
// 先忽略
}
public synchronized V put(K key, V value) {
// 先忽略
}
等。。。
get() 方法
概括一下步骤。
- 获取哈希值。
- 将哈希值和 0x7FFFFFFF 进行 & 操作,然后取模获得下标 index 。
- 获取 table 数组下标 index 处的元素,遍历匹配相等的元素。
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
-
& 0x7FFFFFFF 这个操作又出现了,还记得我们在 ConcurrentHashMap 的文章中专门对这块的解释吗?遗忘的话可以复习一下,简单来说就是获得一个大于 0 的哈希值。
-
对 table 数组对应下标位置的元素的遍历中,没有出现对二叉树的判断,所以 HashTable 对哈希冲突的处理就是链表。
put() 方法
概括一下步骤。
- 对 value 值进行校验,如果为null则抛出空指针。
- 计算下标 index,并对 table 数组对应下标位置元素进行判断。
- 如果对应元素不为空,则说明存在哈希冲突,此时遍历到相等于的元素并进行更新。
- 如果对应元素为空,则说明是新增元素的场景,进入 addEntry 方法。(涉及扩容)
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();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
addEntry() 新增元素方法
- 先对 modCount 进行自增
- 判断是否达到扩容条件,如果是的话调用 rehash 方法进行扩容,并根据新的数组长度获得计算出新的 index 下标
- 在 table 数组对应 index 下标位置插入元素
- count 自增。
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
rehash() 扩容方法
- 计算扩容后的数组长度,并于最大值进行校验。如果已经达到最大值,则直接退出。
- 创建新数组,计算新的扩容阈值。
- 倒序遍历旧 table 数组,计算新的 index 位置,放入新 table 对应位置。
@SuppressWarnings("unchecked")
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// 将原来的 table 数组长度乘以2再1。(二进制下左移一位就是x2)
// overflow-conscious code
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++;
// 计算新的下一轮扩容阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
// 倒序遍历旧 table 数组,计算新的 index 位置,放入新 table 对应位置。
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;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
一个有意思的问题
对 Fail-Fast 快速失败机制的争吵
本文开头的链接文章中我们讲过 HashMap 的 Fail-Fast 机制,可以复习一下。
这个问题非常有意思,因为我看到网上关于这个问题的博客下经常有人吵架,吃瓜属性迫使我要一探究竟!
重点来了:
- HashMap 只有一个 Iterator 迭代器,它在遍历的时候判断了 expectedModCount != modCount 抛出 ConcurrentModificationException 异常,所以它是 Fail-Fast 的。
- HashTable 有两个迭代器,两个!一个也是 Iterator 迭代器,也是 Fail-Fast 的,另一个是 Enumerator 迭代器,这个迭代器在遍历的时候没有判断 expectedModCount != modCount ,也没有抛出 ConcurrentModificationException 异常,所以这个迭代器是 Fail-Safe 的。
所以,吵半天两人吵的不是一个方法吧?
HashMap 中的 Enumerator 迭代器
public boolean hasMoreElements() {
Entry<?,?> e = entry;
int i = index;
Entry<?,?>[] t = table;
/* Use locals for faster loop iteration */
while (e == null && i > 0) {
e = t[--i];
}
entry = e;
index = i;
return e != null;
}
@SuppressWarnings("unchecked")
public T nextElement() {
Entry<?,?> et = entry;
int i = index;
Entry<?,?>[] t = table;
/* Use locals for faster loop iteration */
while (et == null && i > 0) {
et = t[--i];
}
entry = et;
index = i;
if (et != null) {
Entry<?,?> e = lastReturned = entry;
entry = e.next;
return type == KEYS ? (T)e.key : (type == VALUES ? (T)e.value : (T)e);
}
throw new NoSuchElementException("Hashtable Enumerator");
}
HashMap 中的 Iterator 迭代器
@Override
public synchronized void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action); // explicit check required in case
// table is empty. final int expectedModCount = modCount;
Entry<?, ?>[] tab = table;
for (Entry<?, ?> entry : tab) {
while (entry != null) {
action.accept((K)entry.key, (V)entry.value);
entry = entry.next;
if (expectedModCount != modCount) {
throw new ConcurrentModificationException();
}
}
}
}
本地自测方法,别光看,跑一跑代码吧,Iterator 这个不仅会报错,还会死循环。
Enumerator 迭代器,通过 .keys() 方法获得
public static void main(String[] args) {
Hashtable<String, String> ht = new Hashtable<>();
ht.put("key1", "好");
ht.put("key2", "很好");
ht.put("key3", "非常好");
ht.put("key4", "好到爆炸");
Enumeration<String> keys = ht.keys();
while (keys.hasMoreElements()) {
ht.remove("key1");
String value = keys.nextElement();
System.out.println(value);
}
}
Iterator 迭代器,通过 .keySet().iterator() 方法获得
public static void main(String[] args) {
Hashtable<String, String> ht = new Hashtable<>();
ht.put("key1", "好");
ht.put("key2", "很好");
ht.put("key3", "非常好");
ht.put("key4", "好到爆炸");
Iterator<String> iterator = ht.keySet().iterator();
while (iterator.hasNext()) {
ht.remove("key1");
String value = iterator.next();
System.out.println(value);
}
}