java hash槽_Java基础之HashMap(二)

本文详细解析HashMap的数据结构设计,包括初始容量、最大容量的决定机制,以及tableSizeFor方法的作用。讨论了hash槽长度的计算策略,重点讲解了链表与红黑树的转换条件。还涉及序列化问题和为何不序列化table。深入理解HashMap的核心原理。
摘要由CSDN通过智能技术生成

5927f72e958f4ebf85bdd1d121187305.png

1. 导读
经过上期的分享, 相信大家对HashMap已经有了个初步的印象, 今天将围绕下面几个问题展开:
.1 HashMap的数据结构在java中是如何设计的;
.2 HashMap序列化的问题;
2. hash槽长度的确定
我们将围绕下面HashMap的数据结构和关键代码来看HashMap的设计:

39fcfe1326f1f07942cb96e5c7faa8a1.png

11c2d2a5535dfa24e1e9e7996ed24126.png


DEFAULT_INITIAL_CAPACITY定义了上图中hash槽的默认大小; 而MAXIMUM_CAPACITY则定义了hash槽的最大值;那么hash槽的长度是[2^4, 2^30], 设计者是采用了何种机制来实现这个的呢?

616747882e8f5119f5b701daa56d6128.png


HashMap::tableSizeFor是确定hash槽长度的核心方法, 我们来看下这个方法干了什么:
.1 因为hash槽的编码是从0开始的, 所以需要将传入的长度-1;
.2 经过5次扰动以后得到新的hash槽长度;
.3 如果结果小于0, 则返回0; 否则判断是否大于最大值了, 如果大于, 则返回最大值, 这个时候不管冲突如何严重, HashMap不会再次扩容了; 反之则直接返回结果;
我们来看下为什么要经过5次扰动;
在DEFAULT_INITIAL_CAPACITY上面设计者特别强调了HashMap的初始化值一定要是2的倍数(为什么这么设计的原因我们后面说), 但是我们通过HashMap的构造函数指定一个非2的倍数的数字, 这个时候不就违背了这个设计吗?

1ab3bf74032aad6a18dec38aeec84b03.png


通过HashMap的构造函数, 构造函数还是去调用HashMap::tableSizeFor来计算当前hash槽的所需的长度, 然后赋值给threshold(这个具体作用后面说, 这里理解为当前hash槽的长度即可);
那么经过这5次扰动就一定可以得到2的倍数的长度吗? 我们以27为例( | 的作用是只有都为0时, 结果才为0, 否则都是1):

5b3a0a60099b16e2fe7599776a94dfd4.png


可以看到在右移四位的时候, 结果就已经固定了, 就是32, 所以经过HashMap::tableSizeFor扰动得到的长度一定是大于等于输入值得(只有在输入2的倍数时才会等于, 否则都会得到一个离输入值最近的大于输入值得2的倍数, 且这个值必定大于等于16);
所以new HashMap(1), 是不会像我们所想的那样指定长度为1的hash槽的, 他的长度是2;
3. hash槽和数据节点的设计

f95cb1b43c251948d318b6f875b28965.png


通过源码, 我们可以看到设计者将HashMap的存储节点设计成了单链表, 每个数据节点存储key(键值), value(存储的值), hash(当前节点的哈希码)和 next(指向下个节点的指针);
但是在JKD8中又引入了红黑树, 我们来看下红黑树的存储结构:

beacc8c4804d349d224a31258f4902b1.png


通过HashMap.TreeNode的源码可看到:
.1 维护了parent(父节点), left(左子节点), right(右子节点)的双向链表(继承了LinkedHashMap.Entry是双向链表);
.2 red: 标识该节点的颜色;
.3 prev放到红黑树的删除中讲解;
那么JDK8中是如何判断什么时候链表会升级成红黑树, 红黑树又在什么条件时变为链表呢?

985ab026d4b8faf101ce2f6e87784fc4.png


TREEIFY_THRESHOLD定义的是当链表长度大于8时, 需要升级成红黑树;
UNTREEIFY_THRESHOLD定义的是当红黑树的节点数少于6时, 需要变为链表;
至于为何是8 和 6, 因为log(8)的结果是平均查找长度为3, 而链表的平均查找长度是4, 所以才有了转换的必要; 同理当长度是6的时候, 链表的平均查找长度也是3了, 那么从树转为链表也就能理解了;
至于为什么不是只采用8作为黑红树和链表的转换条件, 那是因为6 和 8之间有个7可以作为缓冲; 如果HashMap某个槽的长度不断地做增删, 就不要频繁的做树与链表的变换了;
4. hash槽的设计

9f914dfdb801996dc1681d1c8b0a8029.png


.1 table是hash槽的底层实现, 可以看到是一个Node(数据节点)数组;
.2 threshold记录的是当前的hash槽的长度;
不知道大家有没有疑问, 我们获取hash槽的长度不就是table.length吗? 为什么还需要设计一个threshold属性来存储呢?
这就需要说到HashMap的设计了:

a9d866c630bac57e31e3a85f82a7a2b0.png


我们关注下Serializable这个关键字, 这表示HashMap是可序列化的;但是我们也要关注到table也有个关键修饰:transient; transient是告诉虚拟机, 不要序列化这个字段, 至于为什么不序列化table这个字段, 我们后面再说; 正因为这么设计, 所以才需要通过threshold在反序列化的时候告诉虚拟机, 这个HashMap的hash槽长度是多少;
5. hashMap的序列化问题
前面我们说到了table(hash槽)是不会被序列化的, 那么HashMap为什么这么设计呢?
这个就需要用到前面的知识了:HashMap::hash, 这个方法是用来确定key的hash值得, 然后再来确定这个key位于哪个hash槽; 但是我们要知道,Object::hashCode是依赖于虚拟机的底层实现的, 所以当一个HashMap在不同虚拟机之间序列化的时候, 同一个key是有可能位于不同的hash槽的,所以HashMap在序列化的使用了HashMap::writeObject来输出必须字段, 然后再使用
HashMap::readObject来读取并重新生成HashMap;
HashMap::writeObject来输出必须字段: .1 当前table的长度; .2 每个Node的key, value; HashMap::readObject: .1 根据获取到的table的长度重新生成一个对应长度的hash槽; .2 然后把每个Node插入到新的hash槽中;
本期的分享主要是这些, 如果有不足之处欢迎指正;
如果上面的内容对你有帮助, 烦请点赞转发, 谢谢;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值