HashMap分析

Map

HashMap

看源码前,先看一下类上带的注释,很多问题和答案你在这里都能够找到:

Map接口的基于哈希表的实现。此实现提供所有可选的映射操作,并允许空值和空键。 (HashMap 类大致等同于 Hashtable,只是它是非同步的并且允许空值。)该类不保证映射的顺序;特别是,它不保证订单会随着时间的推移保持不变。 此实现为基本操作(get 和 put)提供恒定时间性能,假设散列函数在存储桶中正确分散元素。迭代集合视图需要的时间与 HashMap 实例的“容量”(桶的数量)加上它的大小(键值映射的数量)成正比。因此,如果迭代性能很重要,则不要将初始容量设置得太高(或负载因子太低),这一点非常重要。 HashMap 的实例有两个影响其性能的参数:初始容量和负载因子。 容量是哈希表中的桶数,初始容量就是哈希表创建时的容量。 负载因子是衡量哈希表在其容量自动增加之前允许达到多满的指标。 当哈希表中的条目数超过负载因子和当前容量的乘积时,重新哈希表(即重建内部数据结构),使哈希表具有大约两倍的桶数。 作为一般规则,默认负载因子 (.75) 提供了时间和空间成本之间的良好折衷。 较高的值会减少空间开销,但会增加查找成本(反映在 HashMap 类的大多数操作中,包括 get 和 put)。 在设置其初始容量时,应考虑映射中的预期条目数及其负载因子,以尽量减少重新哈希操作的次数。 如果初始容量大于 最大条目数除以负载因子,不会发生重新哈希操作。 如果要在一个 HashMap 实例中存储许多映射,则创建具有足够大容量的映射将允许更有效地存储映射,而不是让它根据需要执行自动重新散列以增加表。 请注意,使用具有相同 hashCode() 的多个键是降低任何哈希表性能的可靠方法。 为了改善影响,当键是 Comparable 时,此类可以使用键之间的比较顺序来帮助打破联系。 请注意,此实现不是同步的。 如果多个线程并发访问一个散列映射,并且至少有一个线程在结构上修改了映射,则必须在外部进行同步。 (结构修改是添加或删除一个或多个映射的任何操作;仅更改与实例已包含的键关联的值不是结构修改。)这通常由 同步一些自然封装地图的对象。 如果不存在这样的对象,则应使用 Collections.synchronizedMap 方法“包装”地图。 这最好在创建时完成,以防止对地图的意外不同步访问: Map m = Collections.synchronizedMap(new HashMap(…)); 此类的所有“集合视图方法”返回的迭代器都是快速失败的:如果在迭代器创建后的任何时间对映射进行结构修改,除了通过迭代器自己的 remove 方法以外的任何方式,迭代器都会抛出 ConcurrentModificationException . 因此,面对并发修改,迭代器快速而干净地失败,而不是在未来不确定的时间冒着任意、非确定性行为的风险。 请注意,无法保证迭代器的快速失败行为,因为一般而言,在存在非同步并发修改的情况下不可能做出任何硬保证。 快速失败迭代器 尽最大努力抛出 ConcurrentModificationException。 因此,编写一个依赖此异常来确保其正确性的程序是错误的:迭代器的快速失败行为应该仅用于检测错误。

自己如何去实现一个最初版的HashMap?

HashMap涉及知识点:

  1. 散列表实现
  2. 扰动函数
  3. 初始化容量
  4. 负载因子
  5. 扩容元素拆分
  6. 链表树化
  7. 红黑树
  8. 插入
  9. 查找
  10. 删除
  11. 遍历
  12. 分段锁

HashMap常见面试题

HashMap默认的容量为什么是16?

16是2^4,主要得是2次幂,因为key计算索引的公式为key.hashcode() & (初始容量 -1)。这样设计的原因,是单纯用key.hashcode()散列碰撞太高,而 二次幂-1 的二进制除了最高位,其他全是1,可以在保证不数据越界的情况下,由hashcode二进制的后几位决定最终索引,这样计算后可以减少散列碰撞。选取2^4只是因为官方觉得它是一个合适的数据,至于是否有数学上的考虑,没有给与官方说明。

HashCode为什么使用31作为乘数 ?

HashMap为什么线程不安全?

HashMap的数据结构和底层原理?

HashMap的特性?

HashMap中的put是如何实现的?

谈一下hashMap中什么时候需要进行扩容,扩容resize()又是如何实现的?

谈一下hashMap中get是如何实现的?

谈一下HashMap中hash函数是怎么实现的?还有哪些hash函数的实现方式?

为什么不直接将key作为哈希值而是与高16位做异或运算?

为什么是16?为什么必须是2的幂?如果输入值不是2的幂比如10会怎么样?

为什么是16的原因:

首先容量必须得是2的次幂,经过官方测试,16的话相对于8、32之类的来说,不大不小正好合适。

2次幂的原因:

我们初衷是为了使用hashkey计算数组下标,但是hashkey取值范围太大,会数组越界,所以需要 & 数组长度来约束一下,如果随便定义一个数,比如5或者6,它的二进制低位会有很多0, & 操作后会影响hashkey的算法,造成很多重复,产生碰撞。而 2^n 作为长度,& 操作的时候 2^n - 1,就不会有这种其多余影响。与其造成麻烦,不如减少麻烦。

e.hash & (newCap - 1) //e就是key,newCap就是初始容量16

在HashMap源码有这么一段,为了减少散列碰撞,计算key的索引位置的公式为key.hashcode()& 2^n - 1。为了 & 运算得到合适结果,除了最高位其余都得是1,只有出现2的倍数减1的时候,才会出现01111这样的值。

谈一下当两个对象的hashcode相等时会怎么样?

会碰撞,插入到同一个桶下的链表内,如果链表长度大于8,桶数组容量<64,还会进行扩容。

如果两个键的hashcode相同,你如何获取值对象?

看看是不是两个key的值是不是相同,看看是不是TreeNode类型,如果是树类型,使用红黑树的getTreeNode。如果是不是,则遍历链表,利用key的不同查找。

如果HashMap的大小超过了负载银子(load factor)定义的容量,怎么办?

先判断,当前容量有没有到达最大容量,再判断如果扩容后的容量有没有达到最大容量。如果没有,则进行<<1扩容。

HashMap和HashTable的区别?

解释下HashMap的参数loadFactor,它的作用是什么?

传统hashMap的缺点?

平时在使用HashMap时一般是使用什么类型的元素作为key?

为什么默认负载因子是0.75?

这个值官方给了解释,说是

 * As a general rule, the default load factor (.75) offers a good
 * tradeoff between time and space costs.  Higher values decrease the
 * space overhead but increase the lookup cost (reflected in most of
 * the operations of the <tt>HashMap</tt> class, including
 * <tt>get</tt> and <tt>put</tt>)
//作为一般规则,默认负载因子 (.75) 在时间和空间成本之间提供了很好的*权衡。较高的值减少了*空间开销,但增加了查找成本(反映在大多数HashMap类的操作中,包括*get和put)。

当然或许有数学上的考究,但是官方没有给与正面的回答。

当然可以用别的负载因子,负载银子本质上阈值。如果你决定用空间换时间,可以把负载因子调的更小一些,减少碰撞。

HashMap存放数据的方法流程?即put方法

在这里插入图片描述

HashMap中链表怎么转成红黑树?

HashMap中的扩容机制?

resize()

  1. 新建成员变量oldcap,oldthr,newcap,newthr,oldTable,newTable
  2. 判断oldcap容量如果大于最大等于容量,则保持当前容量
  3. 判断oldcap容量大于等于初始容量,且扩容后容量没有超过最大容量,则**<< 1 进行扩容,然后赋值给**newTable,
  4. 如果oldcap容量不大于0,则进行初始化
  5. 遍历通数组oldTable
  6. 判断当前通有没有下一个元素,没有则直接插入 newTab[e.hash & (newCap - 1)] = e;
  7. 如果有下一个元素,则判断当前桶的类型是不是TreeNode,如果是则交给红黑树的splict方法拆分
  8. 如果不是红黑树,则根据(e.hash & oldCap) == 0计算,扩容后,元素是在原来的位置newTab[j] ,还是在newTab[j + oldCap]

HashMap里的链表是头插还是尾插?

p.next = newNode(hash, key, value, null);

1.8Jdk尾插。

1.8之前头插,头插在多线程中可能会出现循环链表,get的时候造成死循环。

(e.hash & oldCap) == 0算法的合理性?

oldCap 的二进制是 最高位为1,其余为0,也就是类似这种:

10000

那么e.hash得是最高位为0,类似这种

01001

我们计算扩容后数组坐标的位置公式:e.hash & (newCap - 1)

由于newCap>oldCap且最高位也是1,其余位也是0.,那么可能会这样

100000

最后结果的e.hash & (newCap - 1)和e.hash & (oldCap- 1)

001001& 011111是等同于 001001& 001111。取决于最后几位,两者运算结果相同

所以(e.hash & oldCap) == 0,就说明在新数组中位置没有变。

如果(e.hash & oldCap) != 0,

则说明 e.hash 可能是这样的,对应oldCap的高位处是1

10001

那么最后结果的e.hash & (newCap - 1)和e.hash & (oldCap - 1)

010001& 011111 和010001& 001111相比,最终取决于newCap 非0最高位的1,也就是oldCap << 1的那一部分,相当于位置索引处于多了一个oldCap 。newTab[j + oldCap]

HashMap查找流程图:

在这里插入图片描述

HashMap中的删除流程图?

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值