一文看懂Hashtable源码以及与HashMap的区别

前言

上一篇咱们认识了什么是Map、Hash,了解了Hash处理哈希冲突的几种经常使用方法(拉链法、开放定址法),以及分析了JDK1.8版本的HashMap源码,对Java集合框架有了初步的认识,咱们本篇继续分析JDK1.8版本的Hashtable源码,最后比较HashMap和Hashtable的区别。算法

Hashtable数组

注意是Hashtable不是HashTable(t为小写),这不是违背了驼峰定理了嘛?这还得从Hashtable的出生提及,Hashtable是在Java1.0的时候建立的,而集合的统一规范命名是在后来的Java2开始约定的,而当时又发布了新的集合代替它,因此这个命名也一直使用到如今,因此Hashtable是一个过期的集合了,不推崇你们使用这个类,虽然说Hashtable是过期的了,咱们仍是有必要分析一下它,以便对Java集合框架有一个总体的认知。安全

首先Hashtable采用拉链法处理哈希冲突,是线程安全的,键值不容许为null,而后Hashtable继承自Dictionary,实现Map接口,Hashtable有几个重要的成员变量table、count、threshold、loadFactor多线程

table:是一个Entry[]数据类型,而Entry实际是一个单链表框架

count:Hashtable的大小,即Hashtable中保存的键值对数量函数

threshold:Hashtable的阈值,用于判断是否须要调整Hashtable的容量,threshold = 容量负载因子,threshold=11*0.75 取整即8this

loadFactor:用来实现快速失败机制的spa

clipboard.png

构造函数线程

Hashtable有4个构造函数3d

//无参构造函数 默认Hashtable容量是11,默认负载因子是0.75
 public Hashtable() {
 this(11, 0.75f);
}

 //指定Hashtable容量,默认负载因子是0.75
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}

//指定Hashtable的容量和负载因子
public Hashtable(int initialCapacity, float loadFactor) {  
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
//new一个指定容量的Hashtable
table = new Entry<?,?>[initialCapacity];
//阈值threshold=容量*负载因子
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
//包含指定Map的构造函数
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}

这里的Hashtable容量和HashMap的容量就有区别,Hashtable并不要求容量是2的幂次方,而HashMap要求容量是2的幂次方。负载因子则默认都是0.75。

put方法

put方法是同步的,即线程安全的,这点和HashMap不同,还有具体的put操做和HashMap也存在很大的差异,Hashtable插入的时候是插入到链表头部,而HashMap是插入到链表尾部。

//synchronized同步锁,因此Hashtable是线程安全的
public synchronized V put(K key, V value) {

    // Make sure the value is not null
    //若是值value为空,则抛出异常 至于为何官方不容许为空,下面给出分析
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    //直接取key的hashCode()做为哈希地址,这与HashMap的取hashCode()以后再进行hash()的结果做为哈希地址 不同
    int hash = key.hashCode();
    //数组下标=(哈希地址 & 0x7FFFFFFF) % Hashtable容量,这与HashMap的数组下标=哈希地址 & (HashMap容量-1)计算数组下标方式不同,前者是取模运算,后者是位于运算,这也就是为何HashMap的容量要是2的幂次方的缘由,效率上后者的效率更高。
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    //遍历Entry链表,若是链表中存在key、哈希地址相同的节点,则将值更新,返回旧值
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }
    //若是为新的节点,则调用addEntry()方法添加新的节点
    addEntry(hash, key, value, index);
    //插入成功返回null
    return null;
}

private void addEntry(int hash, K key, V value, int index) {
    modCount++;

    Entry<?,?> tab[] = table;
    //若是当前键值对数量>=阈值,则执行rehash()方法扩容Hashtable的容量
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        rehash();

        tab = table;
        //获取key的hashCode();
        hash = key.hashCode();
        //从新计算下标,由于Hashtable已经扩容了。
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // Creates the new entry.
    @SuppressWarnings("unchecked")
    //获取当前Entry链表的引用 复赋值给e
    Entry<K,V> e = (Entry<K,V>) tab[index];
    //建立新的Entry链表的 将新的节点插入到Entry链表的头部,再指向以前的Entry,即在链表头部插入节点,这个和HashMap在尾部插入不同。
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}

hashCode()为何要& 0x7FFFFFFF呢?由于某些对象的hashCode()多是负值,& 0x7FFFFFFF保证了进行%运算时候获得的下标是个正数

get方法

get方法也是同步的,和HashMap不同,即线程安全,具体的get操做和HashMap也有区别。

//同步
public synchronized V get(Object key) {
    Entry<?,?> tab[] = table;
    //和put方法同样 都是直接获取key的hashCode()做为哈希地址
    int hash = key.hashCode();
    //和put方法同样 经过(哈希地址 & 0x7FFFFFFF)与Hashtable容量作%运算 计算出下标
    int index = (hash & 0x7FFFFFFF) % tab.length;
    //遍历Entry链表,若是链表中存在key、哈希地址同样的节点,则找到 返回该节点的值,否者返回null
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return (V)e.value;
        }
    }
    return null;
}

remove方法

//同步
public synchronized V remove(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>)tab[index];
    //遍历Entry链表,e为当前节点,prev为上一个节点
    for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
        //找到key、哈希地址同样的节点
        if ((e.hash == hash) && e.key.equals(key)) {
            modCount++;
            //若是上一个节点不为空(即不是当前节点头结点),将上一个节点的next指向当前节点的next,即将当前节点移除链表
            if (prev != null) {
                prev.next = e.next;
            } else { //若是上一个节点为空,即当前节点为头结点,将table数组保存的链表头结点地址改为当前节点的下一个节点
                tab[index] = e.next;
            }
            //Hashtable的键值对数量-1
            count--;
            //获取被删除节点的值 而且返回
            V oldValue = e.value;
            e.value = null;
            return oldValue;
        }
    }
    return null;
}

rehash方法

Hashtable的rehash方法和HashMap的resize方法同样,是用来扩容哈希表的,可是扩容的实现又有区别。

protected void rehash() {
    //获取旧的Hashtable的容量
    int oldCapacity = table.length;
    //获取旧的Hashtable引用,为旧哈希表
    Entry<?,?>[] oldMap = table;

    // overflow-conscious code
    //新的Hashtable容量=旧的Hashtable容量  2 + 1,这里和HashMap的扩容不同,HashMap是新的Hashtable容量=旧的Hashtable容量  2。
    int newCapacity = (oldCapacity << 1) + 1;
    //若是新的Hashtable容量大于容许的最大容量值(Integer的最大值 - 8)
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        //若是旧的容量等于容许的最大容量值则返回
        if (oldCapacity == MAX_ARRAY_SIZE)
            // Keep running with MAX_ARRAY_SIZE buckets
            return;
        //新的容量等于容许的最大容量值
        newCapacity = MAX_ARRAY_SIZE;
    }
    //new一个新的Hashtable 容量为新的容量
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

    modCount++;
    //计算新的阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    table = newMap;
    //扩容后迁移Hashtable的Entry链表到正确的下标上
    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 hashtable = new Hashtable();
for (int i = 1; i <= 24; i ++) {
    hashtable.put(String.valueOf(i), i);
}
for (int i = 25; i <= 80; i ++) {
    hashtable.put(String.valueOf(i), i);
}

new一个Hashtable,默认容量是11,负载因子是0.75

执行第一个for循环后,20保存在下标为0的Entry中,即(hash &0x7FFFFFFF) % 容量 -> (1598 &0x7FFFFFFF) % 11 = 0

clipboard.png

执行第二个for循环后,变成了20保存在下标为70的Entry中,由于Hashtable扩容了4次,分别是从容量为默认的11->23->47->95->191,而后此时容量是191,因此(hash &0x7FFFFFFF) % 容量 -> (1598 &0x7FFFFFFF) % 191 = 70

clipboard.png

HashMap和Hashtable区别

到这里咱们分析了HashMap和Hashtable的原理,如今比较如下他们的区别。

不一样点

继承的类不同:HashMap继承的AbstractMap抽象类,Hashtable继承的Dictionay抽象类

应对多线程处理方式不同:HashMap是非线程安全的,Hashtable是线程安全的,因此Hashtable效率比较低

定位算法不同:HashMap经过key的hashCode()进行hash()获得哈希地址,数组下标=哈希地址 & (容量 - 1),采用的是与运算,因此容量须要是2的幂次方结果才和取模运算结果同样。而Hashtable则是:数组下标=(key的hashCode() & 0x7FFFFFFF ) % 容量,采用的取模运算,因此容量没要求

键值对规则不同:HashMap容许键值为null,而Hashtable不容许键值为null

哈希表扩容算法不同:HashMap的容量扩容按照原来的容量2,而Hashtable的容量扩容按照原来的容量2+1

容量(capacity)默认值不同:HashMap的容量默认值为16,而Hashtable的默认值是11

put方法实现不同:HashMap是将节点插入到链表的尾部,而Hashtable是将节点插入到链表的头部

底层结构不同:HashMap采用了数组+链表+红黑树,而Hashtable采用数组+链表

为何HashMap容许null键值呢,而Hashtable不容许null键值呢?这里还得先介绍一下什么是null,咱们知道Java语言中有两种类型,一种是基本类型还有一种是引用类型,其实还有一种特殊的类型就是null类型,它不表明一个对象(Object)也不是一个对象(Object),而后在HashMap和Hashtable对键的操做中使用到了Object类中的equals方法,因此若是在Hashtable中置键值为null的话就可想而知会报错了,可是为何HashMap能够呢?由于HashMap采用了特殊的方式,将null转为了对象(Object),具体怎么转的,这里就不深究了。

相同点

实现相同的接口:HashMap和Hashtable均实现了Map接口

负载因子(loadFactor)默认值同样:HashMap和Hashtable的负载因子默认都是0.75

采用相同的方法处理哈希冲突:都是采用链地址法即拉链法处理哈希冲突

相同哈希地址可能分配到不一样的链表,同一个链表内节点的哈希地址不必定相同:由于HashMap和Hashtable都会扩容,扩容后容量变化了,相同的哈希地址取到的数组下标也就不同。

相关文章
相关标签/搜索