1.HashMap的数据结构?
数组+链表实现,当链表长度超过8时,链表转换为红黑树
2.HashMap的工作原理?
底层是hash数组和单向链表实现,数组中每个元素都是链表,
K/V存储put的过程
1)调用hash(k)方法计算K的hash值,然后结合数组长度,计算数组下标;
2)调整数组大小(当容器的元素个数大于capacity * loadfactor时,容器会进行扩容resize为2n);
3)a)如果K的hash值在HashMap中不存在,则执行插入,若存在,则发生碰撞;
b)如果K的hash值存在,而且两者equals返回true,则更新键值对;
c)如果K的hash值存在,而且两者equals返回false,则插入链表的尾部(尾插法)或红黑树中。
获取对象,将K传给get()方法
1)调用hash(K)方法从而获取该键值所在链表的数组下标
2)顺序遍历链表,equals查找相同Node链表中K值对应的V值
总结:hashCode是定位的,存储位置;equals是定性的,比较两者是否相等
3.当两个对象的hashCode相同会发生什么?
因为 hashCode 相同,不一定就是相等的(equals方法比较),所以两个对象所在数组的下标相同,"碰撞"就此发生。又因为 HashMap 使用链表存储对象,这个 Node 会存储到链表中
4.hash如何实现?为什么要这样?
JDK1.8中,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^(h>>>16),主要从速度、功效和质量来考虑的,减少系统的开销,也不会造成因为高位没有参与下标的计算,从而引起的碰撞。
5.为什么要用异或运算符?
保证对象的hashCode的32位值只要有一位发生改变,整个hash()返回值就会改变。尽可能减少碰撞。
6.HashMap的table的容量如何确定?loadFactor是什么?该容量如何变化?这种变化会带来什么问题?
1)table数组大小是由capacity这个参数确定的,默认16,也可以构造时传入,最大限制1<<30
2)loadFactor是装载因子,用来判断table数组是否要动态扩展,默认值是0.75,比如table大小为16,装载因子为0.75,当table实际大小超过12时就要动态扩容
3)扩容时,调用resize()方法,将table长度变为原来的两倍(是table的长度)
4)如果数据很大的情况下,扩展时将会带来性能的损失,在性能要求很高的地方,这种损失很可能很致命
7. 数组扩容的过程?
如果数据很大的情况下,扩展时将会带来性能的损失,在性能要求很高的地方,这种损失很可能很致命
8.拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?
二叉树在特殊的情况下会变成一条先行结构,遍历查询会非常慢。而红黑树在插入新数据时可能需要左旋、右旋、变色这些操作来保持平衡,引入红黑树就是查找数据块,解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢
9.说说对红黑树的见解?
1)每个节点非红即黑
2)根节点总是黑色的
3)如果节点是红色的,则它的子节点必须是黑色的(反之不一定)
4)每个叶子节点都是黑色的空节点(NIL节点)
5)从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)
10.HashMap、LinkedHashMap、TreeMap的区别?
1)LinkedHashMap:保存了记录的插入顺序,在用Iterator遍历时,选取到的记录肯定是先插入的;遍历比HashMap慢
2)TreeMap:实现SortMap接口,能够把它保存的记录根据键排序(默认按键值升序排序)
11. HashMap和HashTable有什么区别?
1)HashMap线程不安全,HashTable是线程安全的
2)由于线程安全,HashTable的效率比不上HashMap
3)HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而HashTable不允许
4)HashMap 默认初始化数组的大小为16,HashTable 为 11,前者扩容时,扩大两倍,后者扩大两倍+1;
5)HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode;
12.为什么ConcurrentHashMap比HashTable效率要高?
1)HashTable使用一把锁锁住整个链表结构来处理并发问题,多个线程竞争一把锁,容易阻塞
2)ConcurrentHashMap
a)1.7中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个HashMap分成多个段,每段分配一把锁,支持多线程访问。锁粒度:基于Segment,包含多个HashEntry。
b)1.8中使用CAS+synchronized+Node+红黑树。锁粒度:Node(首节点)(实现Map.Entry)。锁力度降低了
12.针对ConcurrentHashMap锁机制具体分析(1.7 vs 1.8)
1.7中采用分段锁的机制,实现并发的更新操作,底层采用数组+链表的存储结构,包括两个核心的静态内部类Segment和HashEntry
1)Segment继承ReentrantLock(重入锁)用来充当锁的角色,每个Segment对象守护每个散列映射表的若干个桶;
2)HashEntry用来封装映射表的键值对
3)每个桶是由若干个HashEntry对象链接起来的链表
1.8中,采用Node + CAS + Synchronized来保证并发安全。取消类Segment,直接用table数组存储键值对;当HashEntry对象组成的链表长度超过TREEIFY_THRESHOLD时,链表转为红黑树,提升性能
13. ConcurrentHashMap在1.8中,为什么要使用内置锁Synchronized来替代重入锁ReentrantLock?
1)粒度降低了
2)JVM开发团队没有放弃synchronized,而且基于JVM的synchronized优化空间更大,更加自然
3)在大量的数据操作下,对于JVM的内存压力下,基于API的ReentrantLock会开销更多的内存