HashTable原理

40 阅读3分钟

基本介绍

  • 存放的元素是键值对,即key-value;
  • Hashtable的使用方法基本上和HashMap一样;
  • Hashtable的键和值都不能为null;
  • 不同于HashMap Hashtable是线程安全的(synchronized);
public synchronized V put(K key, V value) {
        ...
}

简述Hashtable的底层原理

($表示内部类的关系)

  • 底层维护了Hashtable$Entry[] 数组,初始化大小为11,临界值为8
  • 临界值(threshold) = 数组大小*加载因子(loadFactor:0.75)
public Hashtable() {
    //初始容量11,扩容因子0.75
    this(11, 0.75f);
}

不同于HashMap在第一次添加时调用扩容方法,HashTable在创建时就指定了初始容量(这个方式和LinedHashSet一样)

put方法
    //key重复则执行value值的更新,key不存在则正常添此加键值对。
    public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            //value不允许为null,null时会抛出异常
            throw new NullPointerException();
        }

        Entry<?, ?> tab[] = table;
        int hash = key.hashCode();
        //计算此key的索引值index
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K, V> entry = (Entry<K, V>) tab[index];//获得该桶中的头节点
        //依次遍历此hash值链表中的节点
        for (; entry != null; entry = entry.next) {
            //比对是否有 同hash 同key 的重复节点
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                //如果存在重复的key,则进行值覆盖
                entry.value = value;
                return old;
            }
        }

        //如果此key不是重复元素,则调用函数进行节点添加
        addEntry(hash, key, value, index);
        return null;
    }


    //put()方法的具体实现
    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;//tab:记录扩容后的数组引用
            hash = key.hashCode();//更新key的hash值
            index = (hash & 0x7FFFFFFF) % tab.length;//更新索引下标
        }

        // Creates the new entry.
        @SuppressWarnings("unchecked")
        Entry<K, V> e = (Entry<K, V>) tab[index];//e:此桶中的头节点
        //新节点作为头节点,此处选择的是头插法
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

不同于HashMap使用hash()方法计算hash值,HashTable 直接使用 Object.hashCode() 方法计算hash值;

扩容机制

  • 执行方法 addEntry(hash, key, value, index); 添加K-V,封装到Entry对象中;
  • 当 if (count >= threshold) 满足时,就进行扩容;
  • 按照 int newCapacity = (oldCapacity << 1) + 1; 的大小扩容(当前容量*2+1);
    //扩容方法
    protected void rehash() {
        int oldCapacity = table.length;//记录原数组长度
        Entry<?,?>[] oldMap = table;//记录原数组的地址索引

        // overflow-conscious code
        //新数组长度 = 原数组的2倍+1
        //而HashMap中,扩容为原来的2倍:newCap = (oldCap << 1);
        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;
        }
        //以新长度newCapacity创建一个新数组
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

        modCount++;
        //更新 扩容阈值threshold
        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;
                old = old.next;

                //以同样的方式,通过对新数组长度取余的方式计算新索引index
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = (Entry<K,V>)newMap[index];
                newMap[index] = e;//将数据存放到新数组中
            }
        }
    }

和HashMap对比容易忽略的一些区别

    1. 继承父类不同
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    ...
}
public class Hashtable<K,V> extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {
    ...
}
    1. hash值计算不同

HashMap

//经过计算,返回 key的hash值
static final int hash(Object key) {
    int h;
    //如果key!=null,借助hashCode()方法,计算key的hash值。
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

HashTable

int hash = key.hashCode();
    1. 下标的计算不同

HashMap

//index = (table.length - 1) & hash;
i = (n - 1) & hash

HashTable

//获得 key的hash值
int hash = key.hashCode();
//计算此 key的索引值index
int index = (hash & 0x7FFFFFFF) % tab.length;
    1. 添加元素的方式

HashMap JDK1.7用的是头插法,JDK1.8用的是尾插法

(详情看HashMap.putVal()方法)

HashTable中用的是头插法

Entry<K,V> e = (Entry<K,V>) tab[index];//e:此桶中的头节点
//将新加节点作为头节点,此处选择的是头插法
tab[index] = new Entry<>(hash, key, value, e);