当前位置: 移动技术网 > IT编程>开发语言>Java > HashMap源码学习(JDK 1.8)

HashMap源码学习(JDK 1.8)

2020年07月27日  | 移动技术网IT编程  | 我要评论
什么是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对象)

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

如您对本文有疑问或者有任何想说的,请 点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网