HashMap部分源码集解

共 7374字,需浏览 15分钟

 ·

2023-06-05 01:45


        本文将主要分析HashMap的重点方法,包括put(), get(), resize()。并对其重点代码进行解释说明。





#
# 阅读本文需具备基础的知识:



    1. 了解Java基础知识;


    2. 对HashMap有一定的了解并懂得HashMap中一些基本属性的作用;










## put 方法集解




public V put(K key, V value) {


//计算key的hash值


return putVal(hash(key), key, value, false, true);


}












final V putVal(int hash, K key, V value, boolean onlyIfAbsent,


boolean evict) {


Node[] tab; Node p; int n, i;


// Node数组非空判断,为空则调用resize方法进行初始化


if ((tab = table) == null || (n = tab.length) == 0)


n = (tab = resize()).length;


// 通过(n - 1) & hash 的方式计算出key的位置,再对当前位置的元素进行非空判断,为null则直接插入


if ((p = tab[i = (n - 1) & hash]) == null)


        tab[i] = newNode(hash, key, value, null);


else {


Node e; K k;


//判断key是否存在,存在则下面逻辑选择性覆盖旧值


if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))


e = p;


//判断是否为红黑树结构,是的话以红黑树的方式插入


else if (p instanceof TreeNode)


e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);


else {


for (int binCount = 0; ; ++binCount) {


// 链表的下一节点为null,将数据插入尾部


if ((e = p.next) == null) {


p.next = newNode(hash, key, value, null);


//链表长度 >= 8 则转变为红黑树


if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st


treeifyBin(tab, hash);


break;


}


//如果链表中存在key值相等元素则跳出循环,在下面逻辑选择性覆盖旧值


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


break;


p = e;


}


}


// 如果key已存在,则根据onlyIfAbsent选择性用新值替换旧值


if (e != null) { // existing mapping for key


V oldValue = e.value;


if (!onlyIfAbsent || oldValue == null)


e.value = value;


afterNodeAccess(e);


return oldValue;


}


}


// HashMap线程安全的保障


++modCount;


// 当Node数组的长度大于扩容阈值时则进行扩容


if (++size > threshold)


resize();


afterNodeInsertion(evict);


return null;


}


## put重点代码发明


1.  为什么用(n - 1) & hash 的方式计算元素下标?




原因:(n - 1) & hash == hash % n, 并且(n - 1) & hash 的效率要高。(n为2的次幂)


解释:


当n == 8 == 1000, hash == 59 == 111011时。


  (n - 1) & hash == 11 并且 111011 == 11 == 3,


   59 % 8 == 3


  根据位运算规则可知,二进制数中凡是低三位为0的数都可以被100整除,低三位的值即为余数;


利用位与运算,将高位置0,低三位置为原值,结果低三位的原值即为余数。


所以当n为2的次幂时,(n - 1) & hash 即为 hash % n的值



2. putVal方法中onlyIfAbsent的意义及作用



onlyIfAbsent:
    
    - 如果存在新key和旧key相同,是否用新值覆盖旧值;    
    - 如果为true,则新值不覆盖旧值。false则进行覆盖。



3. modCount的作用



记录集合被修改的次数。
当操作HashMap时,发现modCount发生改变时进入快速失败,抛出
ConcurrentModificati
onException








## resize 方法集解




final Node[] resize() {


Node[] oldTab = table;


// 获取原哈希表容量;若原哈希表为null,则容量为0


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


// 获取原哈希表扩容门槛


int oldThr = threshold;


// 初始化新哈希表容量和新哈希表门槛为0


int newCap, newThr = 0;


// 如果原容量大于0,计算扩容后的容量及新的负载扩容门槛


if (oldCap > 0) {


//判断原容量是否大于等于HashMap允许的容量最大值 - 2的30次幂


if (oldCap >= MAXIMUM_CAPACITY) {


// 如果原容量大于等于允许的最大容量,则把HashMap的扩容门槛设置为Integer允许的最大值


threshold = Integer.MAX_VALUE;


//不再扩容,直接返回


return oldTab;


}


// 把原Node数组容量扩大为原来的2倍作为新Node数组的容量。


// 如果新Node数组的容量小于HashMap所允许的容量的最大值: 2的30次幂,并且原Node数组容量大于等于默认的初始化Node数组容量: 2的4次幂 -> 16,则计算新的扩容门槛


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


// 新Node数组的扩容门槛为原Node数组的扩容门槛的2倍


newThr = oldThr << 1; // double threshold


}


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


// 如果原Node数组容量小于等于0并且原Node数组扩容门槛大于0,新Node数组容量为原Node数组扩容门槛大小


newCap = oldThr;


else { // zero initial threshold signifies using defaults


// 如果原Node数组容量小于等于0并且原Node数组的扩容门槛小于等于0,则初始化新Node数组容量为默认初始化容量:1 << 4;


newCap = DEFAULT_INITIAL_CAPACITY;


newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);


}


// 如果新的扩容门槛等于0,则重新计算新的扩容门槛


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"})


//初始化新NodeNode数组


Node[] newTab = (Node[])new Node[newCap];


table = newTab;


//旧Node数组不为null,则进行扩容


if (oldTab != null) {


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


Node e;


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


oldTab[j] = null;


if (e.next == null)


// 计算新Map的Node数组元素下标。如果当前Node节点没有后续节点,则直接放入新位置


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


// 如果当前Node是红黑树,则对树进行处理


else if (e instanceof TreeNode)


((TreeNode)e).split(this, newTab, j, oldCap);


// 处理链表


else { // preserve order


Node loHead = null, loTail = null;


Node hiHead = null, hiTail = null;


Node next;


do {


next = e.next;


// (e.hash & oldCap) == 0 元素放在低位区,否则放在高位区


// 低位区:原Node长度的位置;高位区:原Node长度到扩容后Node长度的位置


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);


// 将低位区的尾结点指向null,并把低位区的头结点放入新Node数组的合适位置


if (loTail != null) {


loTail.next = null;


newTab[j] = loHead;


}


// 将高位区的头结点放到新Node数组合适的位置


if (hiTail != null) {


hiTail.next = null;


newTab[j + oldCap] = hiHead;


}


}


}


}


}


return newTab;


}


## resize重点代码发明



  1.  为什么要进行扩容?







        减少hash碰撞,使HashMap中的结构更简单。



2. 为什么要以2的倍数作为扩容的策略?



        保证HashMap的容量都是2的次幂,便于key的位置计算和扩容的容量计算。







## get方法集解




public V get(Object key) {


Node e;


//获取key的hash值


return (e = getNode(hash(key), key)) == null ? null : e.value;


}







final Node getNode(int hash, Object key) {


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


//Node数组不为null并且其长度大于0并且key位置部位null,则获取当前key位置的Node,否则返回null


if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {


//当前位置的key和入参key相等,则返回当前Node


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


return first;


// 如果首Node节点为空,直接返回null。否则去链表或红黑树中取值


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


//判断是否是红黑树,是的话在树中取值


if (first instanceof TreeNode)


return ((TreeNode)first).getTreeNode(hash, key);


do {


                //在链表中取值,如果key值相等,则取链表中的当前Node节点


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


return e;


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


}


}


//返回null


return null;


}








疑问:


现状:在resize方法中,代码 hiTail.next = null; 表示将高位区的尾结点的next指向null。


问:"这个操作不能将高位区和低位区的Node节点通过指针相连,那么遍历链表时如何遍历到低位区的数据?为什么不将高位区尾节点的next指向低位区的头指针?"





注:


集解 - 引自《本草纲目》,意为收集业内众家释义;


发明 - 引自《本草纲目》,意为笔者独立的见解,有别于他。







浏览 34
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报