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涉及知识点:
- 散列表实现
- 扰动函数
- 初始化容量
- 负载因子
- 扩容元素拆分
- 链表树化
- 红黑树
- 插入
- 查找
- 删除
- 遍历
- 分段锁
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()
- 新建成员变量oldcap,oldthr,newcap,newthr,oldTable,newTable
- 判断oldcap容量如果大于最大等于容量,则保持当前容量
- 判断oldcap容量大于等于初始容量,且扩容后容量没有超过最大容量,则**<< 1 进行扩容,然后赋值给**newTable,
- 如果oldcap容量不大于0,则进行初始化
- 遍历通数组oldTable
- 判断当前通有没有下一个元素,没有则直接插入 newTab[e.hash & (newCap - 1)] = e;
- 如果有下一个元素,则判断当前桶的类型是不是TreeNode,如果是则交给红黑树的splict方法拆分
- 如果不是红黑树,则根据(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]