JDK1.8 --- HashMap源码解析

HashMap是基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 HashTable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。


HashMap源码

  • 成员变量
 /**
  * 默认的初试容量 16 , 容量必须是2的幂次方
  * 
  * The default initial capacity - MUST be a power of two.
  */
  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

 /**
  * 最大容量
  * 
  * The maximum capacity, used if a higher value is implicitly specified
  * by either of the constructors with arguments.
  * MUST be a power of two <= 1<<30.
  */
  static final int MAXIMUM_CAPACITY = 1 << 30;

 /**
  * 默认负载因子 0.75,存储容量达到总容量的0.75被开始扩容
  * 
  * The load factor used when none specified in constructor.
  */
  static final float DEFAULT_LOAD_FACTOR = 0.75f;

 /**
  * threshold表示当HashMap的size大于threshold时会执行resize操作。 
  * threshold=capacity*loadFactor
  * 一个桶的树化阈值 当桶中元素个数超过这个值时 需要使用红黑树节点替换链表节点
  * 
  * The bin count threshold for using a tree rather than list for a
  * bin.  Bins are converted to trees when adding an element to a
  * bin with at least this many nodes. The value must be greater
  * than 2 and should be at least 8 to mesh with assumptions in
  * tree removal about conversion back to plain bins upon
  * shrinkage.
  */
  static final int TREEIFY_THRESHOLD = 8;

 /**
  * 一个树的链表还原阈值 当扩容时,桶中元素个数小于这个值 
  * 就会把树形的桶元素 还原(切分)为链表结构
  * 
  * The bin count threshold for untreeifying a (split) bin during a
  * resize operation. Should be less than TREEIFY_THRESHOLD, and at
  * most 6 to mesh with shrinkage detection under removal.
  */
  static final int UNTREEIFY_THRESHOLD = 6;

 /**
  * 哈希表的最小树形化容量
  * 当哈希表中的容量大于这个值时,表中的桶才能进行树形化
  * 否则桶内元素太多时会扩容,而不是树形化
  * 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
  * 
  * The smallest table capacity for which bins may be treeified.
  * (Otherwise the table is resized if too many nodes in a bin.)
  * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
  * between resizing and treeification thresholds.
  */
  static final int MIN_TREEIFY_CAPACITY = 64;
.
.
.
 /**
  * 哈希表-存放元素,它的大小总是2的幂次方
  * The table, initialized on first use, and resized as
  * necessary. When allocated, length is always a power of two.
  * (We also tolerate length zero in some operations to allow
  * bootstrapping mechanics that are currently not needed.)
  */
 transient Node<K,V>[] table;

 /**
  * 缓存记录集
  * Holds cached entrySet(). Note that AbstractMap fields are used
  * for keySet() and values().
  */
 transient Set<Map.Entry<K,V>> entrySet;

 /**
  * 大小
  * The number of key-value mappings contained in this map.
  */
 transient int size;

 /**
  * fast-fail机制-记录hashMap被修改的次数
  * The number of times this HashMap has been structurally modified
  * Structural modifications are those that change the number of mappings in
  * the HashMap or otherwise modify its internal structure (e.g.,
  * rehash).  This field is used to make iterators on Collection-views of
  * the HashMap fail-fast.  (See ConcurrentModificationException).
  */
 transient int modCount;

 /**
  * 扩容阀值 capacity * load factor
  * The next size value at which to resize (capacity * load factor).
  *
  * @serial
  */
 // (The javadoc description is true upon serialization.
 // Additionally, if the table array has not been allocated, this
 // field holds the initial array capacity, or zero signifying
 // DEFAULT_INITIAL_CAPACITY.)
 int threshold;

 /**
  * 真实的负载因子
  * The load factor for the hash table.
  *
  * @serial
  */
 final float loadFactor;

成员方法

  • 构造方法
 /**
  * 无参的构造方法(初试容量16,负载因子0.75)
  * Constructs an empty <tt>HashMap</tt> with the default initial capacity
  * (16) and the default load factor (0.75).
  */
 public HashMap() {
     this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
 }
  /**
 - 有参数的构造方法
 - Constructs an empty <tt>HashMap</tt> with the specified initial
 - capacity and load factor.
 -  5. @param  initialCapacity the initial capacity
 - @param  loadFactor      the load factor
 - @throws IllegalArgumentException if the initial capacity is negative
 -         or the load factor is nonpositive
   */
  public HashMap(int initialCapacity, float loadFactor) {
      if (initialCapacity < 0)
          throw new IllegalArgumentException("Illegal initial capacity: " +
                                             initialCapacity);
      if (initialCapacity > MAXIMUM_CAPACITY)
          initialCapacity = MAXIMUM_CAPACITY;
      if (loadFactor <= 0 || Float.isNaN(loadFactor))
          throw new IllegalArgumentException("Illegal load factor: " +
                                             loadFactor);
      this.loadFactor = loadFactor;
      this.threshold = tableSizeFor(initialCapacity);
  }

我们来简要分析一下有参的构造方法是如何初始化的。

//initialCapacity = (需要存储的元素(键值对)个数 / 负载因子) + 1。
//HashMap并没有真实容量这个成员属性,但可以通过上面的方式得出
-->HashMap(int initialCapacity, float loadFactor)
  -->this.loadFactor = loadFactor;
  -->this.threshold = tableSizeFor(initialCapacity)
/**
 * Returns a power of two size for the given target capacity.
 */
static final int tableSizeFor(int cap) {
	int n = cap - 1;
	 n |= n >>> 1;
	 n |= n >>> 2;
	 n |= n >>> 4;
	 n |= n >>> 8;
	 n |= n >>> 16;
	 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

HashMap初始化的过程其实就是负载因子,和扩容阀值的设置过程。
其中tableSizeFor方法很有意思:
我们现在假设入参为9,我们先看看这一系列操作到底干了些什么。

int n = cap -1 = 9 -1 = 8 [1000]
n |= n >>> 1;
>>> :代表无符号右移动,也就是向右移位,高位直接补0  n |= 1 等同于 n = n | 1
n |= n >>> 1 = [[1000] >>> 1] | [1000] = [0100] | [1000] = [1100] 
n |= n >>> 2 = [[1100] >>> 2] | [1100] = [0011] | [1100] = [1111]
n |= n >>> 4 = [[1010] >>> 4] | [1111] = [0000] | [1111] = [1111]
...
n = 1111 = 16
return n+1 = 16
由此可见,这一系列操作会使得目标二进制数转化成大于当前数的最小二次方幂 - 1
例如 0001 --> 0001[2-1]       0010 --> 0011[4-1] 
     0100 --> 0111[8-1]       1000 --> 1111[16-1]
最后将结果加一就得到了大于当前数的最小的二次方幂,这个方法非常重要,这个是保证了hashMap的容量始终为2的幂次方的。

tableSizeFor
从源码注释可以看出:java规定HashMap的容量DEFAULT_INITIAL_CAPACITY必须为2的幂次方。那么为什么这么设计呢?下面通过put方法来为大家解答。

  • put方法
-->put(K key, V value)
  -->putVal(hash(key), key, value, false, true)
    -->if ((tab = table) == null || (n = tab.length) == 0)
      -->n = (tab = resize()).length; //扩容
    //将hash值映射到指定到tab索引位
    //一.映射到的索引位为空,直接put  
    -->if((p = tab[i = (n - 1) & hash]) == null)
      -->tab[i] = newNode(hash, key, value, null);
    //2.//映射到的索引位不为空(说明出现hash冲突),往桶内(链表或者树)插入   
    -->else  
      //1.如果key是相同的(equals 或者 ==),key的hash值也是相同的
      -->(p.hash == hash &&
         ((k = p.key) == key || (key != null && key.equals(k))))
        -->if (e != null) && (!onlyIfAbsent || oldValue == null)
          -->e.value = value//覆盖,默认覆盖 
      //2.key值不同,只是hash冲突,桶内是树结构
	  -->if (p instanceof TreeNode) 桶内是树结构
	    //往树内插入
	    -->e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)
	  //3.key值不同,只是hash冲突,桶内是链表结构  
	  -->else //桶内是链表结构
	    //在遍历的过程中每次都需要判断key值是否冲突,冲突直接覆盖(代码见源码)
	    //插入到链表最后
	    -->p.next = newNode(hash, key, value, null); 
	    -->if (binCount >= TREEIFY_THRESHOLD - 1) //如果长度到达桶的树化阀值
	      -->treeifyBin(tab, hash); //将链表转化成树

       接下来我们来分析:容量为什么一定要是2的幂次方,其实,从put方法中定位插入索引位置的时候用了(n - 1) & hash,这里的 n 就是当前Map的容量,这个操作是因为Hash本身太大,不可能直接映射到一个索引,通过这个方式我们可以保证每一个待插入的值都能映射到hash表中,我们现假设容量为 n = 16 ,那么 n-1 = 15 [1111]。(n - 1) & hash 的结果实际上就是截取来hash值的低四位,(2的幂次方 -1 )的低位一定全是 1。那如果容量 n = 9 那么 n-1 = [1000] ,(n - 1) & hash 的结果仅仅只保留了倒数第四位,那么插入的值就只能放在 [0000,1000] , 那么hash冲突的概率将会极高。
       所以,使用2的幂次方作为容量,是为了降低hash冲突考虑。

  • hash方法
 /**
  * 扰动函数
  * Computes key.hashCode() and spreads (XORs) higher bits of hash
  * to lower.  Because the table uses power-of-two masking, sets of
  * hashes that vary only in bits above the current mask will
  * always collide. (Among known examples are sets of Float keys
  * holding consecutive whole numbers in small tables.)  So we
  * apply a transform that spreads the impact of higher bits
  * downward. There is a tradeoff between speed, utility, and
  * quality of bit-spreading. Because many common sets of hashes
  * are already reasonably distributed (so don't benefit from
  * spreading), and because we use trees to handle large sets of
  * collisions in bins, we just XOR some shifted bits in the
  * cheapest possible way to reduce systematic lossage, as well as
  * to incorporate impact of the highest bits that would otherwise
  * never be used in index calculations because of table bounds.
  */
 static final int hash(Object key) {
     int h;
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 }

       在之前的put方法之前,我们需要计算key的hash值,用以确定插入的值到底映射到哪一个位置上,这里并没有使用对象自己的hash方法求hash值,是因为直接将系统给出的hash值,直接取低四位做映射冲突依旧很大,所以这里jdk提供给我们一个hash方法(对hash值做扰动)。
一. key == null ,它的hash值直接给0
二. (h = key.hashCode()) ^ (h >>> 16)
这个操作实际上首先将 hash值 的高十六位移到低十六位去,然后与hash值做按位异或操作,那么这样一来等同于低位保存高位的信息,使得冲突大大降低。这就是扰动函数的作用。
扰动函数

  • resize方法
    当插入元素后到达扩容阀值,开始扩容。
newThr = oldThr << 1; // double threshold

扩容的新数组容量为原数组的两倍

Node<K, V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;// 数组长度
int oldThr = threshold;// 临界值
int newCap, newThr = 0;
if (oldCap > 0) {
    // 扩容
    if (oldCap >= MAXIMUM_CAPACITY) {
        // 原数组长度大于最大容量(1073741824) 则将threshold设为Integer.MAX_VALUE=2147483647
        // 接近MAXIMUM_CAPACITY的两倍
        threshold = Integer.MAX_VALUE;
        return oldTab;
    } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
        // 新数组长度 是原来的2倍,
        // 临界值也扩大为原来2倍
        newThr = oldThr << 1;
    }
} else if (oldThr > 0) {
    // 如果原来的thredshold大于0则将容量设为原来的thredshold
    // 在第一次带参数初始化时候会有这种情况
    newCap = oldThr;
} else {
    // 在默认无参数初始化会有这种情况
    newCap = DEFAULT_INITIAL_CAPACITY;// 16
    newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);// 0.75*16=12
}
if (newThr == 0) {
    // 如果新 的容量 ==0
    float ft = (float) newCap * loadFactor;// loadFactor 哈希加载因子 默认0.75,可在初始化时传入,16*0.75=12 可以放12个键值对
    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ? (int) ft : Integer.MAX_VALUE);
}
threshold = newThr;// 将临界值设置为新临界值
if (oldTab != null) {
    for (int j = 0; j < oldCap; ++j) {
        Node<K,V> e;
        if ((e = oldTab[j]) != null) {
            oldTab[j] = null;
            //1:指定的桶内只有一个元素,直接重新分布
            if (e.next == null)
                newTab[e.hash & (newCap - 1)] = e;
            //2:指定的桶内是一颗红黑树(红黑树的扩容和链表类似)    
            else if (e instanceof TreeNode)
                ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
            //3:指定的桶内是一个链表    
            else { // preserve order
                Node<K,V> loHead = null, loTail = null;
                Node<K,V> hiHead = null, hiTail = null;
                Node<K,V> next;
                do {
                    next = e.next;
                    //假设oldCap=16,hash值的倒数第五位为0,该键值对位置不做改变
                    //将所有值为0的链表连起来
                    if ((e.hash & oldCap) == 0) {
                        if (loTail == null)
                            loHead = e;
                        else
                            loTail.next = e;
                        loTail = e;
                    }
                    //假设oldCap=16,hash值的倒数第五位为1,该键值对位置[原位置+oldCap]
                    将所有值为1的链表连起来
                    else {
                        if (hiTail == null)
                            hiHead = e;
                        else
                            hiTail.next = e;
                        hiTail = e;
                    }
                } while ((e = next) != null); 
                if (loTail != null) {
                    loTail.next = null;
                    //放在原位置 newTab[j] 
                    newTab[j] = loHead;
                }
                if (hiTail != null) {
                    hiTail.next = null;
                    //放在新位置 newTab[j + oldCap]
                    newTab[j + oldCap] = hiHead;
                }
            }
        }
    }
}

以下是扩容简要的过程:
HashMap扩容
总结:
次扩充为:与负载因子无关;第二次及以后为:与负载因子有关。

如果loadFactor很小很小,那么map中的table需要不断的扩容,导致除数越来越大,冲突越来越小!

如果loadFactor很大很大,那么当map中table放满了也不要求扩容,导致冲突越来越多,解决冲突而起的链表越来越长!


TreeNode解析

/**
 * Basic hash bin node, used for most entries.  (See below for
 * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
 */
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;

       以上是TreeNode的组成结构,它继承自Entry,Entry又继承自Node。所以这个时候节点既有树的特性又有链表的特性。

我们重点来看一下它的put方法:(这个方法特别重要,是后面理解split方法的基础)

/**
 * 当存在hash碰撞的时候,且元素数量大于8个时候,就会以红黑树的方式将这些元素组织起来
 * map 当前节点所在的HashMap对象
 * tab 当前HashMap对象的元素数组
 * h   指定key的hash值
 * k   指定key
 * v   指定key上要写入的值
 * 返回:指定key所匹配到的节点对象,针对这个对象去修改V(返回空说明创建了一个新节点)
 */
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                int h, K k, V v) {
    Class<?> kc = null; // 定义k的Class对象
    boolean searched = false; // 标识是否已经遍历过一次树,未必是从根节点遍历的,但是遍历路径上一定已经包含了后续需要比对的所有节点。
    TreeNode<K,V> root = (parent != null) ? root() : this; // 父节点不为空那么查找根节点,为空那么自身就是根节点
    for (TreeNode<K,V> p = root;;) { // 从根节点开始遍历,没有终止条件,只能从内部退出
        int dir, ph; K pk; // 声明方向、当前节点hash值、当前节点的键对象
        if ((ph = p.hash) > h) // 如果当前节点hash 大于 指定key的hash值
            dir = -1; // 要添加的元素应该放置在当前节点的左侧
        else if (ph < h) // 如果当前节点hash 小于 指定key的hash值
            dir = 1; // 要添加的元素应该放置在当前节点的右侧
        else if ((pk = p.key) == k || (k != null && k.equals(pk))) // 如果当前节点的键对象 和 指定key对象相同
            return p; // 那么就返回当前节点对象,在外层方法会对v进行写入
 
        // 走到这一步说明 当前节点的hash值  和 指定key的hash值  是相等的,但是equals不等
        else if ((kc == null &&
                    (kc = comparableClassFor(k)) == null) ||
                    (dir = compareComparables(kc, k, pk)) == 0) {
 
            // 走到这里说明:指定key没有实现comparable接口   或者   实现了comparable接口并且和当前节点的键对象比较之后相等(仅限第一次循环)
        
 
            /*
             * searched 标识是否已经对比过当前节点的左右子节点了
             * 如果还没有遍历过,那么就递归遍历对比,看是否能够得到那个键对象equals相等的的节点
             * 如果得到了键的equals相等的的节点就返回
             * 如果还是没有键的equals相等的节点,那说明应该创建一个新节点了
             */
            if (!searched) { // 如果还没有比对过当前节点的所有子节点
                TreeNode<K,V> q, ch; // 定义要返回的节点、和子节点
                searched = true; // 标识已经遍历过一次了
                /*
                 * 红黑树也是二叉树,所以只要沿着左右两侧遍历寻找就可以了
                 * 这是个短路运算,如果先从左侧就已经找到了,右侧就不需要遍历了
                 * find 方法内部还会有递归调用。参见:find方法解析
                 */
                if (((ch = p.left) != null &&
                        (q = ch.find(h, k, kc)) != null) ||
                    ((ch = p.right) != null &&
                        (q = ch.find(h, k, kc)) != null))
                    return q; // 找到了指定key键对应的
            }
 
            // 走到这里就说明,遍历了所有子节点也没有找到和当前键equals相等的节点
            dir = tieBreakOrder(k, pk); // 再比较一下当前节点键和指定key键的大小
        }
 
        TreeNode<K,V> xp = p; // 定义xp指向当前节点
        /*
        * 如果dir小于等于0,那么看当前节点的左节点是否为空,如果为空,就可以把要添加的元素作为当前节点的左节点,如果不为空,还需要下一轮继续比较
        * 如果dir大于等于0,那么看当前节点的右节点是否为空,如果为空,就可以把要添加的元素作为当前节点的右节点,如果不为空,还需要下一轮继续比较
        * 如果以上两条当中有一个子节点不为空,这个if中还做了一件事,那就是把p已经指向了对应的不为空的子节点,开始下一轮的比较
        */
        if ((p = (dir <= 0) ? p.left : p.right) == null) {  
            // 如果恰好要添加的方向上的子节点为空,此时节点p已经指向了这个空的子节点
            Node<K,V> xpn = xp.next; // 获取当前节点的next节点
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn); // 创建一个新的树节点
            if (dir <= 0)
                xp.left = x;  // 左孩子指向到这个新的树节点
            else
                xp.right = x; // 右孩子指向到这个新的树节点
            xp.next = x; // 链表中的next节点指向到这个新的树节点
            x.parent = x.prev = xp; // 这个新的树节点的父节点、前节点均设置为 当前的树节点
            if (xpn != null) // 如果原来的next节点不为空
                ((TreeNode<K,V>)xpn).prev = x; // 那么原来的next节点的前节点指向到新的树节点
            moveRootToFront(tab, balanceInsertion(root, x));// 重新平衡,以及新的根节点置顶
            return null; // 返回空,意味着产生了一个新节点
        }
    }
}
find方法解析
                 */
                if (((ch = p.left) != null &&
                        (q = ch.find(h, k, kc)) != null) ||
                    ((ch = p.right) != null &&
                        (q = ch.find(h, k, kc)) != null))
                    return q; // 找到了指定key键对应的
            }

            // 走到这里就说明,遍历了所有子节点也没有找到和当前键equals相等的节点
            dir = tieBreakOrder(k, pk); // 再比较一下当前节点键和指定key键的大小
        }

        TreeNode<K,V> xp = p; // 定义xp指向当前节点
        /*
        * 如果dir小于等于0,那么看当前节点的左节点是否为空,如果为空,就可以把要添加的元素作为当前节点的左节点,如果不为空,还需要下一轮继续比较
        * 如果dir大于等于0,那么看当前节点的右节点是否为空,如果为空,就可以把要添加的元素作为当前节点的右节点,如果不为空,还需要下一轮继续比较
        * 如果以上两条当中有一个子节点不为空,这个if中还做了一件事,那就是把p已经指向了对应的不为空的子节点,开始下一轮的比较
        */
        if ((p = (dir <= 0) ? p.left : p.right) == null) {  
            // 如果恰好要添加的方向上的子节点为空,此时节点p已经指向了这个空的子节点
            Node<K,V> xpn = xp.next; // 获取当前节点的next节点
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn); // 创建一个新的树节点(此时x.next =xpn)
            if (dir <= 0)
                xp.left = x;  // 左孩子指向到这个新的树节点
            else
                xp.right = x; // 右孩子指向到这个新的树节点
            xp.next = x; // 链表中的next节点指向到这个新的树节点
            x.parent = x.prev = xp; // 这个新的树节点的父节点、前节点均设置为 当前的树节点
            if (xpn != null) // 如果原来的next节点不为空
                ((TreeNode<K,V>)xpn).prev = x; // 那么原来的next节点的前节点指向到新的树节点
            moveRootToFront(tab, balanceInsertion(root, x));// 重新平衡,以及新的根节点置顶
            return null; // 返回空,意味着产生了一个新节点
        }
    }
}

我们前面有提到TreeNode其实也是一个二叉树,那么它是怎么构建成一个二叉树的呢?
请看下图:
TreeNode插入

  • split
           这个方法是前面扩容的时候有涉及的,这里我们来看一看,上面说过这里和链表的极其相似,因为红黑树我们事先维护来一个链表,所以对于树的处理就基本和链表的处理相似了,唯一不同的是,当元素个数到达阀值的时候会涉及树和链表的转变,请看源码。
/**
 * Splits nodes in a tree bin into lower and upper tree bins,
 * or untreeifies if now too small. Called only from resize;
 * see above discussion about split bits and indices.
 *  * @param map the map
 * @param tab the table for recording bin heads
 * @param index the index of the table being split
 * @param bit the bit of hash to split on
  */
 final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
     TreeNode<K,V> b = this;
     // Relink into lo and hi lists, preserving order
     TreeNode<K,V> loHead = null, loTail = null;
     TreeNode<K,V> hiHead = null, hiTail = null;
     int lc = 0, hc = 0;
     for (TreeNode<K,V> e = b, next; e != null; e = next) {
         next = (TreeNode<K,V>)e.next;
         e.next = null;
         if ((e.hash & bit) == 0) {
             if ((e.prev = loTail) == null)
                 loHead = e;
             else
                 loTail.next = e;
             loTail = e;
             ++lc;
         }
         else {
             if ((e.prev = hiTail) == null)
                 hiHead = e;
             else
                 hiTail.next = e;
             hiTail = e;
             ++hc;
         }
     }

     if (loHead != null) {
         if (lc <= UNTREEIFY_THRESHOLD)
             tab[index] = loHead.untreeify(map);
         else {
             tab[index] = loHead;
             if (hiHead != null) // (else is already treeified)
                 loHead.treeify(tab);
         }
     }
     if (hiHead != null) {
         if (hc <= UNTREEIFY_THRESHOLD)
             tab[index + bit] = hiHead.untreeify(map);
         else {
             tab[index + bit] = hiHead;
             if (loHead != null)
                 hiHead.treeify(tab);
         }
     }
 }
  • treeifyBin
    树化
          HashMap 在设计之初,并没有考虑到以后会引入红黑树进行优化。所以并没有像 TreeMap 那样,要求键类实现 comparable 接口或提供相应的比较器。但由于树化过程需要比较两个键对象的大小,在键类没有实现 comparable 接口的情况下,怎么比较键与键之间的大小呢?
          上面的插入其实已经涉及了,无非就是三个步骤:
          1:比较键与键之间 hash 的大小,如果 hash 相同,继续往下比较。
          2:检测键类是否实现了 Comparable 接口,如果实现调用 compareTo 方法进行比较。
          3:如果仍未比较出大小,就需要进行仲裁了,仲裁方法为 tieBreakOrder , 这个方法比较简单粗暴直接比较hashCode大小。
    树化的过程比较简单,真正需要了解的是红黑树。(网上有很多红黑树教程,可以自行去看)
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值