1.HashTable简述
1.1HashMap与Hashtable继承体系
HashTable继承体系
可以看出,HashTable实现了Map接口,继承了Dictionary类。
HashMap继承体系
HashMap继承于AbstractMap,实现了Map接口。
1.2概述
- HashTable是一个古老的(JDK1.0时就已存在)
线程安全
的容器,其核心方法都是synchronized修饰的。相反HashMap不是线程安全的。
关于key或者value为null的要求
-
HashTable不允许key或者value为NULL。
public synchronized V put(K key, V value) { // 不允许value为NULL。 if (value == null) { throw new NullPointerException(); } //这里调用key的hashCode()方法,所以key也不能为NULL。 int hash = key.hashCode(); // 。。。 }
-
HashMap允许存在一个key为NULL的Entry,但是value为NULL的Entry的个数没有限制。
static final int hash(Object key) { int h; //如果key为NULL那么哈希值为0 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
1.3核心结构
1.3.1Entry节点
private static class Entry<K,V> implements Map.Entry<K,V> {
//key的hash值
final int hash;
final K key;
V value;
//产生hash冲突时要形成链表,next节点
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;
}
1.3.2重要属性
//底层的Entry数组
private transient Entry<?,?>[] table;
//元素(Entry节点)个数
private transient int count;
//扩容阈值 (判断是否需要扩容 threshold = 哈希表长度 * 加载因子)
private int threshold;
//加载因子
private float loadFactor;
/*
* Java中的一种fail-fast(快速失败)机制,每次添加或删除元素(修改不会)modCount都会 * +1,然后使用迭代器遍历时会先讲modCount的值赋给expectedModCount,然后在遍历的
* 时候会检查两者是否还相同(不相同说明在遍历期间有其他线程添加或者删除了元素,这时就 * 会抛出ConcurrentModificationException异常)
*/
private transient int modCount = 0;
1.3.3构造方法
双参构造方法
/*
* @param initialCapacity 初始化容量
* @param loadFactor 加载因子
* 就是根据传入的初始容量构造一个Entry数组,然后计算扩容阈值。
*/
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;
//初始化Entry数组赋值给table。
table = new Entry<?,?>[initialCapacity];
/*
* MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
* 阈值 = 数组长度 * 加载因子,这里跟INF - 7取一个min。
*/
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
其他构造器,调用的还是上面的双参构造方法
//传入初始容量,调用的还是上面的双参构造器,加载因子默认为0.75
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
//空参构造器,默认的初始容量为11,加载因子为0.75
public Hashtable() {
this(11, 0.75f);
}
//传入一个Map,默认的初始容量为 max(2 * t.size(), 11),加载因子为11
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
总结
- 这里跟HashMap的区别就是,HashMap调用无参构造器时,不会初始化Entry数组,只会为加载因子赋值为0.75,只有第一次put时才会创建Entry数组,且默认的数组长度为
16
. - HashTable调用无参构造器时,就会创建一个长度为
11
的Entry数组
1.4核心方法详解
1.4.1put()
/*
* 同步方法。
*/
public synchronized V put(K key, V value) {
// value不允许为NULL。
if (value == null) {
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
//获取key的hashCode值
int hash = key.hashCode();
/*
* 寻址算法
* 0x7FFFFFFF = Integer.MAX_VALUE
* (hash & 0x7FFFFFFF)的作用是将hash变为一个正整数
* 直接对table.length进行取余,得到的值的范围就在 [0, len - 1]。
*/
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
//获取index位置的entry。
Entry<K,V> entry = (Entry<K,V>)tab[index];
//遍历桶位,查找当前key是否已经存在于桶位的链表中。
for(; entry != null ; entry = entry.next) {
//hash值相等 并且 key的equals()结果也相等,进行替换
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
//替换
entry.value = value;
//返回旧的value。
return old;
}
}
//能走到这里,说明当前桶位中没有key相同的entry,需要将当前entry插入进去。
addEntry(hash, key, value, index);
//返回NULL。
return null;
}
当前的哈希表中不存在与当前entry的key相等的entry,这时就需要将当前的entry插入的哈希表中,调用的是addEntry()方法,我们看一下addEntry()方法的源码
private void addEntry(int hash, K key, V value, int index) {
//添加操作 modCount + 1
modCount++;
Entry<?,?> tab[] = table;
//count = 当前哈希表中的元素个数,大于扩容阈值,所以需要扩容。
if (count >= threshold) {
//扩容操作,下面详细分析
rehash();
/*
* 扩容之后,当前entry寻址后的index会发生变化,
* 所以重新计算。
*/
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
//获取当前index桶位的头结点
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
//将当前的entry插入到桶位的头结点,它的next节点是e(原头结点)
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
总结
put大致流程
- 前置条件:key和value都不允许为NULL
- 调用key的hashCode获取hash值,对length取余寻址,获取桶位索引index。
- 遍历当前桶位,判断是否存在相同key的entry,存在直接替换value,然后返回旧的value。
- 不存在,调用addEntry(),判断是否需要扩容,需要扩容就去扩容,然后重新寻址
- 获取寻址后的桶位index,将当前entry直接插入到桶位头节点。结束。
流程图
与HashMap的几点区别
-
HashTable中的entry的key的最终的hash值就是其
hashCod()方法的返回值
,而HashMap中的Entry的key的最终的hash值是hashCode ^ (hashCode >>> 16)
-
HashTable的寻址算法为
(hash % tab.length)
,而HashMap的寻址算法是(hash & tab.length - 1)
1.4.2rehash()
总结
扩容操作很简单,(一般情况)直接扩容为 oldCapacity * 2 + 1
,然后将原哈希表中的每个Entry重新寻址后插入到新表中(头插法)。
@SuppressWarnings("unchecked")
protected void rehash() {
//原哈希表长度
int oldCapacity = table.length;
//oldMap引用原哈希表
Entry<?,?>[] oldMap = table;
/*
* 一般情况: 扩容为原oldCapacity * 2 + 1
*/
int newCapacity = (oldCapacity << 1) + 1;
//情况很少。一般size不会是INF级别的。
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
//根据newCapacity创建一个新Entry数组
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
//重新计算扩容阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
//table引用新的Entry数组
table = newMap;
/*
* 遍历原哈希表,将所有的entry重新寻址插入到新的哈希表中
*/
for (int i = oldCapacity ; i-- > 0 ;) {
//遍历每一个桶位
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
//e指向当前节点
Entry<K,V> e = old;
//old向后走
old = old.next;
//重新寻址
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
//当前节点指向桶位头结点
e.next = (Entry<K,V>)newMap[index];
//桶位头节点变为当前节点,完成插入操作。
newMap[index] = e;
}
}
}
1.4.3remove()
/*
* 同步方法。
* 寻址,然后遍历桶位寻找待删除节点,找到后直接删除即可。
*/
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;
/*
* 获取hash值然后寻址
*/
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
//获取桶位头结点
Entry<K,V> e = (Entry<K,V>)tab[index];
/*
* prev指向当前节点的前驱节点 e指向当前节点
*/
for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
//找到了要删除的entry
if ((e.hash == hash) && e.key.equals(key)) {
//modCount + 1
modCount++;
//prev != null 表示e非头结点
if (prev != null) {
//直接将e干掉
prev.next = e.next;
//e是头结点
} else {
//将头结点变为e的next
tab[index] = e.next;
}
//元素个数-1
count--;
//获取value
V oldValue = e.value;
//将e.value置为NULL,帮助GC
e.value = null;
//将旧值返回
return oldValue;
}
}
//不存在 返回NULL。
return null;
}
1.4.4get()
/*
* 同步方法,获取指定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) {
//查找成功,返回value。
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
//查找失败,返回NULL。
return null;
}
1.5总结
-
Hashtable是线程安全的容器
(方法都加了synchronized)
,底层只有数组 + 链表
-
Hashtable不允许key或者value为NULL。
-
Hashtable存在fast-fail机制,modCount实现。
-
hash值是key的hashCode()的返回值
-
寻址算法
hash % table.length
-
调用无参构造器默认初始容量为
11
,加载因子为0.75
-
一般情况下,扩容为
oldCap * 2 + 1