文章分析源码依托版本为JDK1.8,先看看HashTable
的类关系图,如下:
对于HashTable
而言,我们先看它的容器存储结构,关于这一系列容器的源码首先理解了它们设计的存储结构后,相关方法的基本实现思路就能知道个大概了,这里关于HashTable
而言,它的存储结构设计还是比较简单的,主要理解一下几个参数即可:
-
private transient Entry<?,?>[] table
: 数据存储的数组
-
private transient int count
: 容器中元素个数
-
private int threshold
: 容器扩容阀值
-
private float loadFactor
: 容器扩容的负载因子
如果熟悉HashTable
或者是HashMap
等源码的同学们,复习再看以上四个参数,基本就能够知道设计的大体思路了。但是可以看到数据存储的数组是一个Entry
数组,那么我们这里先看看Entry
元素的组成,代码如下:
private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Entry<K,V> next;
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
...
}
很显然,每一个table
中的Entry
元素的实现是一个链表的形式,并且节点元素会记录当前节点位置的哈希值,即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;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
-
- 只有初始化数组长度的构造
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f); // 默认0.75
}
-
- 无参构造
public Hashtable() {
this(11, 0.75f);
}
这里可以看到,默认情况下如果不指定初始化数组长度,默认的是长度为11,负载因子0.75
-
- 带初始化集合数据的构造
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
这个构造函数好像看用的极少,大家主要有的最多的还是2,3两个构造函数的吧。看完构造函数,继续和之前一样来详细介绍一下平常用的最多的增删改查对应的方法。
put插入
针对插入逻辑,依据不同场景提供了put
和putIfAbsent
两种方法,针对这两个方法分别介绍:
public synchronized V put(K key, V value) {
// HashTable不允许value为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;
}
以上是put
方法的实现,实现步骤很简单明了:
-
- 判断value非null
-
- 通过key获取哈希值找到这个key在数组中的位置index,然后遍历index位置的链表entry,判断链表中是否存在本次需要插入的value,如果存在则直接返回。
-3. 如果遍历完entry,发现value不存在,则执行插入操作。
- 通过key获取哈希值找到这个key在数组中的位置index,然后遍历index位置的链表entry,判断链表中是否存在本次需要插入的value,如果存在则直接返回。
这里具体的插入逻辑,我们进一步跟进addEntry
方法,代码如下:
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();
//完成扩容后,需要重新计算本次要插入的key的哈希值和在table中的槽位
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// 创建新的Entry节点,并插入到table中
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
这个方法的逻辑是新插入一个Entry
到数组table
中,首先会判断容器中元素个数是否达到扩容的阀值,如果达到了阀值,需要先进行扩容。扩容完毕后再创建新节点并插入到table
中。如果数组没有达到扩容的阀值,插入逻辑十分简单,就是直接在上一步put
方法中获取到的table
的index
位置中获取到的entry
作为新节点的next
元素,创建新节点Entry
,然后插入到table[index]
中。
这里,我们继续看一下从addEntry
中出现的rehash
方法的实现,因为这涉及到扩容,属于影响性能的重要实现方法,实现如下:
protected void rehash() {
// 先保留现场,把老的数据暂时留存备用
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// 计算新的扩容数组大小,左移计算可以理解为原来的数值乘以2
int newCapacity = (oldCapacity << 1) + 1; // new = old*2 +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;
// 遍历最开始留存的老的数组中的所有元素,对每个元素重新计算哈希值和在新数组中的槽位,一个不落的插入到新数组中
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;
}
}
}
很显然,rehash
方法如果执行,那么对性能的影响是十分大的,所以我们在使用HashTable
的时候,也是最好能提前预估一下元素的数量,提取设置好初始容量,减少扩容。
以上就是put(K key, V value)
的实现,还有一个方法是putIfAbsent
,这个方法的实现就是建立在put
方法只上的,意思就是如果当前插入的(key,value)
对应的key
不存在,才会进行插入,否则直接作罢。实现逻辑和put
完全一致,只是在遍历entry
判断的时候稍微不一致,在此不做赘述。
删除remove
在理解了HashTable
的存储结构以及put
方法的逻辑后,再理解这里的remove
简直不要太简单哦,它的核心思路简而言之就是删除链表中间位置的一个节点。实现代码如下:
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];
// 遍历链表中的数据,e表示当前节点,pre记录的是上一个节点
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) { // pre不为空,直接上一个节点的next跳过当前节点就是删除当前节点了。
prev.next = e.next;
} else { // pre为空,说明要删除的是头结点
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
查找get
和理解remove
一样,只能说看懂了put
方法后,这里的get
方法的实现,其实已经在put
中有了体现,因为put
中会先判断对应的key
是否已经存在,这个过程其实就是这里get
逻辑的实现,代码如下:
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;
}
最后一点,就是大家需要注意,对于以上这些所有的方法,可以发现方法前都加了修饰符synchronized
,所以可以知道HashTable
是线程安全的,实现线程安全的方法就是所有方法都加锁,这种实现线程安全的方式很简单粗暴,所以导致多线程的时候性能会比较不理想。