HashMap面试题汇总
hashmap
- | JDK7 | JDK8 |
---|
数据结构 | 数组+链表 | 数组+链表/红黑树 |
链表插入 | 头插法 | 尾插法 |
key==null | table[0] | - |
Iterator去删除元素 | modCount 实现 fail-fast | - |
resize | 头插法,链表数据顺序翻转。多线程会造成循环链表 | 尾插法 |
hash实现 | | (h = key.hashCode()) ^ (h >>> 16) |
| Entry | Node |
| 先判断是否需要扩容,再插入 | 先进行插入,插入完成再判断是否需要扩容 |
| 在多线程环境下,扩容时会造成环形链或数据丢失 | 在多线程环境下,会发生数据覆盖的情况 |
| | |
- 当链表长度 >=8 时,链表转换为红黑树。TREEIFY_THRESHOLD = 8;((桶的数量必须大于64,小于64的时候只会扩容))
- 当链表长度 <=6 时,红黑树转换为链表。UNTREEIFY_THRESHOLD = 6;(O(logn))
- 当两个对象的hashCode相同会发生什么?
因为hashCode相同,不一定就是相等的(equals方法比较),所以两个对象所在数组的下标相同,"碰撞"就此发生。又因为HashMap使用链表存储对象,这个Node会存储到链表中。 - JDK1.8中,是通过hashCode()的高16位异或低16位实现的:(h=k.hashCode())^(h>>>16),主要是从速度,功效和质量来考虑的,减少系统的开销,也不会造成因为高位没有参与下标的计算,从而引起的碰撞。
如果当n即数组长度很小,假设是16的话,那么n - 1即为1111 ,这样的值和hashCode直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,这样就很容易造成哈希冲突了,所以这里把高低位都利用起来,从而解决了这个问题。 - 为什么要用异或运算符?
保证了对象的hashCode的32位值只要有一位发生改变,整个hash()返回值就会改变。尽可能的减少碰撞。 - 数组扩容的过程?
创建一个新的数组,其容量为旧数组的两倍,并重新计算旧数组中结点的存储位置。
结点在新数组中的位置只有两种,原下标位置或原下标+旧数组的大小。
当HashMap中的其中一个链表的对象个数如果达到了8个,此时如果数组长度没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链表会变成红黑树,结点类型由Node变成TreeNode类型。当然,如果映射关系被移除后,下次执行resize方法时判断树的结点个数低于6,也会再把树转换为链表 - 拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?
二叉查找树在特殊情况下会变成一条线性结构。
红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题。
红黑树属于平衡二叉树,但是为了保持"平衡"是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。 - LinkedHashMap保存了记录的插入顺序,在用Iterator遍历时,先取到的记录肯定是先插入的;遍历比HashMap慢;
- TreeMap实现SortMap接口,能够把它保存的记录根据键排序(默认按键值升序排序,也可以指定排序的比较器)
- HashMap是线程不安全的,HashTable是线程安全的;
由于线程安全,所以HashTable的效率比不上HashMap;
HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而HashTable不允许;
HashMap默认初始化数组的大小为16,HashTable为11,前者扩容时,扩大两倍,后者扩大两倍+1;
HashMap需要重新计算hash值,而HashTable直接使用对象的hashCode; - HashMap的底层数组长度为何总是2的n次方?
使数据分布均匀,减少碰撞
当length为2的n次方时,h&(length - 1) 就相当于对length取模,而且在速度、效率上比直接取模要快得多 - 为什么默认是16呢?怎么不是4?不是8?
这应该就是个经验值,既然一定要设置一个默认的2^n 作为初始值,那么就需要在效率和内存使用上做一个权衡。这个值既不能太小,也不能太大。 - JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是JDK1.8不会倒置。
- 把默认容量设置成initialCapacity / 0.75F + 1.0F是一个在性能上相对好的选择,但是,同时也会牺牲些内存。
红黑树
性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3. 每个叶节点是黑色的。
性质4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
concurrentHashmap
- | JDK7 | JDK8 |
---|
DEFAULT_CONCURRENCY_LEVEL | 16 | |
MIN_SEGMENT_TABLE_CAPACITY | 2 | |
segment | Segment<K,V> extends ReentrantLock | sun.misc.Unsafe UNSAFE;//CAS |
rehash(node) | 扩容只是针对某个segment扩容,链表是头插法 | |
| 分段的数组+链表 | 数组+链表/红黑二叉树 |
| 分段锁 | 并发控制使用 synchronized 和 CAS 来操作 |
- HashMap的键值对允许有null,但是ConCurrentHashMap都不允许
- 在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
- Unsafe类是"final"的,不允许继承。且构造函数是private的。因此我们无法在外部对Unsafe进行实例化。通过反射来获取Unsafe。
- jdk1.7 concurrentHashMap ,put的时候计算segment[] 下标时为什么取hash值的高位呢?
两层数组长度一样的话,如果计算下标的方法相同,每次两层的下标就会相同,第二层只能用其中的一个位置,其他的位置用不到,会造成空间浪费。 - JDK1.7的ConcurrentHashMap中的put方法,在根据key计算得出Segment数组下标之后,
使用了UNSAFE.getObject(segments, (j << SSHIFT) + SBASE),根据数组对象和偏移量来获取数组下标的元素,
那么为什么不直接使用segments[j]这种方式呢?
直接内存访问,为了加快速度。segments[j]会进行越界检查,效率低。系统类库因为需要大量调用,所以在保证正确的前提下,尽量做优化。 - ReentrantLock.tryLock() //不阻塞
ReentrantLock.lock() //阻塞
while (!tryLock()){} //耗费CPU - ConcurrentHashMap在JDK1.8中,为什么要使用内置锁synchronized来代替重入锁ReentrantLock?
粒度降低了;
JVM开发团队没有放弃synchronized,而且基于JVM的synchronized优化空间更大,更加自然。
在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存。
hashtable
- synchronized关键字