时间:2021-05-20
HashMap底层数据结构是数组+链表,JDK1.8中还引入了红黑树,当链表长度超过8个时,会将链表转成红黑树,以提升其查找性能。
那么,给出一个<key, value>节点,HashMap是如何确定这个节点应该放在具体哪个位置呢?(以JDK1.8为例)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // HashMap没有被初始化,则先进行初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 节点所在index = (n - 1) & hash,该位置没有数据,则直接将新节点放在数组的index位置上 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { // index上已经有节点了 Node<K,V> e; K k; // 如果新key与原来的key一样,则e指向原节点p(后面会用新value替换e所指向的value) if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 如果该节点是树节点,则采用树的插入算法,插入新节点 else if (p instanceof HashMap.TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 该节点是链表节点 for (int binCount = 0; ; ++binCount) { // 将新节点插入到index所在链表的末端 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; } // 同样的,如果key已经存在的话,则不进行插入操作,而是后面进行value替换 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // e != null的情况,就是key已经存在了,这里统一进行了新值value,替换旧值e.value的操作 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // 插入后数组size 大于阈值的话,需要进行扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null;}看源码,节点落在数组中的index = (数组长度 - 1) & key的hashcode,如果该index上没有数据,则直接插到该index上,如果节点已经有数据了,则把新节点插入该index对应的链表中(如果链表节点大于8个,会进行链表转树,之后的插入算法就变成了树的插入算法)。
每次put之后,会检测一下是否需要扩容,size超过了 总容量 * 负载因子,则会扩容。默认情况下,16 * 0.75 = 12个。
1、为什么初始容量是16
当容量为2的幂时,上述n -1 对应的二进制数全为1,这样才能保证它和key的hashcode做&运算后,能够均匀分布,这样才能减少hash碰撞的次数。至于默认值为什么是16,而不是2 、4、8,或者32、64、1024等,我想应该就是个折中处理,过小会导致放不下几个元素,就要进行扩容了,而扩容是一个很消耗性能的操作。取值过大的话,无疑会浪费更多的内存空间。因此在日常开发中,如果可以预估HashMap会存入节点的数量,则应该在初始化时,指定其容量。
2、为什么负载因子是0.75
也是一个综合考虑,如果设置过小,HashMap每put少量的数据,都要进行一次扩容,而扩容操作会消耗大量的性能。如果设置过大的话,如果设成1,容量还是16,假设现在数组上已经占用的15个,再要put数据进来,计算数组index时,发生hash碰撞的概率将达到15/16,这违背的HashMap减少hash碰撞的原则。
补充知识:HashMap只有容量达到阀值才发生扩容吗?大错特错!
看了网上很多文章,说HashMap在元素达到负载因子对应数的时候就发生扩容。如果你看过源码就会发现,其实还有一种情况也可能会发生扩容:树形化的时候。
对象最终是如何放入HashMap中的?
HashMap底层是由数组+链表组成的,为了方便不懂的人更容易理解,那我们就先假设HashMap底层就是数组,先不管链表。
当一个对象add到HashMap中,此时HashMap的add方法是如何来确定这个对象是放在数组中的哪个位置的呢?
拿JDK1.8来说(其他JDK版本稍有不同,但大同小异),大家应该知道每一个对象天生都继承了或程序员自己覆盖了Object类的 hashCode()方法,此方法返回对象的hashcode值。
HashMap会有一个方法,先拿到要add进HashMap中的对象的hashCode,再将这个hashCode异或上对象自身hashCode右移16位(是不是感觉说的不是人话?这个步骤叫扰乱,这样做的目的是为了让hashCode每一位都尽可能用到,如果不理解没关系并不影响接下来的阅读),hashCode经过上述步骤之后再&(数组长度-1),计算的结果就是这个对象在数组中的位置了。我自己都觉得说的不是人话,下面举个例子,便于理解:
这里有一个Student对象的hashCode是:a
先把这个a右移16位 , b=a>>>16;
然后a=a&b;
数组中的位置等于: a&(数组长度-1);
上述源码如下:
h=key.hashCode();
h = key.hashCode()) ^ (h >>> 16)
数组位置=h&(数组长度-1);
好了, 我们已经知道元素是如何在hashMap中的数组上如何定位了,现在假设一个极端情况(不可能发生,但是我用这个举例子):
假设数组长度为1,根据源码:
数组位置=h&(数组长度-1)
那么有:
数组位置=h&(1-1)=0 ,无论什么对象,都定位到数组的第0个位置。
这个很好理解吧。无论元素是否一样,由于数组长度为1,所以元素通通定位到数组中第0个位置。大家都知道一个数组只能放一个元素啊?那怎么办呢?我们用链表来解决这个问题,把定位到这个位置的元素通过链表连接。这就是我一开始说的:hashMap是数组+链表。
那树形化又是什么东东呢?
想一下我们为什么要用HashMap,是因为通过Hash算法在理想情况下时间复杂度O(1)就能找到元素,特别快,但是我都说了是理想情况,如果遇到上述发生hash碰撞(谁jb取的名字,就是上面我才说的,两个元素定位到数组中同一个位置),且hash碰撞比较频繁的话,那么当我们get一个元素的时候,定位到了这个数组,还需要在数组中遍历一次链表最终才能找到要get的元素,是不是已经失去一部分使用HashMap的初心了?(因为需要遍历链表,所以时间复杂度就比之前高了)
所以JDK1.8使用红黑树这种数据结构来解决链表过长的问题(可以简单理解为用红黑树遍历比链表遍历速度快,时间复杂度低,不懂红黑树的可以去搜搜看),默认链表长度达到8就将链表树形化(变为红黑树)。
回到最最开始我提到的,那为什么树形化的时候可能会发生扩容呢?
想想刚刚的例子数组长度为1,所有元素全部在数组的第0个位置形成一条链表,这例子是一种极端情况,数组长度过小,那自然就会经常发生hash碰撞,那形成长链表是肯定的,这个时候树形化其实是治标不治本,因为引起链表过长的根本原因是数组过短,所以在JDK1.8源码中,执行树形化之前,会先检查数组长度,如果长度小于64,则对数组进行扩容,而不是进行树形化。
所以发生扩容的时候有两种情况,一种是元素达到阀值了,一种是HashMap准备树形化但又发现数组太短,这两种情况均可能发生扩容。
以上这篇HashMap容量和负载因子使用说明就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持。
声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。
HashMap扩容前言:HashMap的size大于等于(容量*加载因子)的时候,会触发扩容的操作,这个是个代价不小的操作。为什么要扩容呢?HashMa
支付宝的钱怎么转到银行卡支付宝使用说明支付宝的钱怎么转到银行卡支付宝使用说明支付宝的钱怎么转到银行卡支付宝使用说明支付宝的钱怎么转到银行卡支付宝使用说明支付宝的
HashMap简单总结:1、HashMap是链式数组(存储链表的数组)实现查询速度可以,而且能快速的获取key对应的value;2、查询速度的影响因素有容量和负
复制代码代码如下:/**函数名称:DateUtil*作者:yithcn*功能说明:日期函数*使用说明:*创建日期:2010.10.14*/varDateUtil
概述:@Valid是使用Hibernatevalidation的时候使用@Validated是只用SpringValidator校验机制使用说明:java的JS