HashMap(jdk1.8) put原理分析

1、什么是HashMap

        HashMap是基于hash表的一个Map接口实现,数组+链表的存储方式

        数组的特点:查询快,新增和删除慢

        链表的特点:增删快,查询慢

        数组+链表将两者的特点结合使用

        如下图:

       

   其中每个tab[x] 都是一个node

  Node:是一个内部类

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    ...
}

字段的理解:hash(根据key获取的hash值);key(put数据的key); value(put数据的value)

 next(链表中的下一个node)

  

2、HashMap做什么用?

        HashMap是用于存储key-value键值对的集合,用于存储和获取对象数据

3、HashMap 怎么用

        常用方法:put和get,remove

 1、创建HashMap对象:Map<String,Object> map = new HashMap<>();

创建对象,需要注意几个参数:

capacity(参考容量);threshold(扩容阈值);loadFactor(加载因子) 

capacity(参考容量)

用来作为Map中Node[]数组创建的默认长度,默认长度16,可以在创建时自己定义capacity值;Map<String,Object> map = new HashMap<>(capacity);

但是hashMap内部有一个机制:创建map对象中的Node[]数组的初始长度必须要是2的n次方,当你设置长度是23的时候,HashMap会把初始化长度设置为32,因为23在16(2的4次方)到32(2的5次方)之间,取最大数32

 threshold(扩容阈值)和 loadFactor(加载因子)

hashMap在新增数据的过程中达到扩容阈值,Node[]数组长度进行扩容

默认阈值:参考容量*加载因子

加载因子默认0.75f,可以修改不建议修改

2、首次put数据:map.put("testOne","testOne值”)

(1)获取“testOne”的hash值

int hash = hash("testOne");

 (2)判断数组是否为空

 判断Node[]数组是否为空,如果为空,创建初始化长度的数组空间

 (3)对数组长度做减1和hash值做与运算得数组下标     

直接算出来的hash值可能非常大,不可能直接当作数组下标的,对此hashMap的设计者有自己的解决方案:求余

也就是:index = hash值%数组长度

这样index的值永远都在数组长度之内,也就可以作为数组下标了,但是这样做有一个缺点:效率低,于是hashMap的设计者把求余改成了一个效率更高的运算:数组长度减一和hash值与运算

也就是:index = hash值&(数组长度-1)

为什么这样得出来的index也在数组长度之内呢?可以看下例子(由于是位运算,需要把hash值和数组长度分解成二进制,这样看的更清楚,假设它两二进制只有八位):

数组长度:    0001 0000

数组长度-1: 0000 1111

hash值:       1101 0101

与操作:        0000 0101

可以看到,数组长度-1后,前四位变成了0,跟hash值作与操作之后,hash值前四位不管是什么,都会变成0,而后四位由于对方都是1,因此得以保留下来。这样得到最后的结果永远都不会超过数组长度。

 这里必须满足一个前提条件:数组的长度必须是2的n次方,这样长度减一,最前面的二进制数组会从1变为 0,与运算之后长度一定在数组长度之内

(4)把值赋给对应的node

数组下标拿到了,要插入的位置也就基本确定了。在插入之前,hashMap会去判断当前数组位置上没有元素,由于我们这是第一次插入,因此这里就是直接插入元素

这里插入的方式很简单,就是把node的四大参数(hash值、key、value、next)赋给当前数组位置上的node。由于是位置上第一个元素,后继没有链表元素,next的值就是null。

(5)插入数组之后操作

插入之后,hashmap的全局变量:size,也就是已有元素数量,加一,然后看下有没有大于扩容阈值,如果大的话就要扩容。

(6)效果如图(这里假设“testOne”获取的下标是2):

       

3、 put不同key的情况:map.put("testTwo","testTwo不同的key")

        流程如上2,效果图如下:

4、put相同key的情况:map.put("testOne","testOne另外一个值”)

得到下标数据只之后,发现位置上已经有了元素数据这种情况叫做:hash碰撞

这就要判断此位置上的元素是不是同一个key,是的话就替换,不是的话就追加到后续链表-这就是链表的作用

(1)hash值是否相等

        首先看一下算出的hash值是否相等,这里算出的是相等的

(2)key是否相等

   hash相等,两个key不一定相等,因为我们算hash值需要调用hashCode()方法,可以hashCode()方法我们是可以重写,这样就有可能出现相等的hash值

这里就需要对两个key进行=和equals比较, 此比较结果是相等的,覆盖原值

为什么是先判断hash值是否相等,后判断的key:性能

hash不相等的,一定不是同一个key;两个key相等,hash值一定相等

效果图如下:

5、首次put不同key值但index下标相等:map.put("testThree","testThree的值”)

假设"testOne"和"testThree"获取到的index下标是相同的

在此前提下,会出现hash碰撞,逻辑如4,不同之处在于在判断两个key时,两个key的hash值是不相等的,两个key自然也不是同一个key

因为是首次,所以"testOne"链表的.next是null,直接插入到“testOne”的后面成为.next;"testThree"的.next赋值为null

效果图如下:

6、 继续put不同key但index下标相等:map.put("testFour","testFour的值”)

 假设“testOne”和“testFour”算出来的index下标相同

流程和5相同,不同的是遍历链表,判断hash和key,如果相等直接赋值

效果如下图:

 7、put数据时链表长度大于等于8的情况(扩容或转为红黑树):

        map.put("testEight","testEight的值”)

假设"testEight"和"testOne"算出的index下标是一致的

假设“testOne”的链表长度达到了7

注:随着put的数据越来越多,hash碰撞的频率也越来越高,会造成链表长度越来越长,这样每次put数据遍历的时间也越来越长,使hashmap的性能越来越差,怎么提高性能:扩容或者链表转红黑树

(1)扩容机制

扩容是增加数值长度,减少hash碰撞的频率,从而提高性能.

当链表长度大于等于8时,会判断数值长度是否小于64,如果小于,执行

resize();进行扩容

(2)链表转红黑树

为什么链表达到一定长度(8)要转为红黑树:

-------新开一遍专门讲解---------

什么是红黑树:红黑树是一种特殊的平衡二叉树,平衡二叉树具备的特征是:二叉树左子树和二叉树右子树的高度差的绝对值不超过1。

putVal代码如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //判断Node[]数组是否为null,如果是,执行扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            //扩容
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)  //取Node[]数组长度减一和hash值进行与                            运算,得数组下标,判断此数组下标里的元素值是否为null,如果是null直接赋值
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //如果此下标下的元素值不为null,判断两hash和两key是否相等,如果相等,
            替换原来的元素值
            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 {
                //如果不等,遍历链表
                for (int binCount = 0; ; ++binCount) {
                    //如果此链表的下一个元素(next)是否为null,如果是null,直接赋值next,
                    新的元素next设置为null
                    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;
                    }
                    //如果此链表的下一个元素(next)不为null,比较两hash和两key,如果都
                    相等,直接赋值
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e; //如果不能继续循环
                }
            }
            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;
    }

8、resize()数组扩容

       https://blog.csdn.net/zhang2383906154/article/details/121082377icon-default.png?t=L9C2https://blog.csdn.net/zhang2383906154/article/details/121082377

总结:

(1)先计算出对应key的hash值,然后去判断当前Node[]数组是不是为空,为空就新建,不为空就对hash值作减一与运算得到数组下标
(2)然后会判断当前数组位置有没有元素,没有的话就把值插到当前位置,有的话就说明遇到了哈希碰撞
(3)遇到哈希碰撞后,就会看下当前链表是不是以红黑树的方式存储,是的话,就会遍历红黑树,看有没有相同key的元素,有就覆盖,没有就执行红黑树插入
(4)如果是普通链表,则按普通链表的方式遍历链表的元素,判断是不是同一个key,是的话就覆盖,不是的话就追加到后面去
(5)当链表上的元素达到8个的时候,如果不满足扩容条件,链表会转换成红黑树;如果满足扩容条件,则hashmap会进行扩容,把容量扩大到以前的两倍
 

参考:HashMap put原理详解(基于jdk1.8)_Python研究所-CSDN博客

       

   

        

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值