jdk 源码系列之HashMap

Posted by Sinsy on October 31, 2020 About 23 k words and Need 64 min

jdk 源码系列之 HashMap

JDK 源码系列

前言

了解 HashMap,理解扩容机制、数据结构、阈值以及初始化机制,对使用、优化等有所裨益。

继承关系

1
2
3
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
}

从这段代码,我们可知,HashMap 继承 AbstractMap 同时实现 Map 类。换句话说,Map 作为顶级父类,只定义方法,实现由子类实现,还继承 AbstractMap 那也是说,也可以调用 AbstractMap 里面的方法。

这里先摁住,还不清楚 HashMap 调了 AbstractMap 里面哪些方法。

HashMap

点进去先看看是数据结构是什么。

数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
 * Basic hash bin node, used for most entries.  (See below for
 * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
 */
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;
        }

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

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

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

向上继承 Map.Entry<K,V> 接口,并实现了 getKey getValue toString hashCode setValue equals 这六个方法。之后自己在实现一个链表 Node 的内部类,这个链表的节点都保存三个东西,hash key value 这三个变量。

接下来,我们从创建一个 HashMap 入手。

创建 HashMap

通常来讲,HashMap 有三种 new 的方式。

1
2
3
4
5
6
7
Map<String, String> first = new HashMap<>();

Map<String, String> second = new HashMap<>(12);

Map<String, String> third = new HashMap<>(first);

Map<String, String> fourth = new HashMap<>(12 , 1);

第一种是无参构造,第二种是传入一个 int 值,第三种是允许传入一个 Map 实现的任何子类,第四种是两个 int 类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
 */
// first 的构造
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and the default load factor (0.75).
 *
 * @param  initialCapacity the initial capacity.
 * @throws IllegalArgumentException if the initial capacity is negative.
 */
// second 的构造
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * Constructs a new <tt>HashMap</tt> with the same mappings as the
 * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
 * default load factor (0.75) and an initial capacity sufficient to
 * hold the mappings in the specified <tt>Map</tt>.
 *
 * @param   m the map whose mappings are to be placed in this map
 * @throws  NullPointerException if the specified map is null
 */
// third 的构造
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and load factor.
 *
 * @param  initialCapacity the initial capacity
 * @param  loadFactor      the load factor
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive
 */
// fourth 的构造
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);
}

这里提到三个变量,分别 initialCapacityloadFactorthreshold

看中文意思,分别是初始化容量、负载因子、阈值。其中负载因子,也就是 loadFactor 默认是 0.75f

1
2
3
4
5
/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

third 这个构造函数里面,有个 putMapEntries 的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    /**
     * Implements Map.putAll and Map constructor.
     * 构造方法的时候就全部添加进去 hashMap
     * @param m the map 传入的Map
     * @param evict false when initially constructing this map, else
     * true (relayed to method afterNodeInsertion). 构造使用到就是 false 非构造就是 true
     */
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        // Map 的大小
        int s = m.size();
        if (s > 0) {
            // 由于是构造调用的,table = null
            if (table == null) { // pre-size
                // 在调用次方法的时候,先赋值了 loadFactor = 0.75。所以ft = (Map 长度 / 0.75) + 1.0
                float ft = ((float)s / loadFactor) + 1.0F;
                // 判断是否超出 MAXIMUM_CAPACITY = 1 << 30 也就是 2 的30次幂大小
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                // 起始 threshold = 0,这个也在 fourth 的构造里面使用到了,下面有解释
                if (t > threshold)
                    // 如果 t 的大小在 2 的(n-1)次幂 与 2 的 n 次幂之间,则 t 的大小为 2 的n次幂。不懂没关系,下面有解释、举例
                    threshold = tableSizeFor(t);
            }
            // Map的长度 是否 大于 刚刚 tableSizeFor 之后的长度
            // 首次是不会大于这个值的
            else if (s > threshold)
                // 扩容,后面对这个方法讲解
                resize();
            
            // 将 Map 的子类对象 放进HashMap
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                // 先通过位运算减少 key 的冲突,在放进去
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

putMapEntries 这个方法就是 HashMap 在构造的时候,放进 Map 的对象,那么就会将他是所有 k v 都放进去,然后初始化长度,这个长度是当前 Map 长度靠近 2 n次幂 长度。比如

  • Map 7,则HashMap 初始化8,
  • 如果是12 则是 16,
  • 如果是16 则是 16.

之后判断是否需要扩容,由于是首次,所有不需要,接着就是放进 HashMap。先通过位运算计算 Hash 减少冲突,在pull。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    /**
     * Computes key.hashCode() and spreads (XORs) higher bits of hash
     * to lower.  Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     *
     * 通过位运算减少 Hash 冲突
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

fourth 这个构造函数里面,还有一个这 tableSizeFor 的方法,上面有用到。

1
2
3
4
5
6
7
8
9
10
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;
}

进行了位运算,由于前面好保证了 cap 非负数。

  • »> 右移,高位补零。比如 0001,使用 »> 1 之后就是 00001,对其长度,则1去掉。
  • ** ** 或操作,不同取1,相同1取1,相同0取0

tableSizeFor 方法里面的意思就是,如果 cap 介于 2 (n - 1)次幂2 n次幂之间,cap 的值为 2 n次幂 - 1 大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int n = 18;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
System.err.println(n); // 31

int n = 16;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
System.err.println(n);  // 31

int n = 14;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
System.err.println(n); // 15

最后只要算的 n 非负数,且不大于 int 类型最大值,则 n + 1。也就是说这个初始容量,无论设置什么数字,都会是 2 的倍数。

HashMap 添加

我们常常使用 kv 的形式存储。

1
    map.put("sin", "sy");

点进去看看,HashMap 如何存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    // 首先来到父类定义的
    V put(K key, V value);


    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     * hashMap 的添加
     */
    // 然后实现父类定义的方法
    public V put(K key, V value) {
        // hash 方法上面有提到作用
        return putVal(hash(key), key, value, false, true);
    }

pull value 的时候,传入先经过位运算的 key 的 hashCode,然后传 key value、false,true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value 这个值在pull 的时候是false
     * @param evict if false, the table is in creation mode. 这个值在pull 的时候是 true
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // 初始化两个容器 一个 k v 数组链表、一个是链表
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 第一次这里是null,所以 n = (tab = resize()).length;
        if ((tab = table) == null || (n = tab.length) == 0)
            // resize 下面有讲解
            // 是否扩容处理,没有扩容,则是当前长度,有则是之前的两倍
            n = (tab = resize()).length;
        // 因为数组从0开始计算,所以是 n - 1 取模,确定值放置在数值链表位置
        // 如果当前数组链表为null,同时 p = 链表table
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 创建一个链表
            // 没有key值,就给你创建一个
            tab[i] = newNode(hash, key, value, null);
        // 有值的情况下
        else {
            // 数组链表头有数据
            // 初始化 链表 泛型k
            Node<K,V> e; K k;
            // hash 值一致且key值的地址也一样 或者 key不为null 且key的值相同
            // 大概就是相同key,替换value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 如果 p 是 树结构
            else if (p instanceof TreeNode)
                // 按树的形式 pul 进去
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 值不相同,也不是树结构,且链表头还相同,同一节点底下还有链表
            else {
                // 向下遍历
                for (int binCount = 0; ; ++binCount) {
                    // 链表下一节点为null
                    if ((e = p.next) == null) {
                        // p的下一节点指向创建一个新的链表
                        p.next = newNode(hash, key, value, null);
                        // 当链表的长度大于等于7,因为从1开始,也是说链表头有值,所以才从一开始。另外这是 ++1 性能比 i++好,i++比起++i多了一个创建临时变量(放入数据栈)的步骤,因此效率低一些
                        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;
                }
            }
            // 当链表不为null,建立映射,kv
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                // onlyIfAbsent 如果是 true 不改变现有的值,false 改变,或者 oldValue == null
                if (!onlyIfAbsent || oldValue == null)
                    // 完成关联
                    e.value = value;
                // 尾插,链表直接插到当前链表的最后一个位置上
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 快速失败机制,比较当前操作次数与记录次数,是否不一样
        ++modCount;
        // 因为完成一个 pulValue 所以增加一次次数,同时再次比较是否来到阈值
        if (++size > threshold)
            // 来到阈值,2倍扩容容器
            resize();
        // true 如果是头结点则删除
        afterNodeInsertion(evict);
        return null;
    }


    /**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     * 具有初始化容器大小、2倍扩容容器等功能
     * @return the table
     */
    final Node<K,V>[] resize() {
        // 首次 table 是null
        Node<K,V>[] oldTab = table;
        // 判断 oldCap 是否为 null 为null 则为0,反之则是容器长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 阈值 首次是0
        int oldThr = threshold;
        // 新容器容量0 新容器阈值0
        int newCap, newThr = 0;
        // 容量大于0
        if (oldCap > 0) {
            // 大于等于最大能容纳的长度
            if (oldCap >= MAXIMUM_CAPACITY) {
                // 阈值等于最大能容纳的长度
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 容器长度是否大于默认16,且新容器是旧容器的两倍大小且不大于最大容纳长度,
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 新阈值也是旧阈值的两倍大小
                newThr = oldThr << 1; // double threshold
        }
        // 旧阈值 大于 0
        else if (oldThr > 0) // initial capacity was placed in threshold
            // 新容器长度 = 旧阈值
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            // 首次 pull的时候
            // 新容器长度 = 16
            newCap = DEFAULT_INITIAL_CAPACITY;
            // 新阈值等于 负载因子 0.75 * 默认长度 16 = 12
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 这个判断只有在第一次使用构造方法传入一个 Map 才会用到,且这个Map 有值,其他是都基本走不到这里
        if (newThr == 0) {
            // 容器长度 * 负载因子 0.75
            float ft = (float)newCap * loadFactor;
            // 只要不大于最大容纳的长度,则 newThr = ft
            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;
        // 旧容器不为null
        // 这是是首次 oldTab 为 null 不走这里,比如第一次 pull 构造方法放入map的子类
        // 之后 pull 这里都会走一遍
        if (oldTab != null) {
            // 链表循环指向下一个节点
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                // 先赋值,当前数组链表的链表给 e 链表且不为null
                if ((e = oldTab[j]) != null) {
                    // 设置数组链表为null
                    oldTab[j] = null;
                    // 链表的下一节点为null
                    if (e.next == null)
                        // 只有数组链表头有数据,子节点没有数据
                        // hash 取模,这个模的长度是数组的长度,e 接到新的容器底下
                        newTab[e.hash & (newCap - 1)] = e;
                    // 判断 e 是否是树结构
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    // e 不是树结构,且数组链表的子节点也有数据
                    // 这里应该是计算不同hash的节点,到不同链表上
                    // 然后迁移到新的容器里,对应的数组链表的位置
                    else { // preserve order
                        // 
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        // 下一节点
                        Node<K,V> next;
                        do {
                            // e 指向下一节点
                            next = e.next;
                            // e 的hash 取模 oldCap,分散。
                            if ((e.hash & oldCap) == 0) {
                                // 第一次为null
                                if (loTail == null)
                                    // loHead 是第一值
                                    loHead = e;
                                else
                                    // loTail的下一节点 指向 e的下一节点
                                    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;
                        }
                        // 2倍扩容之后的数组长度,换句话说,之前的数据,如果 hash 取模之后不等于,之前最后面的位置,则一律变成之前的位置 加上 旧数组长度的。
                        // 如果之前是 2 数组链表长度是 8,之后这个位置来到 10
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

总结下,在 pulValue 的时候做了什么。

  1. 先初始化数据结构,数组链表
  2. 如果没有设置初始化大小,则默认长度为16,同时设置阈值为 0.75 * 当前长度
  3. 如果长度来到阈值大小,则进行当前容量二倍扩容,数据大部分会移到新的长度链表里面,少部分会遗留在旧容器长度里面
  4. 查看插入 key 是否存在,有则尾插入,插入之后是否来到 8 ,链表长度来到8则开始转成红黑树。没有,直接插入
  5. 然后 key 的位置里,在放入 value。完成关联
  6. 记录操作次数
  7. 插入后,在继续判断是否扩容

上面,少了链表如何转红黑树。接下来,我们继续看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
    /**
     * 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;
        // 链表长度是否小于 64,换句话说,树长最长只有64
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            // 当链表长度,来到 8 的时候也会判断是否扩容
            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);
        }
    }


    /**
     * Forms tree of the nodes linked from this node.
     */
    final void treeify(Node<K,V>[] tab) {
        TreeNode<K,V> root = null;
        for (TreeNode<K,V> x = this, next; x != null; x = next) {
            next = (TreeNode<K,V>)x.next;
            x.left = x.right = null;
            // 起始 red = false,当 = false 时候就是 黑色,同时根节放入值,只有首次才会走到
            if (root == null) {
                x.parent = null;
                x.red = false;
                root = x;
            }
            else {
                K k = x.key;
                int h = x.hash;
                Class<?> kc = null;
                // 遍历链表
                for (TreeNode<K,V> p = root;;) {
                    int dir, ph;
                    K pk = p.key;
                    if ((ph = p.hash) > h)
                        dir = -1;
                    else if (ph < h)
                        dir = 1;
                    else if ((kc == null &&
                                (kc = comparableClassFor(k)) == null) ||
                                (dir = compareComparables(kc, k, pk)) == 0)
                        dir = tieBreakOrder(k, pk);

                    TreeNode<K,V> xp = p;
                    if ((p = (dir <= 0) ? p.left : p.right) == null) {
                        x.parent = xp;
                        if (dir <= 0)
                            xp.left = x;
                        else
                            xp.right = x;
                        // 这里通过左旋 或者右旋,使得 黑的节点变成父节点,自旋完成后,父节点为黑色,而子节点都是红色
                        root = balanceInsertion(root, x);
                        break;
                    }
                }
            }
        }
        moveRootToFront(tab, root);
    }

转红黑树的过程中,首先会进行一次判断扩容处理,之后先变成一个个的树节点,在变成红黑树。

其中 red = false 时,为黑,反之则是红。通过左右旋,使其父节点变成黑色,子节点为红色,完成一次平衡处理。

图如下: red-black-tree

首次,root节点就是只有一个,所以直接是黑。当当前树高相差为3,会完成一次自旋,使其平衡,如果是不平衡的节点,则会变成红色,平衡的节点则为黑色。

HashMap 的数据结构图解

从上面,我们基本了解了,hashMap是怎么扩容的,如何链表转红黑树、如何定位位置。接下来我们看看HashMap的数据结构是如何。

起始的结构是由数组链表里面且是一个kv结构。头部是一个数组,k的位置是一个 hash 取模(位扰动之后的hash)之后,确定的位置。 node-listt.png

后来当,hash冲突变多的时候,数组底下的链表就是存储 hash 冲突之后存储的位置,这个长度大于8的时候转红黑树,包括头,也就是数组,底下的链表长度是7。

node-list-translat-red-black-tree

优化以及使用 HashMap 的建议

既然读源码,就是为了更好的了解这个东西,只为读而读,为面试而读,这个源码读的没什么意思。学完得要有自己体会吧~

  1. 当使用的 HashMap 是固定不变或者未达到 2 的n次方长度,比如只使用到3,而当前 HashMap 长度为4,可以直接设置负载因子,固定其长度,避免过早的扩容,而浪费空间。
  2. HashMap 默认初始化大小为16,如果添加的值,不到16的话,甚至只有它的一半,这样很容易造成空间上的浪费,之后过多的空间浪费,从而触发 GC 回收,降低整个系统的响应时间。所以尽可能的初始化大小,避免空间的浪费
  3. 当使用的 HashMap 长度超过16,都是没有初始化容器大小,来到阈值大小,频繁的触发扩容机制,导致 HashMap 性能下降。所以也尽可能的初始化大小,避免频繁触发扩容机制。
  4. HashMap 线程不安全,这里主要是这两点(JDK8)
    1. 通过源码我们知道,在添加元素的时候,在添加前以及添加后都会判断是否扩容,而改变 kv 的分布。在多线程中,如果 A 线程先添加完值,改变了整个容器值的分布,而 B 线程拿到的是原先容器大小来判断位置,从而导致插入的值不在正确的位置上。
    2. HashMap 里面有记录操作次数,在多线程中,当前的操作次数,与记录操作次数,可能会不正确,(可能拿到旧值)而出发快速失败机制。
  5. 只要是继承了 Map 接口的子类,都可以转成 HashMap,在创建对象的时候。

声明

作者: Sinsy
本文链接:https://blog.sincehub.cn/2020/10/31/jdk-hashmap/
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文声明。
如您有任何商业合作或者授权方面的协商,请给我留言:550569627@qq.com

引用