HashMap源码分析学习笔记

1,180 阅读7分钟

IT楠老师:space.bilibili.com/384087053/c…

必备知识

  • 效率有保障
  • 查找效率o(1)
  • 链表的查找效率o(n)
  • 红黑树的查找效率o(log(n))

移位运算

1 <<< 8 有符号左移

0000 0000 0000 0000 0000 0000 0000 0001

0000 0000 0000 0000 0000 0001 0000 0000

1 >> 8 无符号右移

异或运算

相同为0,不同为1。

HashMap构造方法

《阿里巴巴开发手册》推荐在HashMap初始化时指定集合初始值大小。

image-20210412103400265

源码

image-20210412200222896

初始化容量指的是数组的初始化容量。

HashMap的成员变量

image-20210412104754262

image-20210412111621376

构造方法分析

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

image-20210412105923293

当存放的节点数量大于这个阈值,数组就会扩容。

tableSizeFor方法

/**
 * Returns a power of two size for the given target capacity.
 */
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;
}

image-20210412200535663

返回大于等于cap的最小的2次幂。如果传入是3,返回4。传入4,返回4。传入30,返回32。

测试一下这个方法,传入不同的cap值:

public class HashMapStudy {
    public static void main(String[] args) {
        for (int i = 0; i < 50; i++) {
            System.out.println(i + " <====tableSizeFor===> " +tableSizeFor(i));
        }
    }

    public static 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 + 1;
    }
}

结果:

0 <====tableSizeFor===> 0
1 <====tableSizeFor===> 1
2 <====tableSizeFor===> 2
3 <====tableSizeFor===> 4
4 <====tableSizeFor===> 4
5 <====tableSizeFor===> 8
6 <====tableSizeFor===> 8
7 <====tableSizeFor===> 8
8 <====tableSizeFor===> 8
9 <====tableSizeFor===> 16
10 <====tableSizeFor===> 16
11 <====tableSizeFor===> 16
12 <====tableSizeFor===> 16
13 <====tableSizeFor===> 16
14 <====tableSizeFor===> 16
15 <====tableSizeFor===> 16
16 <====tableSizeFor===> 16
17 <====tableSizeFor===> 32
18 <====tableSizeFor===> 32
19 <====tableSizeFor===> 32
20 <====tableSizeFor===> 32
21 <====tableSizeFor===> 32
22 <====tableSizeFor===> 32
23 <====tableSizeFor===> 32
24 <====tableSizeFor===> 32
25 <====tableSizeFor===> 32
26 <====tableSizeFor===> 32
27 <====tableSizeFor===> 32
28 <====tableSizeFor===> 32
29 <====tableSizeFor===> 32
30 <====tableSizeFor===> 32
31 <====tableSizeFor===> 32
32 <====tableSizeFor===> 32
33 <====tableSizeFor===> 64
34 <====tableSizeFor===> 64
35 <====tableSizeFor===> 64
36 <====tableSizeFor===> 64
37 <====tableSizeFor===> 64
38 <====tableSizeFor===> 64
39 <====tableSizeFor===> 64
40 <====tableSizeFor===> 64
41 <====tableSizeFor===> 64
42 <====tableSizeFor===> 64
43 <====tableSizeFor===> 64
44 <====tableSizeFor===> 64
45 <====tableSizeFor===> 64
46 <====tableSizeFor===> 64
47 <====tableSizeFor===> 64
48 <====tableSizeFor===> 64
49 <====tableSizeFor===> 64

Process finished with exit code 0

可以发现结果有规律:

0
1
10
100
1000
10000
100000 
1000000    

返回比cap大的最小二次幂。

put方法

image-20210412201015931

key的hash值计算

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

image-20210412201239839

(key == null) ? 0说明key可以是null。当key不为空的时候,用key的hashCode值与key的hashCode右移16位的值进行异或运算,结果为key的hash值。

为什么要与h >>> 16异或,直接返回h = key.hashCode()可以吗?

int占32位。>>>是无符号右移。h >>> 16 用来取出h的高16位

image-20210412204936256

putVal方法中,需要计算数组中的下标[i = (n - 1) & hash]。其中n表示数据长度。而在HashMap的构造函数中,我们得知数组的长度都是经过计算之后得到的2次幂。

image-20210412212219459

当把数组长度减一之后,低位就全部变成了1。

image-20210412213003454

绝大多数情况下数组的length一般都小于2^16即小于65536。所以i = (n - 1) & hash结果始终是hash的低16位n-1进行与运算。

假设length=8。hash值随便假设一个。则(n - 1) & hash的结果为:

image-20210412214655146

对于数组长度为8时,发现与之后的结果就是hash值的后3位。取其他值进行验证,可以发现:

  • length=8时 下标运算结果取决于哈希值的低3位
  • length=16时 下标运算结果取决于哈希值的低4位
  • length=32时 下标运算结果取决于哈希值的低5位
  • length=2^N, 下标运算结果取决于哈希值的低N位

原因总结

由于和数组长度-1进行与运算,数组长度绝大多数情况小于2的16次方。所以始终是hashcode的低16位(甚至更低)参与运算。要是高16位也参与运算,会让得到的下标更加散列

这样高16位是用不到的,所以才有hash(Object key)方法。让Object的hashCode()自己的高16位进行异或^运算。(h >>> 16)得到高16位,然后与hashCode()进行^运算。

image-20210412222341443

为什么用^而不用&和|

&|都会使得结果偏向0或者1 ,并不是均匀的概念,所以用^。

为什么HashMap的容量是2的次幂

  • &运算速度快,至少比%取模运算块。
  • 能保证索引值肯定在 capacity中,不会超出数组长度。
  • (n - 1) & hash,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n。
  • 存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀。

image-20210413103128174

putVal方法

// 底层数组
transient Node<K,V>[] table;

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;    // 数组为空,初始化HashMap
    if ((p = tab[i = (n - 1) & hash]) == null)     // 计算插入位置:n表示数组长度, 
        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;   // 判断key一致,替换元素
        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);
                    // 达到树化条件,链表元素个数大于等于8,进行树化
                    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; // 更新p的指向,指向下一个链表节点
            }
        }
        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;
}

树化和扩容

HashMap的初始化

HashMap不会在构造方法中初始化化,而是在第一次put的时候。

image-20210413110209294

扩容时机

image-20210413110438012

resize()扩容方法

// 数组
transient Node<K,V>[] table;
// The next size value at which to resize (capacity * load factor)
// 构造方法如果指定了初始容量,就会调用tableSizeFor(initialCapacity)方法进行计算这个值
int threshold;

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    // 旧数组容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 旧数据阈值
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 旧数组长度大于0,即旧数组达到了扩容条件
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 新数组容量 = 旧数组容量 * 2。两倍扩容。
        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;
    // 初始化没有指定初始容量,使用默认的16
    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"})
    // 用扩容后的newCap创建新数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    // table指向新创建的数组
    table = newTab;
    // 旧数组中的元素存放到新数组中
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                //老桶数组j位置的节点只有1个元素,重新hash计算该节点位于新桶数组的位置
                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 
                    // 低位:loHead头链 loTail尾链
                    Node<K,V> loHead = null, loTail = null;
                    // 高位:hiHead头链 hiTail尾链
                    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、链表长度大于等于8

image-20210413114523017

2、容量 >= 64

image-20210413115046647

树化的过程

/**
 * Replaces all linked nodes in bin at index for given hash unless
 * table is too small, in which case resizes instead.
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

1、Node转化成TreeNode

2、调用treeify进行树化

get方法

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

image-20210413125208397

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // tab索引位置只有一个元素,比较返回结果
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode) // 红黑树的节点
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 链表查找元素
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

image-20210413125326716

JDK1.7和1.8中的改动

头插法(多线程会产生循环链表)、尾插法。

数据结构:加了红黑树。