【java集合】HashMap源码分析

本文深入解析HashMap的工作原理,包括其内部结构、扩容机制、树化与反树化过程及核心方法实现,帮助读者掌握其高效运作的秘密。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

继承体系

在这里插入图片描述

重要的成员变量

	/**
     * 默认数组初始值大小
     * 0b 0001 左移四位 0b 1000 => 0d 16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * 数组最大长度
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默认负载因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 树化阈值,Node链表长度大于8会触发树化操作
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 树转化为链表的阈值,TreeNode树节点数小于6会将树转化为Node链表
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 树化的条件之一:数组长度大于等于64才会树化,否则优先进行扩容操作
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

	/**
	 * 哈希桶(数组),由于TreeNode继承于Node,所以树化后也可以存放TreeNode节点(父类指针可以指向子类引用)
	 */
	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;

    /**
     * 元素个数
     */
    transient int size;

    /**
     * 当前Hash表的修改次数,元素增加或减少都会加1,替换不影响
     * 		HashMap、TreeMap、ArrayList、LinkedList等都有modCount属性
     * 		Fail-Fast 机制 
     * 				HashMap 不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,
     * 				那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。
     * 				这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,
     * 				对HashMap 内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋
     * 				给迭代器的 expectedModCount。在迭代过程中,判断 modCount 跟expectedModCount 
     * 				是否相等,如果不相等就表示已经有其他线程修改了Map
     */
    transient int modCount;

    /**
     *  扩容阈值=数组长度*负载因子,元素个数超过这个阈值会触发扩容
     */
    int threshold;

    /**
     * 负载因子
     */
    final float loadFactor;

数据节点Node和TreeNode

在这里插入图片描述

构造函数

	/**
	 * 1. 给定数组初始长度和负载因子
	 */
	public HashMap(int initialCapacity, float loadFactor) {
		// 数组初始长度不能小于 0,否则抛异常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        // 数组的初始长度不能大于最大值
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        // 负载因子小于0,或者not a number则 抛出异常, 
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        // 设置负载因子
        this.loadFactor = loadFactor;
		// 计算数组的容量,暂时存到threshold(扩容阈值)
        this.threshold = tableSizeFor(initialCapacity);
    }

    /**
     * 2. 给定数组容量,用默认的负载因子0.75
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * 3. 使用默认的数组长度16和负载因子0.75
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    /**
     *  4. 用一个map集合去初始化另一个map集合
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
/**
     * 计算一个出一个比给定值大的2的n次方的数作为数组容量
     */
    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;
    }

构造函数1.2.3都不会创建table数组,HashMap会在第一次put数据时才创建table数组

几个关键流程

put流程

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
@Override
public V putIfAbsent(K key, V value) {
    return putVal(hash(key), key, value, true, true);
}
/**
 * onlyIfAbsent参数 
 * 		putIfAbsent方法传true,当key已经存在且value不为null时直接返回旧值,不更新
 * 		其他情况传false,key已经存在时更新旧值
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // 临时表量 tab=》table数组,p =》临时node节点, n =》table长度, i =》临时桶下标
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 判断是否初始化table数组  table为null,长度为0 表示未初始化
    if ((tab = table) == null || (n = tab.length) == 0)
    	// 调用扩容方法进行初始化,后面扩容部分再分析
        n = (tab = resize()).length;
    // 通过hash码计算出桶下标,该桶位还没有放置元素
    if ((p = tab[i = (n - 1) & hash]) == null)
    	// 直接构造一个node节点作为首节点
        tab[i] = newNode(hash, key, value, null);
    // 该桶位已经有元素了(只有一个node、node链表 或者 红黑树)
    else {
        Node<K,V> e; K k;
        // 第一个元素就发生hash冲突
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p; // 临时变量e暂时指向key值相同的node,后续根据putIfAbsent决定是否替换旧的value值
        // 桶位放置的是红黑树 (TreeNode继承于Node)
        else if (p instanceof TreeNode)
        	// 红黑树中没有相同的key,插入成功,返回null
        	// 红黑树中已经存在相同的key,临时变量e暂时指向key值相同的node,后续根据putIfAbsent决定是否替换旧的value值
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 链表情况
        else {
        	// 遍历链表
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) { // 遍历到链表尾部,构造Node 完成插入
                    p.next = newNode(hash, key, value, null);
                    // 插入完成后检查链表长度是否超过 8
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    	// 尝试树化,树化前要检查table数组的长度是否小于64,小于64时优先扩容
                        treeifyBin(tab, hash);
                    break;
                }
                // 遍历链表过程中发生hash冲突,有相同的key
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 直接结束循环,e已经指向了那个key相同的Node
                    break;
                p = e; // e =>遍历的当前节点, p => 上一个节点
            }
        }
        // e ==null 没有遇到相同的key,已经完成了插入
        // e != null 遇到相同的key值,统一处理
        if (e != null) { // existing mapping for key
            V oldValue = e.value; // 先保存旧值
            // onlyIfAbsent为false 或者 旧值为空 做更新操作
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); // 节点访问后序处理
            return oldValue; // Hash冲突,涉及更新操作时,返回旧值
        }
    }
    ++modCount; // 插入成功(没有发生更新操作)结构修改次数加1
    if (++size > threshold) // 元素个数超过阈值,触发扩容
        resize();
    afterNodeInsertion(evict); // 节点插入后续处理
    return null;  // 没有发生hash冲突,插入成功返回null
}

补充一下后续处理
以下这三个函数定义于HashMap,作用分别是:节点访问后、节点插入后、节点移除后做一些事情。LinkedHashMap继承于HashMap,重新实现了这3个函数,也就是说LinkedHashMap会用自己的实现做后置处理,而对HashMap来说啥也没做(方法体为空)!

// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }

reverse流程(table初始化和扩容)

扩容入口
2. 链表长度大于8,执行树化流程时发现table长度没到64,优先进行扩容
3. 插入元素后检查元素个数超过阈值threShold

先看看扩容过程中高链和低链的概念
以table长度16扩容到32为例,如图,当hash码后四位为1111时,和数组长度N-1进行与操作(N-1) & hash得到的下标都是(16-1) & hash = 15,扩容后这个链表的元素重新计算下标(32-1) & hash,此时hash码的第五位参与计算,得到下标有两种情况:

  1. hash码第五位为0时,下标不变,还是15,这些元素组成新的链表(低链),在新数组中桶下标不变
  2. hash码第五位为1时,下标需要加上旧数组的长度16,等于31,这些元素组成新的链表(高链),在新数组中的桶下标加在原来的基础上加原数组的长度

HashMap扩容时不管是链表还是红黑树都通过hash & 旧数组长度计算得到高低链,然后分别将链表放入新的桶位中,比如:

  1. …01111 & 1000 == 0 node放到低链
  2. …11111 & 1000 != 0 node放到高链
    其中红黑树完成两个链表的转移之后还要进一步判断数否需要树化(拆分后的两个链表长度还有可能大于8)。
    在这里插入图片描述
/**
 * Initializes or doubles table size.  If null, allocates in
 * accord with initial capacity target held in field threshold.
 * Otherwise, because we are using power-of-two expansion, the
 * elements from each bin must either stay at same index, or move
 * with a power of two offset in the new table.
 *
 * @return the table
 */
final Node<K,V>[] resize() {
	// 临时变量接收旧table
    Node<K,V>[] oldTab = table;
    // 旧table的长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 旧table触发扩容的阈值
    int oldThr = threshold;
    // 新数组的长度和阈值都先给0
    int newCap, newThr = 0;
    // oldCap等于0表示初始化时调用resize,大于0就是扩容场景
    if (oldCap > 0) {
    	// 数组长度不能超过最大值 1 << 30
        if (oldCap >= MAXIMUM_CAPACITY) {
        	// 阈值给大,防止再次触发扩容
            threshold = Integer.MAX_VALUE;
            return oldTab; // 直接返回
        }
        // 新数组长度乘2后不超过数组的最大值 且 旧数组长度大于等于16
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold 阈值翻倍
    }
    /*
     * oldCap 等于0  初始化table场景
     * 在构造函数没有给定数组大小的情况下 threshold为int类型的默认值0
     * 在构造函数给出数组长度的情况下 threshold暂时存储计算出来的数组长度
     * 对应的构造函数
     * 		HashMap(int initialCapacity)
     * 		HashMap(int initialCapacity, float loadFactor)
     */
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr; // 初始化前threshold存的是计算的数组长度
    // oldThr 等于0 对应无参构造 数组长度用默认19,阈值使用16*0.75 = 12
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    /* 1. 旧数组长度不到16时触发扩容的场景
     * 2. 构造函数中给出initialCapacity时初始化table场景
     * 以上两个场景需要单独计算新的扩容阈值
     */
    if (newThr == 0) {
    	// 数组长度*负载因子
        float ft = (float)newCap * loadFactor;
        // 判断是否到达数组长度的上限,如果到达上限直接把扩容阈值给最大值Integer.MAX_VALUE,防止再次触发扩容
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr; // 接收临时变量计算好的值
    @SuppressWarnings({"rawtypes","unchecked"})
    // 扩容/初始化正式开始
    // 构建table数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab; // 赋值给table成员变量,到此初始化完成
    // 扩容
    // oldTab == null为初始化场景,不执行if块里的代码
    if (oldTab != null) {
    	// 遍历旧数组
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            // 桶位不为空
            if ((e = oldTab[j]) != null) {
            	// 临时变量e已经接收了这个同为的值,原来桶位置null,方便GC
                oldTab[j] = null;
                // 情况1 只有一个元素,则放到新数组对应下标的桶位即可
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 情况2 e是红黑树的头结点
                else if (e instanceof TreeNode)
                	// 数据迁移,后面分析
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // 情况三 e是链表头结点
                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;
                        // 低链:loHead => e1 -> e2 ->...-> en <= loTail 
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 高链:hiHead => e1 -> e2 ->...-> en <= hiTail
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null); // 遍历到null为止

					/*
					 * 两个链表放到新数组对应的桶位,loHead和loTail只是收尾节点的引用,方法执行完生命周期结束 
					 * 		loHead => e1 -> e2 ->...-> en <= loTail 
					 * 		newTab[j] -> e1 -> e2 ->...-> en
					 *  
					 * 		hiHead => e1 -> e2 ->...-> en <= hiTail
					 * 		newTab[j + oldCap]-> e1 -> e2 ->...-> en
					 */
                    if (loTail != null) {
                        loTail.next = null;
                        // 低链桶下标不变
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        // 高链桶下标加旧数组的容量
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

get、replace流程

get和replace都走的是getNode方法,代码很简单

// 核心方法:getNode
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

// 核心方法:getNode
public V replace(K key, V value) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) != null) {
        V oldValue = e.value;
        e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }
    return null;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 校验table并确定桶下标
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 总是会判断第一个是不是目标,正好是就返回首节点
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 判断有没有第二个,没有就返回null,有就区分链表和红黑树两种场景找目标节点
        if ((e = first.next) != null) {
            if (first instanceof TreeNode) // 红黑树调用getTreeNode
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do { // 链表,利用next指针依次迭代
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

remove流程

讨论一个问题,hashmap在put元素时会触发自动扩容,那么remove元素时会不会触发自动缩容呢?
答案是不会,在知乎上有一个关于对缩容存在的意义的讨论:为什么java的hashmap不支持动态缩小容量?
总结来说就是没有什么必要,真正遇到table数组很大占用着较大的内存空间而存储的元素却没几个的情况,完全可以手动处理(新创建一个HashMap,并把数据迁移过来即可)。

public boolean remove(Object key, Object value) {
	return removeNode(hash(key), key, value, true, true) != null;
}
/**
 * Implements Map.remove and related methods.
 *
 * @param hash key的hash值
 * @param key 要删除的元素的key
 * @param value 要删除的元素的value,matchValue参数为true时提供,元素的值匹配上才能删除
 * @param matchValue 是否值匹配才删除
 * @param movable 删除后是否移动节点
 * @return 返回删除的节点,删除失败返回null
 */
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    // 先查
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        // 老套路了,先看首节点
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p; // 从这里直接跳到删除逻辑,在删除逻辑中通过 node == p 判断是不是删除的首节点 
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    // 如果没找到目标节点,下一次循环 p.next == e
                    // 如果找到了node,则 p.next == e, 同时p.next == node,这个关系用在删除逻辑中
                    p = e; 
                } while ((e = e.next) != null);
            }
        }
		// 查到的目标节点node

		// 再删
		/**
		 * 如果不需要匹配value,直接删除
		 * 如果需要匹配value,“==” 或者 equals了才删
		 */
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
            	/* 走红黑树删除流程,movable参数表示删除后是否移动节点
            	 * 参数this代指HashMap对象
            	 * 为什么参数中不传递要删除的元素node  QAQ
            	 * 因为是这样node.removeTreeNode调用的,所以在removeTreeNode里面this就是要删除的节点了
            	 */
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p) // 要删除的节点node是首节点
                tab[index] = node.next; // 将node.next作为首节点,方法执行完node指向的对象进入GC
            else //  删除前 p.next == node,删除后 p.next == node.next,方法执行完node指向的对象进入GC
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

treeifyBin

table某个桶位node链表长度超过8时,会调用treeifyBin,检查table数组长度达到64的情况下,先把原来的单链表结构变成了双链表结构,然后调用TreeNode::treeify(table)进行树化。红黑树的结构同时维持着双链表的结构,

/**
 * tab:table数组
 * hash:hash值(增加的键值对的key的hash值)
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // table数组长度不到64时,优先进行扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    // 获取首节点,判断不为空
    else if ((e = tab[index = (n - 1) & hash]) != null) {
    	// 定义头结点head和尾结点tail
        TreeNode<K,V> hd = null, tl = null;
        // do-while循环 将把Node对象转换成了TreeNode对象,把单向链表转换成了双向链表:
        // hd => p1 <=> p2 <=> ... <=> pn <= tl 
        do {
        	// 将该Node节点转换为TreeNode节点
            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);// 双向链表转红黑树,后面分析
    }
}
// 将该Node节点转换为TreeNode节点
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
	return new TreeNode<>(p.hash, p.key, p.value, next);
}

简单理解下单链表转双链表的过程

  • 首先将Node节点转换为TreeNode节点,TreeNode节点中的next和prev分别维护双向链表的两个指针
  • hd和tl分别指的是双向链表头尾两个节点的引用,给tl赋值prev和next,就等于赋值最后一个节点赋值。
  • 方法执行完hd和tl作为局部变量生命周期结束,双链表的头结点的地址放在了table的对应桶位tab[index] = hd
    在这里插入图片描述

// TODO if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 这个tab==null的判断对应的场景暂时没搞清楚 后面搞懂了再补充

红黑树的相关操作

TreeNode::putTreeVal

put流程调用putTreeVal将元素插入红黑树

/**
 * map => HashMap对象
 * tab => table
 * h => hash值
 * k => key
 * v => value
 */
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;
	// 根节点赋值给临时变量p,二分查找
	for (TreeNode<K,V> p = root;;) {
	    int dir, ph; K pk;
	    // 传入的hash值h小于p节点的hash值,将dir赋值为-1,代表向p的左子树查找
	    if ((ph = p.hash) > h)
	        dir = -1;
	    // 传入的hash值大于p节点的hash值, 将dir赋值为1,代表向p的右子树查找
	    else if (ph < h)
	        dir = 1;
	    // 传入的hash值h等于p节点的哈希值,进一步判断key值等于p节点的key值, 如果相等就是找到与要插入的key相同的节点。将这个节点返回,在上层方法中决定是否替换value值
	    else if ((pk = p.key) == k || (k != null && k.equals(pk)))
	        return p;
	    /* 走到这里说明hash相同而key不相同, ph == h,pk !=k, k.equals(pk) == false
	     * 这意味着涉及到传入的k于当前节点key值pk之间的比较了
     * 		判断:
	     * 		如果没有实现Comparable<C>接口或者 实现该接口 并且 k与pk Comparable比较结果相同
	     * 		否则就是实现了comparable接口且比较不相等,直接到插入流程
	     */
	    else if ((kc == null &&
	              (kc = comparableClassFor(k)) == null) ||
	             (dir = compareComparables(kc, k, pk)) == 0) {
	        // 由于要用递归的方式在左右子树中搜索,所以如果已经搜索过,那么把searched设置为true,避免下次循环重复搜索
	        if (!searched) {
	            TreeNode<K,V> q, ch;
	            searched = true;
	            // 在左右子树递归的寻找 是否有key的hash相同 并且equals相同的节点
	            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;
	        }
	        // 说明红黑树中没有与之equals相等的 那就必须进行插入操作 必须将两个key分出大小 dir的结果必须是-1或1  
	        dir = tieBreakOrder(k, pk);
	    }
	
	    TreeNode<K,V> xp = p;
	    /*
        * 如果dir <= 0
        * 	p.left == null,把要添加的元素作为当前节点的左节点
        * 	p.left != null,p = p.left,下一轮循环
        * 如果dir>0
        * 	p.right == null,把要添加的元素作为当前节点的右节点
        * 	p.right !=null,p = p.right,下一轮循环
        */
	    if ((p = (dir <= 0) ? p.left : p.right) == null) {
	    	// 到这里p已经为null,且xp为p的父节点,至少一个子树是null
	        Node<K,V> xpn = xp.next;
	        // 创建一个新的树节点,注意这里设置了x.next为xpn,如果xpn不为null,name插入完成后还要设置xpn.prev为x
	        TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
	        if (dir <= 0)
	            xp.left = x; // 新节点作为左孩子
	        else
	            xp.right = x; // 新节点作为右孩子
	        // 维护双链表 并设置新节点的父节点
	        xp.next = x; // xp.next可能之前就有值,暂时保存在xpn中
	        x.parent = x.prev = xp;
	        // 如果原来的xp.next,即xpn节点不为空时,下面操作相当于把xpn接在了x后面
	        if (xpn != null) //  新节点覆盖了之前xp.next 的值
	        	/*
	        	 * 原来的xp.next 是 xpn
	        	 * 现在xp.next是x,x.prev 是 xp,xpn.prev 是 x,x.next早在创建x时就设置为xpn了
	            ((TreeNode<K,V>)xpn).prev = x;
	        // balanceInsertion 重新平衡
	        // 经过平衡,根节点可能已经过改变,为了查询方便需要把新的根节点放到table数组桶上
	        moveRootToFront(tab, balanceInsertion(root, x));
	        return null;
	    }
	}
}

补充说明:

  1. dir = tieBreakOrder(k, pk); hashCode一般是经过重写的,而且在HashMap中经过了二次哈希得到key的hash,这个方法调用identityHashCode获取k和pk重写前对象的哈希值来做比较。
    一句话解释identityHashCode是根据Object类hashCode()方法来计算hash值,无论子类是否重写了hashCode()方法。而obj.hashcode()是根据obj的hashcode()方法计算hash值

  2. for (TreeNode<K,V> p = root;;) {...} 比较key是否相等,用到一个约定:对象相等,hash一定相等,hash相等,对象不一定相等,这个技术细节总结在hashCode和equals中,所以在上面源码中到了hash相等的情况需要进一步判断key值是否equals,如果key不equals,必须想办法分出大小(调用dir = tieBreakOrder(k, pk);比较重写前对象的hash值),做插入操作。但是,在这之前需要先判断之前有没有插入过重复的key,然后进入第3条的描述。

  3. 需要再判断是否实现Comparable接口,没有实现Comparable接口或者Comparable比较结果相同,这种情况只能从子树中找是否有key的hash相同 并且equals相同的节点(如果之前这种情况发生,那肯定也是没找到才插入到子树中的,那这一次肯定先找再插入),找到了返回。其他情况调用dir = tieBreakOrder(k, pk);比较重写前的hash,区分出大小后插入。

  4. 在红黑树中插入元素后还要维护双向链表,
    x节点插入到xp节点的子树时,假设xp节点的后继节点xpn是null,则将x放到双链表后面即可
    在这里插入图片描述

x节点插入到xp节点的子树时,假设xp节点的后继节点xpn不为空,则将x放到双链表xp和xpn之间
在这里插入图片描述

下面两个方法,插入、查询、树化操作会用到
static Class<?> comparableClassFor(Object x) 大佬的文章
static int compareComparables(Class<?> kc, Object k, Object x) 比较简单,返回compareTo的比较结果
下面方法,插入、树化操作会用到,查询不用的原因看后面getTreeNode中的分析
static int tieBreakOrder(Object a, Object b)

TreeNode::treeify

将双链表转为红黑树
小细节: 正如章节标题TreeNode::treeify的意思,treeify的作用域是TreeNode类,调用它必然是TreeNode对象.treeify那么这个方法内部的this就指代这个TreeNode对象
调用链表首节点.treeify则this就指代首节点了,后续节点用next指针迭代就可以了。

/**
 * Forms tree of the nodes linked from this node.
 */
final void treeify(Node<K,V>[] tab) {
	// 定义树的根节点
    TreeNode<K,V> root = null;
    // 遍历链表,x当前节点,this为首节点,next下一个节点
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        // 某个桶位扩容前是红黑树,扩容后还是红黑树的情况下,x的左右子树不为null。
        x.left = x.right = null;
        // 第一次循环还没有设置根节点
        if (root == null) {
            x.parent = null; // 根节点没有父节点
            x.red = false; // 根节点一定是得是黑色
            root = x; // 设置根节点
        }
        // 第二次以后的循环已经存在根节点了
        else {
        	// 获取当前链表节点的key和hash值
            K k = x.key;
            int h = x.hash;
            // 定义当前链表节点key的运行时类型
            Class<?> kc = null; 
            // 遍历插入红黑树
            for (TreeNode<K,V> p = root;;) {
            	// dir 插入方向, ph 当前树节点的hash值
                int dir, ph;
                K pk = p.key; // pk 当前树节点的key
                // 如果当前树节点hash值 > 当前链表节点的hash值
                if ((ph = p.hash) > h)
                    dir = -1; // 标识向左树插入
                else if (ph < h)
                    dir = 1;  // 右树
                /* ph = h,hash值相等 做以下判断(PS:和putTreeVal过程不同的是,这里不用判断key是否相等,因为已经形成链表了,key必然不会相等了)
                 * 	kc == null &&(kc = comparableClassFor(k)) == null)
                 * 		true:当前链表节点的key没有实现comparable,直接执行dir = tieBreakOrder(k, pk);
                 * 		false:实现了comparable接口,执行 || 后面的表达式
                 * 	dir = compareComparables(kc, k, pk)) == 0
                 * 		true:通过compareTo比较大小,结果为0,没办法确定dir正负值,执行dir = tieBreakOrder(k, pk);
                 * 		false:已经确定dir的正负值,即确定了插入左边还是右边
                 */
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);
				// 插入逻辑,之前分析过了
                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    // 每插入一个元素就重新平衡,然后返回平衡后的根节点
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    // 目前放在table数组桶上的是链表的第一个节点,经过多次平衡这个节点很有可能不是root节点了,为了查询方便需要把root节点放到table数组桶上
    moveRootToFront(tab, root);
}

TreeNode::untreeify

/**
 * 反树化过程中将TreeNode转换成Node节点
 *   从结果来看TreeNode节点转为Node节点,TreeNode额外的属性没了,next属性不变
 * 	 即双链表转为单链表next维护的顺序没变
 */
final Node<K,V> untreeify(HashMap<K,V> map) {
	// 定义指向node链表头尾的指针(引用)
    Node<K,V> hd = null, tl = null;
    // q:遍历的当前节点,this:链表首节点(上层方法中首节点调用的untreeify)
    // q定义为Node,实际对象的类型为TreeNode
    for (Node<K,V> q = this; q != null; q = q.next) {
    	// 将q节点从TreeNode转为Node p
        Node<K,V> p = map.replacementNode(q, null);
        // 单链表: =>表示引用,->表示next的指向
        // tl => p1 -> p2 -> ... -> pn <= tl
        if (tl == null)
            hd = p;
        else
            tl.next = p;
        tl = p;
    }
    return hd;
}
// TreeNode转为Node
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
	return new Node<>(p.hash, p.key, p.value, next);
}

TreeNode::split

/**
 * 扩容过程中的红黑树元素的转移
 *
 * @param map 当前Map
 * @param tab 扩容后的新table
 * @param 旧table中红黑树的索引位置
 * @param bit 扩容前的容量
 */
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
	// this:第一个节点(上层方法中是第一个节点调用的split)
	// 由于红黑树中插入元素和链表的树化操作都会平衡和root节点置顶,所以桶上的第一个节点肯定是root节点,但不一定是双链表的首节点
	TreeNode<K,V> b = this;
    // 定义高低链表首尾节点的引用
    TreeNode<K,V> loHead = null, loTail = null;
    TreeNode<K,V> hiHead = null, hiTail = null;
    int lc = 0, hc = 0; // 定义高低链表的长度
    // 循环的当前节点e 下一个节点next
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        next = (TreeNode<K,V>)e.next;
        // 原来保存的next属性置空(因为第一次循环不会重新赋值),prev属性每次循环都会重新赋值
        e.next = null; 
        // 低链
        if ((e.hash & bit) == 0) {
        	
        	// 第一次遍历 loHead  loTail 都是null
            if ((e.prev = loTail) == null) // 设置最后一个节点的prev属性
                loHead = e; 
            else
                loTail.next = e; // 设置倒数第二个节点的next属性
            loTail = e; // loTail从倒数第二个节点指向最后一个节点(第一次循环从null指向e1)
            ++lc; // 链表长度加1
            /* 第一次循环执行结果:loHead => e1 <= loTail
             *		e1(即loTail)的next和prev暂时都指向自己
             * 第n次循环执行结果:loHead => e1 <-> e2 <->...<-> en <= loTail 
             * 		en(即loTail)的next在最开始已经置为null,下次循环else块中才会设置(但是没有下次循环了)
             */ 
        }
        // 高链,同上
        else {
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;
        }
    }

    if (loHead != null) {
    	// 低链元素个数小于等于 6
        if (lc <= UNTREEIFY_THRESHOLD)
        	// 红黑树转化为单链表, 低链在新table的下标不变
            tab[index] = loHead.untreeify(map);
        else {
        	// 先将双链表放到数组上
            tab[index] = loHead;
            // 高链为空说明之前的红黑树节点都在低链上,上面也分析了遍历的第一个节点是root节点也成为了双链表首节点,上面的操作只是让双链表的顺序发生了变化,无需操作
            if (hiHead != null) // (else is already treeified)
            	// 高链不为空则需要重新进行树化操作
                loHead.treeify(tab);
        }
    }
    // 同上,高链放置的桶下标 index + bit
    if (hiHead != null) {
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
                hiHead.treeify(tab);
        }
    }
}

TreeNode::getTreeNode

/**
 * 调用TreeNode::find方法
 */
final TreeNode<K,V> getTreeNode(int h, Object k) {
	// root() 返回根节点
	return ((parent != null) ? root() : this).find(h, k, null);
}
/**
 * h  k的hash值
 * k  元素的key
 * kc k的运行时类型
 */
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;
        if ((ph = p.hash) > h)
            p = pl; // 二分查找向左
        else if (ph < h)
            p = pr; // 向右
        // hash值相等时,key相等,则找到目标节点,返回
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;

		/*
		 * ph == h 且 k.equals(pk) == false时 才会进入以下几个判断
		 * 根据之前的putTreeValue代码分析可知,如果树中存了这个key,那么必然存在当前节点的子树中
         */ 

		// 左树都已经是null了,在之后的代码中判断向左子树还是右子树查询或者对pl递归查就没有意义了,直接将右子节点作作为当前节点,进入下一次循环,
        else if (pl == null)
            p = pr;
        // 同上
        else if (pr == null)
            p = pl;
        else if ((kc != null ||
                  (kc = comparableClassFor(k)) != null) &&
                 (dir = compareComparables(kc, k, pk)) != 0)
            // 到这里说明,左子节点与右子节点均不为null,目标key实现了Comparable接口,且与当前节点比较不为0,确定了从左子树查找还是从右子树查找
            p = (dir < 0) ? pl : pr;
        /* 目标key没有实现Comparable接口,或者comparaTo比较出来是0,则直接在右子树中查询,
         * 这个方法并没有在左子树中循环,因为这是一个递归方法,先遍历右子树并判断是否查找到,若无则将左子树根节点作为当前节点,不用遍历左子树依然可以覆盖全部情况
        */
        else if ((q = pr.find(h, k, kc)) != null)
            return q;
        else
            p = pl;
    } while (p != null);
    return null;
}

说明:

  1. 所有找到目标节点的代码位置都是这一行else if ((pk = p.key) == k || (k != null && k.equals(pk))) return p;
  2. 结合插入的过程,当遇到key的hash相同但不equals时,对于dir = tieBreakOrder(k, pk);确定出插入到左子树还是右子树的节点来说,查询时并没有用同样的方法确定向左查还是向右查,而是采用了递归的方式,递归时,先遍历右子树并判断是否查找到,如果没找到则将左子树根节点作为当前节点,不用遍历左子树依然可以覆盖全部情况

TreeNode::removeTreeNode

TreeNode::moveRootToFront

TreeNode::balanceInsertion

TreeNode::rotateLeft、TreeNode::rotateRight

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值