Map的实现类:
HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap
底层结构:
hash表:数组+链表
HashMap:数组+链表/红黑树
LinkedHashMap:数组+链表+双向链表
ConcurrentHashMap:数组+链表/红黑树
new HashMap时:
(构造方法可以指定初始大小和负载因子,但是是懒惰创建数组,put时才创建)
默认初始大小为16,负载因子为0.75
hash的大小只能是2次幂的。如传10,初始值为16;传7,初始值为8。
负载因子的大小决定了哈希表的扩容和哈希冲突
以初始大小16和负载因子0.75为例,意味着数组最多只能放12个元素,即16*0.75个,一旦超过12个,hash表就需要扩容。每次put元素进去的时候,都需要检查有没有超过这个阈值,如果有,就扩容。扩容大小默认为原来的2倍(维持2次幂的规则)
负载因子的默认大小为何是0.75?
在空间占用与查询时间之间取得较好的权衡
大于这个值,空间节省了,但链表就会比较长影响性能
小于这个值,冲突减少了,但扩容就会更频繁,空间占用多
扩容肯定是耗时的,那我是否可以将负载因子调高,比如1,这样就到长度为16的时候才扩容,减少扩容的次数呢?
可以,但不推荐。负载因子调高,意味着hash冲突的概率会增大,会导致查找的速度变慢,同样会耗时。
hashMap中如何判断一个元素是否相同?
先比较hash值,再用==或者equels比较值是否相等,两者都相同,说明元素是同一个。
hashMap结构为:数组+链表/红黑树,什么情况下会用到红黑树呢?
当数组的大小大于等于64,且链表的长度大于8的时候,链表才会改为红黑树,当红黑树大小为6时,会退化为链表。
退化为链表主要是对查询和插入时性能的考量。链表查询时间复杂度O(N),插入时间复杂度O(1),红黑树查询和插入时间复杂度O(log2N)。
为什么要用红黑树?
链表长度过长影响查询效率,因此在链表长度超过一定阈值的时候,就转变为红黑树来优化查询效率。
为什么一上来不用红黑树?
短链表性能比红黑树要好,没有必要树化。链表的底层是node,红黑树的底层是treenode,treenode里的成员变量要比链表多很多,占用更多的内存。从性能和内存占用上来看,没有必要一上来就树化。
链表会超过8吗?
会。长度小于64,但key的原始hash值又相等,扩容并不能缩短链表。
扩容的时机?
元素超过16*0.75,或者链表的长度大于8但长度又小于64。
为什么阈值选择8?
红黑树本身应当被视为一种偶然情况,用来防止DoS攻击,防止链表超长而性能下降。常规业务中很难遇到链表长度为超过8的情况。hash值如果足够随机,应当符合泊松分布,在负载因子为0.75,出现长度超过8的链表导致树化的概率为亿分之6。
树退化的两种情况:
1、扩容时,树拆分后元素小于等于6;
2、remove节点时,根节点、左子、右子、左孙有一个为null,退化成链表,四个都存在,则不会退化。
如何计算索引?
计算对象的hashCode(),再进行调用HashMap的hash()方法进行二次哈希,最后&(capacity-1)(按位与数组容量-1)得到索引(桶下标),比普通的取模运算效率更高。前提:数组长度的2的n次幂。
如何得到二次hash值?为什么要用二次哈希来计算?
1.8源码:(h = key.hashCode()) ^ (h >>> 16)
原始hash与其高16位做异或运算得到二次hash值。
对原始hash进行一个扰动,让高位的数字也参与运算,使其分布的更加均匀,避免链表过长的情况。
为什么数组长度是2的n次幂?
有2点好处:
1、计算索引时可以用按位与代替取模,提高计算性能;
2、扩容时,重新计算索引时可以进行计算的优化。用二次hash值按位与原始容量,结果为0不用移动,结果不为0需要移动,新的索引即为原始索引+原始容量。
缺点:hash分布性不如长度为质数的时候。
综合考虑,设计者选择性能更优,因此选择2的n次幂作为数组容量。
put方法的实现?
1,对key做hash运算,得到key所在的index;
2,如果没有碰撞,直接放到数组中;
3,如果碰撞了
1)判断是链表结构还是红黑树结构,根据结构进行插入;
2)如果key值相同,替换原来的值;
4,判断hash表是否满了,满了则扩容。(先扩容,再迁移)
get方法实现?
1,对key做hash运算,得到key所在的index;
2,如果没有碰撞,直接取出
3,如果碰撞了,判断是链表结构还是红黑树结构,根据结构取出;
HashMap是线程安全的吗?
HashMap不是线程安全的。
1.7:扩容死链(头插法导致,变量e和next,next指向了上一个元素,导致链表中的元素循环引用)
1.7、1.8:数据错乱(HashMap有可能出现多线程操作下并发丢失数据的现象)
HashMap的key可以为null吗?作为key的对象有什么要求?
hashMap可以,其他的不行。
要求:重写hashCode和equals方法,并且key的内容不能修改。
String的hashCode是如何设计的?为什么每次都是乘31?
hashCode的设计的目的应该是让其具有更好的散列性。
乘31是因为:
1,31有更好的散列性,即分布更加均匀;
2,31的乘法可以转变为位运算(左移5位再做减法),使计算更加高效。
关于LinkedHashMap:
底层:数组+链表+双向链表。实际就是在HashMap的基础上,维护了一个双向链表,可以使得插入有序(非大小),在遍历的时候也是采用双向链表,所以其大小不会影响到遍历的性能。
关于TreeMap:
底层:红黑树。TreeMap的key不能为null,TreeMap有序是通过Comparator来进行比较的,如果Comparator为null,则使用自然排序。
关于ConcurrentHashMap:
ConcurrentHashMap是线程安全的Map实现类。
还有Hashtable,或用Collections包装出一个线程安全的Map,但由于这两种线程安全都是在外层套synchronize,所以我们一般有线程安全问题考量的,都使用ConcurrentHashMap。
ConcurrentHashMap底层数组+链表/红黑树,支持高并发的访问和更新,是线程安全的。
ConcurrentHashMap是如何实现线程安全的?
分段加锁,1.8之后优化了锁的细粒度,对每个元素进行CAS操作,即比较再交换
HashMap 1.8和1.7的区别
HashMap | 1.8 | 1.7 |
---|---|---|
底层结构 | 数组+链表/红黑树 | 数组+链表 |
链表插入时 | 尾插法 | 头插法 |
扩容时机 | 大于阈值就扩容 | 大于阈值且没有空位 |
扩容计算 | 计算索引时根据按位与优化计算 | 无优化 |