HashMap(1.8)源码分析

 HashMap是我们日常开发中使用非常频繁的集合框架,平时只会使用,却不知道底层是如何实现的,今天在这里对HashMap的底层实现做一系列的分析。如有不对的地方,欢迎指正。

我们知道HashMap是用来存储数据的一个容器,使用key-value来存储数据,当key重复的时会覆盖前一次的value值。那么HashMap是如何做到存储数据和读取数据的呢?我们知道1.8之后的HashMap底层使用了数组、链表+红黑树的数据结构,那么具体的实现是什么样的呢?让我一步一步介绍。

首先,HashMap是在什么时候初始化的,是new HashMap()的时候吗?我们来看一下源码:

我们看到它又四个构造方法,这四个构造方法没有对HashMap做初始化的操作(具体的代码请自行查找,这里就不贴出来了),而点到put("key","value");的方法里它继续调用了一个putVal(hash(key), key, value, false, true);方法,然后继续跟进,在该方法里有一个resize();方法,这个方法部分源码如下:

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        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];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        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 {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

我们具体关注两句代码,

1:newCap = DEFAULT_INITIAL_CAPACITY;

2:Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

我们先看一下DEFAULT_INITIAL_CAPACITY是什么:

上图可以见,就是一个int类型的变量,赋值16,然后第2句代码就很明显了,创建了一个长度为16的Node数组,那么Node又是什么呢?我们继续跟进去看一下:

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

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

上述可知,Node就是一个对象,有四个属性,分别是hash值,key值,value和next,这就很明显了,HashMap底层就是用这个Node对象来存储数据的,hash值用作标识,next指向下一个对象,key和value就不用多说了。那么这个hash是谁的hash呢,我们根据Node的构造方法往回找,找到一个putMapEntries(Map<? extends K, ? extends V> m, boolean evict)方法,我们来看一下:

 final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)
                resize();
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

在最后一句,我们终于找到hash是谁的了,那就是我们put值的时候的key的Hash,跟进hash(key),得到

 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

也就是key调用看hashCode()方法,因为所有对象都是Object的子类,所以这个key可以根据父类的hashCode()方法获得哈希值。

我们知道,hashCode值就是一串的数字如:106079,说白了,hashCode是用来定位的,我们前面初始化了一个长度为16的数字,那么我们在插入元素的时候就需要知道下标,那么如果是十进制的哈希值,定位这16个位置,我们可以通过取余,但是这样做的话冲突频率太高了,于是乎,这里将十进制的哈希值转换为了二进制,我们可以看到上面的这句代码:return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);如果key为null那么哈希值直接返回0,否则,将哈希值的低16位与哈希值的高16位做异或运算,这样,就将该key的哈希值全员多参与了运算,会大大降低hash冲突的几率,然后我们再看resize()方法:

其他的一堆判断我们先不管他,我们看这里,我们每插入一个数据,它这里会做判断,直到next为null了,就往这个地方存放心的数据,上述代码我们可以看到他是用e.hash & (newCap - 1)来定位下标索引值的,这里用了&运算,通过将前面得到的key的哈希值再与当前数组容量的大小减1做与运算,这样同样可以获得0-15的值(假设当前数组容量为16,不懂&运算的同学可以自行百度一下)。下标定位的问题解决了,我们再看一下,如果出现冲突了他是怎么处理的。

我们回到putVal();方法:

第一个if分支,大概就是如果这个插入的新值的key和之前存的数值相同,那么直接用新的值替换老的值,第二个分支如果是树节点,加入到树,第三个分支,如果是链表遍历,然后找到最后一个节点,往下加入新值。

接下来,我们先看一下,链表是怎么使用的。如果key值相等就直接替代了,如果key值不相等而hash相等了怎么办,这个时候就会在这个数组的节点,延伸出一个链表,可以想象为以这个数组节点为头结点,往下用链表来存储,画一个图来说明一下:

就是这样,往下延伸了一个链表,我们继续看,在第三个分支有一个

if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    treeifyBin(tab, hash);

这里就是做了一个将链表转换为红黑树的一个方法,具体不展开,我们看一下条件binCount >= TREEIFY_THRESHOLD - 1,binCount就是循环的索引值,上面的代码可以回去看一下,TREEIFY_THRESHOLD是什么呢?点进去看得到:

那么,也就是说当链表的长度大于7的时候就转为红黑树了,我们再看第二个分支

((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

这个方法点进去有一个

if (lc <= UNTREEIFY_THRESHOLD)
    tab[index] = loHead.untreeify(map);

UNTREEIFY_THRESHOLD是什么呢

这里就是说当红黑树的节点小于等于6,就又转回链表了;

这种机制就防止了链表太长而导致搜索太慢,而红黑树的结构复杂,如果节点少,用红黑树可能会更耗资源,在这两者之间做了平衡。

我们现在思考一个问题,如果这个数组的容量只有16,会出现什么问题?随着数据的不断put,不断的hash冲突,然后链表和红黑树的节点越来越多,那么查找效率肯定是会越来越差的,所以,HashMap还有一个扩容。那么,什么情况下它才会扩容呢?我们回到源码找一下答案,我们在putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)的方法里找到这样一段代码:

  if (++size > threshold)
            resize();

先说明一下这两个参数:

size:
 /**
     * The number of key-value mappings contained in this map.
     */
    transient int size;

从源码注释可以看出,这个size就是当前map中存在的K-V键值对的数量,也就是当前存了多少对键值对

threshold:
/**
     * The next size value at which to resize (capacity * load factor).
     *
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;

先说明一个单词threshold,百度翻译得到的结果是:门槛;门口;阈;界;起始点;开端;起点;入门。根据threshold这个单词的翻译可以认为threshold是一个阈值,当当前的map容量到达这个阈值就会做一次resize,重新计算容量,也就是扩容。

我们继续跟进resize()方法,可以找到底下这段代码:

        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }

看一下这几个变量:

oldThr:原起始点,可以理解为前一次创建数组的起始点,比如:如果是第一次调用,也就是刚刚初始化,那么他的阈值就是0,也就是当它的大小为0的时候就
        触发一次数组创建,初始化阶段创建一个容量为16的数组。
newCap:新的数组的容量
newThr:新的起始点,可以理解为将要创建的新的数组的起点,也就是当到达这个阈值的时候,就要新触发一次数组容量的扩充。
然后我们看最后一句代码:
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

DEFAULT_LOAD_FACTOR 是扩展因子,源码中是这样定义的:

static final float DEFAULT_LOAD_FACTOR = 0.75f;

DEFAULT_INITIAL_CAPACITY 前面已经介绍过了,就是默认的容量:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

那么也就是说,当这个阈值到达16*0.75=12 就会触发一次数组容量的扩充。

我们再回到putVal()方法看

  if (++size > threshold)
            resize();

这下就清楚了,当map当前的size(当前已经存放了多少键值对)大于这个threshold,假设当前容量是16的话,这个threshold就是12,当大于12,就会进行一次扩容操作。

于是乎,又出现一个新的疑问,扩容的容量是多少呢?我们接着在源码中寻找答案,再一次回到resize()方法:

        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }

当oldCap(原数组的容量)> 0(也就是已经初始化过了),的时候有两个分支,我们先看第一个

oldCap >= MAXIMUM_CAPACITY
     /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

MAXIMUM_CAPACITY:最大的容量数。

这个分支就是判断,是否超过了最大容量,当大于这个数值,就不再进行扩充了。

重点是第二个分支,

(newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY

这里将原容量左位移1位赋值给了新的容量,左位移1位,也就是扩大了一倍。

现在我们找到答案了,每次扩容为原来的一倍,但是,如果超过了最大的容量,就不进行扩容了。

关于扩容后的数据转移,我们继续看源码,以下是resize()方法中的一段代码:

                    else { // preserve order
                        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 {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }

这段代码不太好形容,我就概述一下它的逻辑,e是一个Node对象,拿e的hash和原数组容量做&运算,如果等于0,就在原数组节点下“挂上”一个链表节点,否则,拿当前的索引加上原数组的容量,比如当前索引为2,原容量为16,那么这个Node就会在索引值为18的数组节点为头节点,往下“挂”一个链表节点。红黑树的数据转移与链表是相似的。

HashMap通过以这种方式处理了随着容量的扩充,迁移原数组的数据,解决了原结构与新结构的数据分布不均匀的问题。

总结:

1、HashMap在put元素的时候进行初始化,初始化一个容量大小为16的Node数组。

2、通过对key的hash做一系列的运算,来尽可能的避免hash冲突。

3、由于hash冲突不可避免,当发生hash冲突的时候,有两种选择:

      1)如果key相同,直接覆盖;

      2)key值不同,用链表(或红黑树)存储;

4、发生hash冲突(忽略key相同的情况),存储数值的时候,如果当前链表长度小于8则以链表形式存储,如果大于等于8则以红黑树存储;如果红黑树的叶子节点小于6,则又会变为链表。

5、扩容因子为0.75,当新加入的节点的下标超过了,当前数组容量8扩容因子的时候,就会触发扩容。例如:假设当前数组容量为16,那么16*0.75=12,当新加入的值存在第13号索引的时候就会进行扩容操作。扩容主要是为了避免数组容量太小,随着hash冲突的不断发生,导致链表或红黑树过于庞大,而导致查找性能低下。

6、扩容之后,扩容前的结构还是相对拥挤,会通过相关算法,将数据进行转移,一部分留在原地,一部分移到当前索引值加上原数组容量大小的地方。如:当前下标为2,那么会有一部分仍留在以数组索引值为2并以其为头节点,往下继续存储,另一部分则转移到数组索引为18的位置,并以其作为头节点,以链表方式存储。这样就可以使存储的数据分散开来,方便查询。

猜你喜欢

转载自blog.csdn.net/qq_32285039/article/details/106751836