HashMap源码解析

1,213 阅读4分钟

这里聊JDK1.8的源码,采用图文结合的方式,文章会有些长,1.8的可读性实在没有1.7的好,请大家跟着我的思路,耐心的去看.简单的new 一个HashMap ,put一个k,v 进去.

image.png

一.先看1

image.png 无参的构造方法没什么可说的,看下注释Constructs an empty HashMap with the default initial capacity (16) and the default load factor (0.75)., 直接给一个默认16 负载因子为0.75的HashMap对象.

有参的this 点进去看

image.png

上面都是一些判断抛异常的,主要是红框内的方法

image.png 这段是什么意思呢?看下注释 Returns a power of two size for the given target capacity.(返回一个大于等于cap的2的幂次函数) 也就是说 有参构造方法传10 Map的容量是 16 ,传 17 容量就是32.

new 一个HashMap 实际上并没有初始化,在执行put方法时,才会真正的初始化,1.8的初始化和扩容都是在同一个resize()方法里的,下面的源代码我加了注释

image.png

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table; // 初始化时 此时 oldTab 为null  会按顺序执行 步骤一 二 三 四
    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;//步骤四
}

初始化完成后,HashMap是怎样存储k,v的呢? 注意这段代码

image.png

这里 ,n是 HashMap 内部Node<K,V> 数组的长度,他是通过数组长度减1 与hash 的一个运算计算出key在Node数组中的下标的,这里引申出了一个面试题: HashMap的容量为什么是2的幂次函数?,我们先来思考一下,HashMap对存储数据的要求是能够随机并且均匀 的分散在Node数组中 ,不能 在某个位置聚合,比如index[8]的位置已经形成了很长的链表,但其他位置还没有数据.理解了这个 ,再来看下面的例子

以初始化容量16 举例 16 的二进制数为 0001 0000,如果与随机生成的hashcode 例如 0011 1111进行&运算,同为1 才为1 否则为0.因为16 的高三位和低四位都是0 所以无论hashcode怎么变化,最终的结果都只有两个 要么 高第四位为1 要么为0 很明显不能满足容量为 16 的Node数组的索引0~15的需求.n-1=15 15 的二进制 0000 1111,那么 和hashcode的&运算产生的低四位的结果 是在 0000-1111区间内,恰好满足0~15的需求,后期扩容也是如此.总结: HashMap的容量为2的幂次函数,归根结底是为了和hashcode进行&运算,使值可以分散的分布在Node数组中的每个索引,增加空间利用率,不至于使某个索引位的链表长度过长

看下put方法做了哪些事?

image.png

1.计算出key所在索引位置,如果为null,直接生成一个新的Node对象并赋值

2.如果不为空判断新的key与旧的key 是否相同,如果相同,则进行值覆盖

3.如果两个key不相同,则判断当前key所在的索引位置的下一个节点(next)是否为空 如果为空,则生成一个node赋值给当前key所指向的下一节点(尾插法).

这里又引申出一个问题,大家都知道HashMap是非线程安全的,在put方法里就可以说明这点,试想A,B两个线程通知执行put方法,A走到了操作1 的位置判断当前索引位置为空,但是这时A线程挂起了,B线程这时对该索引位置赋值,然后A线程恢复执行,再赋值,是不是就把B的值给覆盖掉了?另外看下图,多线程走到这里,如果两个线程同时走到这里,也会导致重复的resize().

image.png

解决这一问题的办法Java中提供了 HashTable,ConcurrentHashMap 等办法,HashTable因为性能受限,基本已经淘汰了,下一篇从源码上分析ConcurrentHashMap 为什么是线程安全的.

感谢耐心收看,欢迎批评指导