注:本文基于jdk1.8
hashTable与hashMap相同,都是使用散列表来实现存储结构,在功能上,他们两也基本相同,除了hashMap可以使用null的键值对和hashTable是线程安全的。
散列表的结构在jdk1.8之后略有不同,hashMap采用了链表+红黑树的方式来解决hash冲突,但hashTable仍只使用链表来解决。
hashTable所继承的类与HashMap不同,它是继承于Dictionary类,这是一个已经废弃的类,不建议使用。当然,hashMap和hashTable都实现了Map接口。
类的继承关系:
hashTable的主要成员变量:
//hash表
private transient Entry<?,?>[] table;
//键值对的数量(注意不是hash表的长度)
private transient int count;
//加载阈值
private int threshold;
//加载因子
private float loadFactor;
//表结构变化次数,用于快速失败
private transient int modCount = 0;
构造方法:
hashTable的构造方法与hashMap基本相同,但是无参的构造方法的默认配置不同:
public Hashtable() {
this(11, 0.75f);
}
hashMap的默认大小为16,使用DEFAULT_INITIAL_CAPACITY存储;而hashTable的默认大小为11,且是硬编码的。
重要方法:
public synchronized V put(K key, V value) {
//确保value不为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();
//根据hash码计算存储位置
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
//已存在相同的key,更新值
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为null的处理实际上在object的hashCode方法中,这是一个native方法,会对null值抛出异常。
private void addEntry(int hash, K key, V value, int index) {
//结构变化次数加一
modCount++;
Entry<?,?> tab[] = table;
if (count >= threshold) {
//超过加载阈值,rehash
rehash();
tab = table;
hash = key.hashCode();
//计算存储位置
index = (hash & 0x7FFFFFFF) % tab.length;
}
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
//注意,原来的链表e放在了新链表的后头,也就是说新节点总是插入在链表的头部
tab[index] = new Entry<>(hash, key, value, e);
//键值对数量增加
count++;
}
addEntity方法总是将新加入的节点放置在链表的头部,在jdk1.7中hashMap也是这样,但是在jdk1.8中hashMap则会将新节点放置在链表的尾部。
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
//原来容量乘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++;
//计算新的加载阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
//将老元素一一重新计算hash放置在新hash表中
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操作对容量的变化与HashMap不同,hashMap总是变为原来的两边且保证为2的次幂,而hashTable为原来的两倍加一,保证为质数。这是因为他们的计算数组下标的方式不同导致的。
hashMap计算元素位置的方式为 hashCode&(length-1) ,hash码与长度减一进行与操作,当length为2的次幂时,length-1总为011**111形式,这样在与操作时都为自己本身,操作较快,且当出现某一位为0时,任何数只在该位不同的数操作的结果都是相同的,这大大增加了hash碰撞的几率,因此HashMap要求容量为2的次幂。
hashTable计算元素位置的方式为(hash & 0x7FFFFFFF) % tab.length,由于存在一部取余操作,所以当hash的低位相同高位不同时会很容易因为高位失效出现hash冲突,因此要求为质数。
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;
//计算hash码,这一步会进行key为null的处理
int hash = key.hashCode();
//计算位置
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
//遍历该位置的链表,找到对应的key将其移除
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;
}
remove方法比较简单,找到对应的链表挨个查找将其除掉。
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);
}
}
hashTable的克隆方法为浅度克隆,仅仅克隆了结构并没有复制键值对,而是在克隆的对象中保存了一个键值对的地址,要实现深度克隆需要重写此方法。
hashTable的主要方法就为这些,注意上面的public方法都带有synchronized关键字,因此hashTable是线程安全的,但是这种线程安全的实现并不是很好,如果需要线程安全的类最好使用 Collections. synchronizedMap ()。