深入浅出 ~ HashMap的底层原理透析

网友投稿 553 2022-10-26

深入浅出 ~ HashMap的底层原理透析

深入浅出 ~ HashMap的底层原理透析

创作宗旨:化繁为简,绝不冗余,点到为止

直接开门见山,就事论事!

什么是 HashMap?

HashMap类继承了父类AbstractMap<K,V>,实现了接口Map<K,V>, Cloneable, Serializable,AbstractMap<K,V>实现了对Map的基本操作,Serializable是序列化接口,可实现序列化,Map<K,V>接口规范了k-v对操作的抽象方法,Cloneable规范了继承了Object后去使用clone()方法前必须得实现Cloneable接口,否则抛出CloneNotSupportedException错误。

一、介绍

1. HashMap 的线程不安全的场景

死锁

所以原因的头插法得以佐证,CPU的轮询切换也演绎的很到位。线程不安全讲完,下面就讲原理,去潜移默化它。

2. HashMap 实现哈希命中均匀

哈希也就是k的hashcode计算后得到的下标,而hashcode是一个32位的int类型,要让分布均匀,即让不同的k不能出现有相同的效果,也就是得有唯一性,就要完全去利用高16位和低16位,而如果直接用32位去取模运算,高16就可能会用不到,导致hashcode高16位不同的k依旧命中同一个桶位。所以JDK8采取的做法是对高16位与低16位进行异或运算,这样就变相保留了高位和低位的所有特征了。哈希均匀,JDK8之后

return (key == null) ? h = key.hashCode() ^ (h >>> 16);

取模运算,JDK7之前

h & (length - 1)

3. HashMap 参数以及扩容机制

初始容量16,达到阈值扩容,阈值等于最大容量 * 负载因子,扩容每次2倍,总是2的n次方。

扩容机制使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有Entry数组元素拷贝到新的Entry数组里,Java1.7重新计算每个元素在数组中的位置。而Java1.8它不需要重新计算每个元素,它会先比较元素的hashcode & 原来数组的大小的结果,如果为0,就以元素的原下标加入到新数组中,如果为1,就在原下标加上原数组的大小加入到新数组中,这样这两种情况就命中到新数组的前一半和后一半了,也就是将原本全在原数组大小的所有元素又均匀分布一些元素到扩容未使用的下标中。那么就解释一下,假设加入的元素是随机的,也就是所有情况的hash值是均等出现的,而&原数组大小可以这样比如,有64个元素均等的从0到63,那么0到15分配给了扩容后数组大小为32的左半部分,16-31分配给了右半部分,32-48分配给了左半部分,48-63分配给了右半部分,就比较均匀分散在了两侧,而且机会均等,很巧妙! // 分布到左侧 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } // 分布到右侧 else { if (hiTail == null) // 初始化 hiHead = e; else // 尾插法 hiTail.next = e; // 自增,下次尾插法必要的变量 hiTail = e; } 4. HashMap 的 get 和 put 操作 get 让我们见识下此时无图胜有图的效果吧!

get由源码解读是先获取key的hashCode,然后定位到对应的数组位置,再去遍历该元素处的链表在这里才讲equals我觉得是最合适的,可以区分它与hashCode的区别,如果hashCode相同的两个元素在同一个数组下标,equals就开始发挥作用了,不得不说,当然别说我是话痨,我觉得HashMap加入这两个的初衷是如果在一个庞大的Map中,要去get某一个key的value,有一种最直接的做法,就是直接整个Map去equals比较,这没错,而且还是有用的,但加入hashCode之后,hashCode能够给equals先筛选一番,因为不同的key,它的hash值一般是不同的,理解这里的一般,所以hash值不同的equals肯定就不同,就没必要再去对不同下标元素来equals比较了,好理解吧!由于HashMap中新加入了红黑树,就多了一种get到key的方法,是一种二分法,对于大量数据的链表是不推荐的,所以采用红黑树(总是平衡的二叉树)更加地高效!

put

put的实现主要针对两种key,一种是不存在的key,一种是已存在的key局部源码

public V put(K key, V value) { //采用hash(key)来计算key的hashCode值。 return putVal(hash(key), key, value, false, true); }

static final int hash(Object key) { int h; //当key等于null的时候,不走hashCode()方法 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }

如果为前者,而且key值为null,就在数组下标为0的链表中去寻找key为null的值,当然找不到,就创建key为null的结点。如果为前者,而且key值不为null,就调用该元素hash计算具体的下标,在该下标的链表中寻找,当然也寻找不到,就尾插法加入该k-v结点。如果为后者,即找到了,直接赋值就可以了,不需要考虑是否为null。

这里要分开来put是因为,在HashTable上,是不允许有key为null的情况出现,因为当key为null时,key.hashCode()会报空指针异常。所以在HashMap做了特别的处理,对key为null做了单独处理,这样就解决了key为null的情况。还有一点,HashMap的value如果设置为null,虽然你put了,但再去取key的时候,依旧是不存在!

二、附语

不足之处请大佬予以佐证,不希望哪个地方出现的错误误导同行,结果闹出笑话来,总之你们的阅读和评论是对作者最大的支持!谢谢大家,我会继续努力,只为力争创作高质量的文章,分享给各位有需要的读者。我的技术专栏:https://github.com/fyupeng

专注品质,热爱生活。交流技术,寻求同志。

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Rat: 编写shell命令来构建交互式终端应用程序
下一篇:SpringMVC中事务是否可以加在Controller层的问题
相关文章

 发表评论

暂时没有评论,来抢沙发吧~