【数据结构学习】关于HashMap的那些事儿

HashMap UML

涉及数据结构

  • 红黑树
  • 链表
  • 哈希

从CRUD说起

预热知识:
DEFAULT_INITIAL_CAPACITY = 1 << 4, HashMap默认容量为16(n << m意思是n*2^m)
MAXIMUM_CAPACITY = 1 << 30最大容量,2^30即10,7374,1824.
DEFAULT_LOAD_FACTOR = 0.75f负载因子0.75,扩容时需要用到

TREEIFY_THRESHOLD = 8

The table, initialized on first use, and resized as necessary. When allocated, length is always a power of two. (We also tolerate length zero in some operations to allow bootstrapping mechanics that are currently not needed.)
首次使用时初始化,必要时进行调整。调整之后的容量通常为2的次幂。(在一些操作中也允许长度为零,以允许当前不需要的自举机制)

Node<K,V>[] table 存储KV对

The next size value at which to resize (capacity * load factor).

threshold:因为牵涉到扩容,而map的扩容(即对table进行扩容操作)不是到了存满了才扩,是以容量*负载因子作为临界点进行扩容的。

简单讲下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;
    }
}

看了之后发现什么,这就是个简单的实现了Map.Entry接口的单链表。


new HashMap()

有的时候,大家写代码可能会不指定初始大小Map<String, String> map = new HashMap<>();

运行map.put("test", "zhangsan");的时候,首先运行一次resize(),此时table = new Node[16]

image-20210531223131703.png

(p = tab[i = (n - 1) & hash]) == null经过hash与n-1的与运算,找到一个位置(比如4),如果这个位置值为null,那么tab[4]就放上一个Node(图中仅仅以value做区分,但请大家注意,这实际上是Node,有key,有value,有hash,有next,尽管刚开始next是null)

image-20210531224840604.png

假如此时接着执行了map.put("test", "liss");会走p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))这个if分支,然后将原来的value替换为liss但正常情况下我们写业务逻辑,都不会这么写的对吧。

假如继续put("test2", "zhangsan2")按照这样的方式put下去。插入7条数据之后如下

image-20210531231326537.png

当执行map.put("test8", "zhangsan8");时,由于test8的hash与15做运算,得到的位置为4,而4这个位置已经有值了。

for (int binCount = 0; ; ++binCount) {
    if ((e = p.next) == null) {
        // 新的值就挂在上一个的next指针上
        p.next = newNode(hash, key, value, null);
        // 大于等于7的时候,树化
        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
        break;
    }
    // 在找key相等的节点,如果找到了,就替换掉
    if (e.hash == hash &&
        ((k = e.key) == key || (key != null && key.equals(k))))
        break;
    p = e;
}

然后就这样很开心的继续put,直到遇见map.put("test13", "zhangsan13");

image-20210531232813649.png

此时,size > threshold(16 * 0.75 = 12),因此进入resize(),长度扩为16 * 2 = 32进入如下逻辑。

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)
                // 对于单个Node节点,直接放到原来的位置+n的位置,比如原来在0,则扩容后在16
                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;
                }
            }
        }
    }
}

如果e.hash在16对应的那个位置是1,那么就放到hiHead那条链表上,位置处于[j + oldCap]处。
image-20210601000516548.png


resize()方法

调整map的大小,实现map初始化或扩展为原有尺寸的二倍。
HashMap默认大小16,当存储了12个的时候,如果再put,会先将新的值存于原来的map中,然后发现++size > threshold(这里是12),就会进行调整resize。调整的时候,新的table会变成32,threshold变成24,并且需要将原有的值复制进新的table中。

扩容为原来大小的2倍代码

int hash(Object key)

key == null,hash为0;否则,(h = key.hashCode()) ^ (h >>> 16)即计算hashCode,与hashCode右移16位(除以65536得到的商)做异或(同0异1)运算。
计算单个字符的hash结果就是对应的ASCII码,例如hash('a')=97

V put(K key, V value)

put是允许key为null的
put操作

get

image.png

当你准备好了,机会来临的时候,你才能抓住
原文地址:https://www.cnblogs.com/studentytj/p/9657313.html