hashmap中红黑树旋转仅在树化后插入/删除违反性质时触发,由balanceinsertion/balancedeletion自动调用包私有rotateleft/rotateright,不暴露给用户,且需严格维护parent、next等指针一致性。

红黑树左旋右旋在HashMap里到底什么时候触发
不是所有插入都会触发旋转,HashMap只在树化后、且节点数 ≥ TREEIFY_THRESHOLD(默认8)并满足容量 ≥ MIN_TREEIFY_CAPACITY(默认64)时才构造红黑树;旋转只发生在插入或删除导致违反红黑树性质时,比如某节点的右子节点为红色,而该节点本身是黑色且无左子节点——这时必须右旋来重平衡。
常见错误现象:put()后发现树结构异常、get()返回null但键明明存在、调试时看到TreeNode的left/right指针乱指。本质不是旋转写错了,而是没意识到:JDK 8+ 的HashMap中旋转逻辑被封装在balanceInsertion()和balanceDeletion()里,不暴露给用户,你没法手动调用rotateLeft()或rotateRight()。
- 真正能干预的只有
hashCode()实现——若大量键返回相同哈希值,会加速树化,也更容易暴露旋转逻辑缺陷 -
TreeMap才公开了旋转细节(如fixAfterInsertion()),但HashMap的树节点是内部类TreeNode,旋转完全由其静态辅助方法控制 - 不要试图通过反射修改
TreeNode的red字段来“验证”旋转——这会破坏状态一致性,后续操作可能直接抛ConcurrentModificationException
看懂HashMap源码里的rotateLeft和rotateRight实现
JDK 源码中这两个方法定义在HashMap.TreeNode内部类里,是包私有(package-private)的静态方法,签名长这样:static <k> TreeNode<k> rotateLeft(TreeNode<k> root, TreeNode<k> p)</k></k></k></k>。参数p是待旋转的子节点,root是当前子树根,返回新根。
关键点在于:它们不修改parent指针以外的任何引用关系——比如rotateLeft(p)会把p.right提为新父,p变成其左孩子,但p.right.left原指向的节点会被挂到p.right左边,这个“断链再接”的过程极易漏掉parent更新。
立即学习“Java免费学习笔记(深入)”;
- 典型坑:
p.right.left != null时,必须显式设置p.right.left.parent = p,否则该节点脱离树结构 -
root参数实际只用于定位整个子树的入口,旋转后若p原是root,则返回p.right;否则返回传入的root - 性能影响:单次旋转是O(1),但频繁树化+旋转说明哈希分布极差,此时应优先检查
hashCode()而非优化旋转代码
为什么你的自定义红黑树测试总和HashMap行为对不上
因为HashMap的树节点TreeNode继承自Node,复用了next字段做链表后继,同时又用left/right维护树结构——这是个“链-树二叉混合结构”。标准红黑树教材里的纯树模型,不处理next和prev指针,自然对不上。
更隐蔽的问题是着色规则:教材说“根必须黑”,但HashMap在插入过程中允许临时红根,靠balanceInsertion()最后统一染黑;教材要求“红节点的孩子必为黑”,而HashMap在插入中途会短暂违反,再靠旋转+变色修复。
- 调试建议:打日志时别只看
left/right,务必检查next是否还连着原链表节点,否则split()扩容时会丢数据 - 兼容性注意:JDK 19+ 对树化阈值做了微调,
treeifyBin()现在会先检查表长度,再决定是扩容还是树化,老版本逻辑不能直接套用 - 别拿
TreeMap的旋转步骤硬套——它没有next字段,也不参与哈希桶链表,旋转后无需同步更新链式结构
想改旋转逻辑?先看清楚HashMap的不变量约束
你能安全动的地方极少:balanceInsertion()末尾的root.color = BLACK不能删,rotateLeft()里对p.right.left.parent的赋值不能跳过,否则下一次find()遍历时会因parent == null进入无限循环。
真正容易被忽略的是“双黑”处理:删除时若被删节点是黑且无红孩子,会产生“双黑”缺陷,必须靠balanceDeletion()向上推黑——这个过程可能触发多次旋转,但每次旋转前都先尝试变色,仅当兄弟节点也黑且侄子全黑时才转。
- 最常漏的检查:
if (r == null || r.red)——这里的r是兄弟节点,如果误写成r != null && !r.red,逻辑就反了 - 参数命名陷阱:源码里
xp表示x.parent,xpl是xp.left,看着像缩写,实则是避免重复计算,不是随意简写 - 别碰
moveRootToFront()——它负责把旋转后的新根挪到桶首,和链表头保持一致,改错会导致整个桶不可见










