Hashtable的结构
和HashMap类似,Hashtable主要由一张哈希表构成,这张表由Entry<?,?>类型的数组构成,
它和HashMap的主要却别是,它是线程安全的,且不能添加null键值对。在早期的JDK版本,
Hashtable只继承了Dictionary类,在JDK 1.2之后它实现了Map接口,成为了Collections Framework的一部分。
Hashtable可以算是历史遗留类,它的实现比较简单,通过同步方法来保证线程安全,但效率比较低,因为线程只能互斥的调用同步方法,
就是说同一时刻只有一个线程能读取或者放入键值对。
Hashtable的静态成员变量
/** |
MAX_ARRAY_SIZE
Hashtable的哈希表的最大容量,即数组table的最大长度,值为Integer.MAX-8。
Hashtable的实例域
/** |
table
Entry<?,?>[] table
Entry类型的数组,代表一张哈希表,Entry的具体结构如下:/**
* Hashtable bucket collision list 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;
}
...........................其他方法已省略
}
可以看到Entry的结构和HashMap.Node的结构是一样的,实例域都是hash,key,value,next这4个
count
int类型,表中的键值对的个数
threshold
int类型,哈希表的阈值,当表中的键值对个数超过这个值的时候,就要对表进行扩容。计算公式为:threshold = (int)(capacity * loadFactor)
loadFactor
float类型,哈希表的负载因子,表示使用的数组元素个数最多能达到数组长度的百分比。
比如数组长度为16,负载因子为0.75,则最多能使用该数组的16 * 0.75 = 12个元素
modCount
int类型,哈希表被修改的次数,比如放入元素、移除元素都算一次修改,用于实现fail-fast机制。
keySet
Set<K> keySet
,键的集合
entrySet
Set<Map.Entry<K,V>>
键值对的集合
values
Collection<V>
值的集合
Hashtable的构造方法
Hashtable有4个构造方法
/** |
put
/** |
可以看到这个方法有synchronized关键字修饰,是同步方法。这个方法的主要步骤如下:
检查键值对的值是否为null,如果是则抛出异常。这里没有检测键是否为空,因此当我们可以传一个为null的键进去时,
后面的key.hashCode()
就会抛出空指针异常,因为key为null,所以这个Hashtable的设计是有问题的。
由于key和value为空都会使该方法抛出异常,因此不能往Hashtable里面添加null键值对。调用key的hashCode方法计算hash,再用hash和0x7FFFFFFF做与运算,得到的结果和table.length取余,
最后得到该键值对的在数组中的索引,再利用索引取出里面的元素。如果通过上面计算出的索引取出的元素不为空,说明该索引的位置已经存储了元素,接下来对该元素的所在的链表进行遍历,
遍历过程中寻找与要插入的键值对的键相等的元素,如果找到这样的元素,则直接更新这个元素的值,接着返回这个元素更新前的值。
如果没有这样的元素,则调用addEntry方法将键值对插入。
接下来看看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();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
"unchecked") (
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
该方法首先使modCount错的值自增1
判断当前的元素个数是否大于等于阈值(threshold),如果是,则调用rehash()扩容,扩容后,
使用扩容后的数组长度重新计算要插入的键值对所对应的数组索引为新的键值对创建一个新的Entry对象,把该对象的next指向该数组位置原先的元素(如果元素存在的话),
然后把该对象放到上面计算出的数组索引位置上,count自增1,即Hashtable的元素个数增加1。
从这里可以看出,当插入一个键值对时,如果要插入的位置已经有元素了,则使新插入的键值对的next变量指向这个元素,
因此Hashtable的插入是在链表的头部插入,而HashMap是在尾部。
rehash方法的实现如下:/**
* Increases the capacity of and internally reorganizes this
* hashtable, in order to accommodate and access its entries more
* efficiently. This method is called automatically when the
* number of keys in the hashtable exceeds this hashtable's capacity
* and load factor.
*/
"unchecked") (
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// 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;
for (int i = oldCapacity ; i-- > 0 ;) { //遍历旧数组的所有元素
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) { //遍历链表
Entry<K,V> e = old; //将当前遍历到的元素存储到e
old = old.next; //old指向它的下一个元素
int index = (e.hash & 0x7FFFFFFF) % newCapacity; //计算元素在新空间的索引
e.next = (Entry<K,V>)newMap[index]; //使当前遍历到的元素的next指向链表的头部
newMap[index] = e; //让当前遍历到的元素变成链表的头部
}
}
}
将当前的table存储到oldMap变量中,当前的table的长度存储到oldCapacity变量中
将新容量设置为(oldCapacity << 1) + 1,存储到newCapacity变量中,如果新容量大于MAX_ARRAY_SIZE,
则继续判断oldCapacity是否已经达到了最大值(oldCapacity == MAX_ARRAY_SIZE),如果是,则不进行扩容,直接返回。
如果不是,则将新容量设置为最大值为table分配新的空间,长度为newCapacity,使modCount自增1,重新计算threshold
遍历旧数组的所有元素,计算元素在新空间上的索引,将这些元素移动到新的空间上,移动之后,原来的链表上的元素位置顺序会发生反转,
比如原来是table[x]->1->2->3,移动之后会变成table[y]->3->2->1
get
/** |
同样的,get方法也不能传入null,会引发空指针异常。它的实现也非常简单,直接根据hash计算索引,
取出索引位置的元素,从这个元素开始遍历它所在的链表,直到找到key相等的元素,和HashMap相比,
没有了从红黑树中查找元素的过程。
remove
/** |
remove方法的实现也比较简单,首先查找key,找到相同的key后就进行删除元素的操作。
clear
/** |
该方法将清除table数组上的所有元素的引用,并将count设置为0