1.7底层是数组 + 链表实现
1.8底层是数组 + 链表 + 红黑树实现
以插入key-value元素的过程来说的话:
首先会计算key的hash值,1.7的时候用的是一堆异或操作(hash散列和扰动),没记住
1.8的时候是调用hashCode方法,计算出hash值,并且与hash值的高16位做了异或操作。 ==> 保证hash值尽可能的唯一,防止hash冲突
如下:(h = key.hashCode()) ^ (h >>> 16)
然后用计算出的hash值与上数组的长度-1,计算出元素在数组中存放的位置;(这里用与运算代替了取余运算hash % length,效率更高,也是要求数组长度为2的整数倍的原因)
如果计算出的位置没有元素,则直接放入。当已经存在元素的时候,会遍历整个链表的每个元素,比较key是否相同,相同的话,直接修改value的值,如果不相同则将新元素插入到链表中;
1.7使用的是头插法;1.8使用的是尾插法。头插法多线程容易产生死循环。
扩容:
在添加元素的时候,如果所用的容量达到了阈值(capacity * 负载因子),则进行扩容。底层数组会扩容到原来长度的2倍。
在扩容的时候,会将旧的数据重新放入新数组中,1.7会重新计算元素的hash值;1.8没有重新计算hash值。
1.8是通过hash值与上老数组的容量(e.hash & oldCap)来判断元素的hash值的最高位是不是1,如果是1,则存放的位置为旧数组下标 + 老数组的容量oldCap,如果是0,说明存放的下标不变,就是旧数组下标。
树化:
jdk8开始,如果一条链表的元素个数>=树化阈值TREEIFY_THRESHOLD(默认是8),并且底层数组的长度是>=64(默认是64)的时候,就会进行树化,将链表转为红黑树;如果底层数组的长度没有达到64,会直接扩容底层数组。
当红黑树中的节点数目<=6的时候,会将红黑树转换成链表
细节:
1.8 构造方法没有创建底层数组,是在调用putVal方法的时候,调用resize方法创建的。
1.7在构造方法中就创建了底层数组。
问题:
为什么用红黑树?
当hash冲突较多的时候,链表中的元素会增多,插入、删除、查询的效率会变低,退化成O(n)
使用红黑树可以优化插入、删除、查询的效率,logN级别。
转换时机:
链表上的元素个数大于等于8 且 数组长度大于等于64;
数组长度小于64的时候,会直接扩容;
链表上的元素个数小于等于6的时候,红黑树退化成链表。
为什么数组长度大于等于64才转为红黑树?
1.避免过早的树化。如果过早的在小数组上发生了树化,可能会因为扩容导致树化的开销白白浪费(扩容了,冲突就少了,不就白树化了?)
2.减少内存占用。红黑树比链表更加占用内存。
为什么不直接用红黑树?
红黑树节点的大小是普通节点的两倍,更占用内存,为了节约空间,只有当节点数达到一定大小的时候,才转成红黑树。
定义的大小是8,符合泊松分布,大概的意思就是说,扩容因子为0.75的情况下,链表长度变成8的概率很低,这时候链表
长度小遍历还是很快的。
红黑树节点除了保存普通节点的数据和左右子节点指针外,还需要存储额外的信息,比如:
父节点指针:便于在插入、删除过程中回溯和调整树结构。
颜色属性:标识节点是红色还是黑色,用于维护树的平衡性。
为什么数组长度小于等于6才退化为链表?
平衡时间与空间,节点数少的时候,链表遍历也很快,没必要用红黑树。
用6这个数,而不是7或者8,是为了避免反复的横跳,比如一个节点反复的进行添加和删除,导致反复的树化和退化。
扩容因子为什么是0.75?
如果扩容因子设置的比较大,比如1,那么只有当元素的个数大于了数组的长度的时候,才会进行扩容,这样大大提高了空间利用率;但是很容易产生hash碰撞,也就很容易产生链表,查询效率就会很低;
如果扩容因子设置的比较小,比如0.5,那么数组还没有完全被利用的时候,就会进行扩容,空间利用率低;但是就不容易产生hash碰撞,也就不容易产生链表,查询效率高;
0.75在空间效率和时间效率之间提供了比较好的折中。
默认初始容量是16,扩容因子是0.75.
hashCode和equals方法的重要性:
hashCode会被用来计算key的hash值,equals方法会被用来判断两个键是否相同。put操作是如果两个键的hashCode相同,但是equals返回false,会认为两个键不相同。
2028

被折叠的 条评论
为什么被折叠?



