HashMap的底层实现原理

1,219 阅读7分钟

一、简介

HashMap是可以实现高效访问的数据结构,不同于数组和链表,数组是查询方便、增删复杂;链表是增删方便,查询复杂;HashMap很好的结合了二者的优点。

在添加时,如果计算的哈希值不重复,就直接放入数组,简单高效;而当哈希值重复了,则会按照链表的方式插入,这恰号用到了链表的优点。查询时,可以快速按照索引定位到某一个位置,而如果某个索引上有多个元素,在元素不多的时候按照链表的方式查询,效率也可以接收,当元素很多时,可以变成红黑树,同样时间复杂度降低,可以说HashMap真的是大佬们智慧的结晶。

二、底层实现原理

1、HashMap的底层是什么

如今,面试的过程中HashMap的底层实现原理可以说是一个合格程序员必须了解的知识,但是还是有必要记录以下,便于后面复习。

这里为了方便就截取有用的源码部分了(本来也不是给大家看的,如果有谁恰好看到了,可以自行查看相关源码的具体实现,这里是东拼西凑的代码),首先看下面这一段:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    
     static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        ...
     }
    
    transient Node<K,V>[] table;
}

上面是说HashMap类,首先继承了AbstractMap类,AbstractMap类又实现了Map接口,可见Map这一家族和Collection家族并不属于同一个大的家族。

HashMap的底层有一个静态内部类Node<K,V>,这相当于就是一个又一个的节点了,这个静态内部类实现了Map.Entry<K,V>接口

在Map.Entry中我们可以看到下面代码(依然只截取有用的部分):

interface Entry<K,V> {
    K getKey();
    V getValue(); 
    V setValue(V value);
   	int hashCode();
}

里面写了一些最最常用的方法,计算hash值,获取key、value等。

另外,不难看出来,HashMap的底层有一个Node节点的数组,名为table,这也就是我们常说的数组 + 链表的那个数组table了。

2、HashMap的基本参数

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    
    //这是初始容量16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    
    //加载因子0.75,当达到数组容量的75%就开始扩容
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    //链表长度大于8以后会树化(当然table大小要大于64才会树化),也就是变成红黑树
    static final int TREEIFY_THRESHOLD = 8;
    
    //只有链表长度变为6以后才会回到链表状态,避免在长度为8之间来回增删元素,来回树化、链表化
    static final int UNTREEIFY_THRESHOLD = 6;
    
    //最小树化容量
    static final int MIN_TREEIFY_CAPACITY = 64;

3、HashMap的源码分析:放入元素的过程

测试的用例:

public static void main(String[] args) {
    HashMap map = new HashMap();
    map.put("key1","value1");
    map.put("key2","value2");
    map.put("key1","value3");
    map.put("key4","value4");
    map.put("key5","value5");
    map.put("key6","value6");
    map.put("key7","value7");
    map.put("key8","value8");
    map.put("key9","value9");
    map.put("key10","value10");
    map.put("key11","value11");
    map.put("key12","value12");
    map.put("key13","value13");
    map.put("key14","value14");
    map.put("key15","value15");
    map.put("key16","value16");
}

首先使用Debug进入HashMap,会先进入:

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

这相当于是使用创建HashMap时,设置了默认加载因子0.75,DEFAULT_LOAD_FACTOR也是上面的一个参数。

因为往map中放入了key和value,所以调用了:

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

进入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;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        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;
                }
                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);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

一段一段看,因为最开始创建map,没有初始化table,所以(tab = table) == null肯定是成立的,所以会走下面一个分支:n = (tab = resize()).length;

此时的resize()方法就等于是初始化了,简单看一下resize()的源码片段:

//新的默认初始化容量是16
newCap = DEFAULT_INITIAL_CAPACITY;

//新的扩容容量是16 * 0.75 = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

//初始化一个新的数组,长度是16
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;

接着看putVal的下一行:

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

这里说的是经过计算在table中找到了一个位置来放元素,如果这个位置没有元素,那么就创建一个新的节点,把这个值放入,这里去查看就能发现已经有一个元素了。

然后看下面一行:

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

上面两行的作用是判断是否要做扩容,size初始值为0,++size变为1,threshold就是扩容的值,这里是12,肯定是不满足条件的,所以不会调用resize()。

这样一个值就放好了,现在继续put元素,还是会进入putVal()方法:

这次是因为table已经初始化了,所以不会进入第一个if,而是进入下面一个的if语句,还是计算位置,放入元素。

if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

4、HashMap的源码分析:放入相同key的情况

执行到put的第三行,key = key1,已经存在了元素,还是会进入下面这行:

计算出当前key的哈希,去table里找,发现里面不为空,会进入else分支。

if ((p = tab[i = (n - 1) & hash]) == null)
    ...
else {
    ...

下面就是存放一个key的hash值相同元素的情况了:

  • 第一种情况,当hash值相同,且key值也和原来的值一模一样,说明是要做value的替换,这个时候就会进入最下面的if语句中,使用e.value = value来替换元素
  • 第二种情况,如果这个也就是说不相等,这个时候要添加元素了,当该列已经树化了,那么直接调用putTreeVal()即可
  • 第三种情况比较复杂,如果是要添加元素,而此时不是树的情况,就需要考虑链表的长度是否会造成树化了,还有链表当中存不存在这个key了,如果大于8了,会变成红黑树,调用treeifyBin()方法,如果链表当中的哈希值相同,并且key也相同,就会进行替换
  • 如果p.next为空说明找到了链表最后一个位置,然后p.next = newNode()添加新元素即可,还要考虑是否达到了TREEIFY_THRESHOLD,达到了树化,如果不满足条件,说明没遍历到最后一个元素,这个时候让p = e,也就是p = p.next,继续向后遍历即可
if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
    e = p;
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;
        }
        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);
    return oldValue;
}

5、HashMap的源码分析:数组需要扩容的时候

前12个应该都没问题,可以看看添加的元素到table的位置。

image-20220128234046052

创建新数组阶段

当执行到第十三个的时候,putVal()方法中:

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

这里的threshold = 12,而++size会使得size变为13,从而调取resize()方法。

看看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;
}

首先会进入第一行:

因为oldTab就是最开始创建的表,它的长度就是16,所以oldCap = 16;而oldThr = threshold 就是刚才进行比较的值12。

int oldCap = (oldTab == null) ? 0 : oldTab.length;	//16
int oldThr = threshold;		//12

接下来会进入第一个条件判断:

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分支就是把oldCap扩大一倍,oldThr也扩大一倍,也就是数组长度变为32(16 × 2),数组扩容的临界值变为24(12 × 2)。

后面有几步都是赋值操作,不需要看,后面的代码是:

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

这样就把数组创建出来了。

旧数组元素移动阶段

再下面就是移动阶段了,如果oldTable 不等于空,说明有元素,那么遍历旧数组,看看哪个位置的元素不为0。

if (oldTab != null) {
    for (int j = 0; j < oldCap; ++j) {
        Node<K,V> e;
        if ((e = oldTab[j]) != null) {	//遍历数组,看看哪个位置不为0

找到不为0的元素,把旧位置变为0,看看这个元素是什么类型的,如果e.next == null,说明它不是链表或者红黑树,仅仅是单独的元素,那就计算其所在新数组的位置放入即可。

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