HashMap源码解读(JDK1.7和JDK1.8)

2,066 阅读14分钟

HashMap 是双列存储元素,存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。JDK1.7之前 HashMap 由数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突(通过算法得到的地址可能一致,然而hash值或者value值不一样,采取多次比较的方法)而存在的(“拉链法”解决冲突).JDK1.8 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。

JDK1.8解读

HashMap的继承关系图谱


通过图谱可以观察出来HashMap是继承了AbstractMap,进一步对HashMap解读:可以看到HashMap实现了Map、Cloneable 、Serializable接口等。


定义静态常量


构造函数



对构造函数进一步解读

//    无参构造函数
     public HashMap() {
//         负载因子为默认值0.75f
         this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
     }
//     带参构造函数,参数为自定义容量
     public HashMap(int initialCapacity) {
         this(initialCapacity, DEFAULT_LOAD_FACTOR);
     }
//     带参构造函数,参数为自定义容量和自定义加载因子
    public HashMap(int initialCapacity, float loadFactor) {
//         如果自定义容量小于0
        if (initialCapacity < 0)
//            抛出异常,容量非法
            throw new IllegalArgumentException("Illegal initial capacity: " +
                    initialCapacity);
//        如果自定义容量大于最大容量值,则将自定义容量替换为最大容量值
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
//        如果负载因子小于0或者负载因子为null,抛出负载因子非法
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                    loadFactor);
        this.loadFactor = loadFactor;
//        调用tableSizeFor方法,
        this.threshold = tableSizeFor(initialCapacity);
    }
//    带参构造函数,参数为map
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
//        调用putMapEntries
        putMapEntries(m, false);
    }

HashMap数据结构


  static class Node<K,V> implements Map.Entry<K,V> {
//        哈希值不可改变
        final int hash;
//        key值不可以改变
        final K key;
        V value;
//        下一个节点的引用
        Node<K,V> next;
//        Node结构存储哈希值,键,值,下一个节点的引用
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }
        public final int hashCode() {
//            返回对象的键的哈希值与运算上对象的值的哈希值
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        public final boolean equals(Object o) {
            if (o == this)
                return true;
//            对象实例化一个Entry
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
//                如果对象的键与实例化对象的键相等并且对象键与实例化值相等,返回true
                if (Objects.equals(key, e.getKey()) &&
                        Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    } 

 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        //节点的父亲
        TreeNode<K, V> parent;  
        //节点的左孩子
        TreeNode<K, V> left;    
        //节点的右孩子
        TreeNode<K, V> right;  
        //节点的前一个节点  
        TreeNode<K, V> prev;    
        //true表示红节点,false表示黑节点
        boolean red;            
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
//获得树的根节点
        final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }

hash值

  //    传入对象的键值
    static final int hash(Object key) {
//        初始化一个变量来存储键的哈希值
        int h;
//        如果对象的键为不为null,键的哈希值与运算16得到一个哈希值;这里跟jdk1.7 有非常大的不同
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

数组大小


threshold 临界值


threshold = capacity * loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,这个的意思就是: 衡量数组是否需要扩增的一个标准。

loadFactor 加载因子

loadFactor加载因子是控制数组存放数据的疏密程度,分布均匀。 loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。通过科学计算,泊松定理,官方给出的是0.75f。 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。因此,与ArrayList一样,应该尽可能去减少触发扩容机制。

添加键值对

put()方法:

 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    //初始化一个Node数组tab
    Node<K,V>[] tab; 
    //初始化一个节点Node
    Node<K,V> p;
    int n, i;
    // table未初始化或者长度为0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已经存在元素
    else {
        Node<K,V> e; K k;
        // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                // 将第一个元素赋值给e,用e来记录
                e = p;
        // hash值不相等,即key不相等;为红黑树结点
        else if (p instanceof TreeNode)
            // 放入树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 为链表结点
        else {
            // 在链表最末插入结点
            for (int binCount = 0; ; ++binCount) {
                // 到达链表的尾部
                if ((e = p.next) == null) {
                    // 在尾部插入新结点
                    p.next = newNode(hash, key, value, null);
                    // 结点数量达到阈值,转化为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    // 跳出循环
                    break;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        // 表示在桶中找到key值、hash值与插入元素相等的结点
        if (e != null) { 
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替换旧值
                e.value = value;
            // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 结构性修改
    ++modCount;
    // 实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
} 

putMapEntries方法:


resize()方法

   final Node<K,V>[] resize() {
//        将初始定义的数组赋值给oldTable
        Node<K,V>[] oldTab = table;
//        判断初始数组是否为null,确定数组容量大小
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
//        获得原先的临界值
        int oldThr = threshold;
        int newCap, newThr = 0;
//        如果原先容量大于0
        if (oldCap > 0) {
//            如果原先容量大于最大值,把临界值改为最大值
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
//            新容量大小等于原先容量右移一位,相当于扩大了2倍,新容量要小于最大值,旧容量要大于默认容量大小
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
//                新临界值等于旧临界值右移一位,相当于扩大原来的2倍
                newThr = oldThr << 1; // double threshold
        }
//        如果原先临界值大于0,将原先的临界值给新临界值
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {
            // zero initial threshold signifies using defaults
//            新容量等于默认容量16
            newCap = DEFAULT_INITIAL_CAPACITY;
//            新临界值等于默认负载因子*默认初始容量
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
//        如果新临界值等于0,新容量*负载因子
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
//            新容量小于最大容量,新临界值小于 新容量*负载因子小于最大容量 新临界值为 新容量*负载因子小于最大容量
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
      @SuppressWarnings({"rawtypes", "unchecked"})
        Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
        //修改hashMap的table为新建的newTab
        table = newTab;
        //如果旧table不为空,将旧table中的元素复制到新的table中
        if (oldTab != null) {
            //遍历旧哈希表的每个桶,将旧哈希表中的桶复制到新的哈希表中
            for (int j = 0; j < oldCap; ++j) {
                Node<K, V> e;
                //如果旧桶不为null,使用e记录旧桶
                if ((e = oldTab[j]) != null) {
                    //将旧桶置为null
                    oldTab[j] = null;
                    //如果旧桶中只有一个node
                    if (e.next == null)
                        //将e也就是oldTab[j]放入newTab中e.hash & (newCap - 1)的位置
                        newTab[e.hash & (newCap - 1)] = e;
                        //如果旧桶中的结构为红黑树
                    else if (e instanceof TreeNode)
                        //将树中的node分离
                        ((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                    else {  
                        Node<K, V> loHead = null, loTail = null;
                        Node<K, V> hiHead = null, hiTail = null;
                        Node<K, V> next;
                        //遍历整个链表中的节点
                        do {
                            next = e.next;
                            // 原索引
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            } else {// 原索引+oldCap
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 原索引放到bucket里
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 原索引+oldCap放到bucket里
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
    
    final void treeifyBin(Node<K, V>[] tab, int hash) {
        int n, index;
        Node<K, V> e;
        //如果桶数组table为空或者桶数组table的长度小于64不符合转化为红黑树的条件
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            //调用扩容
            resize();
            //如果符合转化为红黑树的条件,而且hash对应的桶不为null
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            // 红黑树的头、尾节点
            TreeNode<K, V> hd = null, tl = null;
            //遍历链表
            do {
                //替换链表node为树node,建立双向链表
                TreeNode<K, V> p = replacementTreeNode(e, null);
                // 确定树头节点
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            //遍历链表插入每个节点到红黑树
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }
    

添加元素思路

首先,调用newKey所在类的hashCode()计算newKey哈希值,通过算法得到在node数组中的存放位置;
如果此位置不存在数据,就直接将键值对插入到数组中;
如果此位置已经存在数据,则表明该位置可以存在一个或者多个数据(链表或者是红黑树),依次比较newKey与多数据的哈希值:
如果newKey的哈希值与已经存在的数据哈希值不一样,添加到链表的末尾,注意会添加到红黑树中;
如果newKey的哈希值与已经存在的数据哈希值一样,接着比较,调用newKey所在类的equals():
如果返回是false,添加节点;
如果返回true,替换原来的value。
最后需要注意的是:在添加过程中涉及到扩容问题,默认扩容方式为扩容为原先容量的2倍,将所有的数据复制过来,重新计算hash,找到对应的位置存放。

get()方法


 public V get(Object key) {
        Node<K, V> e;
        //传入key,调用hash方法得到hash值查询node节点,判断节点是否存在,存在则返回该节点的value值
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    final Node<K, V> getNode(int hash, Object key) {
        Node<K, V>[] tab;
        Node<K, V> first, e;
        int n;
        K k;
        //如果哈希表不为空,而且key对应的桶上数据不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {
            //如果桶中的第一个节点就和指定参数hash和key匹配上了
            if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                //返回桶中的第一个节点
                return first;
            //如果桶中的第一个节点没有匹配上,而且有后续节点
            if ((e = first.next) != null) {
                //如果当前的桶采用红黑树,则调用红黑树的get方法去获取节点
                if (first instanceof TreeNode)
                    return ((TreeNode<K, V>) first).getTreeNode(hash, key);
                //如果当前的桶不采用红黑树,即桶中节点结构为链式结构
                do {
                    //遍历链表,直到key匹配
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        //如果哈希表为空,或者没有找到节点,返回null
        return null;
    } 
//得到树节点
 final TreeNode<K,V> getTreeNode(int h, Object k) {
            return ((parent != null) ? root() : this).find(h, k, null);
        }

以上是对1.8版本的几个方法解读,有兴趣对其他方法可以参考文末给出的两篇参考文章。

JDK1.7解读

全局变量


构造函数:


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);

        this.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();
    }

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        inflateTable(threshold);

        putAllForCreate(m);
    } 

JDK 1.8 的无参构造函数相比于 JDK 1.7 无参构造函数,并没有一开始就创建一个长度为16的数组,而是在调用put()方法才新建数组。

数据结构


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

        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 Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
        }

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

        void recordAccess(HashMap<K,V> m) {
        }

        void recordRemoval(HashMap<K,V> m) {
        }
    } 

JDK 1.8 的 数据结构相比于 JDK 1.7 数据结构用Node数组代替了Entry数组,添加了treeNode,底层增加了红黑树,改善因链表过长,导致查询效率低下的问题。

hash值


JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,减少了扰动。

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。

put()方法


//定义一个空的Entry数组 
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
 public V put(K key, V value) {
//如果数组为空则调用inflateTable()方法
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
//如果传入的key是null,调用putForNullKey()方法,把值传入
        if (key == null)
            return putForNullKey(value);
//如果key不是null,调用hash()得到哈希值
        int hash = hash(key);
//调用indexFor()方法,传入哈希值,与数组的长度,得到该值在数组中的索引位置
        int i = indexFor(hash, table.length);
//对数组进行遍历,得到每一个entry
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
//如果entry的哈希值并且键或者值一致,则将原来的值取出来,把当前值赋值进去,调用recordAccess()方法
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue; }
        }
//结构改变
        modCount++;
//调用addEntry () 添加一个entry 参数是哈希值,键,值,和索引
        addEntry(hash, key, value, i);
        return null;
    }

//调用私有方法
 private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }
//空数组调用私有方法tosize
  private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
//得到容量
        int capacity = roundUpToPowerOf2(toSize);
//数组临界值 在数组容量乘负载因子与最大容量+1相比取最小
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//初始化entry数组大小
        table = new Entry[capacity];
//初始化容量大小
        initHashSeedAsNeeded(capacity);
    }

    void addEntry(int hash, K key, V value, int bucketIndex) {
//如果size大于临界值并且添加entry计算出来的数组索引的值不为null
        if ((size >= threshold) && (null != table[bucketIndex])) {
//调用扩容方法,参数是2*数组长度
            resize(2 * table.length);
//key不等于null ,计算出key的哈希值
            hash = (null != key) ? hash(key) : 0;
//得到在数组中位置
            bucketIndex = indexFor(hash, table.length);
        }
//创建一个entry
        createEntry(hash, key, value, bucketIndex);
    }
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    } 

对比JDK1.8put方法

如果定位到的数组位置没有元素 就直接插入;
如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的key比较;
如果key的哈希值相同且值一样就直接覆盖,不同就采用头插法插入元素。

resize()方法


//扩容机制
 void resize(int newCapacity) {
//将数组赋值给一个
        Entry[] oldTable = table;
//旧数组的长度
        int oldCapacity = oldTable.length;
//如果旧数组的常长度等于最大容量,临界值为Integer.MAX_VALUE
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
//创建一个新数组,容量为原来数组的2倍
        Entry[] newTable = new Entry[newCapacity];
//将原来数组中的数据复制到新数组
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
//得到一个新数组
        table = newTable;
//临界值为新数组的容量*负载因子与最大容量+1相比取最小的数
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    } 

get()方法


  public V get(Object key) {
//如果键为null,调用getForNullKey
        if (key == null)
            return getForNullKey();
//得到一个entry
        Entry<K,V> entry = getEntry(key);
//如果entry不为null,返回entry的值
        return null == entry ? null : entry.getValue();
    }


参考1:github.com/wupeixuan/J…

参考2:github.com/Snailclimb/…

特别注意:本文部分摘抄文字版权属于原作者!!!