目录
一、扩容机制
HashMap看这篇就够了~_技术交流_牛客网
二、HashMap底层原理
一:HashMap的节点
二:HashMap的数据结构
三:HashMap存储元素的过程
四: HashMap具体的存取过程
put存值的方法,过程如下:
get取值的方法,过程如下:
五、HashMap的负载因子为啥是0.75?
侧面回答:
正面回答:
六、为什么 数组容量 是 2 的整数倍?
解决 hash 冲突的常见方法
一、扩容机制
初始容量 | 增长后 | 增长触发条件 | |
arrayList | 10 | 旧容量*1.5+1 | n>10 |
vector | 10 | 旧容量*2 | n>10 |
hashSet | 16=2^4 | 旧容量*2 | n>旧容量*0.75 |
hashMap | 16=2^4 | 旧容量*2 | n>旧容量*0.75 |
“如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?”
HashMap的扩容阈值(threshold = capacity* loadFactor );
threshold = capacity * loadFactor,当 Size>=threshold的时候,那么就要考虑对数组的扩增了;
就是通过它和size进行比较来判断是否需要扩容。默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,将会创建原来HashMap大小的两倍的bucket数组(jdk1.6,但不超过最大容量),来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。
JDK1.8 之前: HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。
JDK1.8 以后: HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n-1) &
HashCode
相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。
进行扩容,会伴随着一次重新 hash 分配,并且会遍历 hash 表中所有的元素,是非常耗时的。在编写程序中,要尽量避免 resize。
二、HashMap底层原理
一:HashMap的节点
HashMap是一个集合,键值对的集合,源码中每个节点用Node表示,Node是一个内部类,这里的key为键,value为值,next指向下一个元素,可以看出HashMap中的元素不是一个单纯的键值对,还包含下一个元素的引用。
二:HashMap的数据结构
HashMap的数据结构为 数组+(链表或红黑树),入下图:
为什么采用这种结构来存储元素呢?
数组的特点:查询效率高,插入,删除效率低。
链表的特点:查询效率低,插入删除效率高。
在HashMap底层使用数组加(链表或红黑树)的结构完美的解决了数组和链表的问题,使得查询和插入,删除的效率都很高。
三:HashMap存储元素的过程
有这样一段代码:
HashMap<String,String> map = new HashMap<String,String>();
map.put("刘德华","张惠妹");
map.put("张学友","大S");
现在我要把键值对 “刘德华”,”张惠妹”存入map:
第一步:计算出键“刘德华”的hashcode,该值用来定位要将这个元素存放到数组中的什么位置;
刘德华的hashcode为20977295 数组长度为 16则要存储在数组索引为 20977295%16=1的地方;
第二步:数组索引为1的地方是空的,这种情况很简单,直接将元素放进去就好了。
若已经有元素占据了索引为1的位置,这种情况下我们需要判断一下该位置的元素和当前元素是否相等,使用equals来比较。
如果两者相等则直接覆盖,如果不等则在原元素下面使用链表的结构存储该元素;
链表中元素太多的时候会影响查找效率,所以当链表的元素个数达到8的时候使用链表存储就转变成了使用红黑树存储,原因就是红黑树是平衡二叉树,在查找性能方面比链表要高.
四: HashMap具体的存取过程
之前定位桶用的是【取模运算】,后来变成了【位与运算】;据说是位与运算的代价远高于位与运算;
put存值的方法,过程如下:
eg: map.put("张三+语文",91);
①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
【链表---->红黑树】的2个条件:(链表长度大于8) and (数组长度到64.)
【红黑树---->链表】的条件: 红黑树节点数小于等于6个就链表化;
get取值的方法,过程如下:
eg: map.get("张三+语文");
①.指定key 通过hash函数得到key的hash值
int hash=key.hashCode();
②.调用内部方法 getNode(),得到桶号(一般为hash值对桶数求模)
int index =hash%Entry[].length; 取余
jdk1.6版本后使用位运算替代模运算,int index=hash&( Entry[].length - 1);
③.比较桶的内部元素是否与key相等,若都不相等,则没有找到。相等,则取出相等记录的value。
④.如果得到 key 所在的桶的头结点恰好是红黑树节点,就调用红黑树节点的 getTreeNode() 方法,否则就遍历链表节点。
getTreeNode 方法使通过调用树形节点的 find()方法进行查找。
由于之前添加时已经保证这个树是有序的,因此查找时基本就是折半查找,效率很高。
⑤.如果对比节点的哈希值和要查找的哈希值相等,就会判断 key 是否相等,相等就直接返回;不相等就从子树中递归查找。
五、HashMap的负载因子为啥是0.75?
侧面回答:
正面不好回答你就侧面说:
若load Factor = 0.25,那就意味着array的4格被全部填满就会达到扩容阈值扩容到32,这就导致28个没利用很浪费空间;
若load Factor = 1,那就意味着array意味着本该在0.75时扩容没扩,新添加的元素只能挤到链表里,会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利;
总结:负载因子太小,虽然时间效率提升了,但是空间利用率降低了;
负载因子过大,虽然空间利用率上去了,但是时间效率降低了;
故而,经过验证,0.75最合适;
正面回答:
在HashMap注释中有这么一段:
在理想情况下,使用随机哈希吗,节点出现的频率在hash桶中遵循泊松分布,同时给出了桶中元素的个数和概率的对照表。
从上表可以看出当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为负载因子,每个碰撞位置的链表长度超过8个是几乎不可能的。
hash容器指定初始容量尽量为2的幂次方。
HashMap负载因子为0.75是空间和时间成本的一种折中。
Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million
*
六、为什么 数组容量 是 2 的整数倍?
key.hashcode % array.length = key.hashcode & (array.length - 1) 等式成立的前提是 array.length 是2的整数倍!即 array.ength长度是2的次幂时!
解决 hash 冲突的常见方法
针对哈希表直接定址可能存在hash冲突,举一个简单的例子,例如:
第一个键值对A进来,通过计算其 key 的 hash 得到的 index=0 。记做: Entry[0] = A 。
第二个键值对B, 通过计算其 index 也等于0, HashMap 会将 B.next = A, Entry[0] = B,
第三个键值对C, 通过计算其 index 也等于0,那么 C.next = B, Entry[0] = C;
这样我们发现 index=0 的地方事实上存取了 A,B,C 三个键值对, 它们通过next这个属性链接在一起。 对于不同的元素,可能计算出了相同的函数值,这样就产生了hash 冲突,那要解决冲突,又有哪些方法呢?具体如下:
a. 链地址法: 将哈希表的每个单元作为链表的头结点,所有哈希地址为 i 的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。
b. 开放定址法: 即发生冲突时,去寻找下一个空的哈希地址。只要哈希表足够大,总能找到空的哈希地址。
c. 再哈希法: 即发生冲突时,由其他的函数再计算一次哈希值。
d. 建立公共溢出区: 将哈希表分为基本表和溢出表,发生冲突时,将冲突的元素放入溢出表。
HashMap采用哪种方法解决冲突的呢?
HashMap 就是使用 链地址法 来解决冲突的(jdk8中采用平衡树来替代链表存储冲突的元素,但hash() 方法原理相同)。当两个对象的 hashcode 相同时,它们的 bucket 位置相同,碰撞就会发生。此时,可以将 put 进来的 K- V 对象插入到链表的 尾部 。对于储存在同一个bucket位置的链表对象,可通过键对象的equals()方法用来找到键值对。