java集合类(四)-HashMap的原码分析

目录

前言

原码分析

 继承关系

 类中属性

 构造函数

 核心方法

总结

附录1 HashMap与HashSet关系?

附录1  HashMap初始化如何保证容量是2的幂?

附录3 HashMap 如何计算节点所在数组(桶)的位置

附录4 HashMap 扩容时元素位置如何迁移?


前言

     基于哈希表的 Map 接口的非同步实现,此实现提供所有可选的映射操作,并允许使用 null 值和 null 键,不保证映射的顺序。另外,HashMap的数据结构是数组+链表/红黑树。

原码分析

接下来我们针对其原码做以下分析:继承关系类中属性构造函数核心方法 四个方面分析

 public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
  • Map: 接口用于存储元素对(称作“键”和“值”),其中每个键映射到一个值
  • AbstractMap:实现了Map接口部分方法,为map各自子类实现公共的方法
  • Cloneable:支持接口,实现Cloneable接口,支持Object.clone()方法(CloneNotSupportedException)
  • java.io.Serializable:标记接口,支持序列化

 继承关系

类中属性

      

  • DEFAULT_INITIAL_CAPACITY:缺省的初始化容量16
  • DEFAULT_LOAD_FACTOR:缺省的负载因子常量0.75
  • MAXIMUM_CAPACITY:最大容量(int型只能左移动30位,31位就变为负数的了)
  • TREEIFY_THRESHOLD:链表节点数大于8个链表转红黑树
  • UNTREEIFY_THRESHOLD:链表节点小于6个红黑树转链表
  • MIN_TREEIFY_CAPACITY:当数据大于等于64的时候才触发链表转红黑树
  • loadFactor:负载因子
  • threshold:临界值 = 容量 * 负载因子
  • size:映射中存在的键值对的数量
  • entrySet:具体元素存放集
  • table:以Node<k,v>数组存储元素,长度为2的次幂
  • modCount:多线程并发修改触发fail-fast机制

构造函数

           

核心方法

接下来我们从初始化、新增、修改、查询、删除分析下原码

初始化 Map<k, v> hashMap = new HashMap<>();

  •      构建初始容量为16,负载因子为0.75的HashMap

新增 hashMap.put(key,value)

1)根据key计算hash值

2)判断当前hashMap集合的数组tab[]是否为空;如果为空则执行resize()扩容。

3)根据hash值计算数组的位置i,求得数组tab[i]的值;如果tab[i]=null新建节点并添加到tab[i]并跳转到4),否则跳转到a)

             a) tab[i]的首个元素是否和key相等跳转到d),否则继续执行

             b) tab[i]是否为红黑树,如果是执行红黑树的元素的插入操作并跳转到d),否则继续

             c) 遍历tab[i]直到找到相同的key,或者遍历节点的nextnull,在此节点尾插入新增节点

             d) 判断节点是否新增节点还是修改节点,如果是修改节点则替换原值并跳转到7),否则跳转到4)

4) 修改计数器modCount+1

5) 修改集合元素大小size+1

6) 判断size>threshold(扩容阀值),满足条件扩容,否则结束

7) 结束流程

扩容步骤:

  • 根据原容量的值是否>0计算新容量和扩容阀值并跳转到4),不大于0则继续
  • 根据原阀值是否>0计算新容量和扩容阀值并跳转到4),不大于0则继续
  • 初始化默认的容量(16)和阀值(16*0.75)
  • 判断新阀值是否为0,为0则重新计算新阀值
  • 创建新数组(新容量大小)
  • 迁移老数组数据至新数组

public V put(K key, V value)方法:

public V put(K key, V value) {
    //hash(key)根据key计算hash值;在下文中用hash值计算key分布在数组的位置
    return putVal(hash(key), key, value, false, true);
}


final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // tab-hashMap的数组; p-存放(k,v)的节点;n-数组长度;i-数组索引
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // tab为空说明数组还未初始化,HashMap未保存过数据,故需要扩容resize()
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //根据hash值计算key保存在数组的位置i;tab[i]=null,此处无其它元素,直接插入
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        //数组位置已经存在其它元素,接下来从链表、红黑树等添加元素
        Node<K,V> e; K k; //e-需要添加的元素;k-需要添加元素k
        //链表的首个元素与需要添加key相等;把当前元素p赋值给e,后面替换其键值对value
        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 {
            //链表的首个元素非需要添加的key,循环遍历单向链表
            for (int binCount = 0; ; ++binCount) {
                //遍历整个链表均无相同的key,新增元素节点并添加到链表末尾并退出
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //如果链表长度>6,考虑链表转换为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //循环遍历链表过程中发现存在的key,则退出等待后面的键值对value替换值
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                //循环遍历链表时,当前节点后移
                p = e;
            }
        }
        //e不为空说明待添加的key存在,需要替换value;并退出整个方法,不修改modCount,size
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // onlyIfAbsent为ture代表元素缺少或者value为null才添加
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //修改计数器
    ++modCount;
    //size+1,并判断是否需要扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

扩容方法:resize()

final Node<K,V>[] resize() {
    // oldTab = 扩容前hashMap的数组结构
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    // newCap 需要扩容的容量; newThr扩容阀值(临界值)
    int newCap, newThr = 0;
    //原容量>0说明原hashMap已经存在元素,一般扩容为其2倍
    if (oldCap > 0) {
        //如果原容量已经是最大值,仅调整临界值为Integer的最大值
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 扩容后容量为其原容量的2倍(newCap = oldCap << 1)
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //扩容阀值扩容为其原阀值2倍
            newThr = oldThr << 1; // double threshold
    }
    //原容量<=0但原阀值>0,新容量=原阀值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
     //原容量、原阀值均<=0,HashMap为初始化状态,容量和阀值扩容为缺省值
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //新阀值为0,从新计算新阀值(新容量*负载因子)
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //扩容阀值=新计算的扩容阀值
    threshold = newThr;
    //根据新容量创建HashMap创建扩容后需要的数组
    @SuppressWarnings({"rawtypes","unchecked"})
     Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        //遍历原数组赋值;由于是扩容为2倍且为2的幂;扩容后的位置为其原位置或者原位置+原容量
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                //原数组的额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;
                        //扩容后位置不变(思考下2的幂bit位只有一个1)
                        if ((e.hash & oldCap) == 0) { //结果只有0和oldCap
                            if (loTail == null)
                                //数组当前位置首次添加loHead=loTail=e
                                loHead = e;
                            else
                                //数组位置非首次添加,后来元素添加到前一个元素末尾成单向链表
                                loTail.next = e;
                            //loTail始终定位到正在遍历的元素,为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;
                        //当前位置+oldCap添加扩容后新组装的链表
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
  • 关于红黑树的操作,后期单独分析
  • 扩容后元素位置为原位置或者原位置+原容量下文详细分析

修改 hashMap.replace(key,value)

      1)调用hash()方法计算key的hash值

      2)调用getNode()获得需要替换的节点(参数为上一步的计算的hash值和key)

                 a)根据hash值计算查询节点所在数组位置;并获得当前位置的的首个元素first

          b) first的key与需要查询的key相等return first,否则继续

          c) first数据类型为红黑树,查询红黑树的节点返回,否则继续

          d) 遍历first查找相同的key返回,或者返回null

      3)节点不为null,替换当前节点的value并返回原value;节点为null退出return;

查询 hashMap.get(key)

  • 调用hash()方法计算key的hash值
  • 调用getNode()获得查找节点(参数为上一步的计算的hash值和key)
  • 返回key对应的value


删除 hashMap.remove(key)

  • 根据hash()计算key的hash值,用于后面定位key的位置

               A) 同getNode()方法类似根据key和其hash值定位删除的节点,如有则继续

               B) 删除对应的节点node

               C) 删除后改变对应的数组或者链表或者红黑树的连接

               D) 修改计数器modCount+1;集合的容量size-1;

              E) 返回需要删除的节点

  • 调用removeNode()方法删除节点

remove()方法

removeNode()方法

总结

  • HashMap实现了Map接口对键值对进行映射, 并允许使用null值和null,但中不允许重复的键
  • HashMap数据结构为数组+链表/红黑树(链表长度>8且数组长度>=64转换为红黑树)
  • HashMap 的实例有两个参数影响其性能:初始容量 和加载因
  • HashMap采用了数组和链表的数据结构,能在查询和修改方便继承了数组的线性查找和链表的寻址修改

附录1 HashMap与HashSet关系?

 HashSet底层实现是HashMap<k,v>;   v是固定对象PERSENT

附录2  HashMap初始化如何保证容量是2的幂?

初始化时调用方法tableSizeFor()保证容量为2的幂:

n |= n >>> 1 等价于 n = n | n>>>1 ; 接下来主要分析下n|n>>>1运算结果

  • 首先二进制bit位仅且仅0、1;
  • 桶容量如果为2的幂,二进制仅有一个1(例:01=1,10=2,100=4,1000=8,….)
  • n>>>1相当于n的值无符号右移动1位,高位补0

我得到们接下来实际演示一下具体的例子就能了解其原理:(见下图)

  • 目的确保左侧第一个1后面的位均为1
  • 操作完成最后获得的n再+1(n+1)后,原数的最左侧1进一位,且后面bit位均为0

下图的n为001*….经过运算后001后面的所有位均为1(0011 1111 1111 1111 1111 1111 1111 1111);

执行n+1后的结果为:0100 0000 0000 0000 0000 0000 0000 0000

附录3 HashMap 如何计算节点所在数组(桶)的位置

计算key所在数组的下标经过2步骤:

  • Step1:hash()方法计算key的hash值
  • Step2:根据hash值和数组长度计算key所在数组位置(hash值以数组长度为模)

hash()方法通过key对应hashCode的1616异或操作得到key的hash值

hash & (数组长度-1) 获得key对应的数组位置即为取模操作;理解稍费劲,接下来我们看下具体的例子:

例如:数组的长度(桶容量)为8 (二进制:1000) ;

  • 8-1=7的二进制位0111
  • hash & 0111只看后3位(二元位操作符&代表2个运算的数都是1则为1否则为0)
  • hash & 0111相当于与8取模(结果在[0-7]之间)

十进制

hash值(二进制)

桶容量(8) - 1

hash&(cap-1)

数组位置

1

0001

0111

0001

1

2

0010

0111

0010

2

8

1000

0111

0000

0

9

1001

0111

0001

1

10

1010

0111

0010

2

16

1 0000

0111

0000

0

17

1 0001

0111

0001

1

18

1 0010

0111

0010

2

32

10 0000

0111

0000

0

33

10 0001

0111

0001

1

34

10 0010

0111

0010

2

附录4 HashMap 扩容时元素位置如何迁移?

       hashMap扩容后原节点扩容后位置在原位置 or 原位置+原容量,当hash & 原容量=0则在原位置,否则在原位置+原容量;引起这个的原因主要是由于①容量为2的幂 ②扩容后容量为原容量的2倍(计算公式为:hash & 原容量=0或者原容量)

       首先我们先看下hash & 原容量的值为0或者原容量,为什么?因此原容量是2的幂且二进制表示的时候仅有一位bit位为1,与其它数按位&操作,要么全为0,要么跟原容量相等。

       解决了上一个问题,那为什么节点扩容后位置为原位置或者原位置+原容量呢?其实这个原因主要还是我们HashMap扩容量为其原来的2倍(二进制位表示的话为原容量左移1位),接下来我们分析下一组数据即可找到原因,例如原容量为8现在扩容为16(你会发现数组位置16个数一个循环[0-15])

        Hash

                           桶容量为8

                               桶容量为16

 

十进制

二进制

cap - 1

hash&(cap-1)

数组位置

cap- 1

hash&(cap-1)

数组位置

hash&原cap (8)

1

0001

0111

0001

1

0 1111

0001

1

0

2

0010

0111

0010

2

0 1111

0010

2

0

8

1000

0111

0000

0

0 1111

1000

8

1

9

1001

0111

0001

1

0 1111

1001

9

1

10

1010

0111

0010

2

0 1111

1010

10

1

16

1 0000

0111

0000

0

0 1111

0000

0

0

17

1 0001

0111

0001

1

0 1111

0001

1

0

18

1 0010

0111

0010

2

0 1111

0010

2

0

22

1 0110

0111

0110

6

0 1111

0110

6

0

24

1 1000

0111

0000

0

0 1111

1000

8

1

32

10 0000

0111

0000

0

0 1111

0000

0

0

33

10 0001

0111

0001

1

0 1111

0001

1

0

34

10 0010

0111

0010

2

0 1111

0010

2

0

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Java中的Map接口提供了一种将键映射到值的对象。其中一种实现是HashMap,它使用哈希表来实现。HashMap允许 null 键和 null 值,并且没有顺序保证。HashMap 的操作复杂度为 O(1) 平均复杂度,因此在许多情况下非常高效。 ### 回答2: Java集合类中的Map用来保存键-值对,HashMap是其中的一种实现方式。HashMap的内部实现是基于哈希表的,它可以将任意对象作为键,并且保证键的唯一性。在HashMap中,键和值都允许为null,但是同一个HashMap中,键不能重复,如果重复了,新的value会覆盖旧的value。 HashMap内部是通过hashCode()和equals()方法来判断键是否相等的。当我们向HashMap中添加一个键-值对时,系统会先计算出键的hashCode值,然后用该值来确定存放该键值对的位置。如果有多个键的hashCode值相同,称为哈希冲突,那么HashMap会在这些键值对所在的位置上,以链表的形式组成一个链表存储。 HashMap的优点在于插入、删除和查找都比较快,时间复杂度均为O(1),对于大量的数据,它的效率优于List或Set等集合。但是,在内存使用上,HashMap会比List或Set等集合耗费更多的内存空间,因为它需要额外的空间来存储哈希值和链表指针。 值得注意的是,在多线程环境下,HashMap是不安全的,会出现并发修改导致的数据不一致问题,这时可以使用ConcurrentHashMap或者加锁机制来保证线程安全。 总之,HashMapJava中非常实用的集合类,适用于大量键值对的存储和检索。我们应该了解HashMap的内部实现原理,并且在使用过程中需要注意其线程安全性等问题。 ### 回答3: Java中的Map是一种键值对的集合,其中每个元素都由一个键和一个值组成。在Map中,每个键必须是唯一的,而值可以重复。 在Map中,HashMap是最常用的实现类之一。它是基于哈希表实现的,可以通过键快速查找值。哈希表是一种支持常数时间快速查找的数据结构,因为它可以将键映射到与其对应的值的位置,因此查找时只需要计算键的哈希码即可找到对应的值。 HashMap的实现类中使用了两个数组来存储键值对:一个数组用于存储键,另一个数组用于存储值。当插入键值对时,首先会将键的哈希码计算出来,然后通过哈希码将键映射到键数组的位置,将值存储在值数组的相同位置。当需要查找一个键时,只需计算其哈希码并定位到键数组的位置,然后从值数组中取出对应的值即可。 与集合一样,HashMap也是线程不安全的,因此在多线程环境下需要使用ConcurrentHashMap或通过synchronized关键字来保证线程安全性。此外,在使用HashMap时应该尽量避免使用null作为键,因为null的哈希码为0,可能与其他非null键的哈希码相同,导致哈希碰撞,影响HashMap的性能。 总之,HashMap是一种高效的键值对集合,通过哈希表实现快速的查找和插入操作。在正确使用和注意安全性的前提下,使用HashMap可以大大提升代码效率和性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值