写点什么

开发多年 put、get、resize 不知道,springcloud 视频讲解

用户头像
极客good
关注
发布于: 刚刚

if (++size > threshold)


resize();


afterNodeInsertion(evict);


return null;


}


虽然写我加了注释,但是我还是简单说一下这个的逻辑吧


1.首先判断哈希表,是否存在,不存在的时候,通过 resize 进行创建


2.然后在通过索引算法计算哈希表上是否存在该数据,不存在就新增 node 节点存储,然后方法结束


3.如果目标索引上存在数据,则需要用 equals 方法判断 key 的内容,要是判断命中,就是替换 value,方法结束


4.要是 key 也不一样,索引一样,那么就是哈希冲突,HashMap 解决哈希冲突的策略就是遍历链表


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


,找到最后一个空节点,存储值,就像我的图一样。灵魂画手有木有,很生动的表示了 HashMap 的数据结构


5.最后一步就是判断是否到扩容阀值,容量达到阀值后,进行一次扩容,按照 2 倍的规则进行扩容,因为要遵循哈希表的长度必须是 2 次幂的概念


好,put 告一段落,我们继续 get 吧


public V get(Object key) {


Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value;


}


get 方法,恩,好,很简单。hash 一下 key,然后通过 getNode 来获取节点,然后返回 value,恩。get 就讲完了,哈哈。开个玩笑。我们继续看 getNode 吧


final Node<K,V> getNode(int hash, Object key) {


Node<K,V>[] tab; Node<K,V> first, e; int n; K k;


//哈希表存在的情况下,根据 hash 获取链表的头,也就是 first 对象


if ((tab = table) != null && (n = tab.length) > 0 &&


(first = tab[(n - 1) & hash]) != null) {


//检测第一个 first 是的 hash 和 key 的内容是否匹配,匹配就直接返回


if (first.hash == hash && // always check first node


((k = first.key) == key || (key != null && key.equals(k))))


return first;


//链表的头部如果不是那就开始遍历整个链表,如果是红黑树节点,就用红黑树的方式遍历


//整个链表的遍历就是通过比对 hash 和 equals 来实现


if ((e = first.next) != null) {


if (first instanceof TreeNode)


return ((TreeNode<K,V>)first).getTreeNode(hash, key);


do {


if (e.hash == hash &&


((k = e.key) == key || (key != null && key.equals(k))))


return e;


} while ((e = e.next) != null);


}


}


return null;


}


我们再整理一下,get 方法比 put 要简单很多,核心逻辑就是取出来索引上的节点,然后挨个匹配 hash 和 equals,直到找出节点。


那么 get 方法就搞定了


再来看一下 resize 吧。就是 HashMap 的扩容机制


final Node<K,V>[] resize() {


Node<K,V>[] oldTab = table;


//检测旧容器,如果旧容器是空的,就代表不需要处理旧数据


int oldCap = (oldTab == null) ? 0 : oldTab.length;


//保存扩容阀值


int oldThr = threshold;


int newCap, newThr = 0;


if (oldCap > 0) {


if (oldCap >= MAXIMUM_CAPACITY) {


threshold = Integer.MAX_VALUE;


return oldTab;


}


// 对阀值进行扩容更新,左移 1 位代表一次 2 次幂


else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&


oldCap >= DEFAULT_INITIAL_CAPACITY)


newThr = oldThr << 1; // double threshold


}


else if (oldThr > 0) // initial capacity was placed in threshold


newCap = oldThr;


else { // zero initial threshold signifies using defaults


newCap = DEFAULT_INITIAL_CAPACITY;


newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);


}


//如果哈希表是空的,这里会进行初始化扩容阀值,


if (newThr == 0) {


float ft = (float)newCap * loadFactor;


newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?


(int)ft : Integer.MAX_VALUE);


}


threshold = newThr;


@SuppressWarnings({"rawtypes","unchecked"})


Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];


table = newTab;


//处理旧数据,把旧数据挪到 newTab 内,newTab 就是扩容后的新数组


if (oldTab != null) {


for (int j = 0; j < oldCap; ++j) {


Node<K,V> e;


if ((e = oldTab[j]) != null) {


oldTab[j] = null;


//如果当前元素无链表,直接安置元素


if (e.next == null)


newTab[e.hash & (newCap - 1)] = e;


//红黑树处理


else if (e instanceof TreeNode)


((TreeNode<K,V>)e).split(this, newTab, j, oldCap);


else { // preserve order


//对链表的索引重新计算,如果还是 0,那说明索引没变化


//如果 hash 的第 5 位等于 1 的情况下,那说明 hash & n - 1 得出来的索引已经发生变化了,变化规则就是 j + oldCap,就是索引内向后偏移 16 个位置


Node<K,V> loHead = null, loTail = null;


Node<K,V> hiHead = null, hiTail = null;


Node<K,V> next;


do {


next = e.next;


if ((e.hash & oldCap) == 0) {


if (loTail == null)


loHead = e;


else


loTail.next = e;


loTail = e;


}


else {


if (hiTail == null)


hiHead = e;


else


hiTail.next = e;


hiTail = e;


}


} while ((e = next) != null);


if (loTail != null) {


loTail.next = null;


newTab[j] = loHead;


}


if (hiTail != null) {


hiTail.next = null;


newTab[j + oldCap] = hiHead;


}


}


}


}


}


return newTab;


}


resize 方法的作用就是初始化容器,以及对容器做扩容操作,扩容规则就是 double


扩容完了之后还有一个重要的操作就是会对链表上的元素重新排列


(e.hash & oldCap) == 0


在讲这个公式之前,我先做个铺垫


16 的二进制是 0001 0000


32 的二进制是 0010 0000


64 的二进制是 0100 0000


我们知道 HashMap 每次扩容都是左移 1 位,其实就是 2 的 m+1 次幂,也就是说哈希表每次扩容都是 16、32、64........n


然后我们知道 HashMap 内的索引是 hash & n - 1,n 代表哈希表的长度,当 n=16 的时候,就是 hash & 0000 1111,其实就是 hash 的后四位,当扩容 n 变成 32 的时候,就是 hash & 0001 1111,就是后五位


我为啥要说这个,因为跟上边的 (e.hash & oldCap) == 0 有关,这里其实我们也可以用


假设我们的 HashMap 从 16 扩容都了 32。


其实可以用 e.hash & newCap -1 的方式来重新计算索引,然后在重排链表,但是源码作者采用的是另外一种方式(其实我觉得性能上应该一样)作者采用的是直接比对 e.hash 的第五位(16 长度是后四位,32 长度是后五位)进行 0 1 校验,如果为 0 那么就可以说明 (hash & n - 1)算出来的索引没有变化,还是当前位置。要是第五位校验为 1,那么这里(hash & n - 1)的公式得出来的索引就是向数据后偏移了 16(oldCap)位。


所以作者在这里定义了两个链表,


loHead 低位表头,loTail 低位表尾(靠近索引 0)


hiHead 高位表头,hiTail 高位表尾(远离索引 0)


然后对链表进行拆分,如果计算出来索引没有变化,那么还让他停留在这个链表上(拼接在 loTail.next 上)


如果计算索引发生了变化。那么数据就要放置在高位链表上(拼接在 hiTail.next)上


最后来个灵魂配图,链表重排

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
开发多年put、get、resize不知道,springcloud视频讲解