java8 HashMap面试问题点整理

参考:

https://blog.csdn.net/v123411739/article/details/78996181

https://blog.csdn.net/v123411739/article/details/78996181

https://csp1999.blog.csdn.net/article/details/117192375

建议先通读上面的博客,了解一些HashMap源码的知识

jdk8 和jdk7中HashMap实现有什么区别?

1 java8中的HashMap引入了红黑树和链表并存的结构存储数据,而java7中桶上存的是链表,这里注意,无论哪个版本的JDK,桶数组上的链表都是单链表,只能单向遍历

2 java7中的HashMap在多线程情况下添加数据再调用get方法会报无限循环的错误

java7 8两个版本的HashMap都不是线程安全的,因此sun认为这个问题不算是一个bug,本身就不该在多线程场景下操作HashMap,这里说下java7中的HashMap在多线程情况下会产生这个错误的原因。

参考这里疫苗:Java HashMap的死循环 | 酷 壳 - CoolShell

do {
    Entry<K,V> next = e.next; // <--假设线程一执行到这里就被调度挂起了
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
} while (e != null);

java7中HashMap扩容时,迁移数据是“头插法”,原链表的根部在插入后一般会是新链表的头部,当两个线程一起操作,B线程假如已经完成了迁移,链表从1 -> 2->3->4变成了4->3->2->1,而A线程的数据副本还是1 -> 2->3->4,它也要倒装链表到新桶数组中,一来二去成了环装链表,调get遍历链表自然报错

java8中将数据迁移改成了尾插法,避免了这个问题

HashMap是如何避免hash冲突的?

先要了解,HashMap存储数据是数组,元素(链表或者红黑树的节点)在数组上的位置是通过hash算法确定的。从初衷上来,元素key值计算出来的下标都不相同是最好的,这样数组中每个位置都是不同的元素,看看java8计算下标的方法

// 代码1
static final int hash(Object key) { // 计算key的hash值
    int h;
    // 1.先拿到key的hashCode值; 2.将hashCode的高16位参与运算
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 代码2
int n = tab.length;
// 将(tab.length - 1) 与 hash值进行&运算
int index = (n - 1) & hash;

key的hash值对数组长度取模运算就可以获得相对均匀的hash分布了,计算机中&运算的效率是高于取模运算的,使用上面代码的位与运算来代替模运算。这个方法非常巧妙,它通过 “(table.length -1) & h” 来得到该对象的索引位置,这个优化是基于以下公式:

x mod 2^n = x & (2^n - 1)。我们知道 HashMap 底层数组的长度总是 2 的 n 次方,并且取模运算为 “h mod table.length”,对应上面的公式,可以得到该运算等同于“h & (table.length - 1)”。

这里还有一个点,hash值不是直接与数组长度-1作&运算,而是先与自己的高16位进行异或运算再把得到的值参与运算,这是为了避免hash值高位变化频繁而低位变化较少时候,最后运算得到的数组下标会有很多一致的,导致冲突

为什么HashMap长度总是2的n次幂

HashMap的初始容量是16,每次扩容会比之前大一倍,基于上一个问题,我们知道HashMap容量一定是2的n次幂,但是HashMap明明可以通过构造方法来指定初始容量和扩容因子(默认0.75),我指定个容量为7不行么?

看这个方法

//在执行完这个方法后,会得到大于cap的最小的n次幂
//原理就是不断右移作或运算,低位全是1,高位全是0,最后加1后得到2^n
static final int tableSizeFor(int cap) {
  int n = cap - 1;
  n |= n >>> 1;
  n |= n >>> 2;
  n |= n >>> 4;
  n |= n >>> 8;
  n |= n >>> 16;
  return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

构造方法重也会调用这个方法,最终结果就是,桶数组的长度就一定会是2^n

为什么 Map 桶中结点个数超过 8 才转为红黑树?

Because TreeNodes are about twice the size of regular nodes, we use them only when bins
contain enough nodes to warrant use (see TREEIFY_THRESHOLD). And when they become too
small (due to removal or resizing) they are converted back to plain bins.  In usages with
well-distributed user hashCodes, tree bins are rarely used.  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:

翻译:因为树结点的大小大约是普通结点的两倍,所以我们只在箱子包含足够的结点时才使用树结点(参见TREEIFY_THRESHOLD)。
当它们变得太小(由于删除或调整大小)时,就会被转换回普通的桶。在使用分布良好的用户 hashCode 时,很少使用树箱。
理想情况下,在随机哈希码下,箱子中结点的频率服从泊松分布。
默认调整阈值为0.75,平均参数约为0.5,尽管由于调整粒度的差异很大。忽略方差,列表大小k的预朗出现次数是(exp(-0.5)*pow(0.5, k) / factorial(k)
第一个值是:

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

TreeNodes(树) 占用空间是普通 Nodes(链表) 的两倍,所以只有当 bin(bucket 桶) 包含足够多的结点时才会转成 TreeNodes,而是否足够多就是由 TREEIFY_THRESH〇LD 的值决定的。当 bin(bucket 桶) 中结点数变少时,又会转成普通的 bin(bucket 桶)。并且我们查看源码的时候发现,链表长度达到 8 就转成红黑树,当长度降到 6 就转成普通 bin(bucket 桶)。

这样就解释了为什么不是一开始就将其转换为 TreeNodes,而是需要一定结点数之后才转为 TreeNodes,说白了就是权衡空间和时间。数组长度不超过64不树化也是这个道理

这段内容还说到:当 hashCode 离散性很好的时候,树型 bin 用到的概率非常小,因为数据均匀分布在每个 bin 中,几乎不会有 bin 中链表长度会达到阈值。但是在随机 hashCode 下,离散性可能会变差,然而 jdk 又不能阻止用户实现这种不好的 hash 算法,因此就可能导致不均匀的数据分布。不理想情况下随机 hashCode 算法下所有 bin 中结点的分布频率会遵循泊松分布,我们可以看到,一个 bin 中链表长度达到 8 个元素的槪率为 0.00000006,几乎是不可能事件。所以,之所以选择 8,不是随便決定的,而是裉据概率统计决定的

说人话就是,树的内存开支要大于链表,而节点数到达8个事不太可能的事情,只有到了8个,才有花费内存去做树化的价值(你给三个元素整棵树,还不如链表来的快)
 

简述一下HashMap的扩容机制

当 HashMap 中的元素个数超过数组大小(数组长度)*loadFactor(负载因子)时,就会进行数组扩容,loadFactor 的默认值是 0.75。默认初始化长度是16,负载因子是0.75,所以默认扩容长度是12,HashMap 在进行扩容时,使用的 rehash 方式非常巧妙,因为每次扩容都是翻倍,与原来计算的 (n - 1) & hash 的结果相比,只是多了一个 bit 位(2n二进制高位比n多个1),所以结点要么就在原来的位置,要么就被分配到 “原位置 + 旧容量” 这个位置,这意味着扩容后的hash分布至少是与扩容前相同的,或者是优于扩容前的。

把HashMap和HashTable作个比较吧(老生常谈)

1 HashMap线程不安全,HashTable通过加锁的方式实现了线程安全

2 HashMap 键 值都允许为null HashTable则都不允许

3 二者解决hash冲突方式不同

  • HashMapindex = hash & (tab.length – 1)
  • HashTableindex = (hash & 0x7FFFFFFF) % tab.lengt

HashMap长度初始化为16,HashTable为11,HashMap每次扩容是两倍,HashTable扩容后是原来长度n的2n+1,就是始终维持桶数组长度为奇数个

4 java8中HashMap是数据 + 链表 + 红黑树实现的,HashTable是数组+链表实现的

HashMap有哪些遍历方式

1 迭代器

Iterator<Map.Entry<String, Object>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry<String, Object> mapEntry = iterator.next();
    System.out.println(mapEntry.getKey() + "---" + mapEntry.getValue());
}
 

2 keySet entrySet

Set<String> keySet = map.keySet();
for (String str : keySet) {
    System.out.println(str + "---" + map.get(str));
}
 

3 foreach

HashMap<String,String> map = new HashMap();
map.put("001", "zhangsan");
map.put("002", "lisi");
map.forEach((key, value) -> {
    System.out.println(key + "---" + value);
});
 

推荐使用迭代器或者是foreach进行遍历

最后:

原本准备整理源码阅读的,但在收集资料的过程中发现源码有很多人已经做过了而且做的很好,有大量现成的学习资料了,而且为了看HashMap还研究了一周多的红黑树,敖丙的博客将红黑树和234 23树一起讲解给了我很大的帮助,但是到现在都没有发现关于红黑树的删除节点平衡操作的学习资料,偶尔查到一些也是很难理解。所以到这里,源码只作阅读,整理做类似笔记的博客,而源码的学习主要通过转载别的优秀博客来进行

关于HashMap,主要问点就是扩容机制 解决hash冲突的办法 以及红黑树的数据结构,偶尔可能问到java7中多线程下HashMap写入数据再get报错的问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值