HashMap底层原理及常见问题解答
- 一.HashMap几个重要参数
- 二.HashMap数据结构
- 三.相关问题:
- 1.HashMap的工作原理(put和get操作过程)
- 2.什么是Hash碰撞?
- 3.当两个对象的 hashCode 相同会发生什么?
- 4.JDK1.7和1.8中HashMap的区别在哪里?
- 5.为什么链表长度大于8才用红黑树?
- 6.为什么不用二叉查找树?
- 7.hash 的实现?为什么要这样实现?
- 8.为什么要用异或运算符?
- 9.为什么HashMap是线程不安全的?
- 10.Hashtable,ConcurrentHashMap和HashMap有什么区别?
- 11.HashMap扩容的过程?
- 12.HashMap 的 table 的容量如何确定?loadFactor 是什么?该容量如何变化?这种变化会带来什么问题?
- 13.HashMap的数组长度为什么要保证是2的幂?
- 14.HashMap和Hashtable有什么区别?
- 15.对红黑树的理解?
- 16.HashMap,LinkedHashMap,TreeMap 有什么区别?
- 17.谈谈ConcurrentHashMap
一.HashMap几个重要参数
HashMap最大支持容量:2^30;
treeify_threshold:桶的链表长度大于该值,转化为红黑树
桶中Node被树化的最小hash表容量:64 (hash表长度大于64桶链表才会转化为红黑树,当桶链表长度大于threshold时,若容量小于64,优先进行扩容)
table:存储元素的数组,长度为2的幂
size:HashMap存在的键值对的数量
threshold:扩容的临界值 = (loadFactor * size),超过该值,进行扩容
loadfactor:加载因子
二.HashMap数据结构
-
JDK1.7:数组+链表
-
JDK1.8: 引入了红黑树,在链表长度大于8时用红黑树,小于8时红黑树又退化成链表
1.初始大小:HashMap默认初始大小为16;
注:可以预估数据量大小来设置初始大小,这样可以减少动态扩容的次数,提高HashMap的性能;
2.动态扩容:最大装载因子默认为0.75,当元素个数超过0.75*capacity(Hash表的容量),会启动扩容,扩容为原来的两倍大小
3.Hash函数
int hash(Object key){
int h = key.hashCode();
return(h ^ (h >>> 16)) & (capicity - 1);
}
4.缺点:
-
存不了大数据,多次扩容性能下降
-
线程不安全,不支持多线程,多线程采用ConcurrentHashMap
三.相关问题:
1.HashMap的工作原理(put和get操作过程)
- HashMap在JDK1.7中采用数组+链表
- HashMap在JDK1.8中引入了红黑树,在链表长度大于8且Hash表长度大于64时用红黑树
- HashMap采用get和put方法进行数据的读取和存储
- put(key,value)方法:
- 1.首先通过对key进行hash方法求出对应的哈希值,然后结合数组的长度得出数组的下标;
- 2.如果数组大小超过扩容临界值,需要进行扩容,如果没有则不需要进行下一步;
-
- 如果数组该位置为空,则直接将该位置的键值对信息更新,完成。
- 如果该数组位置已有元素,则对比该位置链表上所有元素的hash是否存在
- 如果存在,则用equals方法进行比较,结果为true时,更新键值对,结果为false,则插入到链表或者红黑树的尾部
- 如果不存在,则执行则插入到链表或者红黑树的尾部;
- 4.JDK 1.7 之前使用头插法、JDK 1.8 使用尾插法
- get(key)方法
- 计算key的hash值,并结合数组长度得到该键值对应的下标,遍历所在下标的链表或者红黑树,用equals方法查找相同的k对应的value并返回
2.什么是Hash碰撞?
当两个对象进行哈希操作后得到的数组下标相等时,就发生了哈希碰撞
3.当两个对象的 hashCode 相同会发生什么?
因为 hashCode 相同,不一定就是相等的(equals方法比较),所以两个对象所在数组的下标相同,"碰撞"就此发生。又因为 HashMap 使用链表存储对象,这个 Node 会存储到链表中。
4.JDK1.7和1.8中HashMap的区别在哪里?
- new HashMap()的过程中,底层原理中:JDK1.7创建一个长度为16的数组,JDK1.8没有创建,首次put时才会创建
- 底层数组JDK1.7Entry[]数组,JDK1.8为Node[]数组
- HashMap在JDK1.7中采用数组+链表,在JDK1.8中引入了红黑树,在链表长度大于8且Hash表长度大于64时用红黑树
5.为什么链表长度大于8才用红黑树?
- 红黑树左旋右旋需要消耗性能,如果在链表长度比较短的情况下,直接用链表性能更好,因此链表长度在比较短的时候采用原始链表即可,数据量较小的时候,红黑树维持平衡的性能成本比起链表上,性能优势并不明显。
6.为什么不用二叉查找树?
- 红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会退化成链表的线性结构,遍历查询效率低。
- 红黑树是一种平衡树,在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,保证二叉树的深度不会太深;
- 引入红黑树就是为了查找数据快,解决链表查询深度的问题。
7.hash 的实现?为什么要这样实现?
- JDK 1.8 中,是通过 hashCode() 的高 16 位异或低 16 位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度,功效和质量来考虑的,减少系统的开销,也不会造成因为高位没有参与下标的计算,从而引起的碰撞。
8.为什么要用异或运算符?
保证了对象的 hashCode 的 32 位值只要有一位发生改变,整个 hash() 返回值就会改变。尽可能的减少碰撞。
9.为什么HashMap是线程不安全的?
首先HashMap是线程不安全的,其主要体现:
- 在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。
- 在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。
10.Hashtable,ConcurrentHashMap和HashMap有什么区别?
- Hashtable采用syncronized关键字,很影响插入性能,性能差
- ConcurrentHashMap采用分段锁(1.7),
11.HashMap扩容的过程?
触发扩容的条件:当hashmap中的键值对的个数>阈值threshold时,会进行扩容;
- jdk1.7的hashmap扩容时需要重新计算每个元素的hash,并重新确认位置;
- jdk1.8中如果该桶没有链表只有一个元素,则和jdk1.7一样直接计算下标放置元素到新数组中,如果该桶有链表则采用优化算法进行拆分。
12.HashMap 的 table 的容量如何确定?loadFactor 是什么?该容量如何变化?这种变化会带来什么问题?
-
table 数组大小是由 capacity 这个参数确定的,默认是16,也可以构造时传入,最大限制是1<<30;
-
loadFactor 是装载因子,主要目的是用来确认table 数组是否需要动态扩展,默认值是0.75;
-
扩容时,调用 resize() 方法,将 table 长度变为原来的两倍;
-
如果数据很大的情况下,扩展时将会带来性能的损失,在性能要求很高的地方,这种损失很可能很致命。
13.HashMap的数组长度为什么要保证是2的幂?
- 假如数组长度不为2的幂,那么(n-1)的二进制串的低位中肯定会有0,因此,不论hash低位上的值是0还是1,结果都会为0。这样就造成了一个性能问题:存在不同的hash值会得出相同的下标。也就是数组上的某些位置经常被拿来用,而某些位置则可能永远没被用。hashmap数组上的元素分布不均匀。
14.HashMap和Hashtable有什么区别?
- 线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法经过 synchronized 修饰。
- 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高。另外,HashTable 基本被淘汰,不要在代码中使用它;
- 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。
- 初始容量大小和每次扩充容量大小的不同:
- 创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
- 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。
- 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制;
15.对红黑树的理解?
- 1每个节点非红即黑,2根节点总是黑色的,3每个叶子节点都是黑色的空节点(NIL节点),4从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点;
- 红黑树在查找方面和AVL树操作几乎相同。但是在插入和删除操作上,AVL树每次插入删除会进行大量的平衡度计算,红黑树是牺牲了严格的高度平衡的优越条件为代价,它只要求部分地达到平衡要求,结合变色,降低了对旋转的要求,从而提高了性能。红黑树能够以O(log2 n)的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。
- 相比于BST,因为红黑树可以能确保树的最长路径不大于两倍的最短路径的长度,所以可以看出它的查找效果是有最低保证的。在最坏的情况下也可以保证O(logN)的,这是要好于二叉查找树的。因为二叉查找树最坏情况可以让查找达到O(N)。
16.HashMap,LinkedHashMap,TreeMap 有什么区别?
LinkedHashMap 保存了记录的插入顺序,在用 Iterator 遍历时,先取到的记录肯定是先插入的;遍历比 HashMap 慢;
TreeMap 能够把它保存的记录根据键排序(默认按键值升序排序,也可以指定排序的比较器)
17.谈谈ConcurrentHashMap
线程安全的实现方式:
-
JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。
-
JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。