目录
- 参考和引用
- 1、HashMap底层实现
- 2、HashMap的扩容机制
- 3、HashMap的初始容量为什么是16?
- 4、HashMap为什么每次扩容都是2的整数次幂进行扩容?
- 5、HashMap的扩容因子为什么是0.75?
- 6、HashMap扩容后会重新计算Hash值吗?
- 7、HashMap中当链表长度大于等于8时,会将链表转化为红黑树,为什么是8?
- 8、HashMap为什么线程不安全?
- 9、为什么HashMapJDK1.7中扩容时要采用头插法,JDK1.8又改为尾插法?
- 10、HashMap是如何解决哈希冲突的?
- 11、HashMap为什么使用红黑树而不是B树或平衡二叉树AVL或二叉查找树?
- 12、HashMap(JDK1.8)、ConcurrentHashMap(JDK1.8)、Hashtable的区别?
- 13、HashMap和HashSet的区别?
- 14、HashSet和TreeSet的区别?
- 15、HashMap的遍历方式?
参考和引用
作者:云飞扬°
链接:https://www.nowcoder.com/discuss/820700
来源:牛客网
1、HashMap底层实现
分为JDK1.7和JDK1.8来答
- JDK1.7时HashMap的底层数据结构是 数组+链表
- JDK1.8时HashMap的底层数据结构是 数组+链表/红黑树
流程图:
2、HashMap的扩容机制
- 初始容量为16,以2的次方进行容量扩充(优点是使用足够大数组、位运算取代模运算)。
- 当(当前元素个数 > 数组容量 * 0.75)时进行容量扩充。
- 当(链表长度 > 8) && (Entry数组长度 > 64)时链表转化为红黑树。
3、HashMap的初始容量为什么是16?
- 权衡效率和内存;小了会频繁扩容,大了会浪费内存。
4、HashMap为什么每次扩容都是2的整数次幂进行扩容?
因为HashMap是通过key的Hash值来确定存储位置,但Hash值的范围是(2(32-1),2(32-1)-1),建立这么大个数组显然不太可能。因此在计算完Hash值后会对数组的长度进行取余操作,届时就可以用&代替%,如:Hash%length == (length - 1)&Hash。
5、HashMap的扩容因子为什么是0.75?
主要是考虑空间利用率和查询成本的一个折中。扩容因子过大,空间利用率提高,但是会使得Hash冲突的概率增加;扩容因子过低,会频繁扩容,Hash冲突的概率降低,但是会使得空间利用率变低。0.75这个值是通过数学分析和行业规定一起得到的。
6、HashMap扩容后会重新计算Hash值吗?
- JDK1.7中会重新计算Hash值。
- JDK1.8在扩容时会先创建一个新数组,然后根据以下规则将旧数组中的数据转移到新数组上。
规则:(e.Hash & oldArray.length) == 0 ? 新旧位置相同 : 新位置等于旧数组中的位置+旧数组长度;
7、HashMap中当链表长度大于等于8时,会将链表转化为红黑树,为什么是8?
- 在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,长度为8的概率仅为0.00000006。
8、HashMap为什么线程不安全?
- 在JDK1.7中,当并发执行扩容操作时会造成链接死循环(头插法)和数据丢失的情况。
- 在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。如:假设A、B线程同时put操作、Hash值一样且该位置数据为null。A线程通过了Hash判断还未来得急插入数据就挂起,这时线程B正常运行,B插入完成后A获得CPU时间片,直接进行put操作,到时候A线程插入的数据就会覆盖B线程插入的数据,发生线程不安全。
9、为什么HashMapJDK1.7中扩容时要采用头插法,JDK1.8又改为尾插法?
- 采用头插法的目的是:避免遍历链表,提高插入效率。
- 采用尾插法是为了避免出现多线程环境下扩容时采用头插法出现死循环的问题。
10、HashMap是如何解决哈希冲突的?
- 拉链法
- hash函数:key的hash值经过两次扰动,key的hashCode值与key的hashCode值右移16位的值进行异或,然后对数组长度取余(实际为hash&(length-1)),这样做可以让hashCode的高位也参与运算,进一步降低hash冲突的概率,使得数据分布更均匀。
- 红黑树:当链表长度过长,性能变差,当链表长度大于8时,将链表转化为红黑树,可以提高遍历链表的速度;当链表长度小于等于6时将红黑树转化为链表。
11、HashMap为什么使用红黑树而不是B树或平衡二叉树AVL或二叉查找树?
- 不适用二叉查找树
极端情况下会出现线性结构。如:插入0,1,2,3时便是线性结构。 - 不适用平衡二叉树
平衡二叉树是严格的平衡树,红黑树是不严格平衡的树,平衡二叉树在插入或删除后维持平衡(LR,RL等)的开销要大于红黑树。红黑树虽然查询性能略低于平衡二叉树,但在插入和删除上性能要优于平衡二叉树。 - 不适用B树/B+树
HashMap本来就是数组+链表的形式,链表查找效率低下,需要用查找效率更高的树结构替换。B和B+树在数据量不是很多的时候,数据都会“挤在”一个结点里面,这时候就退化成了链表。
12、HashMap(JDK1.8)、ConcurrentHashMap(JDK1.8)、Hashtable的区别?
名称 | HashMap(JDK1.8) | ConcurrentHashMap(JDK1.8) | Hashtable |
---|---|---|---|
底层实现 | 数组+链表/红黑树 | 数组+链表/红黑树 | 数组+链表 |
线程安全 | 不安全 | 安全(synchronized修饰Node节点) | 安全(synchronized修饰整个表) |
效率 | 高 | 较高 | 低 |
扩容 | 初始16,每次扩容成2n | 初始16,每次扩容成2n | 初始11,每次扩容成2n+1 |
是否支持null key和null value | 可以有一个null key对应多个null value | 不支持 | 不支持 |
13、HashMap和HashSet的区别?
名称 | HashMap(JDK1.8) | HashSet |
---|---|---|
底层实现 | 数组+链表/红黑树 | 底层就是HashMap实现的(除了clone()、writeObject()、readObject()外) |
实现的接口 | 实现了Map接口 | 实现了Set接口 |
存储内容 | 键值对 | Object |
添加元素方法 | put | add |
计算hashCode | 用key | 用Object,在hashCode相等情况下,使用equals()方法判断对象的相等性 |
存储细节 | <key,value> | HashSet中的元素由HashMap中的key来保存,而HashMap的value则保存一个静态的Object对象(private static final Object PRESENT = new Object();) |
14、HashSet和TreeSet的区别?
- 相同点
元素不能重复且线程不安全 - 不同点
1. HashSet中的元素可以为null,TreeSet中的元素不能为null .
2. HashSet中元素无序,TreeSet支持自然排序、定制排序二种排序方式。
3. HashSet底层使用哈希表实现的,TreeSet底层是采用红黑树实现的。
4. HashSet的add,remove,contains方法的时间复杂度为O(1),TreeSet中的为O(logn)。
15、HashMap的遍历方式?
- 通过map.keySet()获取key,根据key获取到value
for(String key:map.keySet()){
System.out.println("key : "+key+" value : "+map.get(key));
}
- 通过map.keySet()遍历key,通过map.values()遍历value
for(String key:map.keySet()){ //遍历map的key
System.out.println("键key :"+key);
}
for(String value:map.values()){ //遍历map的值
System.out.println("值value :"+value);
}
- 通过Map.Entry(String,String) 获取,然后使用entry.getKey()获取到键,通过entry.getValue()获取到值
for(Map.Entry<String, String> entry : map.entrySet()){
System.out.println("键 key :"+entry.getKey()+" 值value :"+entry.getValue());
}
- 通过Iterator
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while(it.hasNext()){
Map.Entry<String, String> entry = it.next();
System.out.println("键key :"+entry.getKey()+" value :"+entry.getValue());
}