基于Java8的HashMap源码分析

196 阅读24分钟

首先,我们来确认下HashMap是什么的问题。 Java JDK1.8之前,HashMap是基于哈希表,底层以数组+单向链表实现的以key-value键值对存储数据的一种数据结构。key-value都允许为null,但key只能有一个,value可以有任一个;

Java JDK1.8及之后,HashMap是基于哈希表,底层以数组+单向链表+红黑树实现的以key-value键值对存储数据的一种数据结构。key-value都允许为null,但key只能有一个,value可以有任一个;

本文将基于Java JDK1.8,展开对HashMap的源码分析。老规矩,我们从HashMap的成员变量、构造方法、普通方法的顺序开始。

HashMap成员变量

private static final long serialVersionUID = 362498820763181265L; // 序列化

// 0000 0001,移位操作后,值为16,他是HashMap初始化时默认的长度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

static final int MAXIMUM_CAPACITY = 1 << 30; // HashMap的最大容量

// 根据统计学的结果, hash冲突符合泊松分布, 冲突概率最小的是在7-8之间, 小于百万分之一; 所以加载因子loadFactor只要
// 在7-8之间选取任意值即可, 但是为什么就选了3/4呢?因为:table.length * 3/4可以被优化为
// (table.length >> 2) << 2) - (table.length >> 2) == table.length - (table.lenght >> 2)。
// 说人话就是:JAVA的位运算比乘除运算的效率更高, 加载因子取3/4, 便能保证在hash冲突概率最小的情况下,同时兼顾效率;
// 而加载因子loadFactor取7-8之间的其他值则没有此优势;
// HashMap的默认加载因子,DEFAULT_LOAD_FACTOR:map.size / (capacity * loadFactor)
static final float DEFAULT_LOAD_FACTOR = 0.75f; 

// 当HashMap中,某个数组下标索引index对应的链表长度不小于该值阈值时,系统就会启动转换工作:
// 将当前的单向链表结构实现转换为红黑树实现
static final int TREEIFY_THRESHOLD = 8;

// 当HashMap中,某个数组下标索引index对应的链表长度小于等于该值阈值时,系统就会启动转换工作:
// 将当前的红黑树实现转换为单向链表结构实现
static final int UNTREEIFY_THRESHOLD = 6;

// 当哈希表中的容量 > 该值时,才允许树形化链表 (即 将链表 转换成红黑树),否则,
// 若桶内元素太多时,则直接扩容,而不是树形化
// 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
// 说人话就是:当某个数组下标索引index对应的链表长度不小于该值阈值,并且,
// 当HashMap的size>=64时,两个条件同时满足时,
// 才会允许将链表转换为红黑树,否则,调用resize()方法进行扩容操作。
static final int MIN_TREEIFY_CAPACITY = 64;

transient int size; // hashmap的长度(包含的Item的总数量)

transient int modCount; // 他的变更,表示HashMap发生了结构性的改变

transient Node<K,V>[] table; // 该节点在首次使用时初始化,并根据需要调整大小。分配时,长度始终是 2 的幂次方。

transient Set<Entry<K,V>> entrySet; // 保存缓存的entrySet()。

// HashMap的扩容阈值:threshold = capacity * loadfactor。当map.size > threshold时,需要调用resize()方法
// 执行扩容操作.举例:
// capacity = 16,loadfactor = 0.75,则table的长度是16,threshold = capacity(16) * loadfactor(0.75) = 12,
// 当HashMap存储的Item数量达到12,再继续存入数据时,就会调用resize()方法执行扩容操作。
int threshold;

final float loadFactor; // 填充因子

HashMap的公共成员变量及各自含义,作用见注释,不赘述。

需要注意的是,公共成员变量中,有不少变量被关键字transient修饰,这表示,被该关键字修饰的变量,在HashMap被序列化时,不被序列化。

构造方法

1、无参构造HashMap()

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

无参构造很简单,就是将 DEFAULT_LOAD_FACTOR 赋值给 填充因子loadFactor。

2、有参构造HashMap(int initialCapacity)

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

在这里,入参initialCapacity表示的是初始化HashMap时的初始长度,默认值是16,这里被重新定义为initialCapacity,DEFAULT_LOAD_FACTOR为加载因子,系统默认定义的,取值0.75。 然后调用HashMap的另一个构造方法实现更具体的逻辑。

3、有参构造HashMap(int initialCapacity, float loadFactor)

入参initialCapacity和loadFactor的含义上一小节已经做了详细解释,这里不再提及。我们直接看他的内部实现:

// 构造一个具有指定初始容量和负载因子的空 HashMap。初始容量 threshold = 2倍 initialCapacity;
// 负载因子 = loadFactor;
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0) { // initialCapacity定义的HashMap初始长度 < 0,直接抛出异常
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    }
    // 当 入参initialCapacity > HashMap在JDK中允许的最大长度时,就将HashMap的长度控
    // 制在最大长度:MAXIMUM_CAPACITY
    if (initialCapacity > MAXIMUM_CAPACITY) {
        initialCapacity = MAXIMUM_CAPACITY;
    }
    // 当 加载因子loadFactor <= 0 或者是其他无效数据时,抛出异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
        throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    }
    // 为加载因loadFactor赋值
    this.loadFactor = loadFactor;
    // 为HashMap的扩容阈值 threshold 赋值:
    this.threshold = tableSizeFor(initialCapacity);
}

// 返回给定目标容量的两倍大小的容量。
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

主时钟已经很详细的解释了各个要点的含义,不再多说。

4、入参为Map的构造HashMap(Map m)

源码实现如下:

public HashMap(Map<? extends K, ? extends V> m) {
    // 初始化加载因子
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false); // 调用 putMapEntries()方法将map集合m存到当前HashMap中
}

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size(); // 获取 map的长度。
    if (s > 0) {
        if (table == null) { // pre-size
            // 计算当前HashMap需要的容量,DEFAULT_INITIAL_CAPACITY表达的作用
            float ft = ((float)s / loadFactor) + 1.0F;
            // 如果 当前HashMap需要的容量 < JDK允许的最大容量,那么当前HashMap的容量就取值 ft,
            // 否则取值JDK允许的最大容量。
            int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
            // 如果 当前HashMap的容量 大于 扩容阈值,更新 扩容阈值(为当前HashMap的容量的2倍)
            if (t > threshold) { 
                threshold = tableSizeFor(t);
            }
        } else if (s > threshold) {
            resize(); // 调用 resize() 方法进行扩容。
        }
        // 这里for循环开启遍历,然后调用 putVal()方法开始插入集合m
        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); // putVal()的详情我们后面再讲
        }
    }
}

注释中有非常详细的解说,这里就不再赘述。

hash与Rehash

首先我,我们简单的聊一下什么是Hash。

hash运算(哈希运算)是一种将任意长度的输入通过某种特定算法转换成固定长度输出(通常是一串整数,称为“哈希值”或者“散列值”或者“hashCode”)的过程。这个过程是不可逆的,即通常无法从哈希值恢复出原始输入数据。哈希运算在多个领域有广泛应用,包括数据加密、数据检索、错误检测等。

hash运算与特点

1、固定输出长度:无论输入数据的大小如何,哈希hash()方法都会生成一个固定长度的输出。
2、快速计算:哈希hash()方法的设计应该能够快速计算出哈希值,以便在实际应用中高效地使用。
3、单向性:哈希运算通常是单向的,即从哈希值恢复原始数据在计算上是不切实际的。
4、雪崩效应:当输入数据稍有变动时,哈希值应该发生显著变化,这有助于确保数据的完整性和安全性。
5、不同的输入数据,经Hash运算之后,输出的固定长度的Hash值有可能是相同的,这就是Hash冲突。

在Java中,hashCode是一个对象的标识,对象的hashCode是一个int类型值。 HashMap中的hash实现方法如下:

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

这里使用(h >>> 16)运算,目的是使得高位的信息也参与到hash运算中去,这应能降低Hash冲突的概率。

实现原理

HashMap是基于拉链法处理碰撞的散列表的实现,一个存储整型元素的HashMap的内部存储结构如下图所示:

HashMap实现原理.png
偷了个懒,该图来自:Android开发分享1

我们可以看到,HashMap是采用数组+链表实现的,在JDK 1.8中,对HashMap做了进一步优化,引入了红黑树。当某条链表的长度大于8,且HashMap中总长度size > 64 时,就会将链表转换为红黑树;但是,当当某条链表的长度小于6时,又会将红黑树转换为单向链表。

HashMap的节点信息

HashMap的节点信息类Node是HashMap的static类型的内部类,implements实现了Map.Entry<K,V>接口。源码如下:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash; // hash值
    final K key; // HashMap存储的键key,从这里,我们可以看到,key是不可以更改的
    V value; // HashMap存储的键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;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        // 这里key和value都参与hashCode计算,并将二者异或^操作
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value; // 暂存当前值value
        value = newValue; // 将新的值 赋值给value,以此更新当前key对应的值
        return oldValue; // 返回当前key对应的旧的value值
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

铺垫了这么多,我们现在开始HashMap各基本方法实现原理的分析

put(K key, V value)

put(K key, V value)方法源码实现如下:

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

// 通过key值进行hash计算
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

其实put(K key, V value)内部并没有处理什么逻辑,直接调用putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)方法来处理,下面我们来看看putVal()方法的实现过程:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab;
    Node<K,V> p;
    int n, i;
    // 步骤1.TODO:
    //      若 table数组 为空 或 table.length 为 0,则调用resize()方法扩容
    //      先将 table 数组赋值给 tab数组,然后再判断 tab数组 是否为null,这样相当于同时判断了 tab
    if ((tab = table) == null || (n = tab.length) == 0) {
        // 这里至少有三个重要操作:
        //      1、调用resize()方法扩容;
        //      2、resize()方法扩容完成后,返回扩容的Node<K,V>数组并赋值给tab变量;
        //      3、将tab数组变量的长度length赋值给n;
        n = (tab = resize()).length;
    }
    // 步骤2.TODO:
    //      让入参 hash(在这里,本质上就是key值通过Hash计算得到的hash值) 与 n-1 做 “与&” 运
    //      算,从而得到目标Node的索引.若该索引处为 null,说明该索引处为null,没存Node元素,直接
    //      通过hash值、key-value键值对创建Node节点并存值
    if ((p = tab[i = (n - 1) & hash]) == null) {
        // 通过key得到的hash值,key,value,下一个节点信息next,创建一个新的 Node节点,
        // 这里节点信息next为null
        tab[i] = newNode(hash, key, value, null);
    } else {
        //若索引处不为null,则判断key是否存在
        Node<K,V> e;
        K k;
        //步骤3.TODO:
        //      若key存在,说明该索引对应的"坑"里已经存有Node元素,且hash值和key值都相等,
        //      先获取引用,后面会用来替换值(若key存在,则直接覆盖value)
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
            e = p;
        // 步骤4.TODO:
        //      判断该链是否是红黑树
        // 若key不存在,则判断table[i]是否为TreeNode
        } else if (p instanceof TreeNode) {
            // 若是的话,说明此处为红黑树,直接插入key-value 键值对
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //步骤5.TODO:
        //      该链是链表,遍历链表
        } else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    // 通过key得到的hash值,key,value,下一个节点信息next,创建一个新的 Node节点,
                    // 这里节点信息next为null
                    // 步骤5.1 TODO:
                    //      注意这个地方跟Java7不一样,是插在链表尾部。也就是通常意义上的尾插法;
                    //      Java7是插在链表头部,也就是通常意义上的头插法。
                    p.next = newNode(hash, key, value, null);
                    //步骤5.2.TODO:
                    //       链表长度超过8,则转为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) { // -1 for 1st
                        treeifyBin(tab, hash); // 将单向链表转为红黑树
                    }
                    break;
                }
                //步骤5.2.TODO:
                //       链表中已存在且hash值和key值都相等,先获取引用,后面用来替换值(若key已经
                //       存在则直接覆盖value)
                //
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                    break;
                }
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null) {
                // 统一替换原来的值
                e.value = value;
            }
            afterNodeAccess(e); // 空实现,这个是回调给 LinkedHashMap子类用的
            // 返回原来的值
            return oldValue;
        }
    }
    ++modCount;
    //步骤6.TODO:
    //      键值对数量超过阀值,扩容
    if (++size > threshold) {
        resize();
    }
    afterNodeInsertion(evict); // 空实现,这个是回调给 LinkedHashMap子类用的
    return null;
}

上面 putVal()方法中,通过 key获取到的hash值,key-value键值对,创建新的节点的方法实现如下:

// 通过key得到的hash值,key,value,下一个节点信息next,创建一个新的 Node节点
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    return new Node<>(hash, key, value, next);
}

总结起来,putVal方法主要处理了以下逻辑:

1、判断容器大小是否为null或者0,是的话使用resize()方法对容器进行初始化,Java8把初始化数组和扩容全写在resize方法里了,这一点,我们放在resize()方法中去介绍。
2、接下来要把元素插入到Map中。计算当前元素存的位置(n - 1) & hash
——a、当前位置如果为空,则新建一个节点存入当前位置,使用方法newNode()
——b、不为空的话,判断判断是否为链表或者红黑树,然后进行插入操作。其中需要判断链的大小(binCount)来决定是否与红黑树相互转换
————这里需要注意的是:这里链表使用尾插法,扩容也是尾插法,这样多线程扩容不会导致死循环。
——c、如果之前key存在if (e != null),则时更新当前的值,返回之前的值。
3、之前不存在key,增加sizemodcount
4、判断size和threshold的大小,决定是否扩容resize

总结起来,put()方法执行流程如下图: Java8HashMap的put()方法流程图.png

下面我们来看看扩容方法resize()的实现。

resize()

// 方法作用:初始化或把table容量翻倍。
//      1、如果table是空,则根据 threshold属性 的值去初始化HashMap的容量。
//      2、如果不为空,则进行扩容,因为我们使用2的次幂来给HashMap进行扩容,所以每个桶里的元素
//        必须保持在原来的位置或在新的table中以2的次幂作为偏移量进行移动
final Node<K,V>[] resize() {
    // 创建一个临时变量,用来存储当前的table
    Node<K,V>[] oldTab = table;
    // 获取原来的table的长度(大小)当前table如果为空,则 oldCap为0,否则以table的长度作为oldCap的大小
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 创建临时变量用来存储旧的阈值,把旧table的阈值赋值给oldThr变量
    int oldThr = threshold;
    // 定义变量 newCap 和 newThr 来存放新的table的 容量 和 阈值,默认都是0
    int newCap, newThr = 0;
    // 判断旧容量是否大于0
    if (oldCap > 0) {
        // 判断旧容量oldCap是否大于等于 允许的最大值,2^30,如果是,则将旧的threshold阈值限制在
        // 允许的最大值MAXIMUM_CAPACITY
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 以int的最大值作为原来HashMap的阈值,这样永远达不到阈值就不会扩容了
            threshold = Integer.MAX_VALUE;
            // 因为旧容量已经达到了最大的HashMap容量,不可以再扩容了,将阈值变成最大值之后,将原table返回
            return oldTab;
        // 如果原table容量不超过HashMap的最大容量,将原容量*2之后,赋值给变量newCap,
        // 如果newCap不大于HashMap的最大容量,并且原容量大于HashMap的默认容量
        // TODO: 这句对于扩容最关键,扩容就在这,扩大两倍;
        // TODO: 扩容会伴随着一次重新 hash 分配,并且会遍历 hash 表中所有的元素,并对所有的元素根据key值的hash值,
        //      重新进行一次换位存储,是非常耗时的。在编写程序中,要尽量避免 resize操作。
        } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
            // 将newThr的值设置为原HashMap的阈值*2
            newThr = oldThr << 1; // double threshold
        }
    // 如果原容量oldCap不大于0,即原table为null,则判断旧阈值是否大于0
    // 如果原table为Null且原阈值大于0,说明当前是使用了构造方法指定了容量大小,只是声明了HashMap但是还没有真正的
    // 初始化HashMap(创建table数组),只有在向里面插入数据才会触发扩容操作进而进行初始化
    } else if (oldThr > 0) { // initial capacity was placed in threshold
        // 将原阈值作为容量赋值给newCap当做newCap的值。由之前的源码分析可知,此时原阈值存储的
        // 大小就是调用构造函数时指定的容量大小,
        // 所以直接将原阈值赋值给新容量
        newCap = oldThr;
    // 如果原容量不大于0,并且原阈值也不大于0。这种情况说明调用的是无参构造方法,还没有真正初始化
    // HashMap,只有put()数据的时候才会触发扩容操作进而进行初始化
    } else {               // zero initial threshold signifies using defaults
        // 则以默认容量作为newCap的值
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 以初始容量*默认负载因子的结果作为newThr值
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 经过上面的处理过程,如果newThr值为0,说明上面是进入到了原容量不大于0,旧阈值大于0的
    // 判断分支。需要单独给newThr进行赋值
    if (newThr == 0) {
        // 临时阈值 = 新容量 * 负载因子
        float ft = (float)newCap * loadFactor;
        // 设置新的阈值 保证新容量小于最大总量   阈值要小于最大容量,否则阈值就设置为int最大值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
    }
    // 将新的阈值newThr赋值给threshold,为新初始化的HashMap来使用
    threshold = newThr;
    // 初始化一个新的容量大小为newCap的Node数组
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    // 将新创建的数组赋值给table,完成扩容后的新数组创建
    table = newTab;
    // 如果旧table不为null,说明旧HashMap中有值
    if (oldTab != null) {
        // 如果原来的HashMap中有值,则遍历oldTab,取出每一个键值对,存入到新table
        for (int j = 0; j < oldCap; ++j) {
            // 创建一个临时变量e用来指向oldTab中的第j个键值对
            Node<K,V> e;
            // 将oldTab[j]赋值给e并且判断原来table数组中第j个位置是否不为空
            if ((e = oldTab[j]) != null) {
                // 如果不为空,则将oldTab[j]置为null,释放内存,方便gc
                oldTab[j] = null;
                // 如果e.next = null,说明该位置的数组桶上没有连着额外的数组
                if (e.next == null) {
                    // 此时以e.hash&(newCap-1)的结果作为e在newTab中的位置,将e直接放置在
                    // 新数组的新位置即可
                    newTab[e.hash & (newCap - 1)] = e;
                // 否则说明e的后面连接着链表或者红黑树,判断e的类型是TreeNode还是Node,
                // 即链表和红黑树判断
                } else if (e instanceof TreeNode) {
                    // 如果是红黑树,则进行红黑树的处理。将Node类型的e强制转为TreeNode,
                    // 之所以能转换是因为TreeNode 是Node的子类
                    // 拆分树,具体源码解析会在后面的TreeNode章节中讲解
                    ((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                // 当前节不是红黑树,不是null,并且还有下一个元素。那么此时为链表
                } else { // preserve order
                     /**
                         这里定义了五个Node变量,其中lo和hi是,lower和higher的缩写,也就是高位和
                         低位, 因为我们知道HashMap扩容时,容量会扩到原容量的2倍,也就是放在链表中
                         的Node的位置可能保持不变或位置变成 原位置+oldCap, 在原位置基础上又
                         加了一个数,位置变高了,这里的高低位就是这个意思,低位指向的是保持原位置
                         不变的节点,高位指向的是需要更新位置的节点
                      */
                    // TODO: Head指向的是链表的头节点,Tail指向的是链表的尾节点
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    // TODO: 指向当前遍历到的节点的下一个节点
                    Node<K,V> next;
                    // 循环遍历链表中的Node
                    do {
                        next = e.next;
                         /**
                             如果e.hash & oldCap == 0,注意这里是oldCap,而不是oldCap - 1。
                             二oldCap是2的次幂,也就是1、2、4、8、16...转化为二进制之后,都是最
                             高位为1,其它位为0。所以oldCap & e.hash 也是只有e.hash值在 oldCap
                            二进制不为0的位对应的位也不为0时,才会得到一个不为0的结果。举个例子,
                             我们知道10010 和00010 与1111的&运算结果都是 0010,但是110010和
                             010010与10000的运算结果是不一样的,所以HashMap就是利用这一点,
                             来判断当前在链表中的数据,在扩容时位置是保持不变还是位置移动oldCap。
                         */
                        if ((e.hash & oldCap) == 0) {
                            // 如果是第一次遍历
                            if (loTail == null) {
                                // 让loHead = e,设置头节点
                                loHead = e;
                            } else {
                                // 否则,让loTail的next = e
                                loTail.next = e;
                            }
                            // 最后让loTail = e
                            loTail = e;
                        /**
                            其实 if 和 else 中做的事情是一样的,本质上就是将不需要更新位置的节点
                            加入到loHead为头节点的低位链表中,将需要更新位置的节点加入到hiHead为
                            头结点的高位链表中。
                            我们看到有loHead和loTail两个Node,loHead为头节点,然后loTail是尾节
                            点,在遍历的时候用来维护loHead,即每次循环,更新loHead的next。我们来
                            举个例子,比如原来的链表是A->B->C->D->E。
                            我们把->假设成next关系,这五个Node中,只有C的hash & oldCap != 0 ,
                            然后这个代码执行过程就是:
                                第一次循环: 先拿到A,把A赋给loHead,然后loTail也是 A;
                                第二次循环: 此时e的为B,而且loTail != null,也就是进入上面的
                                            else分支,把loTail.next = B,此时loTail中即A->B,
                                            同样反应在loHead中也是A->B,然后把loTail = B;
                                第三次循环: 此时e = C,由于C不满足 (e.hash & oldCap) == 0,进
                                            入到了我们下面的else分支,其实做的事情和当前分支的
                                            意思一样,只不过维护的是hiHead和hiTail;
                                第四次循环: 此时e的为D,loTail != null,进入上面的else分支,把
                                            loTail.next =D,此时loTail中即B->D,同样反应在
                                            loHead中也是A->B->D,然后把loTail = D;
                        */
                        } else {
                            if (hiTail == null) {
                                hiHead = e;
                            } else {
                                hiTail.next = e;
                            }
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 遍历结束,即把table[j]中所有的Node处理完
                    // 如果loTail不为空,也保证了loHead不为空
                    if (loTail != null) {
                        // 此时把loTail的next置空,将低位链表构造完成
                        loTail.next = null;
                        // 把loHead放在newTab数组的第j个位置上,也就是这些节点保持在数组中的原位置不变
                        newTab[j] = loHead;
                    }
                    // 同理,只不过hiHead中节点放的位置是j+oldCap
                    if (hiTail != null) {
                        hiTail.next = null;
                        // hiHead链表中的节点都是需要更新位置的节点
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    // 最后返回newTab
    return newTab;
}

上面resize()方法很长,但是都加了很详细的注释说明,理解起来应该不难。 总结起来可以分为三个部分:

第一:计算新的容量新的容量阈值,然后申请新的内存空间。
1、如果容量大于等于最大容量,则不扩容使用最大容量,直接返回。
2、不是则成倍扩容(向左位移1),或者使用默认值初始化容器。
3、申请新的内存空间。
4、替换旧容器。

第二:把旧的容器内容移动到新的容器。
1、遍历旧容器。判断节点是一个的话直接移动。
2、判断为红黑树的话,使用树的split函数划分到新的容器。
3、判断是链表的话,把原来的链表分为两个部分,分到新的容器中(新的位置这里直接加上就容器大小oldCap),这里判断使用(e.hash & oldCap),这个原理等下介绍。

第三:返回新的容器。

扩容方法resize()方法的时间消耗比较大,因为扩容后,要对已及存储的数据重新做hash运算和保存。因此我们在能够估计到大致需要存储的数据量时,应该为其指定一个合适的初始容量。

resize()方法执行流程如下:

Java8HashMap的resize()方法执行流程.png

get(Object key)

相比于put(K key, V value)方法,get(Object key)方法则是简单的多。源码实现如下:

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; // 创建一个临时变量,用来存储当前的table
    // 变量first用于缓存 tab数组在指定索引index处的 Node节点;
    // 变量e用于缓存tab数组在指定索引index处的 Node节点first的下一个节点 first.next
    Node<K,V> first, e;
    int n; // 用于缓存 tab数组的长度length
    K k; // 用于缓存 tab数组在指定索引index处的 Node节点 的key值
    // 若 table 不为空null, 且table的长度length大于0, 且tab数组在指定索引
    //          (这里的索引index即为(n - 1) & hash,通过该索引从tab中获取到对应的Node节点并
    //           赋值给first变量)
    //  处 Node节点元素不为空,则进一步进行其他判断,否则直接返回null。
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        // tab数组在指定索引index处的 的Node节点first(来源/赋值见上面if判断)即为我们要找的Node,
        // 直接返回即可
        // always check first node
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) {
            return first;
        }
        // 目标Node节点 和 first 处于 同一红黑树 或 同一链表,位于first之后
        if ((e = first.next) != null) {
            // 判断 Node节点first 是否为红黑树
            if (first instanceof TreeNode) { // TreeNode是HashMap的static静态内部类
                // Node节点first 是为红黑树,直接调用 TreeNode的getTreeNode(int h, Object k)
                // 方法获取值,并返回该值
                return ((TreeNode<K, V>) first).getTreeNode(hash, key);
            }
            // fNode节点first 是链表
            do {
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                    return e;
                }
            } while ((e = e.next) != null);
        }
    }
    return null;
}

final TreeNode<K,V> getTreeNode(int h, Object k) {
    return ((parent != null) ? root() : this).find(h, k, null);
}

final TreeNode<K,V> root() {
    for (TreeNode<K,V> r = this, p;;) {
        if ((p = r.parent) == null) {
            return r;
        }
        r = p;
    }
}

基于前面对put(K key, V value)方法的分析,get(Object key)方法很好理解,细节都已经在注释中详细说明,这里不再赘述。

总结

JDK8之前与JDK8开始的版本的区别:

1、JDK8开始的版本新增了红黑树,当数组对应的某条链表长度超过8,就会将当前的链表实现改为红黑树实现;当数组对应的某条链表长度不超过6时,就会将当前的红黑树实现改为链表实现。
2、JDK8之前的版本则是一直使用链表结构实现,这在数据量小的时候,不会有什么问题。当数据量很大的时候,就会导致某条链表过长,我们在通过get(Object key)方法获取数据时,查找就会变得困难效率低下
3、发生hash冲突时,JDK8之前采用的是头插法,会在链表头部插入;Java8则是采用尾插法,会在链表尾部插入。
4、线程安全方,相比JDK7使用头插法引发的多线程状态下的死循环问题,JDK8使用尾插法解决了该死循环问题。

JDK7使用头插法引发的多线程状态下的死循环的原因:
在JDK7中,当使用头插法扩容,扩容后链表为逆序。当有多个线程同时进行扩容的时候,第一个线程把链表逆序,第二个线程指向的头结点变为尾结点,然后扩容这样会变成环。

解决方案

使用ConcurrentHashMap或者HashTable或者Collections.synchronizedMap()

##感谢

Android开发分享
图解HashMap
resize()详解