欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

HashMap源码学习(JDK 1.8)

程序员文章站 2022-03-23 22:47:32
什么是hash?把任意长度的输入通过一个hash算法之后,映射成固定长度的输出。在这个过程中会产生一种情况,就是两个不同的值通过hash算法计算出的结果是相同的,这个就是造成了哈希冲突。hash冲突理论上是无法避免的,只能说尽量取避免它。比如说通过该hash算法计算出来的值区间为[m , n] 。 但是现在有(n- m)* 2 个值要用该hash算法计算,那总会造成冲突。以String类来打个比方,String重写了Object类的hashCode() , 该方法返回的是一个int类型的值,也就是....

什么是hash?

hash是散列,杂凑的意思。hash算法可以把任意长度的输入通过一个hash算法之后,映射成固定长度的输出,这个输出内容和源数据每一个字节之间都有紧密的联系。在这个过程中会产生一种情况,就是两个不同的值通过hash算法计算出的结果是相同的,这个就是造成了哈希冲突。hash冲突理论上是无法避免的,只能说尽量取避免它。比如说通过该hash算法计算出来的值区间为[m , n] 。 但是现在有(n- m)* 2 个值要用该hash算法计算,那总会造成冲突。

以String类来打个比方,String重写了Object类的hashCode() , 该方法返回的是一个int类型的值,也就是说他最多能表示一个整形范围的hash值。  但是不同的字符串远远不止这个范围。所以这种hash冲突是无法避免的。

hash值是不能通过计算的方式反推出原值的。一个好的hash算法要尽可能降低hash冲突。

 首先Jdk1.8的HashMap底层数据结构: 数组 + 链表 + 红黑树。(每一个数据单元都是一个Node对象)

HashMap源码学习(JDK 1.8)

Node类中的成员与构造方法如下

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;	//key的hashCode再经过计算之后的值(使哈希值更加散列)
    final K key;	//保存key值
    V value;	        //保存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;
    }
}

HashMap中的一些默认常量:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    private static final long serialVersionUID = 362498820763181265L;

    //默认table大小 --> 16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
    //table的最大长度
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认的负载因子大小0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //默认链表升级为红黑树的阈值 == 8(注意:这只是树化的条件之一)
    static final int TREEIFY_THRESHOLD = 8;
    //树降级为链表的阈值
    static final int UNTREEIFY_THRESHOLD = 6;
    //树化的另一个参数---当哈希表中所有元素个数超过64时才被允许树化
    static final int MIN_TREEIFY_CAPACITY = 64;
}

HashMap中的属性成员:

transient Node<K,V>[] table;	//散列表

transient Set<Map.Entry<K,V>> entrySet;
    
transient int size;		//记录当前哈希表的元素个数
    
transient int modCount;	        //记录元素结构性修改次数
    
int threshold;			//扩容阈值(当哈希表中的元素超过阈值时触发扩容)

final float loadFactor;		//负载因子用于计算扩容阈值	threshold = capacity * loadFactor

HashMap的构造方法(列举其中三个)

//指定初始容量和负载因子
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;
    
    //获取初始的扩容阈值:手动指定initialCapacity为16及其以下得到值为16,传32及其以下得到32
    this.threshold = tableSizeFor(initialCapacity);				
}

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
    
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; 	//设置默认的负载因子0.75
}

双参构造方法需要指定初始容量与负载因子。当所指定的初识容量小于0时抛异常。且容量最大不能超过HashMap中规定的最大限度。负载因子不能为负数或者非法的值,因为负载因子要用于计算扩容阈值。接着得到初始的扩容阈值,利用tableSizeFor(int)方法。该方法的定义如下:

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 == 10  则n 为9 (0000 1001)
n |= n >>> 1:0000 1001 | 0000 0100(4) ==>0000 1101(13) 
n |= n >>> 2:0000 1101 | 0000 0011(3) ==>0000 1111(15)  
n |= n >>> 4:0000 1111 | 0000 0000(0) ==>0000 1111(15)
n |= n >>> 8和n |= n >>> 16 的结果还是15
返回n最终结果为负数时取1,大于 MAXIMUM_CAPACITY时 取 MAXIMUM_CAPACITY 否则取16

若cap == 16 则n为15 (0000 1111)
0000 1111 | 0000 0111 == 0000 1111(15)  最终返回n为16

当没有int n = cap - 1;时n == 16(0001 0000)
0001 0000 | 0000 1000 ==> 0001 1000(24)
0001 1000 | 0000 0110 ==> 0001 1110(30)
0001 1110 | 0000 0001 ==> 0001 1111(31)
0001 1111 | 0000 0000 ==> 31   (返回32)           想要的结果16---得到的结果32


综上发现规律:....
             4以上8及其以下:	最终得到补码0000 0111(8)	        2的3次方减一
             8以上16及其以下:	最终得到补码:0000 1111(15)     	2的4次方减一               
             16以上32及其以下:	最终得到补码0001 1111(31)		2的5次方减一																									                 
                                                                2的6次方减一    
                                                                ......       
		所以最终得到的初始扩容阈值总为2的次方数 
特别的:指定初始容量为0时,得到的初始阈值为0  

单参和无参构造方法是用的负载因子都是默认值。而且通过构造方法可以看出,在实例化对象时并没有为真正的散列表table开辟内存空间。

 

put方法的定义:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

当我们调用put方法是其底层是调用了核心方法:putVal    而且在调用该方法时执行了一次hash(key)

hash方法定义如下:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

可以看到在该方法中并没有直接使用对象的hashCode值,它是对该对象的hashCode值还进行了一次异或运算。这样可以使得hash值更加的散列。具体分析如下:

^运算符:相异为1,相同为0
>>>运算符:无符号右移  高位直接补0

首先不同的key对象,如果hashCode相同,则(h = key.hashCode()) ^ (h >>> 16) 相同 ---> 哈希冲突

h >>> 16   一定是  0000 0000  0000 0000  xxxx xxxx  xxxx xxxx 

对象A的hashCode : 00010100 10110000  00001111 00000001
对象B的hashCode : 01000100  01101001 00000111 00000001
如果没有此处的hash()方法, 直接使用对象的hashCode值,那么计算出的散列表中的桶位为:
    (桶位也就是table数组的下标位置,只不过采用数组加链表的方式,该下标位置的节点可能会形成一条链,就好像
    桶一样将各个节点装起来了一样,所以好多人给他起了个名字,叫桶位或者槽位。  
    计算公式tab[i = (n - 1) & hash],这个公式也叫寻址算法)

假如现在散列表容量为16 ==>  16 - 1 == 15(0000 1111)
对象A在散列表中的位置: table[1]
对象B在散列表中的位置: table[1]

如果加上此处的hash()
对象A : h >>> 16 ==> 00010100 10110000         那么异或运算后的值为:00010100 10110000 00011011 10110001
对象B:  h >>> 16 ==> 01000100  01101001		 那么异或运算后的值为:01000100  01101001 01000011 01101000

对象A在散列表中的位置:table[1]
对象B在散列表中的位置:table[8]

这种方案得到的hash值更加散列

得到hash值之后将其传入putVal方法中。

putVal:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; 	//散列表
    Node<K,V> p; 	//当前定位元素
    int n, i;		//n(散列表的长度)       i(寻址的结果)
		 
    //若当前散列表为null(构造方法并没有初始化散列表)执行resize(),触发第一次扩容(懒汉式)   
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;	//此处是触发第一次扩容
		 
    //如果该桶位没有任何元素(p在此处被赋值)
    if ((p = tab[i = (n - 1) & hash]) == null)
	tab[i] = newNode(hash, key, value, null);			
		 
    //该下标(桶位)处已经有值    -----这里又会被分为两种情况(一种是树结构,一是链表结构)
    else {
	Node<K,V> e; K k;
		   
	//如果这里的判断条件满足,即证明(散列表中已经存在该key对象)
	if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
	    e = p;
		     
	//如果是树结构
	else if (p instanceof TreeNode)				
	    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
		    
	//如果是链表结构
	else {
            //这里循环遍历链表
            for (int binCount = 0; ; ++binCount) {
	        /**
	         * 将当前节点的下一个节点引用保存在e中进行判断是否为null(也就是判断当前链表是不是遍历完了)
	         * 此处是已经遍历完链表的情况。如果已经遍历完了没有重复的元素,那就创造一个Node对象保存数据。并且添加到末尾
	         */
	         if ((e = p.next) == null) {				
		     p.next = newNode(hash, key, value, null);			
		    /**
		     *  TREEIFY_THRESHOLD的值为8。binCount从0开始, 0 -- 7,也就是循环到第8次才进如到下面if中。
		     *  到第8次的时候先进入上面if创造一个新的节点,在进入当前if中去。
		     *  强烈注意:此处执行treeifyBin并不是说直接就升级为树。
		     */
		     if (binCount >= TREEIFY_THRESHOLD - 1) 
		         treeifyBin(tab, hash);				
		     break;	//新数据保存完毕,跳出循环
	          }
		             
                //下面条件如果成立,那就是我们所说的“键重复”了
	        if (e.hash == hash &&  ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
		             
	        p = e;				
            }
        }
        /**
         *当创造出新节点时 p.next = newNode(hash, key, value, null);    并没有让e指向新节点。故e可能为null
         * 如果e != null  那就是“键重复”了,此处是将之前该节点的值(V)替换为新值。
         */
         if (e != null) {
            V oldValue = e.value;
	    if (!onlyIfAbsent || oldValue == null)
	        e.value = value;
	    afterNodeAccess(e);
	    return oldValue;		//结束put方法返回put进的新值
         }
    }

    /**
     * 能执行到这说明 e == null  也即put进了一个新的元素,散列表结构被改变了。
     * 键重复替换那不算  “结构性” 改变,只是某个“节点的值”改变了,所以结构性改变就是这个意思。
     * 该成员配合迭代器来检测并发修改异常。
     */
    ++modCount;				
		 
    //++size(先让散列表中元素的计数加1)再判断是否需要扩容了
    if (++size > threshold)
	resize();
		 
    afterNodeInsertion(evict);	//该方法是一个空方法,其主要目的是为了让子类重写该方法
    return null;
}

扩容方法resize()

首先扩容的是散列表(table数组)的长度。

为什么需要扩容:假如不扩容的情况下是否就不能完成任务,当然可以完成存取任务。因为散列表它本生是数组+链表+红黑树的方式实现,这一点就已经克服了数组的固定长度的短板。链表和树都是可以直接添加节点的。假如说现在没有扩容方法,那么在数据量非常庞大的情况下,可能会造成哈希冲突比较严重,也就是说它形成的树(JDK1.7的时候还没有引入红黑树,只是数组 + 链表的形式)或者链会很长。假如假如假如现在一个链现在有10000个节点,但是你要查找的元素恰好是最后一个节点,注意此处是单向链表。所以你只能从头开始往后一个一个找,这就会造成效率低下。如果能将一个长的链拆分成多个子链,或者说将一个很庞大的树拆分成多颗子树。那么查询效率就会好很多。所以扩容就是为了解决查询效率问题。当然扩容会增加内存占用量。所以这是一个空间换时间的思想。总的来说它是为了解决哈希冲突导致的链化影响查询效率的问题,扩容会缓解该问题。

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;		//保存原table数组的引用
    int oldCap = (oldTab == null) ? 0 : oldTab.length;	//oldCap表示原容量,当第一次扩容时oldTab == null
    int oldThr = threshold;		//触发本次扩容的阈值
        
    int newCap, newThr = 0;		//表示扩容之后的新容量 和 新的扩容阈值
        
    //原容量大于0,只有第一次扩容时原容量为0,这里就是为了区别是否为第一次扩容  
    if (oldCap > 0) {
        //容量都达到最大限度了那就别扩了
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
            
        //先将新容量指定为为原容量的2倍再进行检测,看是否超过最大限度 而且还需要旧容量 >= 默认初始容量才执行
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&  oldCap >= DEFAULT_INITIAL_CAPACITY)
        newThr = oldThr << 1; // 阈值变为原阈值的双倍
    }
        
     /**
      * oldCap == 0 且 oldThr > 0。
      * 就当前列举的三种构造方法而言:(首先需要oldCap == 0,即第一次扩容时才执行)
      * 双参构造方法手动指定阈值是会出现这种情况的(oldThr > 0)。 
      * 单参构造方法也会  默认传入初始阈值为16 
      * 无参则不会(第一次触发扩容时oldCap和oldThr都为0)
      */
    else if (oldThr > 0) 					
        newCap = oldThr;
        
    //oldCap == 0  &&  oldThr == 0   无参构造触发第一次扩容时走这条路
    else {              
        newCap = DEFAULT_INITIAL_CAPACITY;	//16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);	// (int)(0.75f * 16) == 12
        }
        
    /**
     * newThr == 0的情况(即newThr这个局部变量并没有被重新赋值):
     * oldCap > 0时 oldCap >= DEFAULT_INITIAL_CAPACITY不成立,也即手动指定的容量为16以下。
     * oldCap == 0 且 oldThr > 0时  (第一次扩容时 双参和单参构造方法都可能造成这种情况)
     */
    if (newThr == 0) {
         float ft = (float)newCap * loadFactor;        					 
         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?  (int)ft : Integer.MAX_VALUE);
    }
        
    //更新新的阈值
    threshold = newThr;
        
  /*------------------------------上面一大堆都是在计算newCap和newThr----------------------------------------------------------*/      
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];	//创建一个新容量大小的散列表
    table = newTab;
        
    //第一次扩容时不进入下面一堆。直接返回table。  
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {	//遍历原散列表
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {	//将原桶位处头元素的引用临时保存在e中	
                oldTab[j] = null;
                    
                if (e.next == null)		//如果该桶位只有一个节点
                    newTab[e.hash & (newCap - 1)] = e;
                    
                else if (e instanceof TreeNode)	//如果是树结构
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                   
                else {                     //不是树也不是单个节点 ----->   链表的情况
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;	//先获取当前节点的下一个节点
                        /**
                         * 下面是将一条链拆分为两个子链的过程,若现在是从16扩容到32
                         * 假设现在遍历到下标为3的位置,之前存的时候是这样计算的:(e.hash & (cap - 1))== 3
                         * 也就是e.hash & 0000 1111 == 3
                         * 那么之前在这条链上的元素的hash值就可能为  0010 0011 或者 0011 0011
                         * 
                         * e.hash & oldCap:(注意此时j == 3)
                         * 对于hash值为0010 0011 的元素: 0010 0011 & 0001 0000 == 0000 0000  判断成立  将其保存至低位链中。
                         * 最后 loTail != null成立:   那么newTab[j](j == 3)  ---> 这个元素在新数组中的位置还是下标为3
                         * 
                         *  对于hash值为 0011 0011的元素: 0011 0011 & 0001 0000 == 0001 0000 == 16。进入else将其保存至高位链中。
                         *  最后hiTail != null成立:  那么 newTab[j + oldCap] = hiHead; --->这个元素在新数组中的位置为 3 + 16 == 19。下标为19
                         *  
                         *  若不扩容则是将一个hash值(二进制)的低4位拿出来做索引。
                         *  扩容了他就相当于再拿出来一个位来用,用低5位。(利用一条链中那一个位的差别来重新分派桶位(如果均匀的话就是平分了链))
                         *  所以在查询的时候效率会大大提升。
                         *  就这样将原数组中的一个链中的各个节点拆分重组成两条子链
                         */
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                       
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

在整个put的过程中还有一个方法treeifyBin

treeifyBin

 final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; 
    Node<K,V> e;
        
    /**
     * MIN_TREEIFY_CAPACITY == 64
     * 若表为null    或者  表长度 < 64
     * 从这里可以看出树化的两个条件:假如此时某一个桶位处的链表长度已经为8,但是此时散列表长度为16。它还是不会进行树化。只是会扩容一次
     * n在此处被赋值为表长度
     */
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
        
    // 链表长度为8,而且容量大于64的情况。找到该桶位处的索引。  此处将此索引赋值给index变量
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
           
        //这个过程是将链表节点转化为树节点,然后重组成一条由树节点组成的双向链表。TreeNode间接继承与Node。所以它也有前驱和后继
        do {
            /**
             * 实例化一个TreeNode对象。一个二叉树节点,最基本要有三个引用父节点、左孩子、右孩子。 
             * 注意此时构造的树节点 其父节点 左孩子  右孩子  指向都为null
             */
            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 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对象  注意此时red为false(黑) 而且parent left right prev都为null
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }
}

将链表结构转变为红黑树结构的方法

 final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
            
    //变量x在此处声明和赋值,这个循环是遍历链表。x表示当前索引处的元素  next表示下一个节点 
    for (TreeNode<K,V> x = this, next; x != null; x = next) {						
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        //第一次循环只执行if中的语句,再次过程中树根被初次确定
        if (root == null) {
            x.parent = null;	//树根没有父节点
            x.red = false;   	//树根默认为黑色
            root = x;	        //root变量在此处被赋值	
        }
        else {
            K k = x.key;	//该节点的key值
            int h = x.hash;	//该节点的key的hash值
            Class<?> kc = null;					
                    
            //首先让p == root,这个循环目的是将链表一步一步转换调整成红黑树
            for (TreeNode<K,V> p = root;;) {			
                int dir, ph;    //dir用来标记当前节点应该成为树的左孩子还是右孩子。 ph用来保存树节点的hash值
                K pk = p.key;
                if ((ph = p.hash) > h)						
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                        
                //如果两个节点的key的hash值相等,下面判断条件是只是另一种比较方式
                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;                                   
                    //分情况处理  分配左孩子和右孩子 (如果父节点的hash值大于当前节点的hash的情况下是左孩子)
                    if (dir <= 0)	 
                        xp.left = x;
                    else								
                        p.right = x;
                            
                    //传进root与x(当前节点)
                    root = balanceInsertion(root, x);	//旋转方法
                    break;			//注意 这里会跳出内层循环
                }
            }
        }
    }
            
    //旋转调整完之后树根可能发生改变,该方法让散列表中的头元素保存的永远是root树根
    moveRootToFront(tab, root);
}

balanceInsertion方法

 static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                    TreeNode<K,V> x) {
    x.red = true;	//当前节点先设置为红色
    /**
     * xp == 当前节点的父节点
     * xpp ==......爷爷节点
     * xppl == 爷爷的左孩子
     * xppr == 爷爷的右孩子
     */
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
        //如果当前节点的父节点为  null  说明为树根
        if ((xp = x.parent) == null) {	
            x.red = false;			//树根染为黑色	
            return x;
        }

        //如果父节点是黑色  或者 不存在爷爷节点
        else if (!xp.red || (xpp = xp.parent) == null)
            return root;
                
        //如果存在父节点,而且父节点是爷爷节点的左孩子
        if (xp == (xppl = xpp.left)) {
            //如果爷爷节点的右孩子存在(也就是当前节点存在叔叔节点)  而且是红色    
            if ((xppr = xpp.right) != null && xppr.red) {
                xppr.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }

            //如果不存在叔叔节点  或者 叔叔节点为黑色
            else {
                //当前节点是父节点的右孩子的情况
                if (x == xp.right) {
                    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 {
            //如果爷爷节点的左不为null  而且 是红色
            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;
                }
                //如果父节点不为null
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateLeft(root, xpp);		//以xpp为支点左旋
                    }
                }
            }
        }
    }
}

get方法:

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

get方法底层也是调用的核心方法getNode

getNode方法:

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; 				//散列表
    Node<K,V> first, e; 			//first 桶位中的头元素				
    int n; K k;
        
    //如果当前散列表不为空 && 散列表长度大于0  && 该桶位处有值
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        //如果头元素是就直接返回头元素
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        	
        //判断是否含有下一个节点(即是否为一个链)
        if ((e = first.next) != null) {
                
            //树的情况(first为树根)
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                
            //链的情况(循环遍历链)
            do {
                //找到了则直接返回
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

如果是树的情况则执行getTreeNode方法遍历树:

final TreeNode<K,V> getTreeNode(int h, Object k) {
    return ((parent != null) ? root() : this).find(h, k, null);	//先找到树根后调用find方法查找目标
}
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
    TreeNode<K,V> p = this;   //p表示当前定位的元素
    do {
        int ph, dir; K pk;
        TreeNode<K,V> pl = p.left, pr = p.right, q;
                
        if ((ph = p.hash) > h)
            p = pl;	//如果当前定位元素的hash值  大于 目标元素的hash值   则只找定位元素的左子树        
        else if (ph < h)
            p = pr;	//右子树
                
        //此处判断的前提是hash值相等
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))				
            return p;
                
 /*--------------------------------------------------下面情况是hash值相同  key不相同--------------------------*/               
        else if (pl == null)		//左子树为null 则让其往右子树方向
            p = pr;
        else if (pr == null)		//右子树为null 则让其往左子树方向
            p = pl;
             
        //左右子树都不为null   而且 key不相同  按compare区分接下来应该往左还是往右找
        else if ((kc != null || (kc = comparableClassFor(k)) != null) && (dir = compareComparables(kc, k, pk)) != 0)
            p = (dir < 0) ? pl : pr;
        else if ((q = pr.find(h, k, kc)) != null)
            return q;
        else
            p = pl;
    } while (p != null);
            
    return null;  					//没找到则返回null
}

【天下事有难易乎,为之,则难者亦易矣;不为,则易者亦难矣】

本文地址:https://blog.csdn.net/weixin_44641388/article/details/107591912

相关标签: 源码学习