HashMap底层源码解析
2021/7/27 1:06:17
本文主要是介绍HashMap底层源码解析,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
文章目录
- 前言
- 一、HashMap介绍
- 二、源码
- 1.hash方法
- 2.get方法
- 2.put方法
- 3.resize 方法
- 4.remove 方法
前言
1、本文将基于JDK1.8进行编写。
2、转载请注明文章出处
一、HashMap介绍
HashMap是一种以键-值为存储方式的数据结构,在JDK1.8中,底层的数据结构为数组+链表+红黑树
,在链表长度超过8且table数组大小大于等于64
,则将链表转化为红黑树,当红黑树元素个数降为6时
又会将红黑树转化为链表,hashmap默认的大小是16,注意,当我们实例化一个hashmap时没有传参(HashMap<Integer,String> hashMap = new HashMap<>();
),那么这时,hashmap中的table数组为null,只有当我们put一个元素时,会对hashmap进行一个扩容(resize),这个扩容操作会对hashmap进行初始化。
二、源码
1.hash方法
/* 将 hashCode 的高 16 位与 hashCode 进行异或运算, 主要是为了在 table 的 length 较小的时候,让高位也参与运算, 并且不会有太大的开销,目的是为了降低 hash 冲突的概率。 */ static final int hash(Object key) { //将key的hashcode值与key的hashcode值右移16位后的值进行异或 //得到hash值返回 int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
2.get方法
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; /* 1.对table进行校验:table不为空 && table长度大于0 && table索引位置(使用table.length - 1和hash值进行位与运算)的节点不为空 */ if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // 2.检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; // 3.如果first不是目标节点,并且first的next节点不为空则继续遍历 if ((e = first.next) != null) { if (first instanceof TreeNode) // 4.如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { // 5.执行链表节点的查找,向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } // 6.找不到符合的返回空 return null; }
2.put方法
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 1.校验table是否为空或者length等于0,如果是则调用resize方法进行初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 2.通过hash值计算索引位置,将该索引位置的头节点赋值给p,如果p为空则直接在该索引位置新增一个节点即可 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { // table表该索引位置不为空,则进行查找 Node<K,V> e; K k; // 3.判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;//这里赋值后出去后还要判断是否要覆盖老的值 // 4.判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 5.走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,使用binCount统计链表的节点数 for (int binCount = 0; ; ++binCount) { // 6.如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 7.校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点, // 减一是因为循环是从p节点的下一个节点开始的 if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); break; } // 8.如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; // 将p指向下一个节点 } } /*9.如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value, 并返回oldValue,onlyIfAbsent为true时,不用覆盖老的值 */ if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); // 用于LinkedHashMap return oldValue; } } ++modCount; // 10.如果插入节点后节点数超过阈值,则调用resize方法进行扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); // 用于LinkedHashMap return null; }
3.resize 方法
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; //这里的oldCap等于null对应了前面hashmap介绍时,put第一个元素resize的场景 int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; // 1.老表的容量不为0,即老表不为空 if (oldCap > 0) { // 1.1 判断老表的容量是否超过最大容量值:如果超过则将阈值设置为Integer.MAX_VALUE,并直接返回老表, // 此时oldCap * 2比Integer.MAX_VALUE大,因此无法进行重新分布,只是单纯的将阈值扩容到最大 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 1.2 将newCap赋值为oldCap的2倍,如果newCap<最大容量并且oldCap>=16, 则将新阈值设置为原来的两倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } // 2.如果老表的容量为0, 老表的阈值大于0, 是因为初始容量被放入阈值,则将新表的容量设置为老表的阈值 else if (oldThr > 0) newCap = oldThr; else { // 3.老表的容量为0, 老表的阈值为0,这种情况是没有传初始容量的new方法创建的空表,将阈值和容量设置为默认值 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 4.如果新表的阈值为0, 则通过新的容量*负载因子获得阈值 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } // 5.将当前阈值设置为刚计算出来的新的阈值,定义新表,容量为刚计算出来的新容量,将table设置为新定义的表。 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 6.如果老表不为空,则需遍历所有节点,将节点赋值给新表 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { // 将索引值为j的老表头节点赋值给e oldTab[j] = null; // 将老表的节点设置为空, 以便垃圾收集器回收空间 // 7.如果e.next为空, 则代表老表的该位置只有1个节点,计算新表的索引位置, 直接将该节点放在该位置 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; // 8.如果是红黑树节点,则进行红黑树的重hash分布(跟链表的hash分布基本相同) else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order // 9.如果是普通的链表节点,则进行普通的重hash分布 Node<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点 Node<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引位置+oldCap”的节点 Node<K,V> next; do { next = e.next; // 9.1 如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样 if ((e.hash & oldCap) == 0) { if (loTail == null) // 如果loTail为空, 代表该节点为第一个节点 loHead = e; // 则将loHead赋值为第一个节点 else loTail.next = e; // 否则将节点添加在loTail后面 loTail = e; // 并将loTail赋值为新增的节点 } // 9.2 如果e的hash值与老表的容量进行与运算为非0,则扩容后的索引位置为:老表的索引位置+oldCap else { if (hiTail == null) // 如果hiTail为空, 代表该节点为第一个节点 hiHead = e; // 则将hiHead赋值为第一个节点 else hiTail.next = e; // 否则将节点添加在hiTail后面 hiTail = e; // 并将hiTail赋值为新增的节点 } } while ((e = next) != null); // 10.如果loTail不为空(说明老表的数据有分布到新表上“原索引位置”的节点),则将最后一个节点 // 的next设为空,并将新表上索引位置为“原索引位置”的节点设置为对应的头节点 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // 11.如果hiTail不为空(说明老表的数据有分布到新表上“原索引+oldCap位置”的节点),则将最后 // 一个节点的next设为空,并将新表上索引位置为“原索引+oldCap”的节点设置为对应的头节点 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } // 12.返回新表 return newTab; }
扩容后,节点重 hash 为什么只可能分布在 “原索引位置” 与 “原索引 + oldCap 位置” ?
扩容代码中,使用 e 节点的 hash 值跟 oldCap 进行位与运算,以此决定将节点分布到 “原索引位置” 或者 “原索引 + oldCap 位置” 上,这是为什么了?
假设老表的容量为 16,即 oldCap = 16,则新表容量为 16 * 2 = 32,假设节点 1 的 hash 值为:0000 0000 0000 0000 0000 1111 0000 1010,节点 2 的 hash 值为:0000 0000 0000 0000 0000 1111 0001 1010,则节点 1 和节点 2 在老表的索引位置计算如下图计算1,由于老表的长度限制,节点 1 和节点 2 的索引位置只取决于节点 hash 值的最后 4 位。
再看计算2,计算2为新表的索引计算,可以知道如果两个节点在老表的索引位置相同,则新表的索引位置只取决于节点hash值倒数第5位的值,而此位置的值刚好为老表的容量值 16,此时节点在新表的索引位置只有两种情况:“原索引位置” 和 “原索引 + oldCap位置”,在此例中即为 10 和 10 + 16 = 26。
由于结果只取决于节点 hash 值的倒数第 5 位,而此位置的值刚好为老表的容量值 16,因此此时新表的索引位置的计算可以替换为计算3,直接使用节点的 hash 值与老表的容量 16 进行位于运算,如果结果为 0 则该节点在新表的索引位置为原索引位置,否则该节点在新表的索引位置为 “原索引 + oldCap 位置”。
4.remove 方法
/** * 移除某个节点 */ public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; // 1.如果table不为空并且根据hash值计算出来的索引位置不为空, 将该位置的节点赋值给p if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; // 2.如果p的hash值和key都与入参的相同, 则p即为目标节点, 赋值给node if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { // 3.否则将p.next赋值给e,向下遍历节点 // 3.1 如果p是TreeNode则调用红黑树的方法查找节点 if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { // 3.2 否则,进行普通链表节点的查找 do { // 当节点的hash值和key与传入的相同,则该节点即为目标节点 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; // 赋值给node, 并跳出循环 break; } p = e; // p节点赋值为本次结束的e,在下一次循环中,e为p的next节点 } while ((e = e.next) != null); // e指向下一个节点 } } // 4.如果node不为空(即根据传入key和hash值查找到目标节点),则进行移除操作 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { // 4.1 如果是TreeNode则调用红黑树的移除方法 if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); // 4.2 如果node是该索引位置的头节点则直接将该索引位置的值赋值为node的next节点, // “node == p”只会出现在node是头节点的时候,如果node不是头节点,则node为p的next节点 else if (node == p) tab[index] = node.next; // 4.3 否则将node的上一个节点的next属性设置为node的next节点, // 即将node节点移除, 将node的上下节点进行关联(链表的移除) else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); // 供LinkedHashMap使用 // 5.返回被移除的节点 return node; } } return null; }
这篇关于HashMap底层源码解析的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-27消息中间件底层原理资料详解
- 2024-11-27RocketMQ底层原理资料详解:新手入门教程
- 2024-11-27MQ底层原理资料详解:新手入门教程
- 2024-11-27MQ项目开发资料入门教程
- 2024-11-27RocketMQ源码资料详解:新手入门教程
- 2024-11-27本地多文件上传简易教程
- 2024-11-26消息中间件源码剖析教程
- 2024-11-26JAVA语音识别项目资料的收集与应用
- 2024-11-26Java语音识别项目资料:入门级教程与实战指南
- 2024-11-26SpringAI:Java 开发的智能新利器