9 hashMap

cathy_mu

hashMap

非线程安全,
hash冲突采用拉链法解决
多线程操作导致死循环问题主要原因在于并发下的 Rehash 会造成元素之间会形成一个循环链表https://coolshell.cn/articles... 这个写的不是很好

四个构造方法

1.将负载因子变量设为默认值0.75(默认情况下,HashMap 初始容量是16,默认的负载因子是0.75是因为权衡了时间和空间成本)
2.传入初始容量(hashMap里没有字段保存初始容量,构造方法将阈值设为比初始容量大的2的幂,当初始化桶数组时,设初始容量=阈值)+将负载因子设为默认值
3.传入初始容量和负载因子
4.将另一个 Map 中的映射拷贝一份到自己的存储结构中来

查找

先根据(n - 1)&hash定位桶,然后对链表或红黑树进行查找
HashMap 中桶数组的大小length总是2的幂(求位置的时候可以用位运算速度快),此时,(n - 1) & hash 等价于对 length 取余。取余的计算效率没有位运算高。只有n是2的幂次,才能用1111做位运算。
image.png

重新计算的 hash

图中的 hash 由键的 hashCode 产生。计算余数时,由于 n 比较小,hash 只有低4位参与了计算,导致高位数据没发挥作用。为了处理这个缺陷,我们以hash 高4位数据与低4位数据进行异或运算,即 hash ^ (hash >>> 4)。以此加大低位信息的随机性,变相的让高位数据参与到计算中。

遍历

遍历时顺序是稳定的,但是和插入顺序不一样
image.png

插入

当桶数组 table 为空时,通过扩容的方式初始化 table
1查找要插入的键值对是否已经存在,存在的话根据条件判断是否用新值替换旧值
2如果不存在,则将键值对链入链表中,并根据链表长度决定是否将链表转为红黑树
3判断键值对数量是否大于阈值,大于的话则进行扩容操作

扩容

每次扩成原来的两倍
1计算新桶数组的容量 newCap 和新阈值 newThr
2根据计算出的 newCap 创建新的桶数组,桶数组 table 也是在这里进行初始化的
3将键值对节点重新映射到新的桶数组里。如果节点是 TreeNode 类型,则需要拆分红黑树。如果是普通节点,则节点按原顺序进行分组。

键值对节点重新映射的过程

对于树形节点,先拆分红黑树再映射。对于链表类型节点,则需先对链表进行分组,然后再映射。
假如是16个桶的map,每个hash值根据后四位分在16个桶里。当桶的数量扩充到32后,每个hash根据后5位分在32个桶里,所以原来一个桶的元素,只会进入固定的两个新桶而且相对位置不变。
1.7为了防碰撞,hash时加入随机种子以增加hash的随机性,扩容过程中会根据容量判断是否需要重新生成随机种子。1.8因为加了红黑树不怕碰撞了,所以不加随机种子,扩容时不需要重新hash,效率更高。

树化


链表长度大于等于 TREEIFY_THRESHOLD
桶数组容量大于等于 MIN_TREEIFY_CAPACITY
时,才能发生树化

树节点对比

HashMap 在设计之初,并没有考虑到以后会引入红黑树进行优化。所以并没有像 TreeMap 那样,要求键类实现 comparable 接口或提供相应的比较器。但由于树化过程需要比较两个键对象的大小,为了解决这个问题,HashMap 是做了三步处理,确保可以比较出两个键的大小,如下:
1比较键与键之间 hash 的大小,如果 hash 相同,继续往下比较
2检测键类是否实现了 Comparable 接口,如果实现调用 compareTo 方法进行比较
3如果仍未比较出大小,就需要进行仲裁了,仲裁方法为 tieBreakOrder(大家自己看源码吧)
tie break 是网球术语,可以理解为加时赛的意思,起这个名字还是挺有意思的。
?为什么比较键hash的大小可以比较键的大小?

红黑树拆分

在将普通链表转成红黑树时,HashMap 通过两个额外的引用 next 和 prev 保留了原链表的节点顺序。这样再对红黑树进行重新映射时,完全可以按照映射链表的方式进行。新链表如果太长,再进行一次树化。

桶不能序列化

桶数组 table 被申明为 transient。transient 表示易变的意思,在 Java 中,被该关键字修饰的变量不会被默认的序列化机制序列化。
HashMap 并没有使用默认的序列化机制,而是通过实现readObject/writeObject两个方法自定义了序列化的内容。
但序列化 talbe 存在着两个问题:
table 多数情况下是无法被存满的,序列化未使用的部分,浪费空间
同一个键值对在不同 JVM 下,所处的桶位置可能是不同的,在不同的 JVM 下反序列化 table 可能会发生错误。
如果键没有覆写 hashCode 方法,计算 hash 时最终调用 Object 中的 hashCode 方法。Object 中的 hashCode 方法是 native 型的,不同的 JVM 下,可能会有不同的实现,产生的 hash 可能也是不一样的。也就是说同一个键在不同平台下可能会产生不同的 hash。

阅读 230
15 声望
1 粉丝
0 条评论
你知道吗?

15 声望
1 粉丝
宣传栏