HashMap浅谈

HashMap主体

HashMap中的主体就是一个数组,其中数组中的每一个元素就是一个链表:
在这里插入图片描述

hashCode()方法

HashMap中的hashcode()方法:
在这里插入图片描述
对于Objects.hashCode(key)这个调用,得解释一下,它实际上就是调用key的hashCode方法,Objects类就是一个工具类,Objects中的hashCode源码如下:
在这里插入图片描述
要注意区分:Key自带的hashCode()函数和HashMap中的hashCode()函数(即上面这段代码),前者是产生Key对象的hashcode的值,后者是产生hashMap中键值对K-V的hashcode值,不要搞混

equals()方法

对于HashMap中的euqals方法是这样的:
在这里插入图片描述
要注意一个误区:我们常说要重写hashcode()方法和equals()方法,不是指重写HashMap中的hashCode()和equals方法,而是重写Key对象的hashCode()和equals()方法

hash()方法

在这里插入图片描述
我们关注点应该在**(h=key.hashCode())^(h>>>16)**,这里表达的意思是:

  • 首先我们获取Key的hashCode值
  • 然后Key的hashCode值右移16位
  • 将Key的hashCode值的高16位和低16位进行异或操作,获得Key的hash值
这里有一个问题:为什么还需要专门调用这个hash()方法来对Key的hashcode进行包装然后再使用,而不是直接使用Key的hashcode值呢?

上面这段代码其实叫扰动函数
Key.hashCode()函数调用的是Key键值类型自带的hash函数,返回int类型的散列值,理论上散列值是一个int型,如果直接拿散列值作为下标访问HashMap的话,前后加起来大概有40亿个空间,完全超越了HashMap的容量大小,内存是放不下那么长的数组的

putVal()方法

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;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            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;
            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);
                        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;
                }
            }
            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;
    }

首先判断table是否为空,如果为空的话,就给这个table初始化为一个长度为16的,负载因子为0.75的数组。否则就初始化为之前table长度的两倍
在这里插入图片描述
这里就涉及到hashMap的扩容问题:
扩容就是重新计算容量,向HashMap对象里不停的添加元素,当超过数组的容量时,就会扩容,扩容的函数是resize(),后面我们会讲。

接着我们putVal过程,根据数组的长度和key的hash值来寻找在数组的下标位置,如果该位置还没有元素,则直接插入
在这里插入图片描述
如果节点和该节点要插入的位置的首元素相同,则将该节点替换掉首元素
在这里插入图片描述
如果要插入的位置是一颗红黑树,则进行树的节点插入操作
在这里插入图片描述
如果是在数组链表中没有找到,则直接插入链表的尾部,然后判断此时链表长度是否等于8,如果已经到达8,则进行链表转红黑树的判断:
在这里插入图片描述
在链表转红黑树的操作中,我们需要注意的是:并不是当链表长度大于8就转成红黑树,我们看转红黑树的函数可以发现,在链表转红黑树的函数中,里面还有一个判断,就是判断数组的容量是否小于64,如果小于,则进行扩容操作,而不是直接转成红黑树,当数组容量大于64,且链表长度大于8时,才进行链表转红黑树操作
在这里插入图片描述

最后进行扩容检查:
在这里插入图片描述

resize()函数

首先是获取原HashMap表数组的长度,以及数组的临界值
在这里插入图片描述

原数组不为空

如果数组长度已经达到最大临界值(231-1),那么直接将数组的临界值设置为最大临界值,然后返回
在这里插入图片描述
当数组的长度大于16时,就将原数组的长度和临界值扩容成原来的两倍
在这里插入图片描述
当数组为空时,就将数组初始化为容器大小是16,初始临界值为:16*0.75=12
在这里插入图片描述

hashMap常见问题

1.HashMap怎么设定初始容量大小

一般如果new HashMap()不传值,默认大小是16,负载因子是0.75,如果自己传入初始大小k,初始大小为大于k的2的整数次方,其中的算法就是让初始值右移1、2、4、8、16位,分别与自己位或,把高位第一个为1的数不断右移,把高位为1的后面全变为1,最后再进行+1操作

2.HashMap的哈希函数怎么设计?为什么要这样设计

HashMap的哈希函数是先拿到key的hashcode,然后让key的hashcode的高16位和低16位进行异或操作
这样设计的原因在于:自己的高半区和低半区做异或,就是为了混合原始哈希值的高位和低位,这样一来混合后的低位掺杂了高位的部分特征,高位的信息也被变相保留下来,而且加大了低位的随机性,减少了哈希碰撞

3.HashMap的1.8版本相比较1.7版本做了哪些优化

  • 数组+链表改成了数组+链表或红黑树
  • 链表的插入方式从头插法改成了尾插法,简单来说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后
  • 扩容的时候1.7需要对原数组中的元素进行重新hash定位再新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小
  • 在插入时,1.7先判断是否需要扩容再插入,1.先进行插入再判断是否需要扩容

4.HashMap是线程安全吗

不是,在多线程环境下,1.7版本会产生死循环(因为采用了头插法)、数据丢失、数据覆盖的问题,1.8版本中会有数据覆盖问题
在HashMap中,产生多线程安全问题主要是插入元素的过程,在这个过程中会有两个地方存在安全问题:

  • 第一个就是当A线程判断新元素插入的位置时被挂起,此时B线程执行插入操作,按摩当A线程恢复现场的时候,就会将B线程插入的数据覆盖掉
  • 第二个就是++size的过程,我们知道,自增操作不是一个原子性操作,那么当两个线程同时执行时,最后的结果只会加1次
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值