跳到主要内容

引言

我们经常拿HashMap和HashTable来做比较,这到底是为什么呢

因为它们十分相似,它们都实现了Map接口,都是Map接口的一种实现类

它们有什么区别呢?

  • 最显著的一个区别就是,HashMap是线程不安全的,而HashTable是线程安全的,在HashTable的源码中,我们可以看到它的所有方法都使用了synchronized关键字标识,我们知道,synchronized是能保证线程安全的,所以能证明HashTable是线程安全的。

  • 在HashMap中,我们知道如果传入了一个值为null的key,然后HashMap会拿出table数组的第一个元素,也就是table[0],而在HashTable中,我们是不能传入值为null的key的,如果我们强行传入一个值为null的key会爆空指针异常。

  • 在HashTable中其实就比HashMap多了两个方法,一个是contains() 方法,一个是elements() 方法,elements()方法用于返回一个value值的枚举,而contains()方法其实就是containsValue()方法,用于判断某个value是否存在,而其实containsValue()方法就是调用了contains()方法而已。

    在HashMap中是没有这两个方法的,这是因为它们继承的类不同的原因 HashMap继承自AbstractMap类 HashTable继承自Dictionary类

  • 它们初始化容量大小和扩容大小也不同,我们都知道HashMap在发生扩容的时候,每次扩容都是扩大2倍,而HashTable每次扩容都是变成2n+1 ,而在初始化的时候也不相同,在HashMap进行初始化的时候,它的大小总会保持在大于等于你给的容量大小的最小2次幂,而HashTable却不是这样,HashTable的初始化大小,你给他多少,它就会设置多少,它是很听话的。并且它们默认大小也是不相同的,HashTable默认初始化大小为11,而HashMap初始化大小为16

成员属性

    //用于存放数据的哈希桶
private transient Entry<?,?>[] table;
//表示哈希桶中有多少个元素
private transient int count;
//扩容阈值 = 容量 × 装载因子
private int threshold;
//装载因子 用于确定扩容阈值
private float loadFactor;
//修改次数
private transient int modCount = 0;
//序列版本号
private static final long serialVersionUID = 1421746759512286392L;

相较于HashMap(JDK8),在HashTable中,少了很多成员变量,因为这个类没有树化操作,所以它保存元素的方式就是 数组 + 链表

put方法

put方法.png 大致流程为:

  • 先判断value是否为空,如果为空直接抛出空指针异常

  • 根据传入的key计算hash值和哈希桶的位置

  • 遍历链表,判断是否有重复key存在

    • 如果重复,则直接替换然后返回

    • 如果没有重复的,添加一个新的结点即可

get方法

get方法.png 大致流程为:

  • 通过传入的key计算哈希桶的位置

  • 遍历哈希桶寻找传入key对应的结点

    • 若能找到返回该结点的值

    • 若找不到返回null

扩容机制

在前面,我们已经知道了,HashTable的扩容,每次扩容大小为2n+1这样,我们来看一看源代码是怎样实现的,其实就是rehash()这个方法,接下来我们讲解这个方法。

//我们可以看到这个方法是通过`protected`关键字来修饰的,证明这个方法只有本类、同一个包下的类和其子类能使用。
protected void rehash() {
//先记录原先table数组的大小
int oldCapacity = table.length;
//记录原先table数组
Entry<?,?>[] oldMap = table;
//得出新的容量大小,新容量 = 2 * 老容量 + 1
int newCapacity = (oldCapacity << 1) + 1;
//判断新容量是否超过最大容量
if (newCapacity - MAX_ARRAY_SIZE > 0) {
//如果老容量已经达到最大容量,那么直接返回不再进行扩容
if (oldCapacity == MAX_ARRAY_SIZE)
return;
//将新容量设置为最大容量
newCapacity = MAX_ARRAY_SIZE;
}
//创建一个新的table数组,这个table数组的长度为新容量的大熊啊
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

modCount++;
//计算新的扩容阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
//将原先table数组各个哈希桶中的元素重新计算放入新的table数组的哈希桶中!
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中,大部分方法都被synchronized关键字标记,因为synchronized能保证线程安全,所以我们认为HashTable是线程安全的,所以在HashTable中的方法中,大部分都会先获取synchronized锁。