概述

JDK 1.8对HashMap进行了比较大的优化,底层实现由之前的“数组+链表”改为“数组+链表+红黑树”,本文就HashMap的几个常用的重要方法和JDK 1.8之前的死循环问题展开学习讨论。JDK 1.8的HashMap的数据结构如下图所示,当链表节点较少时仍然是以链表存在,当链表节点较多时(大于8)会转为红黑树。

java HashMap详解-LMLPHP

本文地址:http://blog.csdn.net/v123411739/article/details/78996181

几个点:

先了解以下几个点,有利于更好的理解HashMap的源码和阅读本文。

  1. 头节点指的是table表上索引位置的节点,也就是链表的头节点。
  2. 根结点(root节点)指的是红黑树最上面的那个节点,也就是没有父节点的节点。
  3. 红黑树的根结点不一定是索引位置的头结点。
  4. 转为红黑树节点后,链表的结构还存在,通过next属性维持,红黑树节点在进行操作时都会维护链表的结构,并不是转为红黑树节点,链表结构就不存在了。
  5. 在红黑树上,叶子节点也可能有next节点,因为红黑树的结构跟链表的结构是互不影响的,不会因为是叶子节点就说该节点已经没有next节点。
  6. 源码中一些变量定义:如果定义了一个节点p,则pl为p的左节点,pr为p的右节点,pp为p的父节点,ph为p的hash值,pk为p的key值,kc为key的类等等。源码中很喜欢在if/for等语句中进行赋值并判断,请注意。
  7. 链表中移除一个节点只需如下图操作,其他操作同理。java HashMap详解-LMLPHP
  8. 红黑树在维护链表结构时,移除一个节点只需如下图操作(红黑树中增加了一个prev属性),其他操作同理。注:此处只是红黑树维护链表结构的操作,红黑树还需要单独进行红黑树的移除或者其他操作。java HashMap详解-LMLPHP
  9. 源码中进行红黑树的查找时,会反复用到以下两条规则:1)如果目标节点的hash值小于p节点的hash值,则向p节点的左边遍历;否则向p节点的右边遍历。2)如果目标节点的key值小于p节点的key值,则向p节点的左边遍历;否则向p节点的右边遍历。这两条规则是利用了红黑树的特性(左节点<根结点<右节点)。
  10. 源码中进行红黑树的查找时,会用dir(direction)来表示向左还是向右查找,dir存储的值是目标节点的hash/key与p节点的hash/key的比较结果。

基本属性

  1. /**
  2. * The default initial capacity - MUST be a power of two.
  3. */
  4. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认容量16
  5. /**
  6. * The maximum capacity, used if a higher value is implicitly specified
  7. * by either of the constructors with arguments.
  8. * MUST be a power of two <= 1<<30.
  9. */
  10. static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量
  11. /**
  12. * The load factor used when none specified in constructor.
  13. */
  14. static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认负载因子0.75
  15. /**
  16. * The bin count threshold for using a tree rather than list for a
  17. * bin. Bins are converted to trees when adding an element to a
  18. * bin with at least this many nodes. The value must be greater
  19. * than 2 and should be at least 8 to mesh with assumptions in
  20. * tree removal about conversion back to plain bins upon
  21. * shrinkage.
  22. */
  23. static final int TREEIFY_THRESHOLD = 8; // 链表节点转换红黑树节点的阈值, 9个节点转
  24. /**
  25. * The bin count threshold for untreeifying a (split) bin during a
  26. * resize operation. Should be less than TREEIFY_THRESHOLD, and at
  27. * most 6 to mesh with shrinkage detection under removal.
  28. */
  29. static final int UNTREEIFY_THRESHOLD = 6; // 红黑树节点转换链表节点的阈值, 6个节点转
  30. /**
  31. * The smallest table capacity for which bins may be treeified.
  32. * (Otherwise the table is resized if too many nodes in a bin.)
  33. * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
  34. * between resizing and treeification thresholds.
  35. */
  36. static final int MIN_TREEIFY_CAPACITY = 64; // 转红黑树时, table的最小长度
  37. /**
  38. * Basic hash bin node, used for most entries. (See below for
  39. * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
  40. */
  41. static class Node<K,V> implements Map.Entry<K,V> { // 基本hash节点, 继承自Entry
  42. final int hash;
  43. final K key;
  44. V value;
  45. Node<K,V> next;
  46. Node(int hash, K key, V value, Node<K,V> next) {
  47. this.hash = hash;
  48. this.key = key;
  49. this.value = value;
  50. this.next = next;
  51. }
  52. public final K getKey() { return key; }
  53. public final V getValue() { return value; }
  54. public final String toString() { return key + "=" + value; }
  55. public final int hashCode() {
  56. return Objects.hashCode(key) ^ Objects.hashCode(value);
  57. }
  58. public final V setValue(V newValue) {
  59. V oldValue = value;
  60. value = newValue;
  61. return oldValue;
  62. }
  63. public final boolean equals(Object o) {
  64. if (o == this)
  65. return true;
  66. if (o instanceof Map.Entry) {
  67. Map.Entry<?,?> e = (Map.Entry<?,?>)o;
  68. if (Objects.equals(key, e.getKey()) &&
  69. Objects.equals(value, e.getValue()))
  70. return true;
  71. }
  72. return false;
  73. }
  74. }
  75. /**
  76. * Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
  77. * extends Node) so can be used as extension of either regular or
  78. * linked node.
  79. */
  80. static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {// 红黑树节点
  81. TreeNode<K,V> parent; // red-black tree links
  82. TreeNode<K,V> left;
  83. TreeNode<K,V> right;
  84. TreeNode<K,V> prev; // needed to unlink next upon deletion
  85. boolean red;
  86. TreeNode(int hash, K key, V val, Node<K,V> next) {
  87. super(hash, key, val, next);
  88. }
  89. // ...
  90. }

定位哈希桶数组索引位置

不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。前面说过HashMap的数据结构是“数组+链表+红黑树”的结合,所以我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表/红黑树,大大优化了查询的效率。HashMap定位数组索引位置,直接决定了hash方法的离散性能。下面是定位哈希桶数组的源码:

  1. // 代码1
  2. static final int hash(Object key) { // 计算key的hash值
  3. int h;
  4. // 1.先拿到key的hashCode值; 2.将hashCode的高16位参与运算
  5. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  6. }
  7. // 代码2
  8. int n = tab.length;
  9. // 将(tab.length - 1) 与 hash值进行&运算
  10. int index = (n - 1) & hash;

整个过程本质上就是三步:

  1. 拿到key的hashCode值
  2. 将hashCode的高位参与运算,重新计算hash值
  3. 将计算出来的hash值与(table.length - 1)进行&运算

方法解读:

对于任意给定的对象,只要它的hashCode()返回值相同,那么计算得到的hash值总是相同的。我们首先想到的就是把hash值对table长度取模运算,这样一来,元素的分布相对来说是比较均匀的。

但是模运算消耗还是比较大的,我们知道计算机比较快的运算为位运算,因此JDK团队对取模运算进行了优化,使用上面代码2的位与运算来代替模运算。这个方法非常巧妙,它通过 “(table.length -1) & h” 来得到该对象的索引位置,这个优化是基于以下公式:x mod 2^n = x & (2^n - 1)。我们知道HashMap底层数组的长度总是2的n次方,并且取模运算为“h mod table.length”,对应上面的公式,可以得到该运算等同于“h & (table.length - 1)”。这是HashMap在速度上的优化,因为&比%具有更高的效率。

在JDK1.8的实现中,还优化了高位运算的算法,将hashCode的高16位与hashCode进行异或运算,主要是为了在table的length较小的时候,让高位也参与运算,并且不会有太大的开销。

下图是一个简单的例子,table长度为16:

java HashMap详解-LMLPHP

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. final Node<K,V> getNode(int hash, Object key) {
  6. Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  7. // table不为空 && table长度大于0 && table索引位置(根据hash值计算出)不为空
  8. if ((tab = table) != null && (n = tab.length) > 0 &&
  9. (first = tab[(n - 1) & hash]) != null) {
  10. if (first.hash == hash && // always check first node
  11. ((k = first.key) == key || (key != null && key.equals(k))))
  12. return first; // first的key等于传入的key则返回first对象
  13. if ((e = first.next) != null) { // 向下遍历
  14. if (first instanceof TreeNode) // 判断是否为TreeNode
  15. // 如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode
  16. return ((TreeNode<K,V>)first).getTreeNode(hash, key);
  17. // 走到这代表节点为链表节点
  18. do { // 向下遍历链表, 直至找到节点的key和传入的key相等时,返回该节点
  19. if (e.hash == hash &&
  20. ((k = e.key) == key || (key != null && key.equals(k))))
  21. return e;
  22. } while ((e = e.next) != null);
  23. }
  24. }
  25. return null; // 找不到符合的返回空
  26. }
  1. 先对table进行校验,校验是否为空,length是否大于0
  2. 使用table.length - 1和hash值进行位与运算,得出在table上的索引位置,将该索引位置的节点赋值给first节点,校验该索引位置是否为空
  3. 检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点
  4. 如果first的next节点不为空则继续遍历
  5. 如果first节点为TreeNode,则调用getTreeNode方法(见下文代码块1)查找目标节点
  6. 如果first节点不为TreeNode,则调用普通的遍历链表方法查找目标节点
  7. 如果查找不到目标节点则返回空

代码块1:getTreeNode方法

  1. final TreeNode<K,V> getTreeNode(int h, Object k) {
  2. // 使用根结点调用find方法
  3. return ((parent != null) ? root() : this).find(h, k, null);
  4. }
  1. 找到调用此方法的节点的树的根节点
  2. 使用该树的根节点调用find方法(见下文代码块2)

代码块2:find方法

  1. /**
  2. * 从调用此方法的结点开始查找, 通过hash值和key找到对应的节点
  3. * 此处是红黑树的遍历, 红黑树是特殊的自平衡二叉查找树
  4. * 平衡二叉查找树的特点:左节点<根节点<右节点
  5. */
  6. final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
  7. TreeNode<K,V> p = this; // this为调用此方法的节点
  8. do {
  9. int ph, dir; K pk;
  10. TreeNode<K,V> pl = p.left, pr = p.right, q;
  11. if ((ph = p.hash) > h) // 传入的hash值小于p节点的hash值, 则往p节点的左边遍历
  12. p = pl; // p赋值为p节点的左节点
  13. else if (ph < h) // 传入的hash值大于p节点的hash值, 则往p节点的右边遍历
  14. p = pr; // p赋值为p节点的右节点
  15. // 传入的hash值和key值等于p节点的hash值和key值,则p节点为目标节点,返回p节点
  16. else if ((pk = p.key) == k || (k != null && k.equals(pk)))
  17. return p;
  18. else if (pl == null) // p节点的左节点为空则将向右遍历
  19. p = pr;
  20. else if (pr == null) // p节点的右节点为空则向左遍历
  21. p = pl;
  22. else if ((kc != null ||
  23. // 如果传入的key(k)所属的类实现了Comparable接口,则将传入的key跟p节点的key比较
  24. (kc = comparableClassFor(k)) != null) && // 此行不为空代表k实现了Comparable
  25. (dir = compareComparables(kc, k, pk)) != 0)//k<pk则dir<0, k>pk则dir>0
  26. p = (dir < 0) ? pl : pr; // k < pk则向左遍历(p赋值为p的左节点), 否则向右遍历
  27. // 代码走到此处, 代表key所属类没有实现Comparable, 直接指定向p的右边遍历
  28. else if ((q = pr.find(h, k, kc)) != null)
  29. return q;
  30. else// 代码走到此处代表上一个向右遍历(pr.find(h, k, kc))为空, 因此直接向左遍历
  31. p = pl;
  32. } while (p != null);
  33. return null;
  34. }
  1. 将p节点赋值为调用此方法的节点
  2. 如果传入的hash值小于p节点的hash值,则往p节点的左边遍历
  3. 如果传入的hash值大于p节点的hash值,则往p节点的右边遍历
  4. 如果传入的hash值等于p节点的hash值,并且传入的key值跟p节点的key值相等, 则该p节点即为目标节点,返回p节点
  5. 如果p的左节点为空则向右遍历,反之如果p的右节点为空则向左遍历
  6. 如果传入的key(即代码中的参数变量k)所属的类实现了Comparable接口(kc不为空,comparableClassFor方法见下文代码块3),则将传入的key跟p节点的key进行比较(kc实现了Comparable接口,因此通过kc的比较方法进行比较),并将比较结果赋值给dir,如果dir<0则代表k<pk,则向p节点的左边遍历(pl);否则,向p节点的右边遍历(pr)。
  7. 代码走到此处,代表key所属类没有实现Comparable,因此直接指定向p的右边遍历,如果能找到目标节点则返回
  8. 代码走到此处代表与第7点向右遍历没有找到目标节点,因此直接向左边遍历
  9. 以上都找不到目标节点则返回空

代码块3:comparableClassFor方法

  1. /**
  2. * Returns x's Class if it is of the form "class C implements
  3. * Comparable<C>", else null.
  4. */
  5. static Class<?> comparableClassFor(Object x) {
  6. if (x instanceof Comparable) {
  7. Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
  8. if ((c = x.getClass()) == String.class) // bypass checks
  9. return c;
  10. if ((ts = c.getGenericInterfaces()) != null) {
  11. for (int i = 0; i < ts.length; ++i) {
  12. if (((t = ts[i]) instanceof ParameterizedType) &&
  13. ((p = (ParameterizedType)t).getRawType() ==
  14. Comparable.class) &&
  15. (as = p.getActualTypeArguments()) != null &&
  16. as.length == 1 && as[0] == c) // type arg is c
  17. return c;
  18. }
  19. }
  20. }
  21. return null;
  22. }

如果x实现了Comparable接口,则返回 x的Class。

put方法

  1. public V put(K key, V value) {
  2. return putVal(hash(key), key, value, false, true);
  3. }
  4. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  5. boolean evict) {
  6. Node<K,V>[] tab; Node<K,V> p; int n, i;
  7. // table是否为空或者length等于0, 如果是则调用resize方法进行初始化
  8. if ((tab = table) == null || (n = tab.length) == 0)
  9. n = (tab = resize()).length;
  10. // 通过hash值计算索引位置, 如果table表该索引位置节点为空则新增一个
  11. if ((p = tab[i = (n - 1) & hash]) == null)// 将索引位置的头节点赋值给p
  12. tab[i] = newNode(hash, key, value, null);
  13. else { // table表该索引位置不为空
  14. Node<K,V> e; K k;
  15. if (p.hash == hash && // 判断p节点的hash值和key值是否跟传入的hash值和key值相等
  16. ((k = p.key) == key || (key != null && key.equals(k))))
  17. e = p; // 如果相等, 则p节点即为要查找的目标节点,赋值给e
  18. // 判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点
  19. else if (p instanceof TreeNode)
  20. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  21. else { // 走到这代表p节点为普通链表节点
  22. for (int binCount = 0; ; ++binCount) { // 遍历此链表, binCount用于统计节点数
  23. if ((e = p.next) == null) { // p.next为空代表不存在目标节点则新增一个节点插入链表尾部
  24. p.next = newNode(hash, key, value, null);
  25. // 计算节点是否超过8个, 减一是因为循环是从p节点的下一个节点开始的
  26. if (binCount >= TREEIFY_THRESHOLD - 1)
  27. treeifyBin(tab, hash);// 如果超过8个,调用treeifyBin方法将该链表转换为红黑树
  28. break;
  29. }
  30. if (e.hash == hash && // e节点的hash值和key值都与传入的相等, 则e即为目标节点,跳出循环
  31. ((k = e.key) == key || (key != null && key.equals(k))))
  32. break;
  33. p = e; // 将p指向下一个节点
  34. }
  35. }
  36. // e不为空则代表根据传入的hash值和key值查找到了节点,将该节点的value覆盖,返回oldValue
  37. if (e != null) {
  38. V oldValue = e.value;
  39. if (!onlyIfAbsent || oldValue == null)
  40. e.value = value;
  41. afterNodeAccess(e); // 用于LinkedHashMap
  42. return oldValue;
  43. }
  44. }
  45. ++modCount;
  46. if (++size > threshold) // 插入节点后超过阈值则进行扩容
  47. resize();
  48. afterNodeInsertion(evict); // 用于LinkedHashMap
  49. return null;
  50. }
  1. 校验table是否为空或者length等于0,如果是则调用resize方法(见下文resize方法)进行初始化
  2. 通过hash值计算索引位置,将该索引位置的头节点赋值给p节点,如果该索引位置节点为空则使用传入的参数新增一个节点并放在该索引位置
  3. 判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点
  4. 如果p节点不是目标节点,则判断p节点是否为TreeNode,如果是则调用红黑树的putTreeVal方法(见下文代码块4)查找目标节点
  5. 走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,并定义变量binCount来统计该链表的节点数
  6. 如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部,并校验节点数是否超过8个,如果超过则调用treeifyBin方法(见下文代码块6)将链表节点转为红黑树节点
  7. 如果遍历的e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
  8. 如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
  9. 如果插入节点后节点数超过阈值,则调用resize方法(见下文resize方法)进行扩容

代码块4:putTreeVal方法

  1. /**
  2. * Tree version of putVal.
  3. * 红黑树插入会同时维护原来的链表属性, 即原来的next属性
  4. */
  5. final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
  6. int h, K k, V v) {
  7. Class<?> kc = null;
  8. boolean searched = false;
  9. // 查找根节点, 索引位置的头节点并不一定为红黑树的根结点
  10. TreeNode<K,V> root = (parent != null) ? root() : this;
  11. for (TreeNode<K,V> p = root;;) { // 将根节点赋值给p, 开始遍历
  12. int dir, ph; K pk;
  13. if ((ph = p.hash) > h) // 如果传入的hash值小于p节点的hash值
  14. dir = -1; // 则将dir赋值为-1, 代表向p的左边查找树
  15. else if (ph < h) // 如果传入的hash值大于p节点的hash值,
  16. dir = 1; // 则将dir赋值为1, 代表向p的右边查找树
  17. // 如果传入的hash值和key值等于p节点的hash值和key值, 则p节点即为目标节点, 返回p节点
  18. else if ((pk = p.key) == k || (k != null && k.equals(pk)))
  19. return p;
  20. // 如果k所属的类没有实现Comparable接口 或者 k和p节点的key相等
  21. else if ((kc == null &&
  22. (kc = comparableClassFor(k)) == null) ||
  23. (dir = compareComparables(kc, k, pk)) == 0) {
  24. if (!searched) { // 第一次符合条件, 该方法只有第一次才执行
  25. TreeNode<K,V> q, ch;
  26. searched = true;
  27. // 从p节点的左节点和右节点分别调用find方法进行查找, 如果查找到目标节点则返回
  28. if (((ch = p.left) != null &&
  29. (q = ch.find(h, k, kc)) != null) ||
  30. ((ch = p.right) != null &&
  31. (q = ch.find(h, k, kc)) != null))
  32. return q;
  33. }
  34. // 否则使用定义的一套规则来比较k和p节点的key的大小, 用来决定向左还是向右查找
  35. dir = tieBreakOrder(k, pk); // dir<0则代表k<pk,则向p左边查找;反之亦然
  36. }
  37. TreeNode<K,V> xp = p; // xp赋值为x的父节点,中间变量,用于下面给x的父节点赋值
  38. // dir<=0则向p左边查找,否则向p右边查找,如果为null,则代表该位置即为x的目标位置
  39. if ((p = (dir <= 0) ? p.left : p.right) == null) {
  40. // 走进来代表已经找到x的位置,只需将x放到该位置即可
  41. Node<K,V> xpn = xp.next; // xp的next节点
  42. // 创建新的节点, 其中x的next节点为xpn, 即将x节点插入xp与xpn之间
  43. TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
  44. if (dir <= 0) // 如果时dir <= 0, 则代表x节点为xp的左节点
  45. xp.left = x;
  46. else // 如果时dir> 0, 则代表x节点为xp的右节点
  47. xp.right = x;
  48. xp.next = x; // 将xp的next节点设置为x
  49. x.parent = x.prev = xp; // 将x的parent和prev节点设置为xp
  50. // 如果xpn不为空,则将xpn的prev节点设置为x节点,与上文的x节点的next节点对应
  51. if (xpn != null)
  52. ((TreeNode<K,V>)xpn).prev = x;
  53. moveRootToFront(tab, balanceInsertion(root, x)); // 进行红黑树的插入平衡调整
  54. return null;
  55. }
  56. }
  57. }
  1. 查找当前红黑树的根结点,将根结点赋值给p节点,开始进行查找
  2. 如果传入的hash值小于p节点的hash值,将dir赋值为-1,代表向p的左边查找树
  3. 如果传入的hash值大于p节点的hash值, 将dir赋值为1,代表向p的右边查找树
  4. 如果传入的hash值等于p节点的hash值,并且传入的key值跟p节点的key值相等, 则该p节点即为目标节点,返回p节点
  5. 如果k所属的类没有实现Comparable接口,或者k和p节点的key使用compareTo方法比较相等:第一次会从p节点的左节点和右节点分别调用find方法(见上文代码块2)进行查找,如果查找到目标节点则返回;如果不是第一次或者调用find方法没有找到目标节点,则调用tieBreakOrder方法(见下文代码块5)比较k和p节点的key值的大小,以决定向树的左节点还是右节点查找。
  6. 如果dir <= 0则向左节点查找(p赋值为p.left,并进行下一次循环),否则向右节点查找,如果已经无法继续查找(p赋值后为null),则代表该位置即为x的目标位置,另外变量xp用来记录查找的最后一个节点,即下文新增的x节点的父节点。
  7. 以传入的hash、key、value参数和xp节点的next节点为参数,构建x节点(注意:xp节点在此处可能是叶子节点、没有左节点的节点、没有右节点的节点三种情况,即使它是叶子节点,它也可能有next节点,红黑树的结构跟链表的结构是互不影响的,不会因为某个节点是叶子节点就说它没有next节点,红黑树在进行操作时会同时维护红黑树结构和链表结构,next属性就是用来维护链表结构的),根据dir的值决定x决定放在xp节点的左节点还是右节点,将xp的next节点设为x,将x的parent和prev节点设为xp,如果原xp的next节点(xpn)不为空, 则将该节点的prev节点设置为x节点, 与上面的将x节点的next节点设置为xpn对应。
  8. 进行红黑树的插入平衡调整,见文末的解释2。

代码块5:tieBreakOrder方法

  1. // 用于不可比较或者hashCode相同时进行比较的方法, 只是一个一致的插入规则,用来维护重定位的等价性。
  2. static int tieBreakOrder(Object a, Object b) {
  3. int d;
  4. if (a == null || b == null ||
  5. (d = a.getClass().getName().
  6. compareTo(b.getClass().getName())) == 0)
  7. d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
  8. -1 : 1);
  9. return d;
  10. }

定义一套规则用于极端情况下比较两个参数的大小。

代码块6:treeifyBin方法

  1. final void treeifyBin(Node<K,V>[] tab, int hash) {
  2. int n, index; Node<K,V> e;
  3. // table为空或者table的长度小于64, 进行扩容
  4. if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
  5. resize();
  6. // 根据hash值计算索引值, 遍历该索引位置的链表
  7. else if ((e = tab[index = (n - 1) & hash]) != null) {
  8. TreeNode<K,V> hd = null, tl = null;
  9. do {
  10. TreeNode<K,V> p = replacementTreeNode(e, null); // 链表节点转红黑树节点
  11. if (tl == null) // tl为空代表为第一次循环
  12. hd = p; // 头结点
  13. else {
  14. p.prev = tl; // 当前节点的prev属性设为上一个节点
  15. tl.next = p; // 上一个节点的next属性设置为当前节点
  16. }
  17. tl = p; // tl赋值为p, 在下一次循环中作为上一个节点
  18. } while ((e = e.next) != null); // e指向下一个节点
  19. // 将table该索引位置赋值为新转的TreeNode的头节点
  20. if ((tab[index] = hd) != null)
  21. hd.treeify(tab); // 以头结点为根结点, 构建红黑树
  22. }
  23. }
  1. 校验table是否为空,如果长度小于64,则调用resize方法(见下文resize方法)进行扩容。
  2. 根据hash值计算索引值,将该索引位置的节点赋值给e节点,从e节点开始遍历该索引位置的链表。
  3. 调用replacementTreeNode方法(该方法就一行代码,直接返回一个新建的TreeNode)将链表节点转为红黑树节点,将头结点赋值给hd节点,每次遍历结束将p节点赋值给tl,用于在下一次循环中作为上一个节点进行一些链表的关联操作(p.prev = tl 和 tl.next = p)。
  4. 将table该索引位置赋值为新转的TreeNode的头节点hd,如果该节点不为空,则以hd为根结点,调用treeify方法(见下文代码块7)构建红黑树。

代码块7:treeify方法

  1. final void treeify(Node<K,V>[] tab) { // 构建红黑树
  2. TreeNode<K,V> root = null;
  3. for (TreeNode<K,V> x = this, next; x != null; x = next) {// this即为调用此方法的TreeNode
  4. next = (TreeNode<K,V>)x.next; // next赋值为x的下个节点
  5. x.left = x.right = null; // 将x的左右节点设置为空
  6. if (root == null) { // 如果还没有根结点, 则将x设置为根结点
  7. x.parent = null; // 根结点没有父节点
  8. x.red = false; // 根结点必须为黑色
  9. root = x; // 将x设置为根结点
  10. }
  11. else {
  12. K k = x.key; // k赋值为x的key
  13. int h = x.hash; // h赋值为x的hash值
  14. Class<?> kc = null;
  15. // 如果当前节点x不是根结点, 则从根节点开始查找属于该节点的位置
  16. for (TreeNode<K,V> p = root;;) {
  17. int dir, ph;
  18. K pk = p.key;
  19. if ((ph = p.hash) > h) // 如果x节点的hash值小于p节点的hash值
  20. dir = -1; // 则将dir赋值为-1, 代表向p的左边查找
  21. else if (ph < h) // 与上面相反, 如果x节点的hash值大于p节点的hash值
  22. dir = 1; // 则将dir赋值为1, 代表向p的右边查找
  23. // 走到这代表x的hash值和p的hash值相等,则比较key值
  24. else if ((kc == null && // 如果k没有实现Comparable接口 或者 x节点的key和p节点的key相等
  25. (kc = comparableClassFor(k)) == null) ||
  26. (dir = compareComparables(kc, k, pk)) == 0)
  27. // 使用定义的一套规则来比较x节点和p节点的大小,用来决定向左还是向右查找
  28. dir = tieBreakOrder(k, pk);
  29. TreeNode<K,V> xp = p; // xp赋值为x的父节点,中间变量用于下面给x的父节点赋值
  30. // dir<=0则向p左边查找,否则向p右边查找,如果为null,则代表该位置即为x的目标位置
  31. if ((p = (dir <= 0) ? p.left : p.right) == null) {
  32. x.parent = xp; // x的父节点即为最后一次遍历的p节点
  33. if (dir <= 0) // 如果时dir <= 0, 则代表x节点为父节点的左节点
  34. xp.left = x;
  35. else // 如果时dir > 0, 则代表x节点为父节点的右节点
  36. xp.right = x;
  37. // 进行红黑树的插入平衡(通过左旋、右旋和改变节点颜色来保证当前树符合红黑树的要求)
  38. root = balanceInsertion(root, x);
  39. break;
  40. }
  41. }
  42. }
  43. }
  44. moveRootToFront(tab, root); // 如果root节点不在table索引位置的头结点, 则将其调整为头结点
  45. }
  1. 从调用此方法的节点作为起点,开始进行遍历,并将此节点设为root节点,标记为黑色(x.red = false)。
  2. 如果当前节点不是根结点,则从根节点开始查找属于该节点的位置(该段代码跟之前的代码块2和代码块4的查找代码类似)。
  3. 如果x节点(将要插入红黑树的节点)的hash值小于p节点(当前遍历到的红黑树节点)的hash值,则向p节点的左边查找。
  4. 与3相反,如果x节点的hash值大于p节点的hash值,则向p节点的右边查找。
  5. 如果x的key没有实现Comparable接口,或者x节点的key和p节点的key相等,使用tieBreakOrder方法(见上文代码块5)来比较x节点和p节点的大小,以决定向左还是向右查找(dir <= 0向左,否则向右)。
  6. 如果dir <= 0则向左节点查找(p赋值为p.left,并进行下一次循环),否则向右节点查找,如果已经无法继续查找(p赋值后为null),则代表该位置即为x的目标位置,另外变量xp用来记录最后一个节点,即为下文新增的x节点的父节点。
  7. 将x的父节点设置为xp,根据dir的值决定x决定放在xp节点的左节点还是右节点,最后进行红黑树的插入平衡调整。
  8. 调用moveRootToFront方法(见下文代码块8)将root节点调整到索引位置的头结点。

代码块8:moveRootToFront方法

  1. /**
  2. * 如果当前索引位置的头节点不是root节点, 则将root的上一个节点和下一个节点进行关联,
  3. * 将root放到头节点的位置, 原头节点放在root的next节点上
  4. */
  5. static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
  6. int n;
  7. if (root != null && tab != null && (n = tab.length) > 0) {
  8. int index = (n - 1) & root.hash;
  9. TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
  10. if (root != first) { // 如果root节点不是该索引位置的头节点
  11. Node<K,V> rn;
  12. tab[index] = root; // 将该索引位置的头节点赋值为root节点
  13. TreeNode<K,V> rp = root.prev; // root节点的上一个节点
  14. // 如果root节点的下一个节点不为空,
  15. // 则将root节点的下一个节点的prev属性设置为root节点的上一个节点
  16. if ((rn = root.next) != null)
10-02 21:49