深入理解HashMap原理(一)——HashMap源码解析(JDK 1.8)

柳随风
发布于 2020-9-23 11:12
浏览
0收藏

介绍


HashMap原理是JAVA和Android面试中经常会遇到的问题,这篇文章将通过HashMap在JDK1.7和1.8 中的源码来解析HashMap的原理。

 

相关概念

 

数组

 

采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)

 

线性链表


对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)

 

红黑树

 

红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。
红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:

 

性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3. 每个叶节点(NIL节点,空节点)是黑色的。
性质4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

 

哈希表

 

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

 

哈希冲突

 

如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。

 

HashMap在JDK1.8中的源码


首先我们看下源码中的注释:

//1、哈希表基于map接口的实现,这个实现提供了map所有的操作,并且提供了key和value可以为null,(HashMap和HashTable大致上市一样的除了hashmap是异步的和允许key和value为null),
这个类不确定map中元素的位置,特别要提的是,这个类也不确定元素的位置随着时间会不会保持不变。
Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. 
(The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map;
 in particular, it does not guarantee that the order will remain constant over time. 

//假设哈希函数将元素合适的分到了每个桶(其实就是指的数组中位置上的链表)中,则这个实现为基本的操作(get、put)提供了稳定的性能,迭代这个集合视图需要的时间跟hashMap实例(key-value映射的数量)的容量(在桶中)
成正比,因此,如果迭代的性能很重要的话,就不要将初始容量设置的太高或者loadfactor设置的太低,【这里的桶,相当于在数组中每个位置上放一个桶装元素】
This implementation provides constant-time performance for the basic operations (get and put), assuming the hash function disperses the elements properly among the buckets.
 Iteration over collection views requires time proportional to the "capacity" of the HashMap instance (the number of buckets) plus its size (the number of key-value mappings
). Thus, it's very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.

//HashMap的实例有两个参数影响性能,初始化容量(initialCapacity)和loadFactor加载因子,在哈希表中这个容量是桶的数量【也就是数组的长度】,一个初始化容量仅仅是在哈希表被创建时容量,在
容量自动增长之前加载因子是衡量哈希表被允许达到的多少的。当entry的数量在哈希表中超过了加载因子乘以当前的容量,那么哈希表被修改(内部的数据结构会被重新建立)所以哈希表有大约两倍的桶的数量
An instance of HashMap has two parameters that affect its performance: initial capacity and load factor. The capacity is the number of buckets in the hash table, 
and the initial capacity is simply the capacity at the time the hash table is created. The load factor is a measure of how full the hash table is allowed to get before
 its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table 
is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets.

//通常来讲,默认的加载因子(0.75)能够在时间和空间上提供一个好的平衡,更高的值会减少空间上的开支但是会增加查询花费的时间(体现在HashMap类中get、put方法上),当设置初始化容量时,应该考虑到map中会存放
entry的数量和加载因子,以便最少次数的进行rehash操作,如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。

As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup
 cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken 
into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of
 entries divided by the load factor, no rehash operations will ever occur.

//如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。
If many mappings are to be stored in a HashMap instance, creating it with a sufficiently large capacity will allow the mappings to be stored more efficiently than letting 
it perform automatic rehashing as needed to grow the table

 

HashMap的继承关系

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    /****省略代码****/
    }
 


我们看到HashMap继承自AbstractMap实现了Map,Cloneable,Serializable接口。

 

HashMap的属性

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    //序列号,序列化的时候使用。
    private static final long serialVersionUID = 362498820763181265L;
    /**默认容量,1向左移位4个,00000001变成00010000,也就是2的4次方为16,使用移位是因为移位是计算机基础运算,效率比加减乘除快。**/
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    //最大容量,2的30次方。
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //加载因子,用于扩容使用。
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //当某个桶节点数量大于8时,会转换为红黑树。
    static final int TREEIFY_THRESHOLD = 8;
    //当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。
    static final int UNTREEIFY_THRESHOLD = 6;
    //当整个hashMap中元素数量大于64时,也会进行转为红黑树结构。
    static final int MIN_TREEIFY_CAPACITY = 64;
    //存储元素的数组,transient关键字表示该属性不能被序列化
    transient Node<K,V>[] table;
    //将数据转换成set的另一种存储形式,这个变量主要用于迭代功能。
    transient Set<Map.Entry<K,V>> entrySet;
    //元素数量
    transient int size;
    //统计该map修改的次数
    transient int modCount;
    //临界值,也就是元素数量达到临界值时,会进行扩容。
    int threshold;
    //也是加载因子,只不过这个是变量。
    final float loadFactor;  
    
    /****省略代码****/
    
    }

 

这里有一点就是默认为什么容量大小为16,加载因子为0.75.我们通过注释来看:


As a general rule, the default load factor (.75) offers a good 
tradeoff between time and space costs. Higher values decrease the space 
overhead but increase the lookup cost (reflected in most of the 
operations of the HashMap class, including get and put). The expected 
number of entries in the map and its load factor should be taken into 
account when setting its initial capacity, so as to minimize the number 
of rehash operations. If the initial capacity is greater than the 
maximum number of entries divided by the load factor, no rehash 
operations will ever occur.

 

大致意思就是 16和0.75是经过大量计算得出的最优解,当设置默认的大小和加载因子时,进行的rehhash此书后最少,性能上最优。

 

HashMap的构造方法深入理解HashMap原理(一)——HashMap源码解析(JDK 1.8)-鸿蒙开发者社区

 

我们看到HashMap的构造方法有四个,


第一个:空参构造方法,使用默认的负载因子为0.75;
第二个:设置初始容量并使用默认加载因子;
第三个:设置容量和加载因子,第二个构造方法最终还是调用了第三个构造方法;
第四个:将一个Map转换为HashMap。

 

下面我们看下第四个构造方法的源码:

    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
 
 
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        //获取该map的实际长度
        int s = m.size();
        if (s > 0) {
            //判断table是否初始化,如果没有初始化
            if (table == null) { // pre-size
                /**求出需要的容量,因为实际使用的长度=容量*0.75得来的,+1是因为小数相除,基本都不会是整数,容量大小不能为小数的,后面转换为int,多余的小数就要被丢掉,所以+1,例如,map实际长度22,22/0.75=29.3,所需要的容量肯定为30,有人会问如果刚刚好除得整数呢,除得整数的话,容量大小多1也没什么影响**/
                float ft = ((float)s / loadFactor) + 1.0F;
                //判断该容量大小是否超出上限。
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                /**对临界值进行初始化,tableSizeFor(t)这个方法会返回大于t值的,且离其最近的2次幂,例如t为29,则返回的值是32**/
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            //如果table已经初始化,则进行扩容操作,resize()就是扩容。
            else if (s > threshold)
                resize();
            //遍历,把map中的数据转到hashMap中。
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

 

这里我们看到构造函数中传入了一个Map,然后把该Map转换为hashMap,这里面还调用了resize()进行扩容,下面我们会详细介绍。在上面的entrySet方法会返回一个Set<Map.Entry<K,V>>,泛型为Map的内部类Entry,它是一个存放key-value的实例,为什么要用这种结构就是上面我们说的hash表的遍历,插入效率高。构造函数基本已经讲完了,下面我们重点看下HashMap是如何将key和value存储的。下面我们看HashMap的put(K key,V value)方法.

 

HashMap的put方法

 

public V put(K key, V value) {
        /**四个参数,第一个hash值,第四个参数表示如果该key存在值,如果为null的话,则插入新的value,最后一个参数,在hashMap中没有用,可以不用管,使用默认的即可**/
        return putVal(hash(key), key, value, false, true);
    }


我们看到这里调用了putVal之前调用了hash方法;

 

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


我们看到这里是将键值的hashCode做了异或运算,至于为什么这么复杂,目的大致就是为了减少哈希冲突。
下面我们看看putVal方法的源码:

   final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                  boolean evict) {
       //tab 哈希数组,p 该哈希桶的首节点,n hashMap的长度,i 计算出的数组下标
       Node<K,V>[] tab; Node<K,V> p; int n, i;
       //获取长度并进行扩容,使用的是懒加载,table一开始是没有加载的,等put后才开始加载
       if ((tab = table) == null || (n = tab.length) == 0)
           n = (tab = resize()).length;
       /**如果计算出的该哈希桶的位置没有值,则把新插入的key-value放到此处,此处就算没有插入成功,也就是发生哈希冲突时也会把哈希桶的首节点赋予p**/
       if ((p = tab[i = (n - 1) & hash]) == null)
           tab[i] = newNode(hash, key, value, null);
       //发生哈希冲突的几种情况
       else {
           // e 临时节点的作用, k 存放该当前节点的key 
           Node<K,V> e; K k;
           //第一种,插入的key-value的hash值,key都与当前节点的相等,e = p,则表示为首节点
           if (p.hash == hash &&
               ((k = p.key) == key || (key != null && key.equals(k))))
               e = p;
           //第二种,hash值不等于首节点,判断该p是否属于红黑树的节点
           else if (p instanceof TreeNode)
               /**为红黑树的节点,则在红黑树中进行添加,如果该节点已经存在,则返回该节点(不为null),该值很重要,用来判断put操作是否成功,如果添加成功返回null**/
               e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
           //第三种,hash值不等于首节点,不为红黑树的节点,则为链表的节点
           else {
               //遍历该链表
               for (int binCount = 0; ; ++binCount) {
                   //如果找到尾部,则表明添加的key-value没有重复,在尾部进行添加
                   if ((e = p.next) == null) {
                       p.next = newNode(hash, key, value, null);
                       //判断是否要转换为红黑树结构
                       if (binCount >= TREEIFY_THRESHOLD - 1) 
                           treeifyBin(tab, hash);
                       break;
                   }
                   //如果链表中有重复的key,e则为当前重复的节点,结束循环
                   if (e.hash == hash &&
                       ((k = e.key) == key || (key != null && key.equals(k))))
                       break;
                   p = e;
               }
           }
           //有重复的key,则用待插入值进行覆盖,返回旧值。
           if (e != null) { 
               V oldValue = e.value;
               if (!onlyIfAbsent || oldValue == null)
                   e.value = value;
               afterNodeAccess(e);
               return oldValue;
           }
       }
       //到了此步骤,则表明待插入的key-value是没有key的重复,因为插入成功e节点的值为null
       //修改次数+1
       ++modCount;
       //实际长度+1,判断是否大于临界值,大于则扩容
       if (++size > threshold)
           resize();
       afterNodeInsertion(evict);
       //添加成功
       return null;
   }

 

可以看到这里主要有以下几步:


1、根据key计算出在数组中存储的下标
2、根据使用的大小,判断是否需要扩容。
3、根据数组下标判断是否当前下标已存储数据,如果没有则直接插入。
4、如果存储了则存在哈希冲突,判断当前entry的key是否相等,如果相等则替换,否则判断下一个节点是否为空,为空则直接插入,否则取下一节点重复上述步骤。
5、判断链表长度是否大于8当达到8时转换为红黑树。

 

下面我们看下HashMap的扩容函数resize()

 

HashMap的扩容函数resize()

    final Node<K,V>[] resize() {
        //把没插入之前的哈希数组做我诶oldTal
        Node<K,V>[] oldTab = table;
        //old的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //old的临界值
        int oldThr = threshold;
        //初始化new的长度和临界值
        int newCap, newThr = 0;
        //oldCap > 0也就是说不是首次初始化,因为hashMap用的是懒加载
        if (oldCap > 0) {
            //大于最大值
            if (oldCap >= MAXIMUM_CAPACITY) {
                //临界值为整数的最大值
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //标记##,其它情况,扩容两倍,并且扩容后的长度要小于最大值,old长度也要大于16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //临界值也扩容为old的临界值2倍
                newThr = oldThr << 1; 
        }
        /**如果oldCap<0,但是已经初始化了,像把元素删除完之后的情况,那么它的临界值肯定还存在,        
           如果是首次初始化,它的临界值则为0
        **/
        else if (oldThr > 0) 
            newCap = oldThr;
        //首次初始化,给与默认的值
        else {               
            newCap = DEFAULT_INITIAL_CAPACITY;
            //临界值等于容量*加载因子
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //此处的if为上面标记##的补充,也就是初始化时容量小于默认值16的,此时newThr没有赋值
        if (newThr == 0) {
            //new的临界值
            float ft = (float)newCap * loadFactor;
            //判断是否new容量是否大于最大值,临界值是否大于最大值
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //把上面各种情况分析出的临界值,在此处真正进行改变,也就是容量和临界值都改变了。
        threshold = newThr;
        //表示忽略该警告
        @SuppressWarnings({"rawtypes","unchecked"})
            //初始化
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //赋予当前的table
        table = newTab;
        //此处自然是把old中的元素,遍历到new中
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                //临时变量
                Node<K,V> e;
                //当前哈希桶的位置值不为null,也就是数组下标处有值,因为有值表示可能会发生冲突
                if ((e = oldTab[j]) != null) {
                    //把已经赋值之后的变量置位null,当然是为了好回收,释放内存
                    oldTab[j] = null;
                    //如果下标处的节点没有下一个元素
                    if (e.next == null)
                        //把该变量的值存入newCap中,e.hash & (newCap - 1)并不等于j
                        newTab[e.hash & (newCap - 1)] = e;
                    //该节点为红黑树结构,也就是存在哈希冲突,该哈希桶中有多个元素
                    else if (e instanceof TreeNode)
                        //✨✨✨把此树进行转移到newCap中✨✨✨
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { /**此处表示为链表结构,同样把链表转移到newCap中,就是把链表遍历后,把值转过去,在置位null**/
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            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;
                        }
                    }
                }
            }
        }
        //返回扩容后的hashMap
        return newTab;
    }

 

前面主要介绍了, HashMap的结构为数组+ 链表(红黑树)。
总结一下上面的逻辑就是:


1、对数组进行扩容,
2、扩容后重新计算hashCode也就是key的下标,将原数据塞到新扩容后的数据结构中。
3、当存在hash冲突时,在数组后面以链表的形式追加到后面,当链表长度达到8时,就会将链表转换为红黑树。

 

那么对于红黑树新增一个节点 ,我们考虑到前面所说的红黑树的性质。就需要对红黑树做调整,是红黑树达到平衡。这种平衡就是红黑树的旋转。下面我们看看红黑树的旋转:

 

红黑树的旋转

 

红黑树的旋转分为左旋和右旋,以某个节点为圆心向左或向右旋转,具体我们通过下面的图来看下[https://www.cnblogs.com/CarpenterLee/p/5503882.html]。

 

左旋深入理解HashMap原理(一)——HashMap源码解析(JDK 1.8)-鸿蒙开发者社区

深入理解HashMap原理(一)——HashMap源码解析(JDK 1.8)-鸿蒙开发者社区

 

HashMap中红黑树的左旋

        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)
                    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;
            }
            return root;
        }

 

右旋深入理解HashMap原理(一)——HashMap源码解析(JDK 1.8)-鸿蒙开发者社区

深入理解HashMap原理(一)——HashMap源码解析(JDK 1.8)-鸿蒙开发者社区

 

HashMap中红黑树的右旋

     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;
        }

 

红黑树新增节点的例子


TreeMap的结构也是红黑树,它新增节点的过程如下:这里跟HashMap的红黑树的新增原理一样深入理解HashMap原理(一)——HashMap源码解析(JDK 1.8)-鸿蒙开发者社区

深入理解HashMap原理(一)——HashMap源码解析(JDK 1.8)-鸿蒙开发者社区

 

我们通过这个例子有差不多已经了解了红黑树的原理。我们回到 resize()方法,里面我们看
//✨✨✨把此树进行转移到newCap中✨✨✨
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

 

HashMap中TreeNode.split

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;	// 拿到调用此方法的节点
    TreeNode<K,V> loHead = null, loTail = null; // 存储跟原索引位置相同的节点
    TreeNode<K,V> hiHead = null, hiTail = null; // 存储索引位置为:原索引+oldCap的节点
    int lc = 0, hc = 0;
    for (TreeNode<K,V> e = b, next; e != null; e = next) {	// 从b节点开始遍历
        next = (TreeNode<K,V>)e.next;   // next赋值为e的下个节点
        e.next = null;  // 同时将老表的节点设置为空,以便垃圾收集器回收
        //如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
        if ((e.hash & bit) == 0) {  
            if ((e.prev = loTail) == null)  // 如果loTail为空, 代表该节点为第一个节点
                loHead = e; // 则将loHead赋值为第一个节点
            else
                loTail.next = e;    // 否则将节点添加在loTail后面
            loTail = e; // 并将loTail赋值为新增的节点
            ++lc;   // 统计原索引位置的节点个数
        }
        //如果e的hash值与老表的容量进行与运算为1,则扩容后的索引位置为:老表的索引位置+oldCap
        else {  
            if ((e.prev = hiTail) == null)  // 如果hiHead为空, 代表该节点为第一个节点
                hiHead = e; // 则将hiHead赋值为第一个节点
            else
                hiTail.next = e;    // 否则将节点添加在hiTail后面
            hiTail = e; // 并将hiTail赋值为新增的节点
            ++hc;   // 统计索引位置为原索引+oldCap的节点个数
        }
    }
 
    if (loHead != null) {   // 原索引位置的节点不为空
        if (lc <= UNTREEIFY_THRESHOLD)  // 节点个数少于6个则将红黑树转为链表结构
            tab[index] = loHead.untreeify(map);
        else {
            tab[index] = loHead;    // 将原索引位置的节点设置为对应的头结点
            // hiHead不为空则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)
            // 已经被改变, 需要重新构建新的红黑树
            if (hiHead != null) 
                loHead.treeify(tab);    // 以loHead为根结点, 构建新的红黑树
        }
    }
    if (hiHead != null) {   // 索引位置为原索引+oldCap的节点不为空
        if (hc <= UNTREEIFY_THRESHOLD)  // 节点个数少于6个则将红黑树转为链表结构
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;  // 将索引位置为原索引+oldCap的节点设置为对应的头结点
            // loHead不为空则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)
            // 已经被改变, 需要重新构建新的红黑树
            if (loHead != null) 
                hiHead.treeify(tab);    // 以hiHead为根结点, 构建新的红黑树
        }
    }
}

 

这个方法中我们重点看treeify

 

HashMap中treeify

      final void treeify(Node<K,V>[] tab) {   // 构建红黑树
    TreeNode<K,V> root = null;
    for (TreeNode<K,V> x = this, next; x != null; x = next) {// this即为调用此方法的TreeNode
        next = (TreeNode<K,V>)x.next;   // next赋值为x的下个节点
        x.left = x.right = null;    // 将x的左右节点设置为空
        if (root == null) { // 如果还没有根结点, 则将x设置为根结点
            x.parent = null;    // 根结点没有父节点
            x.red = false;  // 根结点必须为黑色
            root = x;   // 将x设置为根结点
        }
        else {
            K k = x.key;	// k赋值为x的key
            int h = x.hash;	// h赋值为x的hash值
            Class<?> kc = null;
            // 如果当前节点x不是根结点, 则从根节点开始查找属于该节点的位置
            for (TreeNode<K,V> p = root;;) {	
                int dir, ph;
                K pk = p.key;   
                if ((ph = p.hash) > h)  // 如果x节点的hash值小于p节点的hash值
                    dir = -1;   // 则将dir赋值为-1, 代表向p的左边查找
                else if (ph < h)    // 与上面相反, 如果x节点的hash值大于p节点的hash值
                    dir = 1;    // 则将dir赋值为1, 代表向p的右边查找
                // 走到这代表x的hash值和p的hash值相等,则比较key值
                else if ((kc == null && // 如果k没有实现Comparable接口 或者 x节点的key和p节点的key相等
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                	// 使用定义的一套规则来比较x节点和p节点的大小,用来决定向左还是向右查找
                    dir = tieBreakOrder(k, pk); 
 
                TreeNode<K,V> xp = p;   // xp赋值为x的父节点,中间变量用于下面给x的父节点赋值
                // dir<=0则向p左边查找,否则向p右边查找,如果为null,则代表该位置即为x的目标位置
                if ((p = (dir <= 0) ? p.left : p.right) == null) { 
                    x.parent = xp;  // x的父节点即为最后一次遍历的p节点
                    if (dir <= 0)   // 如果时dir <= 0, 则代表x节点为父节点的左节点
                        xp.left = x;
                    else    // 如果时dir > 0, 则代表x节点为父节点的右节点
                        xp.right = x;
                    // 进行红黑树的插入平衡(通过左旋、右旋和改变节点颜色来保证当前树符合红黑树的要求)
                    root = balanceInsertion(root, x);   
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root); // 如果root节点不在table索引位置的头结点, 则将其调整为头结点
}

 

我们重点看这个方法balanceInsertion(root, x)这个方法就是使红黑树达到平衡。我们接着继续看,要平衡红黑树就得左右旋转。

 

HashMap中balanceInsertion

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;;) {
                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 {
                    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);//对红黑树进行左旋
                            }
                        }
                    }
                }
            }
       }

 

看到这里基本思想已经明白了,我们下面总结一下:

 

总结

 

HashMap 的存储结构
我们通过下面一副图来看,数组+链表+红黑树深入理解HashMap原理(一)——HashMap源码解析(JDK 1.8)-鸿蒙开发者社区

 

HashMap的扩容
我们通过下面的图来看看HashMap的扩容过程深入理解HashMap原理(一)——HashMap源码解析(JDK 1.8)-鸿蒙开发者社区

深入理解HashMap原理(一)——HashMap源码解析(JDK 1.8)-鸿蒙开发者社区

 

以上就是本文主要讲解的HashMap 的核心思想,如有不对请指证。

 

 

作者:紫雾凌寒

来源:CSDN

分类
收藏
回复
举报
回复
    相关推荐