1、HashTable概述
HashTable以key-value形式存储数据,key不允许为null,value也不允许为null。
HashTable线程安全。
HashTable对大量的方法都添加synchronized锁,所以性能不高。
HashTable底层是数组,又被称为hash桶,每个桶中存放的是链表,链表中的节点存储着集合中的元素。
当HashTable中的元素数量达到threshold阈值的时候,会触发扩容,hash桶的长度翻倍。
单线程场景下可以使用HashMap,多线程场景下可以使用ConcurrentHashMap,HashTable基本上处于被放弃的状态,所以今天这篇文章也就简单介绍一下HashTable,不会像之前文章HashMap源码分析介绍HashMap那样细致。
2、HashTable计算key在hash桶中落点的算法
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
hash & 0x7FFFFFFF 这一步是为了保证hash值是正数。
HashTable在计算key在hash桶中位置的时候直接采用对hash桶大小取余的方式。
相对于HashMap中对hash值采用扰动函数以及与运算的方式,HashTable中key的分布不够均匀并且计算位置效率低。
3、HashTable的构造方法
HashTable一共有四个构造方法:
public Hashtable() {
this(11, 0.75f);
}
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
public Hashtable(int initialCapacity, float loadFactor) {
//校验initialCapacity
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
//校验initialCapacity
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
//如果initialCapacity == 0 将其设置成1
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
//根据initialCapacity值构建hash桶
table = new Entry<?,?>[initialCapacity];
//计算threshold
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
public Hashtable(Map<? extends K, ? extends V> t) {
//初始化构建HashTable
this(Math.max(2*t.size(), 11), 0.75f);
//将参数 t 中的元素一一put到构造出来的HashTable中
putAll(t);
}
其中前三个构造方法都是初始化一个空的HashTable,第四个构造方法会构造一个HashTable然后将参数中的 t 中的元素一一put到构造出来的HashTable中。
从几构造方法可以看出:
- 默认hash桶长度为11
- 在构造方法执行结束后hash桶就初始化完毕了
由于HashTable采用取余算法来获取key对应hash桶的位置,所以不需要保证hash桶的大小满足 2 ^ n。
4、扩容方法
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
//新hash桶大小为当前hash桶大小两倍加一
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;
}
//根据新hash桶大小创建Entry数组
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
//遍历当前hash桶,将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;
}
}
}
这里HashTable的扩容方法很简单,就是先扩容创建新的hash桶,然后将当前hash桶中的元素一一设置到新的hash桶中。
这里需要注意的是扩容是Hash桶的大小为之前hash桶大小的两倍加一,是为了保证扩容后hash桶的大小一定是奇数,据说是为了hash算法对奇数取余可以分散的更加均匀,具体需要去了解一下hash算法。
这里rehash()方法没有加同步是因为调用reshah()方法的方法都被加上了同步,所以扩容不会存在线程问题。
5、常用的方法
这里get、put等方法源码都很好理解,所以就不带大家一起看源码了。
常用的方法如:size()、isEmpty()、get()、put()、remove()、clear()等等都加上了同步,所以HashTable是线程安全的集合。
有一点需要注意的是get()方法对key是null的情况下会抛出空指针;put()方法对key或value是nukk的情况下会抛出空指针,所以HashTable方法的key和value都不允许为空。
6、总结
由于HashTable基本已经被废弃了,所以对于常用方法的源码就不做过多的介绍了。
HashTable是一个线程安全的集合,但是由于性能低所以很少被使用。
性能低体现在对大量方法加同步,锁住整个HashTable,并且对于hash算法和取余算法都没有做优化。
喜欢这篇文章的朋友,欢迎扫描下图关注公众号lebronchen,第一时间收到更新内容。