HashTable源码解析(JDK1.8)

351 阅读5分钟

HashTable源码解析(JDK1.8)

HashTable是一个数组+链表实现的线程安全的散列表

同样我们还是先上类的关系图

HashTable类的关系图

从类的关系图中可以看出HashTable继承一个抽象类和实现了三个接口,然后分别简单介绍一下:

  • Dictionary:这里主要提供增删改查、keys集合查询、elements集合查询等的相关操作
  • Map:提供队首、队尾增删改查等操作
  • Cloneable:按字段复制操作
  • Serializable:启用其序列化功能操作

属性

属性相关的源码


    private transient Entry<?,?>[] table;

    private transient int count;

    private int threshold;

    private float loadFactor;

    private transient int modCount = 0;

private transient Entry[] table;//存储数据的

private transient int count;//存放元素的数量

private int threshold;//扩容的阀值

private float loadFactor;//负载因子

private transient int modCount = 0;//修改的次数

构造方法


    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;
        table = new Entry<?,?>[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }

    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }

    public Hashtable() {
        this(11, 0.75f);
    }

    public Hashtable(Map<? extends K, ? extends V> t) {
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }

从四个构造方法中可以看出主要是initialCapacity、loadFactor、threshold这三个的不同参数

initialCapacity:初始容量,默认值采用的是11

loadFactor:负载因子,默认值采用的是0.75

threshold:HashTable所能容纳的最大数据量。threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);

HashTable中的节点

HashTable中的节点对象是一个存储hash值,key,value以及下一个节点

    private static class Entry<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Entry<K,V> next;

        protected Entry(int hash, K key, V value, Entry<K,V> next) {
            this.hash = hash;
            this.key =  key;
            this.value = value;
            this.next = next;
        }

        @SuppressWarnings("unchecked")
        protected Object clone() {
            return new Entry<>(hash, key, value,
                                  (next==null ? null : (Entry<K,V>) next.clone()));
        }

        // Map.Entry Ops

        public K getKey() {
            return key;
        }

        public V getValue() {
            return value;
        }

        public V setValue(V value) {
            if (value == null)
                throw new NullPointerException();

            V oldValue = this.value;
            this.value = value;
            return oldValue;
        }

        public boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;

            return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
               (value==null ? e.getValue()==null : value.equals(e.getValue()));
        }

        public int hashCode() {
            return hash ^ Objects.hashCode(value);
        }

        public String toString() {
            return key.toString()+"="+value.toString();
        }
    }

增删改查

插入元素/更新元素

其中0x7FFFFFFF是INT_MAX,转化为二进制为0111 1111 1111 1111 1111 1111 1111 1111

    //synchronized可以看出,是线程安全的
    public synchronized V put(K key, V value) {
        // Make sure the value is not null value不能为null,要不然报空指针异常
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        //得到key的hash值
        int hash = key.hashCode();
        //计算得到index下标
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        //找到数组中index对应的链表的节点
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            //判断如果当前节点的hash和要拆入的key的hash值一样,并且key一样则是更新元素,然后更新节点的value值,然后返回老值
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
        //没有找到相同hash值和key的则是拆入元素,我们来分析addEntry怎么实现拆入的
        addEntry(hash, key, value, index);
        return null;
    }
    
    private void addEntry(int hash, K key, V value, int index) {
        //修改次数modCount+1
        modCount++;
        //当前的数据
        Entry<?,?> tab[] = table;
        //存放元素的数量大于阀值,则需要进入扩容
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            //扩容的方法在下面讲解
            rehash();

            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        @SuppressWarnings("unchecked")
        //拿到数组中index位置的节点
        Entry<K,V> e = (Entry<K,V>) tab[index];
        //HashTable采用的头插法,设置数组index位置新的节点,并设置该节点相关的hash、key、value以及下一个节点是原先这个位置的节点
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }
    
    
    protected void rehash() {
        //老数据的长度
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;

        // overflow-conscious code
        //扩容的新容量=老容量*2+1
        int newCapacity = (oldCapacity << 1) + 1;
        //扩容之后的新容量-最大容量>0
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            //老容量等于MAX_ARRAY_SIZE最大值,则无法扩容老
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            //扩容之后的新容量=最大容量
            newCapacity = MAX_ARRAY_SIZE;
        }
        //新数据数组初始化
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
        //修改次数+1
        modCount++;
        //新的阀值计算
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        //数据赋值
        table = newMap;
        //从老数据的长度位置开始,去循环
        for (int i = oldCapacity ; i-- > 0 ;) {
            //取老数据i位置的节点,判断节点不为null,重新复制数据到新的newMap数组中去
            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;
            }
        }
    }    
    

总结一下:根据的key去取hash,然后取模运算得到index,然后判断一下这个index对应的链表里的节点是否和当前插入的key、hash值都一样,都一样就是更新元素;不一样就是插入元素,插入元素的话需要判断一下是否需要扩容,需要的扩容的话就重新计算找到新的index下标设置新数据,最后再插入新数据到对应位置。putAll()、putIfAbsent()原理类似

删除元素

删除很简单,得到key相关的hash值,及其对应的index,然后找到数组中index中的节点,然后顺着链表往下找,找到了进行和链表一样的删除就行了


    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];
        for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                modCount++;
                if (prev != null) {
                    prev.next = e.next;
                } else {
                    tab[index] = e.next;
                }
                count--;
                V oldValue = e.value;
                e.value = null;
                return oldValue;
            }
        }
        return null;
    }

查找元素

查找也很简单,得到key相关的hash值,及其对应的index,然后找到数组中index中的节点,然后遍历链表


    public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }

其他方法

containsKey()、contains()、clear()这些都是很简单的,都是遍历查找和遍历赋值为null

线程安全问题

  • HashTable通过synchronized实现了线程安全,但是有一定的局限性,加锁和释放锁的时候开销大,在组合操作的时候是线程不安全的

  • ConcurrentHashMap也是线程安全的,这个是找到对应的节点,对对应index位置上的链表或者红黑树进行加锁来保证线程安全的,范围更小,影响更小。put()、remove()是线程安全的,get()不是线程安全的,也不需要