JDK 1.6 HashMap 源码分析

Java基础

浏览数:226

2019-8-15

前言

​ 前段时间研究了一下JDK 1.6 的 HashMap 源码,把部份重要的方法分析一下,当然HashMap中还有一些值得研究得就交给读者了,如有不正确之处还望留言指正。

准备

​ 需要熟悉数组和链表这两个基本数据结构。如果对链表不太熟悉的话,可以来几道leetcode上的相关的链表算法题。熟悉后看 HashMap 就会快很多了。
​ 基本原理:HashMap中的基本数据结构是数组加链表。table 是一个固定的数组。 数组里面的每个坑里面填的是一个叫Entry类。 其实就是一个固定的Entry数组。如果同一个坑里面存在两个不同的数据,那么两个数据就以链表的形式连接起来。最新的在最前面,原因是认为最新的容易经常被访问。

构造函数

​ 基本原理知道了。现在直接研究带参数的构造函数就可以了,其他的构造函数就是调用该方法。

 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        // Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
        table = new Entry[capacity];
        init();
    }

MAXIMUM_CAPACITY = 1 << 30 2的30次方1073741824,也就是HashMaptable数组的大小不能超过该数字。 从上面代码可以看出来table的坑位只能是2的幂次方。如果你传入的initialCapacity为7 那么其实table 的大小为8; 也就是table的大小为传入进来的initialCapacity的数值大于该大小的2的幂次方。threshold 为他的阈值也就是 HashMap 的真正大小不能超过该值,超过了就进行扩容操作。 如果table数组的大小为16时。用它默认的扩容因子0.75f。那么他的阈值就是12。 也就是 table数据,数组中的加上链表的不能超过12。
我们看看第二个构造函数。参数为一个Map 我这里顺便把HashMap中的嵌套类Entry类说一下。可以自己再源码上观看。

 public HashMap(Map<? extends K, ? extends V> m) {
    // 对比该map的size大小,新的map最新的容量为16
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        // 创建所有map
        putAllForCreate(m);
    }


 private void putAllForCreate(Map<? extends K, ? extends V> m) {
            // 对每一个Entry进行迭代
        for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
            Map.Entry<? extends K, ? extends V> e = i.next();
            //创建数据赋值
            putForCreate(e.getKey(), e.getValue());
        }
    }

  private void putForCreate(K key, V value) {
        int hash = (key == null) ? 0 : hash(key.hashCode());
        // 计算table中的位置
        int i = indexFor(hash, table.length);

        /**
         * Look for preexisting entry for key.  This will never happen for
         * clone or deserialize.  It will only happen for construction if the
         * input Map is a sorted map whose ordering is inconsistent w/ equals.
         */
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 相同的值覆盖。
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                e.value = value;
                return;
            }
        }

        // 创建Entry
        createEntry(hash, key, value, i);
    }


     void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        // 头节点插入
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        size++;
    }

// 嵌套类 和HashMap类没关系 独立存在 默认权限 只能本包访问 也就是Java.util下的包访问 HashHap中并没有提供 Map.Entry<K,V>这样的返回对象出去。有的只是一个 Set<Map.Entry<K,V>>
//一个代理类。
    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        final int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

        public final V setValue(V newValue) {
        V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }

        public final int hashCode() {
            return (key==null   ? 0 : key.hashCode()) ^
                   (value==null ? 0 : value.hashCode());
        }

        public final String toString() {
            return getKey() + "=" + getValue();
        }

        /**
         * This method is invoked whenever the value in an entry is
         * overwritten by an invocation of put(k,v) for a key k that's already
         * in the HashMap.
         */
        void recordAccess(HashMap<K,V> m) {
        }

        /**
         * This method is invoked whenever the entry is
         * removed from the table.
         */
        void recordRemoval(HashMap<K,V> m) {
        }
    }

put方法

​ 为什么要从put方法研究起呢。因为HashMap中最常用得就是put方法。而且里面还涉及到扩容操作。如果把这些看懂了还是会很舒服得。

    public V put(K key, V value) {
        if (key == null)
            // 如果key为null的话 直接添加到table[0]的位置 for 循环 table[0]上的元素。如果有元素的话 查看该元素的key是不是null 如果是的话 就更新value值,直到table[0]这个链表结束。 如果结束后还是没有的话,就把为null的key 对应的value 头插法  插入头部。 可以查看 putForNullKey(value) 方法。
            return putForNullKey(value);

        // 计算Hash值 

        int hash = hash(key.hashCode());
        // 取key的Hash值得 二进制数得后几位。 如果key得hash为1101011 。而table这个数组得大小一直都是2的幂次方。 indexFor()方法做的事 key的hash与table.length-1做&运算。假如table数组的大小为16,也就是 11011011 & 1111 会等于 1011 。这个方法的意义也就是只要你得Hash值是随机的,碰撞性低,那么你在table中位置也就是 碰撞低的。
        int i = indexFor(hash, table.length);

        // 查询该table[i] 位置上的链表。
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 如果 key相等 那么就更新 否则 下一位。。。。 直至结束。
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        // 修改次数加一
        modCount++;
        // 头插法 并看size是都大于阈值了,如果大于就要扩容了。
        addEntry(hash, key, value, i);
        return null;
    }

    void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            //扩容操作 2倍扩容
            resize(2 * table.length);
    }
    // 扩容方法 参数为扩容大小
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        // 创建一个新得数组 名字叫做newTable length为 newCapacity
        Entry[] newTable = new Entry[newCapacity];
        // 扩容操作
        transfer(newTable);
        // 重新赋值 
        table = newTable;
        // 阈值
        threshold = (int)(newCapacity * loadFactor);
    }

    // 扩容操作
    void transfer(Entry[] newTable) {
        // 将原先的table数组 赋值给 src
        Entry[] src = table;
        int newCapacity = newTable.length;
        // 逐个操作 从 src[0] 位置上的Entry 开始
        for (int j = 0; j < src.length; j++) {
            // 将src[j]的值给 e变量。
            Entry<K,V> e = src[j];
            // 对这个e 链表进行往下操作
            if (e != null) {
                // 清空
                src[j] = null;
                do {
                    //e 的下面一位 其实就是 next 后移 (这里如果两个线程同时在这里操作的话,A线程在这里执行这条语句后挂起的话,B线程完成扩容操作后,A线程再唤醒时,有可能发生循环链表。然后使用get方法的时候,导致死循环,cpu利用100%)
                    Entry<K,V> next = e.next;
                    
                    // 对e 重新定位。
                    int i = indexFor(e.hash, newCapacity);

                    // 将e.next 从e 断开 并把e.next的值 指到 newTable[i]的值
                    e.next = newTable[i];
                    // 将 e 赋值给 newTable[i] 
                    newTable[i] = e;
                    // e 往后移
                    e = next;
                } while (e != null);
            }
        }
    }

舒服了舒服了。 如果想看怎么发生死循环的可以看小灰的文章 高并发下的HashMap

get方法

get方法相对而言就比较简单了。

 public V get(Object key) {
        if (key == null)
            // 直接查询table[0] 上链表key为 null的值
            return getForNullKey();
        // 定位table上的位置
        int hash = hash(key.hashCode());
        // 链表的查询 
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

remove方法

remove方法相对而言,只要你会链表的删除操作,就很好理解了。如果有不明白的可以。将链表这个数据结构好好学习一下。

     public V remove(Object key) {
        // 移除元素方法
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }


    // 这里其实就是链表的删除操作 。
     final Entry<K,V> removeEntryForKey(Object key) {
        int hash = (key == null) ? 0 : hash(key.hashCode());
        // 定位位置
        int i = indexFor(hash, table.length);
        // 将table[i] 这个链表赋值给prev 
        Entry<K,V> prev = table[i];
        // prev 赋值给 e
        Entry<K,V> e = prev;

        while (e != null) {
            // 下面一位
            Entry<K,V> next = e.next;
            Object k;
            // key是否相等
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                // 如果要删除的时table[i]的头部数据 
                if (prev == e)
                    // table[i] 等于next 删除头部
                    table[i] = next;
                else
                    // 否则 删除这个  
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }

总结

HashMap中的学问,远不止这些。 其中还涉及到设计模式,迭代器等等。上面这些只是常用的。个人非常推荐把数组和链表这个两个非常基础的数据结构好好练习一下。虽然说早就把JDK 1.6的HashMap 源码看了一下,顺便把 ConcurrentHashMap中的一些源码也看了。但是写下来的时候,再看一遍,印象果然深刻多了。先把1.6的看了,在看1.8的吧。

作者:家里那只橘猫