手撕源码系列:深入理解Hashtable源码
0.简介
Hashtable是线程安全的,且它的key、value都不可以为null。此外,Hashtable中的映射不是有序的。Hashtable的实例有两个参数影响其性能:初始容量和负载因子。容量是哈希表桶的数量,初始容量就是哈希表创建时的容量。注意,哈希表的状态为 open:在发生“哈希冲突”的情况下,单个桶会存储多个条目,这些条目必须按顺序搜索。负载因子是对哈希表在其容量自动增加之前可以达到多满的一个尺度。通常,默认负载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。负载因子过高虽然减少了空间开销,但同时也增加了查找某个条目的时间。
table桶数组(竖着看)是Entry实现的,但是它们实际上并不连续,每一个桶都有一个链表(横着看),也是Entry实现的。
Hashtable的数据结构如下所示:
java.lang.Object
↳ java.util.Dictionary<K, V>
↳ java.util.Hashtable<K, V>
public class Hashtable<K,V> extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable { }
主要的变量:
//Entry将键值对的对应关系封装成了对象,用于存放数据
private transient Entry<?,?>[] table;
//table中有多少对键值对
private transient int count;
//一个阈值,当超过这个阈值后会进行rehash操作,threshold=(int)(capacity * loadFactor)
private int threshold;
//负载因子,用于计算threshold
private float loadFactor;
//Hashtable结构化改变的次数,增加删除都会引起modCount改变
private transient int modCount = 0;
其中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;
}
@SuppressWarnings("unchecked")
protected Object clone() {
return new Entry<>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
}
// Map.Entry Ops
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public V setValue(V value) {
if (value == null)
throw new NullPointerException();
V oldValue = this.value;
this.value = value;
return oldValue;
}
public boolean equals(Object o) {
//比较是否同一个类
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
//比较key和value是否相等
return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
(value==null ? e.getValue()==null : value.equals(e.getValue()));
}
public int hashCode() {
//桶的hash值和value值的hashCode进行异或运算
return hash ^ Objects.hashCode(value);
}
public String toString() {
return key.toString()+"="+value.toString();
}
}
1.构造方法
一共3种构造方法,无论使用哪一种,到最后还是会调用含2个参数的构造方法,设置初始容量Capacity和负载因子。
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);
//如果初始容量为0,默认修改为1
if (initialCapacity==0)
initialCapacity = 1;
//设置负载因子
this.loadFactor = loadFactor;
//创建一个容量大小为initialCapacity的Entry给table
table = new Entry<?,?>[initialCapacity];
//计算threshold,阈值为二者中较小的
//其中private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
带有initialCapacity的构造方法。
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
无参构造方法,默认的容量大小为11,负载因子为0.75(HashMap也是0.75)。
public Hashtable() {
//Hashtable(int initialCapacity, float loadFactor)
this(11, 0.75f);
}
2.rehash()
protected void rehash() {
//获取rehash前table的长度赋给oldCapacity
int oldCapacity = table.length;
//保存下旧table
Entry<?,?>[] oldMap = table;
// overflow-conscious code
//新table的容量值为旧容量*2+1
int newCapacity = (oldCapacity << 1) + 1;
//如果,新容量大于最大数组的容量
if (newCapacity - MAX_ARRAY_SIZE > 0) {
//如果oldCapacity == MAX_ARRAY_SIZE说明此时无法再进行扩容了,rehash失败
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
//让新容量值=MAX_ARRAY_SIZE
newCapacity = MAX_ARRAY_SIZE;
}
//创建一个新的newMap,大小为newCapacity
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
//对hashtable的结构进行更改,次数++
modCount++;
//重新计算下新的阈值,当count超过新阈值的时候就会进行下一次hash
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_MAX=0x7FFFFFFF,也就是第一位为0,剩下31位是1的二进制数
//计算新的index值,让旧的挪到新的hashtable中的新位置处,也就是计算e位于哪个桶
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
//将e插入到首个结点的前面(该首节点和e在同一个桶里)
e.next = (Entry<K,V>)newMap[index];
//将e设置位首节点
newMap[index] = e;
}
}
}
扩容前后的对比图如下所示,每次都是从头结点位置插入,省去了查找结点的时间,效率比较高。
2.添加元素
addEntry(int hash, K key, V value, int index)
private void addEntry(int hash, K key, V value, int index) {
//添加了key-value,导致结构改变,修改次数++
modCount++;
Entry<?,?> tab[] = table;
//判断当前桶内的元素数量有没有超过阈值,如果超过则需要rehash
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
//将rehash之后的table赋给tab
tab = table;
//生成key对应的hash值
hash = key.hashCode();
//计算这个key对应的value放到哪个哪个桶里
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
//获取index桶的第一个元素
Entry<K,V> e = (Entry<K,V>) tab[index];
//插入到第一个元素前面,成为新的第一个元素
//令tab[index]的下一个结点指向e
// Entry(int hash, K key, V value, Entry<K,V> next)
tab[index] = new Entry<>(hash, key, value, e);
//结点数+1
count++;
}
put(K key, V value)
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;
//计算key的hash值
int hash = key.hashCode();
//计算key对应的是哪个桶
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
//获取该桶的第一个结点
Entry<K,V> entry = (Entry<K,V>)tab[index];
//遍历该桶,找到同时满足key值相等和hash值相等的结点
//替换旧值old,然后返回old
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
//如果遍历后发现没有对应的key,添加一个新节点,此新节点的下一个结点为旧链表的第一个结点
//添加后,该节点成为新的头节点
addEntry(hash, key, value, index);
return null;
}
putAll(Map<? extends K, ? extends V> t)
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());
}
3.删除元素
remove(Object key)方法:
计算删除key的哈希,映射找到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) {
prev.next = e.next;
} else {
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
clear()
清空所有桶。
public synchronized void clear() {
Entry<?,?> tab[] = table;
modCount++;
for (int index = tab.length; --index >= 0; )
tab[index] = null;
count = 0;
}
remove(Object key, Object value)
public synchronized boolean remove(Object key, Object value) {
//value必须非空,否则空指针异常
Objects.requireNonNull(value);
//下面逻辑和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) && e.value.equals(value)) {
modCount++;
if (prev != null) {
prev.next = e.next;
} else {
tab[index] = e.next;
}
count--;
e.value = null;
return true;
}
}
return false;
}
4.查找元素
get(Object key)
/*查找键是key的值
*计算key对应的hash值,然后映射找到key对应的value放在哪个桶里
*遍历该桶的链表,查找符合条件结点
*/
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;
}
5.修改元素
replace(K key, V oldValue, V newValue)
@Override
public synchronized boolean replace(K key, V oldValue, V newValue) {
//要求hashtable中的值均不为null,故对两个值进行检验
Objects.requireNonNull(oldValue);
Objects.requireNonNull(newValue);
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
//从头结点开始遍历链表,在这里不仅要hash和key相匹配,而且要求value和oldValue相同才能做值替换
//如果不满足或者不存在对应的key那么返回false
for (; e != null; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
if (e.value.equals(oldValue)) {
e.value = newValue;
return true;
} else {
return false;
}
}
}
return false;
}
//逻辑和上面的类似
@Override
public synchronized V replace(K key, V value) {
Objects.requireNonNull(value);
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 (; e != null; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
return null;
}
6.其它方法
containsKey(Object key)
//是否包含key
/*计算hash值,然后映射找到对应key的桶
*遍历桶的链表,查找是否有满足要求的key
*/
public synchronized boolean containsKey(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 true;
}
}
return false;
}
clone()
public synchronized Object clone() {
try {
Hashtable<?,?> t = (Hashtable<?,?>)super.clone();
t.table = new Entry<?,?>[table.length];
for (int i = table.length ; i-- > 0 ; ) {
t.table[i] = (table[i] != null)
? (Entry<?,?>) table[i].clone() : null;
}
t.keySet = null;
t.entrySet = null;
t.values = null;
t.modCount = 0;
return t;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
writeObject(java.io.ObjectOutputStream s)
序列化hashtable。
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
Entry<Object, Object> entryStack = null;
synchronized (this) {
// Write out the threshold and loadFactor
s.defaultWriteObject();
// Write out the length and count of elements
s.writeInt(table.length);
s.writeInt(count);
// Stack copies of the entries in the table
for (int index = 0; index < table.length; index++) {
Entry<?,?> entry = table[index];
while (entry != null) {
entryStack =
new Entry<>(0, entry.key, entry.value, entryStack);
entry = entry.next;
}
}
}
// Write out the key/value objects from the stacked entries
while (entryStack != null) {
s.writeObject(entryStack.key);
s.writeObject(entryStack.value);
entryStack = entryStack.next;
}
}
readObject(java.io.ObjectInputStream s)
反序列化hashtable。
/**
* Reconstitute the Hashtable from a stream (i.e., deserialize it).
*/
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the threshold and loadFactor
//读取阈值和负载因子
s.defaultReadObject();
// Validate loadFactor (ignore threshold - it will be re-computed)
//对负载因子进行合法性检验
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new StreamCorruptedException("Illegal Load: " + loadFactor);
// Read the original length of the array and number of elements
//读取桶数组的初始长度
int origlength = s.readInt();
//读取元素的个数
int elements = s.readInt();
// Validate # of elements
if (elements < 0)
throw new StreamCorruptedException("Illegal # of Elements: " + elements);
// Clamp original length to be more than elements / loadFactor
// (this is the invariant enforced with auto-growth)
//自动增长的不变量
origlength = Math.max(origlength, (int)(elements / loadFactor) + 1);
// Compute new length with a bit of room 5% + 3 to grow but
// no larger than the clamped original length. Make the length
// odd if it's large enough, this helps distribute the entries.
// Guard against the length ending up zero, that's not valid.
/*
*计算新的长度,但不能大于夹紧的原始长度。如果长度足够大,则将其设为奇数
*这有助于分布条目。防止长度以零结尾,这是无效的。
*/
int length = (int)((elements + elements / 20) / loadFactor) + 3;
if (length > elements && (length & 1) == 0)
length--;
length = Math.min(length, origlength);
// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, length);
table = new Entry<?,?>[length];
threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
count = 0;
// Read the number of elements and then all the key/value objects
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
// sync is eliminated for performance
reconstitutionPut(table, key, value);
}
}
hashCode()
public synchronized int hashCode() {
/*
* This code detects the recursion caused by computing the hash code
* of a self-referential hash table and prevents the stack overflow
* that would otherwise result. This allows certain 1.1-era
* applets with self-referential hash tables to work. This code
* abuses the loadFactor field to do double-duty as a hashCode
* in progress flag, so as not to worsen the space performance.
* A negative load factor indicates that hash code computation is
* in progress.
*/
//定义hash值
int h = 0;
if (count == 0 || loadFactor < 0)
return h; // Returns zero
//标志生成hashCode开始
loadFactor = -loadFactor; // Mark hashCode computation in progress
Entry<?,?>[] tab = table;
//遍历整个桶数组,然后Hashtable的实例所对应的hash值为所有桶的hash值之和
for (Entry<?,?> entry : tab) {
while (entry != null) {
h += entry.hashCode();
entry = entry.next;
}
}
//标志hashCode生成结束
loadFactor = -loadFactor; // Mark hashCode computation complete
return h;
}
7.小结
- 关于哈希碰撞
- 我们知道,Hashtable和HashMap(JDK1.7)的底层结构是数组与链表结合实现,Key的hashCode决定对象在数组中存储的位置,相同hashCode的key对象会放在同一个数组角标下。这是最理想的状况,但是存在另外一个问题,当所有的对象的hashCode相同时,所有对象都被放在一个角标中,就会出现hash碰撞问题,这时数组缩为一个链表。这时,get方法本来时间复杂度为O(1)却变成了O(n),性能急剧下降。
- HashMap中哈希碰撞的问题在tJDK1.8中已经解决了,采用数组+链表+红黑树的方式实现,Hashtable可以采用线性探索法和拉链法去解决。
- 哈希表速度比较快的原因
- 在数据结构中,我们对两种数据结构应该会非常熟悉:数组与链表。数组的特点就是查找容易,插入删除困难;而链表的特点就是查找困难,但是插入删除容易。既然两者各有优缺点,那么我们就将两者的有点结合起来,让它查找容易,插入删除也会快起来。哈希表就是讲两者结合起来的产物。
- 小结下Hashtable和HashMap的区别
- HashMap不是线程安全的,但是Hashtable是线程安全的
- HashMap的key和value都可以为NULL,但是Hashtable的key和value都不能为NULL,否则直接报错。
- 在HashMap中的put()方法中,在定位桶数组的下标的时候,使用的是key的hashcode &(n - 1) 跟通数组的长度进行与的位运算,但是在Hashtable中的时候,用到的是
(hash & 0x7FFFFFFF) % tab.length
。这是对桶数组的长度进行取模,效率肯定没有上一步的HashMap的效率高。 - HashMap的初始化默认桶数组的长度是10,负载因子0.75,但是在Hashtable中的通数组的默认长度是11,负载因子是0.75
- 在进行扩容的时候也会有点不一样,在Hashtable中的扩容时用的是(oldCapacity << 1) + 1(也就是长度*2+1)来进行的,而在HashMap中是直接二倍进行扩容的。
- HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。