java1.8 HashMap源码深入解读

概述

在1.8版本之前一直是数组加上链表的结构,1.8版本引入了红黑树,也就是用空间来换取时间,所以现在的结构就变成了数组+红黑树+链表

如下源码解释如果有问题的话,麻烦各位指正,谢谢。

在阅读本文章之前需要了解以下几个知识点

位运算

  byte i_1 = 8; //00001000
        byte i_2 = 31;//00011111
        //=== 1. and运算 & === 相同位只要1个为0就是0
        //00001000 即8
        log.info("i_1 & i_2 {}",(i_2 & i_1));
        //=== 2. or运算 | ===  相同位只要一个为1即为1
        //00011111 31
        log.info("i_1 | i_2 {}",(i_2 | i_1));
        //=== 3. xor异或运算 ^ === 相同位不同则为1,相同则为0。
        //00010111 23
        log.info("i_1 ^ i_2 {}",(i_2 ^ i_1));
        //=== 4. not运算 ~ ===  0和1全部取反 按位取反是对补码进行运算,当运算完后,再将补码变回原码。
        //         00001000
        //按位取反 11110111
        //    减一 11110110
        //  再取反 00001001 -9 符号位取反变为负 ~n = -(n+1)
        log.info("~i_1 {}",(~i_1));
        //=== 5. shl运算 << === a shl b就表示把a转为二进制后左移b位 末尾用0补充 高位遗弃
        //i_1 << 2 00001000 向左移2位  00100000  即32
        log.info("i_1 << 2 {}",(i_1 << 2 ));
        //=== 5. shl运算 >> === a shl b就表示把a转为二进制后右移b位去掉末尾0
        //i_1 >> 2 00001000 向右移2位  00000010  即2 最高位是0,左边补齐0;最高为是1,左边补齐1;
        log.info("i_1 >> 2 {}",(i_1 >> 2 ));
        //=== 6. 无符号位右移 >>> === 无符号右移,忽略符号位,空位都以0补齐
        //i_1 >>> 2

链表结构

链表由一个一个node节点组成,每一个节点都会指向下一个节点

  static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//每个node节点保存该节点key的hash值,利于后面的查找
        final K key;
        V value;
        Node<K,V> next;//保存下一个节点的地址信息,该方法指向下一个节点

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

平衡二叉树结构 红黑树

在这里插入图片描述

红黑树的特性

  1. 每个节点或者是黑色,或者是红色。
  2. 根节点是黑色。
  3. 每个叶子节点(NIL)是黑色。 (注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点)
  4. 如果一个节点是红色的,则它的子节点必须是黑色的。
  5. 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。[这里指到叶子节点的路径]
 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // 父节点
        TreeNode<K,V> left;		//左节点
        TreeNode<K,V> right;	//右节点
        TreeNode<K,V> prev;    // 上一节点
        boolean red;			//红黑树特性,每个节点要么红色,要么其他色(黑色)
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }

        /**
         * 获取根节点
         */
        final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }
   }

hashMap源码解读

成员变量

    /**
     * 默认初始容量—必须是2的幂。
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * 最大容量,如果隐式指定较大的值时使用
	*由任何一个带参数的构造函数。
	*必须是2的幂<= 1<<30。
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 构造函数中没有指定时使用的负载因子。
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 链表节点转换红黑树节点的阈值, 9个节点转
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     *  红黑树节点转换链表节点的阈值, 6个节点转
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     *  转红黑树时, table的最小长度
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
    
     // 存储元素的数组,它的length总是2的幂次倍
    transient Node<K,V>[] table;
 
    // 存放具体元素的集
    transient Set<Map.Entry<K,V>> entrySet;
 
    // entrySet的个数也就是key-value的个数
    transient int size;
 
    // 每次扩容和更改map结构的计数器
    transient int modCount;
 
    // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
    int threshold;

无参构造函数

 /**
     * 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 the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
/**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @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);
        //桶数量最大不能超过MAXIMUM_CAPACITY 1<<30
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        //初始化负载因子0.75f
        this.loadFactor = loadFactor;
        //threshold 赋值大于initialCapacity且最近的2的整数次幂的数
        this.threshold = tableSizeFor(initialCapacity);
    }

int tableSizeFor(int cap)方法

  /**
     * 返回给定目标容量的两倍幂。
     */
    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;
    }
  • 首先让cap-1再赋值给n的目的是另找到的目标值大于或等于原值
  • n |= n >>> 1 运算到 n |= n >>> 16 通过下图能够得出结论,也就是让n的为1的最高位的左边都为1,然后通过n + 1 最高位的1进一位,其他的都为0刚好求得2的次幂数
    - n |= n >>> 1
    在这里插入图片描述
    应该返回2147483648 的咋还返回比1386861906还小的数1073741824呢?
    因为hashMap 现在最大桶数为1<<30(1073741824),而2147483648 则为1<<31
tableSizeFor方法总结

说白了就是如果n的二进制只有一个1的话就返回该数,否则就最左边的高位1进一位(其他位都为0)然后返回该数
构造函数总结
都会初始化负载因子,但是有参的情况下会计算参数的最接近2次幂的数,然后初始化桶的数量

put(K key, V value)

/**
     * 将指定值与此映射中的指定键关联。
	*如果映射以前包含键的映射,则为旧的
	值被替换。
     *
     * @param key 要与指定值关联的键
     * @param value 值与指定的键关联
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

putVal(int hash, K key, V value, booleanonlyIfAbsent,boolean evict)

  /**
     * Implements Map.put and related methods
     *
     * @param hash key的hash值
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent 为真时不会改变原有的值
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //p为(n - 1) & hash 对应索引位置的node头节点
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //如果数组没有被初始化则调用resize方法进行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //i = (n - 1) & hash 计算索引,如果该索引处为空值,就是没有放置node节点,就构建新的节点并放置在此处
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {//如果该索引处有数据,证明存在node节点
            Node<K,V> e; K k;
            //如果存放进来的节点(后面统称为新节点)的hash值与该索引处节点的hash值,key值都相同的话,则将p赋值给e
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode) //如果p为树节点,则以红黑树的方式去查找替换或者插入该节点
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            	//到这一步证明该节点为普通链表节点 遍历节点 binCount为节点个数 
                for (int binCount = 0; ; ++binCount) {
                	//如果p下一节点为空,证明该索引处只有一个node节点,即该节点为头节点
                    if ((e = p.next) == null) {
                    	//对p的下一节点构建新的节点并设置该节点的下一节点为null
                        p.next = newNode(hash, key, value, null);
                        //binCount  大于等于满足转化红黑树阈值8 -1 的大小 减一是因为当前节点为p的下一节点 则调用treeifyBin方法将链表节点转化为红黑树 
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                   //如果hash值 key值相同 证明找到了目标节点 则退出循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果e不为空,证明新节点对应的索引处不为空存在node节点
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //如果onlyIfAbsent 为false 就是允许改变原有的值,或者oldValue 为空,则覆盖原有的值,并返回原有的值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //结构修改次数自责你一
        ++modCount;
        //如果插入节点后节点大于当前阈值的话 则调用resize 方法扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
总结
  1. 调用hash()方法(下面有介绍)计算key的hash值
  2. 如果数组为空或者容量为0,则调用resize进行扩容
  3. 根据(n - 1) & hash (该值后面通称为Index)计算需要存放key在node数组的索引,如果该索引处无数据(没有node节点),则创建新的节点(以该key和value)放置在该索引处
  4. 如果3成立(索引index处无数据),此时如果插入新的节点后当前的size大小(有多少个数组节点被存放了数据)大于当前的阈值,则调用resize方法进行扩容并返回null
  5. 如果3不成立(index索引处有数据,存在node节点)(后面的步骤皆是),如果index对应的Node节点(后面统称为node)的key值,hash值与存放进来的key,对应的hash值相同,则覆盖原有的值返回原有的值(value)
  6. 如果node为树节点,则以红黑树的方式去查找目标,没有找到返回为空,找到了则覆盖原有的值并返回原有的值(value)
  7. 遍历index处的所有节点,如果没有找到目标节点,则将要存放进来的key和value放置该节点的尾节点处,此时如果节点的数量大于等于8 ,就将其转化为红黑树,由于此时新加了一个节点(不是覆盖原有的节点),所以此时回去判断插入了一个节点后满不满足阈值,如果大于,则代用resize方法进行扩容。如果节点数量小于8,就按照链表结构
  8. 如果遍历Index处所有节点找到了目标节点,则覆盖原有的值并返回原有的值

resize()方法

   /**
     *初始化或双精度表大小。如果为空,则分配in
	*符合田间阈值设定的初始产能目标。
	*否则,因为我们使用的是2的幂展开
	*每个bin中的元素必须保持相同的索引,或者移动
	*在新表中以2为偏移量的幂。
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        //原来的数组大小
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //原来的阈值
        int oldThr = threshold;
        int newCap, newThr = 0;
        //以下if的逻辑主要是是否扩展容量并重新对临界值进行相应的计算(负载因子乘以新数组的大小)
        if (oldCap > 0) {
        	//数组最大数不能超过1<<30,否则返回最大数1<<30
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //新数组大小较之前翻倍,翻倍的大小如果小于最大界限值并且原来的数组大小大于默认值16 则新的阈值较之前翻倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // 初始容量设置为阈值
        	//这个时候一般hashMap都进行了初始化了
            newCap = oldThr;
        else {               // 初始阈值为零表示使用默认值
        	// 新数组大小为默认值16
            newCap = DEFAULT_INITIAL_CAPACITY;
            //新的阈值为默认的负载因子乘以默认的数组大小
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {//如果新的阈值为0则重新赋值,新的数组大小乘以负载因子,最大数为Integer.MAX_VALUE
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //将新的阈值赋值给临界值
        threshold = newThr;
        //创建newCap大小的Node数组
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        //以下if语句主要是在原来的数组基础上,对新的newTab数组进行重新初始化操作
        if (oldTab != null) {//如果原来的node数组含有数据
            for (int j = 0; j < oldCap; ++j) {//遍历原来的node数组
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {//获取下标为j的node节点赋值给e并判断空值
                    oldTab[j] = null; //将原来下标为j的节点置空
                    if (e.next == null)//如果e的下一个节点为空,证明原来数组该节点只有一个节点,所以可以直接通过key的hash值 对 新数组的小减一 与 运算得出该key对应的数组下标,然后将节点的值赋值到该位置
                        newTab[e.hash & (newCap - 1)] = e;
                        //如果该节点为红黑树,则以红黑树的方式进行维护节点
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                    	//loHead 低位头节点 loTail 低位尾节点
                        Node<K,V> loHead = null, loTail = null;
                        //hiHead 高位头节点 hiTail 高位尾节点 注意所谓的低节点和高节点的区别就是高节点满足扩容后的条件即(e & (h *2 -1)),而低节点只满足(e & (h -1))无法满足挪移的条件
                        Node<K,V> hiHead = null, hiTail = null;
                        //next 下一个节点
                        Node<K,V> next;
                        do {
                        	//获取到该节点的下一个节点
                            next = e.next;
                            if ((e.hash & oldCap) == 0) { //如果e的hash值与老表进行与运算为0,则扩容后的索引位置与老表保持一致
                                if (loTail == null) //如果低位尾节点loTail为空
                                    loHead = e; //设置新的节点为低位头节点
                                else
                                    loTail.next = e; //否则将新增的节点放到低位尾节点loTail的后面
                                loTail = e;//设置loTail 为新增的节点
                            }
                            else {//如果e的hash值与老表进行与运算不为0,扩展后的索引在原来的索引上加上原来数组的大小
                                if (hiTail == null)//如果高位尾节点为null
                                    hiHead = e;//设置新的节点为高位头节点
                                else
                                    hiTail.next = e; //否则将新的节点放到高位尾节点的后面
                                hiTail = e;//设置hiTail 为新增的节点
                            }
                        } while ((e = next) != null);
                        //如果低位尾节点loTail 不为空
                        if (loTail != null) {
                        	//将低位尾节点loTail的后节点设置为空
                            loTail.next = null;
                            //将原数组索引的节点设置为低位头节点loHead
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                        	//将高位尾节点hiTail的后节点设置为空
                            hiTail.next = null;
                            //在原数组索引的节点上加上原来的数组的大小上设置为高位头节点hiHead
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
  1. 如果原来的数组不为空并且数组大小超过了最大容量1<<30,则将阈值更新为1<<31(最大容量基础上翻倍)并返回原来的数组
  2. 如果原来的数组不为空,则在原来的大小上进行翻倍,这个时候如果翻倍后的容量小于最大容量且原来数组大小大于默认的大小则更新新的阈值为新的数组大小乘以负载因子。
  3. 如果原来的阈值大于0,则新的数组的大小则为原来的阈值大小
  4. 如果原来的数组为空,则新的数组大小为默认值16,新的阈值为16*0.75f(默认负载因子)
  5. 如果新的阈值为0,新的阈值为新的数组大小乘以负载因子,阈值最大数为Integer.MAX_VALUE
  6. 更新新的阈值,创建新的容量(通过上面步骤计算出来的容量)大小的node数组
  7. 如果原来的数组为空,则返回新的数组
  8. 如果原来的数组不为空,遍历原来数组的所有节点
  9. 如果当前遍历的节点不为空且当前的节点只有一个节点(该节点的下一个节点为空),则通过计算节点的hash值 对新数组容量大小减一进行与运算获取到新数组的索引值,然后将该节点赋值到索引对应的位置上
  10. 如果当前遍历的节点不为空并且当前节点的类型为树节点,则调用split方法遍历节点重新进行hash分布,分布之后重新判断节点数是否满足阈值的大小,
  11. 如果当前遍历的节点不为空,通过e的hash值与老表容量大小进行与运算是否0,如果为0,则这些节点(运算为0的节点)拼接后放到原索引位置上,否则将这些节点拼接后放到原索引加上原数组带下的位置上

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);
    }
  1. 通过hash方法获取到key的hash值(h = key.hashCode()) ^ (h >>> 16)
  2. 通过hash值对node数组大小 -1 的与运算获取到该key对应的索引 hash & (length - 1)

现在我们以key Skindow作为参考来做个实验
Skindow 对应的哈希值为-482940273 二进制为 11100011 00110110 11101010 10001111
在这里插入图片描述
通过运算得出Skindow 对应的hash值为 -482997831

然后根据hash & (length - 1) 计算对应的下标
length 默认为16 即与15做与运算

在这里插入图片描述
这里有个巧妙的地方为什么减一,因为我在前面中的tableSizeFor方法提过,node数组的容量永远是2的次幂数,如果减一,那么导致1的右边全部为1,在做与运算,因为1的左边全部是0,所以与运算后这些位置就都是0,然后其他位置做与运算算出来的数(蓝色框处部分)就会小于等于数组的容量减一,而下标是又从零开始的,所以下标最大数为数组容量大小减去一。这里又跟上面进行了巧妙的结合。

那么为什么hash值为什么这样算呢?(h = key.hashCode()) ^ (h >>> 16)
首相h >>> 16 然后在与h做异或运算,通过上图我们可以得出结论让高位参与与低位的异或运算并且高位也被保留了下来,那么这样做的目的又是什么呢?
我个人的理解是,int类型本身是32位,用16位高位(就是自身的一半)去与低位做异或运算,总体来说说就是让高位参与进低位的索引运算,舍去了低位的精度,这样就避免了key精度较小的情况下会定位到同一个桶中的问题,所以这样做的好处是为了做好hash的均匀分布

回过头来再看看resize方法 用(e.hash & oldCap) == 0 来判断 新增的节点是放在原索引位置还是放在原索引位置加上原数组容量的大小的索引上的
我们先看看oldCap的特征,它为2的次幂数,那么将它与哈希值进行与运算,oldCap的为1的位置要么为0要么为1而其他位永远是0,所以进行与运算之后得出的结果要么是0要么是oldCap
所以这里将原来一个链表分成了两个链表,并分配到原来的索引位置上或者加上原来数组大小的索引位置上
在这里插入图片描述
但是我们回过头来想下,就是上图9下标位置上的新节点,算法为(16 -1) & hash值,与 原来的索引((8 -1) & hash) 加上8 按照设计原则两者应该是相等的
我们看下图 假设hash值为9
在这里插入图片描述
果然是相等的,那么我在来看看区别,当容量为8时,只有三位数参与有效运算当中(蓝色标记),当容量为16时,因为是翻倍的原因,所以参与有效运算的从三位增加到了四位,其中这四位中有三位是与之前一样的,这里注意,因为是一样的,所以这里其实可以不用运算,也就是直接将之前运算好的(8 -1) & hash拿来用,也就是如果四位是相同的,那么索引就是二进制1000加上(8 -1) & hash,这里正好和我们讨论的问题点对接上了。

总结

(e.hash & oldCap) != 0 证明翻倍后的容量减一后的最左边的1和hash值对应的位置上数是相同的也是1否则就不是相同的,这种情况就满足将低位节点的元素往高位节点挪移,这样做的目的只有一个尽量保持node节点的均匀分布,减少hash冲突
在这里插入图片描述
上图忽略树节点

split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit)方法,维护树节点

/**
         * 将树仓中的节点分成上下两个树仓,
		*如果现在太小,就不需要树。仅从resize调用;
		*参见上面关于分割位和索引的讨论。
         *
         * @param map 该集合
         * @param tab node数组
         * @param index 当前节点在node数组的下标
         * @param bit 旧数组的容量大小
         */
        final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            TreeNode<K,V> b = this;
            // 重新链接到lo和hi列表,保持顺序
            //loHead 低位头节点 loTail 低位尾节点
            TreeNode<K,V> loHead = null, loTail = null;
             //hiHead 高位头节点 hiTail 高位尾节点 注意所谓的低节点和高节点的区别就是高节点满足扩容后的条件即(e & (h *2 -1)),而低节点只满足(e & (h -1))无法满足挪移的条件
            TreeNode<K,V> hiHead = null, hiTail = null;
            
            int lc = 0, hc = 0;
            //遍历node节点
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
            	//获取节点的下一个节点
                next = (TreeNode<K,V>)e.next;
                //将下一个节点置空
                e.next = null;
                //如果e的hash值与老表进行与运算为0 则扩容后的索引位置与老表保持一致
                if ((e.hash & bit) == 0) {
                //e.prev 上一个节点 如果lotail 为空 证明该节点为头节点
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                    //loTail不为空,则将将该节点e设置为loTail的下一个节点
                        loTail.next = e;
                    loTail = e;
                    //记录低位节点的个数
                    ++lc;
                }
                else {// //如果e的hash值与老表进行与运算不为0 则扩容后的索引位置对应老表原先索引加上原先数组的容量大小
                    if ((e.prev = hiTail) == null)//e.prev 上一个节点 如果lotail 为空 证明该节点为头节点
                        hiHead = e;
                    else  //loTail不为空,则将将该节点e设置为loTail的下一个节点
                        hiTail.next = e;
                    hiTail = e;
                     //记录高位节点的个数
                    ++hc;
                }
            }
			//如果低位头节点不为空
            if (loHead != null) {
            //如果低位节点的个数小于等于红黑树的阈值6
                if (lc <= UNTREEIFY_THRESHOLD)
                //loHead.untreeify按照链表结构拼接(非树节点),就是讲红黑树转化为普通链表结构,然后返回的节点赋值在原先的索引处
                    tab[index] = loHead.untreeify(map);
                else {
                //将低位头节点放置原先索引处
                    tab[index] = loHead;
                    //如果高位头节点不为空
                    if (hiHead != null) // hiHead 不为空,证明该红黑树被分成了两部分,所以需要重新对以loHead为根节点重新进行红黑树分布
                    //按照以loHead为根节点重新构建新的红黑树
                        loHead.treeify(tab);
                }
            }
            //如果高位头节点不为空
            if (hiHead != null) {
            //如果高位节点的个数小于等于红黑树的阈值6
                if (hc <= UNTREEIFY_THRESHOLD)
                //将红黑树转化为链表结构,并放置在原先索引加上原数组容量大小处
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                //将高位头节点放置在原先索引加上原数组容量大小处
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                   //重新对以hiHead为根节点构建新的红黑树
                        hiHead.treeify(tab);
                }
            }
        }
总结

split 方法和 resize 方法里面的遍历node节点一块的逻辑相差不大,只不过split方法会判断节点的个数是否满足红黑树阈值,如果小于等于6 则将该节点转化为链表结构,否则重新构建新的红黑树

final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v)方法

		/**
		 *map 该hashMap实例队形
		 *tab hashMap里的node数组节点
		 *h 需存放key的hash值
		 *k 需存放的key
		 *v 需存放的value
         * Tree version of putVal.
         */
        final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                       int h, K k, V v) {
            Class<?> kc = null;
            boolean searched = false;
            //获取该树节点的根节点
            TreeNode<K,V> root = (parent != null) ? root() : this;
            //遍历以根节点开始
            for (TreeNode<K,V> p = root;;) {
                int dir, ph; K pk;
                //当前节点的hash值大于需存放key的hash值
                if ((ph = p.hash) > h)
                    dir = -1;
                 //当前节点的hash值小于需存放key的hash值
                else if (ph < h)
                    dir = 1;
                 //当前节点的hash值等于需存放key的hash值 直接返回树节点
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                  //如果k没有实现Comparable接口,或者k和pk相同
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                   //搜索标识为false时
                    if (!searched) {
                        TreeNode<K,V> q, ch;
                        //将搜索标识改为true,searched第一次遍历后就设置为false,并且此时第一次的节点为根节点,也就是说默认第一次会根据判断hash值或者key值来决定左右遍历查找目标key
                        searched = true;
                        //如果当前节点左节点不为空就从左节点开始遍历查询,否则就从右节点开始查询
                        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;
                    }
                    //走到这里证明hash值相同且根据key值比较大小来遍布查询并没有找到目标对象,这时返回k和pk两者的系统hash值比较的大小结果,k小于等于pk返回-1否则返回1 给dir,相当于就是比较系统hash值来决定向左查询还是向右查询
                    dir = tieBreakOrder(k, pk);
                }
				//下一行代码p被赋值为当前p的左右节点,所以xp为p下面p的父节点
                TreeNode<K,V> xp = p;
                //比较系统hash值,hash值小于当前节点则向左边遍历查询否则向右边遍历查询,判断为空,是因为需要遍历二叉树找到左路劲还是右路劲下为空的情况,然后将要存入的k,v构建一个新的树节点存放到该出
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    Node<K,V> xpn = xp.next;
                    //构建一个存放进来key,value的树节点,并且下一节点指向当前节点的下一个节点xpn,上一节点指向xp,且父节点也指向xp
                    TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                    if (dir <= 0) //如果系统hash值小于当前节点则向左边遍历
                        xp.left = x;
                    else //否则向右边遍历
                        xp.right = x;
                    //xp下一节点指向x节点
                    xp.next = x;
                    //x的上一节点指向xp,且父节点也指向xp
                    x.parent = x.prev = xp;
                    //如果当前 x 节点下一节点不为空,则将下一节点的上一节点设置为当前节点x
                    if (xpn != null)
                        ((TreeNode<K,V>)xpn).prev = x;
                     //红黑树插入平衡调整
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
            }
        }

static Class<?> comparableClassFor(Object x)

  /** 
     * Returns x's Class if it is of the form "class C implements
     * Comparable<C>", else null.
     */
    static Class<?> comparableClassFor(Object x) {
    	//如果对象x实现了Comparable
        if (x instanceof Comparable) {
            Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
            //如果对象x的实体类对应为String类则直接返回对象x的实体类
            if ((c = x.getClass()) == String.class) // bypass checks
                return c;
             //调用getGenericInterfaces获取实体类c的所有实现类接口里面的type或者类并遍历这些类和type,Type 是 Java 编程语言中所有类型的公共高级接口。它们包括原始类型、参数化类型、数组类型、类型变量和基本类型。
            if ((ts = c.getGenericInterfaces()) != null) {
                for (int i = 0; i < ts.length; ++i) {
                	//ParameterizedType结合getRawType()获取这些实现接口的参数化类型,并且和Comparable相等
                    if (((t = ts[i]) instanceof ParameterizedType) &&
                        ((p = (ParameterizedType)t).getRawType() ==
                         Comparable.class) &&
                         //获取参数化类型里面的参数,并且参数类型只有一个当且类型为c,因为Comparable<T> 只有一个参数泛型,这里是对Comparable<T>的一个校验
                        (as = p.getActualTypeArguments()) != null &&
                        as.length == 1 && as[0] == c) // type arg is c
                        return c;
                }
            }
        }
        return null;
    }

该方法主要检查对象是否实现了Comparable接口,如果没有返回空,否则返回运行类

final TreeNode<K,V> find(int h, Object k, Class<?> kc)

    /**
         * Finds the node starting at root p with the given hash and key.
         * The kc argument caches comparableClassFor(key) upon first use
         * comparing keys.
         */
        final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
            TreeNode<K,V> p = this;
            //上面已经交代过了是从根节点向下遍历
            do {
                int ph, dir; K pk;
                TreeNode<K,V> pl = p.left, pr = p.right, q;
              	//如果当前节点hash值小于存放的hash值则向树节点的左边遍历
                if ((ph = p.hash) > h)
                    p = pl;
               //如果当前节点hash值大于存放的hash值则向树节点的左边遍历
                else if (ph < h)
                    p = pr;
                //如果当前节点key值等于存放的key值则直接返回该节点
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                //如果该节点下没有左节点则遍历右边的节点
                else if (pl == null)
                    p = pr;
                //如果该节点下没有右节点则遍历左边的节点
                else if (pr == null)
                    p = pl;
                //如果k实现了Comparable接口,并且存放的k值小于当前节点的k值则遍历做节点,否则遍历右节点
                else if ((kc != null ||
                          (kc = comparableClassFor(k)) != null) &&
                         (dir = compareComparables(kc, k, pk)) != 0)
                    p = (dir < 0) ? pl : pr;
                //到这一步证明k实体类没有实现Comparable接口,向右边遍历
                else if ((q = pr.find(h, k, kc)) != null)
                    return q;
                else
                //到这一步,证明刚刚pr.find 右边节点遍历没有找到对应的目标key,这时向左边遍历
                    p = pl;
            } while (p != null);
            return null;
        }
总结

通过比较hash值得大小,如果存放的hash值大于当前hash值,则往右边遍历,否则向左边遍历,如果当前节点没有左节点则往右边遍历,没有右节点就左边遍历,如果存放的节点key实现了Comparable接口,则利用比较key值来遍历做左右节点,小就遍历做节点,大就遍历右节点。原则是左小右大

root()

    /**
         * Returns root of tree containing this node.
         */
         //循环遍历父节点,如果父节点为null则证明该节点为根节点,并返回
        final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }

static int tieBreakOrder(Object a, Object b)方法

   /**
         * Tie-breaking utility for ordering insertions when equal
         * hashCodes and non-comparable. We don't require a total
         * order, just a consistent insertion rule to maintain
         * equivalence across rebalancings. Tie-breaking further than
         * necessary simplifies testing a bit.
         */
        static int tieBreakOrder(Object a, Object b) {
            int d;
            //a对象和b对象实体类的类名相同,则判断两者的系统hash值(根据两者的内存地址得出),a小于等于b的系统hash值返回-1 否则返回1
            if (a == null || b == null ||
                (d = a.getClass().getName().
                 compareTo(b.getClass().getName())) == 0)
                d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
                     -1 : 1);
            return d;
        }

static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root)方法

  		/**
         * 确保给定根是其bin的第一个节点。
         */
        static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
            int n;
            //如果root节点不为空,且node数组已经被初始化过
            if (root != null && tab != null && (n = tab.length) > 0) {
            	//获取root节点在node数组处的索引
                int index = (n - 1) & root.hash;
                //通过Index索引获取到对应的头节点first
                TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
                //如果头节点和root节点不相等
                if (root != first) {
                    Node<K,V> rn;
                    //用根节点覆盖原有的头节点
                    tab[index] = root;
                    //获取root节点的上一节点rp
                    TreeNode<K,V> rp = root.prev;
                    //如果root的下一节点不为空,则root的上一节点为root的下一节点的上节点,
                    if ((rn = root.next) != null)
                        ((TreeNode<K,V>)rn).prev = rp;
                    //如果rp不为空,则root的下一节点为root的上一节点的下一节点
                    if (rp != null)
                        rp.next = rn; //上面两个步骤相当于移除了root的角色,将root的上一节点和root的下一节点直接绕过root互相匹配
                        //如果头节点不为空,则root节点为头节点的上一节点
                    if (first != null)
                        first.prev = root;
                     //root的下一节点为first
                    root.next = first;
                    //将root的上一节点置null
                    root.prev = null;
                }
                //红黑树插入检查,不开启某种debug参数,该行代码不会运行,不影响整体流程
                assert checkInvariants(root);
            }
        }

转化红黑树方法treeifyBin(Node<K,V>[] tab, int hash)

   final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,TreeNode<K,V> p)方法 红黑树左旋

 static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                              TreeNode<K,V> p) {
            TreeNode<K,V> r, pp, rl;
            if (p != null && (r = p.right) != null) {
                if ((rl = p.right = r.left) != null)//将p的右节点的左节点作为p的右节点并互相指向子父节点
                    rl.parent = p;
                if ((pp = r.parent = p.parent) == null)//将p的右节点作为p父节点的右节点,相当于替换两者的位置并互相指向子父节点
                    (root = r).red = false;
                else if (pp.left == p)
                    pp.left = r;
                else
                    pp.right = r;
                //将p作为p右节点的左节点并互相指向子父节点
                r.left = p;
                p.parent = r;
            }
            return root;
        }

初始状态(pr 为 r)
在这里插入图片描述
if ((rl = p.right = r.left) != null)
rl.parent = p;
在这里插入图片描述
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
else if (pp.left == p)
pp.left = r;
else
pp.right = r;

在这里插入图片描述
r.left = p;
p.parent = r;

在这里插入图片描述
前后对比图
在这里插入图片描述

  • 第一阶段支点继承其右节点的左节点属性,将该属性与其右节点对应,这时候支点和右节点之间就没有了任何联系
  • 第二阶段这时候的右节点会继承原有p节点的父节点的属性
  • 第三阶段支点与其右节点进行拼接,右节点作为支点的父节点,而支点将作为右节点的左节点
总结

支点与其右节点互换位置,右节点继承支点的父节点属性,而支点则继承右节点的左节点属性,之后支点连接右节点,支点成为右节点的左节点,而右节点成为支点的父节点。
(5.13 以下论点有待商榷,有问题)
我们发现:在左旋之前路径皆为4个,在左旋之后就变成了3和4,可以得出结论,无论是左旋还是右旋都是在合理左旋右旋的前提下,这样做的目的是为了缩小树各个的树路径的平衡差,保持平衡二叉树的特性,左旋和右旋的前提条件是比如左节点必须大于父节点,父节点小于右节点。仔细看上图发现,在左旋之前,p<pr 且 prl<pr 这里就有个疑问了,如果prl>p呢?根据左图是无法得出此结论的,而且根据二叉树搜索原则查询prl时,会多查一次也就是pp -> p ->pr -> prl,如果我们提前直到prl >p的话就可以以p为左旋支点进行左旋,将prl作为p的右节点,这样我们在查询prl时pp ->p ->prl 就能查找得到了,这样就节省了搜索开支。
由此得出,左旋的触发条件为一个节点(p)小于右节点的左节点会发生左旋,这时p的右节点会成为它的父节点,原有的右节点的左节点会成为p的右节点

rotateRight(TreeNode<K,V> root,TreeNode<K,V> p)红黑树右旋

 static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
                                               TreeNode<K,V> p) {
            TreeNode<K,V> l, pp, lr;
            if (p != null && (l = p.left) != null) {
                if ((lr = p.left = l.right) != null)
                    lr.parent = p;
                if ((pp = l.parent = p.parent) == null)
                    (root = l).red = false;
                else if (pp.right == p)
                    pp.right = l;
                else
                    pp.left = l;
                l.right = p;
                p.parent = l;
            }
            return root;
        }

初始状态(pl为l)
在这里插入图片描述
if (p != null && (l = p.left) != null) {
if ((lr = p.left = l.right) != null)
lr.parent = p;
在这里插入图片描述
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
else if (pp.right == p)
pp.right = l;
else
pp.left = l;

在这里插入图片描述
l.right = p;
p.parent = l;
在这里插入图片描述
前后对比图
在这里插入图片描述

  • 第一阶段支点继承其左节点的右节点属性,将该属性与其左节点对应,这时候支点和左节点之间就没有了任何联系
  • 第二阶段这时候的左节点会继承原有p节点的父节点的属性
  • 第三阶段支点与其左节点进行拼接,左节点作为支点的父节点,而支点将作为左节点的右节点
总结

支点与其左节点互换位置,左节点继承支点的父节点属性,而支点则继承左节点的右节点属性,之后支点连接左节点,支点成为左节点的右节点,而左节点成为支点的父节点。
(5.13 以下论点有待商榷,有问题)
左图我们发现,p大于pl,plr也大于pl,但是无法判断p是否大于plr,但是右图我们发现p是大于plr的。
由此得出右旋的触发条件,当一个支点它大于它的左节点的右节点时会发生右旋,它的左节点成为它的父节点并且它成为该父节点的右节点,原有的左节点的右节点会成为它的左节点

通过左旋和右旋我们不难发现,一般发生左旋和右旋之前,都存在右两个数存在不合理的情况,比如无法得出某两个数谁大谁小,再通过左旋右旋之后就可以直观的得出任意两个数的大小。我们可以得出出一个原则就是平衡二叉树不存在任意两数存在模糊不清的关系,一旦有就要通过左旋和右旋来优化这个问题。

在线生成红黑树链接地址

注意看下方代码需要了解红黑树的基本特性,以及树的左旋右旋上面都有介绍
红黑树的插入流程图如下

  • 1.0 插入10,只有一个节点也就是根节点所以为黑色
    在这里插入图片描述
  • 2.0 插入12 ,12 大于10 节点12成为10的右节点,默认为红色,这个时候不需要调整,因为根节点本来就是黑色
    在这里插入图片描述
  • 3.0 插入11 ,11 大于10,进入10的右节点和其比较,小于该节点则成为12的左节点,但是11节点和12节点都为红节点不满足性质4,由于11是父节点12的左节点,而12父节点又为曾祖父节点10的右节点所以需要以11的父节点进行右旋
    在这里插入图片描述
  • 3.1 右旋之后,由于11和12两个节点还都是红色,12和11都为父节点的右节点,所以通过11进行左旋
    在这里插入图片描述
  • 3.2 左旋之后,11位于第一层没有父节点,所以该节点为根节点变成黑色
    在这里插入图片描述
  • 4.0 插入13,根据左大右小,13成为12的右节点
    在这里插入图片描述
  • 由于12 和13都是红色,所以只能通过变色来满足性质4和5,父节点同级的变成黑色,曾父节点变为红色,但是增父节点为根节点,所以又变为黑色
    在这里插入图片描述
  static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                    TreeNode<K,V> x) {
            x.red = true;
            for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
           		//如果当前节点x没有父节点,说明x节点即根节点,按照红黑树特性,设置当前节点为黑节点(非红即黑),并返回当前节点x(根节点)
                if ((xp = x.parent) == null) {
                    x.red = false;
                    return x;
                }
                //如果当前x节点父节点为黑节点,或者x节点父节点的父节点为空,换句话说就是这里判断x节点处于是否处于第二阶级,这些情况都不需要重新调整 返回根节点root
                else if (!xp.red || (xpp = xp.parent) == null)
                    return root;
                if (xp == (xppl = xpp.left)) {//走到这里证明x节点不是根节点,且x节点位于第二层的下面 如果x的父节点在x增父节点的左边
                    if ((xppr = xpp.right) != null && xppr.red) {//如果x的父节点在x增父节点的左边且曾父节点的右节点为红色节点  这是就出现了x节点和xp节点xppr都为红色节点,为了保持红黑树的特性需要变色,上一层级变为红色,上上层变为黑色。
                        xppr.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    }
                    else {//如果x的父节点在x增父节点的左边 且x的曾祖父右节点要么为空要么为黑色节点
                        if (x == xp.right) { //如果这时候x的父节点右节点为x 则以x的父节点进行左旋
                            root = rotateLeft(root, x = xp);
                            xpp = (xp = x.parent) == null ? null : xp.parent;
                        }
                        if (xp != null) {
                            xp.red = false;
                            if (xpp != null) {
                                xpp.red = true;
                                root = rotateRight(root, xpp);
                            }
                        }
                    }
                }
                else {
                    if (xppl != null && xppl.red) { //
                        xppl.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    }
                    else {
                        if (x == xp.left) {
                            root = rotateRight(root, x = xp);
                            xpp = (xp = x.parent) == null ? null : xp.parent;
                        }
                        if (xp != null) {
                            xp.red = false;
                            if (xpp != null) {
                                xpp.red = true;
                                root = rotateLeft(root, xpp);
                            }
                        }
                    }
                }
            }
        }

V get(Object key) 方法

 /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

5.1 这里主要调用了getNode方法,下面我们看看这个方法

  /**
     * Implements Map.get and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //如果数组被初始化了,并且对应的索引处含有值
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //索引处的头节点hash值,key值与存放进来的hash值,key值都相同,则直接返回该节点
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
              //如果索引处的头节点含有下一个节点不为空
            if ((e = first.next) != null) {
            //如果当前节点为树节点,则以红黑树的查找方式查找key,并返回对应的节点
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
             //遍历索引处的所有节点,直到查找到的node节点hash值和key值都相同或者没有下一个节点为止并返回该节点
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        //没有找到返回null
        return null;
    }
总结
  1. 如果数组没有被初始化,则直接返回null
  2. 根据(n - 1) & hash计算索引(后面统称为index)对应的node节头点,如果头节点的hash值和key值和要存放进去的hash值,key值都相同则直接返回该节点(常理来讲都是遍历查找才对的,为什么这里直接判断头节点呢?因为hashMap设计原则就是避免hash冲突,会尽量一个桶对应一个node节点的,所以这里就作为单独的一个步骤放在遍历的前面,少了遍历的性能消耗)
  3. 如果node节点含有下一个节点,首先判断该节点是否为树节点,如果为树节点则根据红黑树的方式查找对应key的节点,找到了直接返回没有找到返回空,否则遍历所有节点,直到查找到的node节点hash值和key值都相同或者没有下一个节点为止并返回该节点
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值