1.基本数据结构:
- jdk1.7,是数组+链表
- jdk1.8,是数组+链表+红黑树
2.树化与退化
树化意义:
- 红黑树用来避免 DoS 攻击,防止链表超长时性能下降,树化应当是偶然情况,是保底策略
- hash 表的查找,更新的时间复杂度是 O(1),而红黑树的查找,更新的时间复杂度是 O(log2n )(log以2为底的n),TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表
- hash 值如果足够随机,则在 hash 表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006,树化阈值选择 8 就是为了让树化几率足够小
树化规则:
- 当链表长度大于树化的阈值8时,先尝试扩容来减少链表的长度,如果数组的容量已经大于等于64的时候,将链表转化为树。
退化规则:
- 在扩容时,如果拆分树时,树元素的个数<=6时,则会退化成链表
- 在移出remove树节点时,若根节点,左孩子,右孩子以及左孙子有一个为null时,也会退化成链表,切记,是在remove之前判断它们四个是否为null
3.索引计算
索引计算方法:
- 首先,计算对象的 hashCode()
- 再进行调用 HashMap 的 hash() 方法进行二次哈希
- 二次 hash() 是为了综合高位数据,让哈希分布更为均匀
- 最后 hash& (capacity – 1) 得到索引
数组容量为何为2的n次幂:
- 1.计算索引时候的效率更高,如果是2的n次幂,就可以使用位与运算代替取模,效率更高
- 2.扩容时重新计算索引的效率更高,hash&oldCap==0,元素留在原来的位置;hash&oldCap!=0,新元素的位置=旧位置+oldCap(此处的Cap代表的是数组的容量)
注意:
- 二次hash是为了配合容量 为2的n次幂这一设计前提,如果hash表的容量不是2的n次幂,则就没又必要去进行二次hash
- 容量是2的n次幂这一设计,计算索引效果更好,但是hash的分散性就不好,需要二次hash进行补偿。但是hashTable并没有采用这一设计。
4.put与扩容
1).HashMap是懒惰创建数组的,只有在调用put方法的时候才去创建数组的。
2).计算索引(桶下标)table[i=(n-1)&hash],相当于hash值对数组容量取模。但是源码中是通过位运算&实现的,因为位运算的效率高
3).如果当前所以位置即桶下标还没有被人占用,就是此位置上没有元素,则创建Node占位并且返回
4).如果桶下标已经有人占用
- 已经是TreeNode,走红黑树的添加或者更新逻辑
- 是普通的Node,走链表对的添加或者更新逻辑,同时,如果链表长度超过树化阈值,则将链表转化为红黑树,再去走树化的逻辑
5).返回前检查容量是否超过阈值,一旦超过进行扩容
5.存入一个键值对的过程
1.计算hash值,取模,求出数组对应的索引值。
2.判断这个位置是否有元素,如果没有元素,则直接存入。
3.如果当前位置有元素
- 判断当前要存入的的元素与已经存入的元素的hash值是否相等,如果不相等,再去考虑存储,如果是桶状结构,把这个数据挂载到桶上,上一个指向下一个,同时还要考虑是否需要转化为树的结构 ,如果本身就是树的结构,直接存入树中即可
- 如果与已经存入的元素的hash值相等,就比较equals,如果equals不相等,则是两个不同的对象,按照上述进行存储。如果equals相等,则认为是同一个对象,认为两个键相等,就用新的值替换就得值
6.1.7 与 1.8 的区别
-
链表插入节点时,1.7 是头插法,1.8 是尾插法
-
1.7 是大于等于阈值且没有空位时才扩容,而 1.8 是大于阈值就扩容
-
1.8 在扩容计算 Node 索引时,会优化
7.扩容(加载)因子为何默认是 0.75f
-
在空间占用与查询时间之间取得较好的权衡
-
大于这个值,空间节省了,但链表就会比较长影响性能
-
小于这个值,冲突减少了,但扩容就会更频繁,空间占用也更多
8.key 的设计要求
-
HashMap 的 key 可以为 null,但 Map 的其他实现则不然
-
作为 key 的对象,必须实现 hashCode 和 equals,并且 key 的内容不能修改(不可变)
-
key 的 hashCode 应该有良好的散列性