JDK1.8中的HashMap
原文: JDK1.8中的HashMap
JDK1.8中的HashMap
相比JDK1.7中的HashMap的最大区别就是在JDK1.8中HashMap中的数组+链表结构变为了数组+链表+红黑树。
问题1:为什么要将1.7中HashMap的链表结构改为红黑树?
在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时也就是链表过长,那么通过key值依次查找的效率较低。而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。在jdk1.8版本后,java对HashMap做了改进,在链表长度大于8的时候,将后面的数据存在红黑树中,以加快检索速度。
在JDK1.6,JDK1.7中,HashMap的扩容条件是当元素个数大于阈值而且产生hash冲突。那么在不产生hash冲突且大于阈值的时候,就会导致位桶上的链表过长,查询速度变慢。而JDK1.8中,HashMap的扩容条件改为元素个数大于阈值时就产生扩容并且将链表转为红黑树这样做既增加了查询速度,又减少了每个位桶上对应链表或树的长度。
HashMap中的基本属性
1 // 默认容量16 2 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 3 4 // 最大容量 5 static final int MAXIMUM_CAPACITY = 1 << 30; 6 7 // 默认负载因子0.75 8 static final float DEFAULT_LOAD_FACTOR = 0.75f; 9 10 // 链表节点转换红黑树节点的阈值, 9个节点转 11 static final int TREEIFY_THRESHOLD = 8; 12 13 // 红黑树节点转换链表节点的阈值, 6个节点转 14 static final int UNTREEIFY_THRESHOLD = 6; 15 16 // 转红黑树时, table的最小长度 17 static final int MIN_TREEIFY_CAPACITY = 64; 18 19 // 链表节点, 继承自Entry 20 static class Node<K,V> implements Map.Entry<K,V> { 21 final int hash; 22 final K key; 23 V value; 24 Node<K,V> next; 25 26 // ... ... 27 } 28 29 // 红黑树节点 30 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { 31 TreeNode<K,V> parent; // red-black tree links 32 TreeNode<K,V> left; 33 TreeNode<K,V> right; 34 TreeNode<K,V> prev; // needed to unlink next upon deletion 35 boolean red; 36 37 // ... 38 }
HashMap中的hash()方法
对比JDK1.7中的HashMap我们发现在JDK1.8中,根据key进行hash运算中的左移,异或等运算明显变少了。原因是因为在1.7中hash方法的目的是为了让hash值更加的散列,也就是让每个位桶上面的链表长度更短防止链表过长从而影响运算效率。而在1.8中因为链表改为红黑树,不仅解决了链表长度问题,也优化了查询效率。而且减少hash方法中的运算过程也节约我们硬件cpu的损耗。
1 static final int hash(Object key) { 2 int h; 3 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 4 }
HashMap中的put()方法
- 不同点:1.对比JDK1.7我们发现在1.7中会创建一个Entry数组,而在1.8中用的是node数组,但是仅仅只是数组名称变了而已,里面的属性并没有发生改变。
- 不同点:2.对比JDK1.7我们发现在1.7中采用的是链表头插法,而在1.8中用的是链表尾插法。
过程:
1.判断数组是否为空,为空则调用resize方法初始化一个数组。注意resize既包括初始化数组也包括数组的扩容。
2.对key进行(h&table.length-1)运算得出key所在数组的下标位置,并判断此下标位置是否为空,如果为空则创建node对象。
3.如果下标位置不为空,有三种情况:第一种情况如果put进来的key位置上存在node对象,那么就修改node对象对应的value值。第二种情况如果put进来的key位置上存在链表,首先判断链表长度是否大于8,大于8且数组长度大于等于MIN_TREEIFY_CAPACITY则将链表转为红黑树,小于8,则判断key位置是否存在冲突,是则进行值覆盖,否则插入到链表尾部。第三种情况如果put进来的key位置上存在红黑树,遍历树后把新的节点插入树中。我们在往红黑树插入新的节点的时候,红黑树的根节点会发生变化。4.modcount操作次数加一,后判断是否需要扩容最后返回null。
1 public V put(K key, V value) { 2 return putVal(hash(key), key, value, false, true); 3 } 4 5 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 6 boolean evict) { 7 Node<K,V>[] tab; Node<K,V> p; int n, i; 8 //判断数组是否为空,为空则初始化一个数组 9 if ((tab = table) == null || (n = tab.length) == 0) 10 n = (tab = resize()).length; 11 //通过取余运算得到key的下标位置 12 //如果此位置是否为空,则创建node对象 13 if ((p = tab[i = (n - 1) & hash]) == null) 14 tab[i] = newNode(hash, key, value, null); 15 //如果此位置不为空 16 else { 17 Node<K,V> e; K k; 18 //判断put进来的key的下标位置是否是e的位置,如果是将p赋值给e 19 if (p.hash == hash && 20 ((k = p.key) == key || (key != null && key.equals(k)))) 21 e = p; 22 //如果key位置上存在红黑树。 23 else if (p instanceof TreeNode) 24 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 25 //如果key位置上存在链表 26 else { 27 //遍历链表 28 for (int binCount = 0; ; ++binCount) { 29 if ((e = p.next) == null) { 30 //如果put进来的key与链表不存在冲突,则插入到链表的尾部 31 p.next = newNode(hash, key, value, null); 32 //判断链表长度是否大于8,如果是将链表改为红黑树 33 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 34 treeifyBin(tab, hash); 35 break; 36 } 37 //判断链表的key是否与put进来的key相等,如果是则跳出循环 38 if (e.hash == hash && 39 ((k = e.key) == key || (key != null && key.equals(k)))) 40 break; 41 p = e; 42 } 43 } 44 //进行值覆盖 45 if (e != null) { // existing mapping for key 46 V oldValue = e.value; 47 if (!onlyIfAbsent || oldValue == null) 48 e.value = value; 49 afterNodeAccess(e); 50 return oldValue; 51 } 52 } 53 ++modCount; 54 //判断是否需要扩容,与1.7里面一样 55 if (++size > threshold) 56 resize(); 57 afterNodeInsertion(evict);//这个方法没有用到,不用考虑 58 return null; 59 }
链表树化:
第一步:首先进行判断如果table为空
或者table.length小于64,则调用resize()方法进行扩容或初始化。然后遍历链表,将node单向链表转为TreeNode双向链表
1 final void treeifyBin(Node<K,V>[] tab, int hash) { 2 int n, index; Node<K,V> e; 3 //如果table为空或者table.length小于64,则调用resize()方法。 4 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 5 resize(); 6 else if ((e = tab[index = (n - 1) & hash]) != null) { 7 TreeNode<K,V> hd = null, tl = null; 8 //遍历链表,将node单向链表转为TreeNode双向链表 9 do { 10 TreeNode<K,V> p = replacementTreeNode(e, null); 11 if (tl == null) 12 hd = p; 13 else { 14 p.prev = tl; 15 tl.next = p; 16 } 17 tl = p; 18 } while ((e = e.next) != null); 19 if ((tab[index] = hd) != null) 20 hd.treeify(tab); 21 } 22 }
第二步:将TreeNode双向链表转为红黑树。大概思路就是将TreeNode头节点作为红黑树的根节点,然后依次将TreeNode的其他节点插入到红黑树中。 注意:生成完成后的红黑树的根节点不一定是原TreeNode链表的头节点也可能是其他的节点。
补充:假设我们的类中重写了HashCode方法想要返回原有的HashCode值可以用System.identityHashCode(testHashCode)得到。
1 final void treeify(Node<K,V>[] tab) { 2 TreeNode<K,V> root = null; 3 //遍历链表 4 for (TreeNode<K,V> x = this, next; x != null; x = next) { 5 next = (TreeNode<K,V>)x.next; 6 x.left = x.right = null; 7 //把链表第一个节点作为根节点 8 if (root == null) { 9 x.parent = null; 10 x.red = false; 11 root = x; 12 } 13 else { 14 K k = x.key; 15 int h = x.hash; 16 Class<?> kc = null; 17 //遍历红黑树 18 for (TreeNode<K,V> p = root;;) { 19 int dir, ph; 20 K pk = p.key; 21 //通过hashCode值判断大小,得到要插入树的具体节点,后插入到树中 22 if ((ph = p.hash) > h) 23 dir = -1;//左移 24 else if (ph < h) 25 dir = 1; //右移 26 else if ((kc == null && 27 (kc = comparableClassFor(k)) == null) || 28 (dir = compareComparables(kc, k, pk)) == 0) 29 dir = tieBreakOrder(k, pk); 30 31 TreeNode<K,V> xp = p; 32 if ((p = (dir <= 0) ? p.left : p.right) == null) { 33 x.parent = xp; 34 if (dir <= 0) 35 xp.left = x; 36 else 37 xp.right = x; 38 root = balanceInsertion(root, x); 39 break; 40 } 41 } 42 } 43 } 44 //将生成的红黑树移到数组当中 45 moveRootToFront(tab, root); 46 }
第三步: 将生成的红黑树的根节点移到数组中table[index]位置中。注意:因为第二步:我们发现红黑树的根节点并不是TreeNode的头节点,下面代码一部分就是将红黑树根节点的的TreeNode节点,移动到链表的头部,作为头节点。
1 static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) { 2 int n; 3 if (root != null && tab != null && (n = tab.length) > 0) { 4 int index = (n - 1) & root.hash; 5 TreeNode<K,V> first = (TreeNode<K,V>)tab[index]; 6 if (root != first) { 7 Node<K,V> rn; 8 tab[index] = root; 9 TreeNode<K,V> rp = root.prev; 10 if ((rn = root.next) != null) 11 ((TreeNode<K,V>)rn).prev = rp; 12 if (rp != null) 13 rp.next = rn; 14 if (first != null) 15 first.prev = root; 16 root.next = first; 17 root.prev = null; 18 } 19 assert checkInvariants(root); 20 } 21 }
HashMap中的get()方法
1 public V get(Object key) { 2 Node<K,V> e; 3 return (e = getNode(hash(key), key)) == null ? null : e.value; 4 } 5 6 final Node<K,V> getNode(int hash, Object key) { 7 Node<K,V>[] tab; Node<K,V> first, e; int n; K k; 8 // 1.对table进行校验:table不为空 && table长度大于0 && 9 // table索引位置(使用table.length - 1和hash值进行位与运算)的节点不为空 10 if ((tab = table) != null && (n = tab.length) > 0 && 11 (first = tab[(n - 1) & hash]) != null) { 12 // 2.检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点 13 if (first.hash == hash && // always check first node 14 ((k = first.key) == key || (key != null && key.equals(k)))) 15 return first; 16 // 3.如果first不是目标节点,并且first的next节点不为空则继续遍历 17 if ((e = first.next) != null) { 18 if (first instanceof TreeNode) 19 // 4.如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode 20 return ((TreeNode<K,V>)first).getTreeNode(hash, key); 21 do { 22 // 5.执行链表节点的查找,向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点 23 if (e.hash == hash && 24 ((k = e.key) == key || (key != null && key.equals(k)))) 25 return e; 26 } while ((e = e.next) != null); 27 } 28 } 29 // 6.找不到符合的返回空 30 return null; 31 }
如果是红黑树节点,则调用红黑树的查找目标节点方法 getTreeNode
1 final TreeNode<K,V> getTreeNode(int h, Object k) { 2 // 1.首先找到红黑树的根节点;2.使用根节点调用find方法 3 return ((parent != null) ? root() : this).find(h, k, null); 4 }
使用根节点调用 find 方法
1 /** 2 * 从调用此方法的节点开始查找, 通过hash值和key找到对应的节点 3 * 此方法是红黑树节点的查找, 红黑树是特殊的自平衡二叉查找树 4 * 平衡二叉查找树的特点:左节点<根节点<右节点 5 */ 6 final TreeNode<K,V> find(int h, Object k, Class<?> kc) { 7 // 1.将p节点赋值为调用此方法的节点,即为红黑树根节点 8 TreeNode<K,V> p = this; 9 // 2.从p节点开始向下遍历 10 do { 11 int ph, dir; K pk; 12 TreeNode<K,V> pl = p.left, pr = p.right, q; 13 // 3.如果传入的hash值小于p节点的hash值,则往p节点的左边遍历 14 if ((ph = p.hash) > h) 15 p = pl; 16 else if (ph < h) // 4.如果传入的hash值大于p节点的hash值,则往p节点的右边遍历 17 p = pr; 18 // 5.如果传入的hash值和key值等于p节点的hash值和key值,则p节点为目标节点,返回p节点 19 else if ((pk = p.key) == k || (k != null && k.equals(pk))) 20 return p; 21 else if (pl == null) // 6.p节点的左节点为空则将向右遍历 22 p = pr; 23 else if (pr == null) // 7.p节点的右节点为空则向左遍历 24 p = pl; 25 // 8.将p节点与k进行比较 26 else if ((kc != null || 27 (kc = comparableClassFor(k)) != null) && // 8.1 kc不为空代表k实现了Comparable 28 (dir = compareComparables(kc, k, pk)) != 0)// 8.2 k<pk则dir<0, k>pk则dir>0 29 // 8.3 k<pk则向左遍历(p赋值为p的左节点), 否则向右遍历 30 p = (dir < 0) ? pl : pr; 31 // 9.代码走到此处, 代表key所属类没有实现Comparable, 直接指定向p的右边遍历 32 else if ((q = pr.find(h, k, kc)) != null) 33 return q; 34 // 10.代码走到此处代表“pr.find(h, k, kc)”为空, 因此直接向左遍历 35 else 36 p = pl; 37 } while (p != null); 38 return null; 39 }
HashMap中的resize()方法
- 不同点:jdk1.7扩容条件是hashmap中存储的元素数大于阈值且发生hash碰撞。而jdk1.8中的扩容条件就是hashmap的存储的元素数大于阈值
- 不同点:在1.8中HashMap的resize方法既包含了初始化的功能又包含了扩容的功能。
- 不同点:在1.8扩容遍历链表的时候,会将链表拆成两个小链表,然后将这两个小链表分别插入新数组的index位置和index+oldTable.length位置。也就是说在进行链表转移的时候,1.7中的HashMap会遍历一个转移一个,而1.8中的HashMap会全部遍历后,在转移。
1 final Node<K,V>[] resize() { 2 Node<K,V>[] oldTab = table; 3 int oldCap = (oldTab == null) ? 0 : oldTab.length; 4 int oldThr = threshold; 5 int newCap, newThr = 0; 6 // 1.老表的容量不为0,即老表不为空 7 if (oldCap > 0) { 8 // 1.1 判断老表的容量是否超过最大容量值:如果超过则将阈值设置为Integer.MAX_VALUE,并直接返回老表, 9 // 此时oldCap * 2比Integer.MAX_VALUE大,因此无法进行重新分布,只是单纯的将阈值扩容到最大 10 if (oldCap >= MAXIMUM_CAPACITY) { 11 threshold = Integer.MAX_VALUE; 12 return oldTab; 13 } 14 // 1.2 将newCap赋值为oldCap的2倍,如果newCap<最大容量并且oldCap>=16, 则将新阈值设置为原来的两倍 15 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 16 oldCap >= DEFAULT_INITIAL_CAPACITY) 17 newThr = oldThr << 1; // double threshold 18 } 19 // 2.如果老表的容量为0, 老表的阈值大于0, 是因为初始容量被放入阈值,则将新表的容量设置为老表的阈值 20 else if (oldThr > 0) 21 newCap = oldThr; 22 else { 23 // 3.老表的容量为0, 老表的阈值为0,这种情况是没有传初始容量的new方法创建的空表,将阈值和容量设置为默认值 24 newCap = DEFAULT_INITIAL_CAPACITY; 25 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 26 } 27 // 4.如果新表的阈值为空, 则通过新的容量*负载因子获得阈值 28 if (newThr == 0) { 29 float ft = (float)newCap * loadFactor; 30 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 31 (int)ft : Integer.MAX_VALUE); 32 } 33 // 5.将当前阈值设置为刚计算出来的新的阈值,定义新表,容量为刚计算出来的新容量,将table设置为新定义的表。 34 threshold = newThr; 35 @SuppressWarnings({"rawtypes","unchecked"}) 36 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 37 table = newTab; 38 // 6.如果老表不为空,则需遍历所有节点,将节点赋值给新表 39 if (oldTab != null) { 40 for (int j = 0; j < oldCap; ++j) { 41 Node<K,V> e; 42 if ((e = oldTab[j]) != null) { // 将索引值为j的老表头节点赋值给e 43 oldTab[j] = null; // 将老表的节点设置为空, 以便垃圾收集器回收空间 44 // 7.如果e.next为空, 则代表老表的该位置只有1个节点,计算新表的索引位置, 直接将该节点放在该位置 45 if (e.next == null) 46 newTab[e.hash & (newCap - 1)] = e; 47 // 8.如果是红黑树节点,则进行红黑树的重hash分布(跟链表的hash分布基本相同) 48 else if (e instanceof TreeNode) 49 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 50 else { // preserve order 51 // 9.如果是普通的链表节点,则进行普通的重hash分布 52 Node<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点 53 Node<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引位置+oldCap”的节点 54 Node<K,V> next; 55 do { 56 next = e.next; 57 // 9.1 如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样 58 if ((e.hash & oldCap) == 0) { 59 if (loTail == null) // 如果loTail为空, 代表该节点为第一个节点 60 loHead = e; // 则将loHead赋值为第一个节点 61 else 62 loTail.next = e; // 否则将节点添加在loTail后面 63 loTail = e; // 并将loTail赋值为新增的节点 64 } 65 // 9.2 如果e的hash值与老表的容量进行与运算为非0,则扩容后的索引位置为:老表的索引位置+oldCap 66 else { 67 if (hiTail == null) // 如果hiTail为空, 代表该节点为第一个节点 68 hiHead = e; // 则将hiHead赋值为第一个节点 69 else 70 hiTail.next = e; // 否则将节点添加在hiTail后面 71 hiTail = e; // 并将hiTail赋值为新增的节点 72 } 73 } while ((e = next) != null); 74 // 10.如果loTail不为空(说明老表的数据有分布到新表上“原索引位置”的节点),则将最后一个节点 75 // 的next设为空,并将新表上索引位置为“原索引位置”的节点设置为对应的头节点 76 if (loTail != null) { 77 loTail.next = null; 78 newTab[j] = loHead; 79 } 80 // 11.如果hiTail不为空(说明老表的数据有分布到新表上“原索引+oldCap位置”的节点),则将最后 81 // 一个节点的next设为空,并将新表上索引位置为“原索引+oldCap”的节点设置为对应的头节点 82 if (hiTail != null) { 83 hiTail.next = null; 84 newTab[j + oldCap] = hiHead; 85 } 86 } 87 } 88 } 89 } 90 // 12.返回新表 91 return newTab; 92 }
红黑树节点,则进行红黑树的重 hash 分布
大概过程就是将TreeNode链表,拆成两个index位置和index+oldTable.length位置的TreeNode,我们将index位置的TreeNode称为为loTail,index+oldTable.length位置的TreeNode称为hiTail。那么我们分成三种情况考虑,如果拆分后的TreeNode的长度小于6,那么我们将该TreeNode转成Node链表。如果拆分后的TreeNode的长度大于6,那么则拆分后的将TreeNode的头节点存到对应位置。如果拆分后的两个TreeNode其中一个长度为0,那么我们直接转移原有TreeNode。
1 /** 2 * 扩容后,红黑树的hash分布,只可能存在于两个位置:原索引位置、原索引位置+oldCap 3 */ 4 final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { 5 TreeNode<K,V> b = this; // 拿到调用此方法的节点 6 TreeNode<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点 7 TreeNode<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引+oldCap”的节点 8 int lc = 0, hc = 0; 9 // 1.以调用此方法的节点开始,遍历整个红黑树节点 10 for (TreeNode<K,V> e = b, next; e != null; e = next) { // 从b节点开始遍历 11 next = (TreeNode<K,V>)e.next; // next赋值为e的下个节点 12 e.next = null; // 同时将老表的节点设置为空,以便垃圾收集器回收 13 // 2.如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样 14 if ((e.hash & bit) == 0) { 15 if ((e.prev = loTail) == null) // 如果loTail为空, 代表该节点为第一个节点 16 loHead = e; // 则将loHead赋值为第一个节点 17 else 18 loTail.next = e; // 否则将节点添加在loTail后面 19 loTail = e; // 并将loTail赋值为新增的节点 20 ++lc; // 统计原索引位置的节点个数 21 } 22 // 3.如果e的hash值与老表的容量进行与运算为非0,则扩容后的索引位置为:老表的索引位置+oldCap 23 else { 24 if ((e.prev = hiTail) == null) // 如果hiHead为空, 代表该节点为第一个节点 25 hiHead = e; // 则将hiHead赋值为第一个节点 26 else 27 hiTail.next = e; // 否则将节点添加在hiTail后面 28 hiTail = e; // 并将hiTail赋值为新增的节点 29 ++hc; // 统计索引位置为原索引+oldCap的节点个数 30 } 31 } 32 // 4.如果原索引位置的节点不为空 33 if (loHead != null) { // 原索引位置的节点不为空 34 // 4.1 如果节点个数<=6个则将红黑树转为链表结构 35 if (lc <= UNTREEIFY_THRESHOLD) 36 tab[index] = loHead.untreeify(map); 37 else { 38 // 4.2 将原索引位置的节点设置为对应的头节点 39 tab[index] = loHead; 40 // 4.3 如果hiHead不为空,则代表原来的红黑树(老表的红黑树由于节点被分到两个位置) 41 // 已经被改变, 需要重新构建新的红黑树 42 if (hiHead != null) 43 // 4.4 以loHead为根节点, 构建新的红黑树 44 loHead.treeify(tab); 45 } 46 } 47 // 5.如果索引位置为原索引+oldCap的节点不为空 48 if (hiHead != null) { // 索引位置为原索引+oldCap的节点不为空 49 // 5.1 如果节点个数<=6个则将红黑树转为链表结构 50 if (hc <= UNTREEIFY_THRESHOLD) 51 tab[index + bit] = hiHead.untreeify(map); 52 else { 53 // 5.2 将索引位置为原索引+oldCap的节点设置为对应的头节点 54 tab[index + bit] = hiHead; 55 // 5.3 loHead不为空则代表原来的红黑树(老表的红黑树由于节点被分到两个位置) 56 // 已经被改变, 需要重新构建新的红黑树 57 if (loHead != null) 58 // 5.4 以hiHead为根节点, 构建新的红黑树 59 hiHead.treeify(tab); 60 } 61 } 62 }
将红黑树节点转为链表节点, 当节点<=6个时会被触发
1 2 final Node<K,V> untreeify(HashMap<K,V> map) { 3 Node<K,V> hd = null, tl = null; // hd指向头节点, tl指向尾节点 4 // 1.从调用该方法的节点, 即链表的头节点开始遍历, 将所有节点全转为链表节点 5 for (Node<K,V> q = this; q != null; q = q.next) { 6 // 2.调用replacementNode方法构建链表节点 7 Node<K,V> p = map.replacementNode(q, null); 8 // 3.如果tl为null, 则代表当前节点为第一个节点, 将hd赋值为该节点 9 if (tl == null) 10 hd = p; 11 // 4.否则, 将尾节点的next属性设置为当前节点p 12 else 13 tl.next = p; 14 tl = p; // 5.每次都将tl节点指向当前节点, 即尾节点 15 } 16 // 6.返回转换后的链表的头节点 17 return hd; 18 }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY