HashMap---1.7源码阅读

JDK1.7

注意点:1、缺省值length为空,初始大小默认为16.每次扩容为原来的2倍,负载因子为0.75.扩容阈值为16*0.75=12,是超过12的时候进行扩容

    2、数组+链表。冲突时,头插法。先扩容,再插入(1.8先插入再扩容)

    3、key为null,且数组不为空,则放在table[0]的位置

    4、hashcode&(length-1)为数组下标。扩容后,元素要么在原位置,要么在原位置+原length的位置

    5、hashMap大小必是2的幂次方数。目的是尽量较少碰撞,也就是要尽量把数据分配均匀

 

1、数组   存放元素的是Entry<K,V>[] table数组,初始为空

1
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE

  无参构造方法,默认空,插入元素的时候初始化为16,负载因子为0.75

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

 

2、put方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public V put(K key, V value) {
        if (table == EMPTY_TABLE) {    //数组为空,则先初始化
            inflateTable(threshold);
        }
        if (key == null)  //key为null,则方法
            return putForNullKey(value);
        int hash = hash(key);    //计算key的hashcode值,先去key的hashcode,再经过4次位移运算和5次异或运算,得到hashcode        int i = indexFor(hash, table.length);  //与运算,得到放在table的数组下表
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {  //该数组位置无参数则直接放,有参则遍历链表,key的hashcode相等且equals相同则覆盖,
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
 
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }private V putForNullKey(V value) {<br>    for (Entry<K,V> e = table[0]; e != null; e = e.next) {    //已有key为null的值则覆盖<br>        if (e.key == null) {<br>            V oldValue = e.value;<br>            e.value = value;<br>            e.recordAccess(this);<br>            return oldValue;<br>        }<br>    }<br>    modCount++;<br>    addEntry(0, null, value, 0);  //无key为null,则放在0的位置  <br>    return null;<br>}final int hash(Object k) {    //计算key的hashcode值<br>    int h = hashSeed;<br>    if (0 != h && k instanceof String) {<br>        return sun.misc.Hashing.stringHash32((String) k);<br>    }<br><br>    h ^= k.hashCode();<br><br>    h ^= (h >>> 20) ^ (h >>> 12);<br>    return h ^ (h >>> 7) ^ (h >>> 4);<br>}static int indexFor(int h, int length) {    //利用hashcode和数组长度-1“与”运算得到数组位置<br>    return h & (length-1);<br>}<br><br>void addEntry(int hash, K key, V value, int bucketIndex) {//插入方法<br>    if ((size >= threshold) && (null != table[bucketIndex])) {<br>        resize(2 * table.length);<br>        hash = (null != key) ? hash(key) : 0;<br>        bucketIndex = indexFor(hash, table.length);<br>    }<br><br>    createEntry(hash, key, value, bucketIndex);<br>}

数组为空,则先初始化

key为null则判断,已有key为null则覆盖,无key为null则放在table数组的0位置

计算key的hashcode值,先去key的hashcode,再经过4次位移运算和5次异或运算,得到hashcode

hashcode和数组长度进行与运算h & (length-1),得到应放的数组下标

该数组位置无参数,则直接放

有参则遍历链表,key的hashcode相等且equals相同则覆盖,无相同则使用头插法,插到链表头部

插入前先判断,是否需要扩容,如果要,则先扩容,在放元素

 

注:先扩容,再插入。头插法。因为是头插法,多线程的情况下会导致死循环。头插法速度比尾插法快

 

3、get方法

1
2
3
4
5
6
7
public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
 
        return null == entry ? null : entry.getValue();
    }private V getForNullKey() {<br>    if (size == 0) {<br>        return null;<br>    }<br>    for (Entry<K,V> e = table[0]; e != null; e = e.next) {      //数组下表为0的元素<br>        if (e.key == null)<br>            return e.value;<br>    }<br>    return null;<br>}final Entry<K,V> getEntry(Object key) {<br>    if (size == 0) {<br>        return null;<br>    }<br><br>    int hash = (key == null) ? 0 : hash(key);<br>    for (Entry<K,V> e = table[indexFor(hash, table.length)];<br>         e != null;<br>         e = e.next) {<br>        Object k;<br>        if (e.hash == hash &&<br>            ((k = e.key) == key || (key != null && key.equals(k))))<br>            return e;<br>    }<br>    return null;<br>}

 先判断key是否为null,是则判断数组是否为空,空则返回null.非空则返回table[0]

  key不是null,先判断数组是否为空,是则返回null.

不是则先计算key的hash值,在找到通过hash&(length-1)数组下标找到对应链表

遍历链表,hashcode相同并且keyequals相同时,找到元素,返回value

 

4、扩容

1
2
3
4
5
6
7
8
9
10
11
12
13
void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
 
        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

每次扩容为原来的2倍

遍历数组和链表,重新计算元素的hashcode和位置,依次放入元素。

补充:扩容后的元素,要么在原位,要么在原位+原数组长度=新位置。1.7是每次都重新计算,1.8做了优化,直接计算高位是否为1,为1则原位+原数组长度=新位置,为0则在原位置

头插法,元素倒叙

 

5、hashMap的中table数组位置的计算方法

1
2
3
4
5
6
7
8
9
10
11
final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
 
        h ^= k.hashCode();
 
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }static int indexFor(int h, int length) {<br>    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";<br>    return h & (length-1);<br>}

问:为什么要进行^运算和>>运算呢?因为要让hashcode的高位数参数计算,减少hash碰撞

举例:length=16      length-1=15 : 0000 1111

h:    :1111  0011

length-1:0000 1111

               0000 0011   结果为3,放在数组【3】的位置

再为进行>>和^运算时,hashcode只有地位进行了运算,高位是什么不影响结果,因此要通过5次异或运算和4次位移运算使hashcode尽量分散,减少碰撞

 

6、hashMap的大小,始终为2的幂次方,无论带参设置多少,都取大于该值得最小2的幂次方

1
2
3
4
5
6
7
8
private void inflateTable(int toSize) {
      <strong> // Find a power of 2 >= toSize</strong>
       int capacity = roundUpToPowerOf2(toSize);
 
       threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
       table = new Entry[capacity];
       initHashSeedAsNeeded(capacity);
   }<br>

 问:为什么要保证为2的幂次方?

因为hashcode&(length-1)=数组位置,只有当length为2的幂次方时,length-1才能是低位为都为1的二进制,进行与运算才不会数组越界。因为都为1111才有与运算的意义,如果都为0,则无论如何都为0,会加剧hashcode的碰撞

16:0001 0000

15:0000 1111   

问:为什么扩容后的数组元素,要么在原位置,要么为原位置+原来length的位置?

length*2-1   实质上是二进制的高位从0变成1

15: 0000 1111

31:0001 1111    

进行与运算时,要么是1,要么是0。是1,则原位置+原来length的位置。是0则位置不变

 

 

7、多线程下,扩容变成死循环的问题   参考:https://www.cnblogs.com/devilwind/p/8044291.html

根本原因:头插法,会使原有元素倒叙

线程1:扩容后,遍历元素前停止

线程2:扩容后,遍历元素,顺利完成

 

  

posted on   潮流教父孙笑川  阅读(88)  评论(0编辑  收藏  举报

(评论功能已被禁用)
编辑推荐:
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· winform 绘制太阳,地球,月球 运作规律
· 上周热点回顾(3.3-3.9)

导航

< 2025年3月 >
23 24 25 26 27 28 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 1 2 3 4 5
点击右上角即可分享
微信分享提示