JDK之HashTable

102 阅读4分钟

引言

我们经常拿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锁。