• Debug HashMap



    最近跟两个正在找工作的同学聊天,说起集合,都是面试的重灾区,必问的选项,而且在实际的面试中并不会单独提问某一个问题,而是围绕核心知识连环炮提问。所以背面试题治标不治本,还是得读一读源码。谁让这是个面试造火箭,工作拧螺丝的市场氛围,就连CSDN的首页第二张轮播图都在蹭这个热点:

    image-20200715110439001

    本文主要包括两部分:

    • HashMap面试必问(总结了一些常见面试题)

    • JDK1.7 & JDK1.8 关于HashMap原理分析

      这部分主要是通过断点debug来分析HashMap中常见操作的过程,但由于步骤繁多,只记录了关键步骤,建议读者也在自己电脑上debug一遍,了解详细流程。(计算机是一门实践性很强的学科,看的再多也不如自己亲自操作一遍,当然理论也同样重要)

    长文警告!!!

    1,HashMap面试必问

    这是笔者在一篇博客中找出来的,很有代表性,实际的面试提问中不会按部就班的问,而是千变万化,所以除了把面试题背住之外,一定要花点时间看看源码具体实现,虽然不会360度无死角,但对源码总体有个大概的把握,回答起来就知道哪些知道哪些不知道,一来方便查漏补缺,二来也能更加灵活的回答问题。

    示例性提问(真实场景下):

    • 你看过JDK的源码吗?

      看过。

    • HashMap是如何通过put添加元素的?

      根据key计算hash值,再将hash值转换为数组下标。

    • 底层数组默认的长度为多少?

      默认为16。

    • 什么时候会触发扩容机制?

      元素个数超过阈值就会触发扩容机制,并且是在新增元素发生hash冲突的情况下。

    • 扩容时,直接将数据从原数组平移到新数组可以吗?

      不行,需要重新计算hash值(更正,是重新计算index值,而不是重新计算hash值,hash值只与key相关,index与table.length相关)

    • 为什么需要重新计算hash值?

      因为数组扩容了,从hash值转换为数组下标这个过程就发生了变化,同时,获取value这个过程也会发生变化。所以必须重新计算,不然之前保存的元素就无法访问。

    一般性问题(建议背住,而后融会贯通):

    • 什么是HashMap?

      HashMap是基于Map接口的实现,主要用于存储键值对(1.7通过Entry对象封装键值对,1.8通过Node封装键值对)

    • HashMap采用了什么数据结构?

      1.7:数组+链表

      1.8:数组+链表+红黑树

    • HashMap是如何解决hash冲突的问题的?

      链表。

    • hash冲突和index冲突的关系?

      hash冲突就会导致index冲突,indexFor方法的两个参数一个是hash值,另外一个是table.length。

    • HashMap的put方法是如何实现的?

      先通过key计算hash值,再通过indexFor方法转换为数组下标。

    • HashMap的扩容机制是什么样的?

      HashMap默认初始容量为16,加载因子为0.75,实际存储大小为12。hashMap容量达到12并且当前加入的元素产生hash冲突时时,进行初始容量的2倍扩容

      • 为什么初始容量为16?

        HashMap重写的hash采用的是位运算,目的是使key到index的映射分布更加均匀

        	static final int hash(Object key) {
                int h;
                return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
            }
            
            也解释了为什么hash允许空值,实际上当key为null时,自动转换为0
        
    • 为什么链表使用头插法?

      HashMap的发明者认为,后插入的Entry被查找的可能性更大

    • hashMap中的链表是单链表还是双链表?

      单链表

       		final int hash;
              final K key;
              V value;
              Node<K,V> next;
      
    • 扩容阈值threshold被赋值了几次?

      • 调用构造函数被赋值,初始化容量大小(默认为16)
      • 数组为空,初始化数组时,被赋值为初始化容量*加载因子(默认为12)
    • hash冲突插入链表的方式?

      1.7:采用头插法:作者认为,后插入的会被优先访问

      1.8:采用尾插法:避免链表死循环

    • hashMap允许key为null值吗?

      允许一个key为null,会转换为数组下标0。当出现第二个key为null,其value会自动覆盖第一个null的值。

    • hashMap中链表过长会导致什么问题?

      查询效率降低。时间复杂度为O(n)【需要遍历链表】

    • jdk7中的HashMap存在哪些问题?

      • 链表过长导致查询效率降低

      • 扩容导致的死循环

      • 线程不安全(个人认为这不是问题,而是在设计上就没有考虑这个,线程安全就会导致效率降低,本质上是效率和安全之间的取舍)

    • jdk7和jdk8处理hash冲突的区别?为什么?

      jdk7计算hash值的运算是非常复杂的,因为如果产生了hash冲突是用链表来进行存储的,效率比较慢,所以在设计上要尽可能避免冲突。

      jdk8计算hash值的方法相对简单,因为采用了红黑树的结构,即使发生了hash冲突,也可以通过转换为红黑树来提高效率。

    • 为什么加载因子是0.75而不是其他值?

      因为加载因子参与indexFor数组下标的计算,return h & (length-1);

      其数值会影响index是否发生冲突,同时也会影响空间利用率,默认情况下table长度为16,但只能存12个值。

      所以这个加载因子是在index冲突和空间利用率之间寻求的一个平衡点。

    • HashMap是否可以存放自定义对象?

      可以,因为HashMap使用了泛型。

    • 为什么JDK8引入红黑树?

      由于hash冲突导致链表查询非常慢,时间复杂度为O(n),引入红黑树后链表长度为8时会自动转换为红黑树,以提高查询效率O(logn)。

    • Java集合中ArrayList,LinkedList,HashMap的时间复杂度分别为多少?

      ArrayList基于数组实现,基于下标查询的话时间复杂度为O(1),如果基于内容查找需要遍历的话,时间复杂度为O(n)。

      LinkedList基于链表实现,查询效率为O(n)

      HashMap在不考虑Hash冲突没有形成链表的情况下时间复杂度为O(1),形成链表后时间复杂度为O(n)

    2,Debug源码的心得体会

    【关注核心步骤,选择性忽略】

    JDK是一个相当庞大的系统,把所有的类和原理全部弄清楚是相当有难度的,所以在debug源码的时候,如果遇见了不相关的类,忽略就是了。

    然而单看HashMap源码(2300行)也是一个较为庞大的代码量,所以对其中不重要或者不常用的方法,最好先选择性忽略。比如计算hash值的各种位运算,研究起来还是得废一些功夫的,这个可以在把握了HashMap的大致框架后再做精细化的研究。

    总的来说,先重点关注核心步骤,选择性忽略更加具体的实现,逐个击破,从而提高阅读效率

    ps:建议把1.7和1.8的jdk都装上,切换着分析。

    3,JDK 1.7

    3.1 用debug分析一个元素是如何加入到HashMap中的【jdk1.7】

    创建一个Main.java类

     		HashMap<String,String> hashMap = new HashMap<>(16);
            
            hashMap.put("x","x");
            hashMap.put("y","y");
    

    在创建HashMap对象上打上断点:

    image-20200715162215255

    debug运行,强制进入方法内部(Alt+Shift+F7):

    调用构造函数:

    image-20200715165233458

    this方法,初始值判空异常(初始值不能小于0大于最大值),加载因子判空异常,

    threshold被初始化容量赋值(threshold为扩容阈值)

    image-20200715165318136

    在插入第一个元素上打上断点:

    image-20200715165820913

    debug运行,强制进入方法内部(Alt+Shift+F7):

    	public V put(K key, V value) {
    		//判断数组是否为空,如果为空进行初始化,inflateTable初始化方法见下文①
    		//threshold:扩容的阈值(当前元素个数超过这个数值就会进行扩容)
            if (table == EMPTY_TABLE) {
                inflateTable(threshold);
            }
            
            //判断key是否为空
            if (key == null)
            	//hashMap处理空值的方法②
                return putForNullKey(value);
                
            //计算key的hash值(主要是各种位运算)
            int hash = hash(key);
            
            //i就是将key的hash值再进行一次转换得出的数组下标
            int i = indexFor(hash, table.length);
            //同样是个处理hash冲突的头插算法
            for (Entry<K,V> e = table[i]; e != null; e = e.next) {
                Object k;
                if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                    V oldValue = e.value;
                    e.value = value;
                    e.recordAccess(this);
                    return oldValue;
                }
            }
    
            modCount++;
            
            //添加元素③
            addEntry(hash, key, value, i);
            return null;
        }
    

    ①inflateTable初始化容量方法:

    private void inflateTable(int toSize) {
            //向上舍入为2的幂
            int capacity = roundUpToPowerOf2(toSize);
    
    	    //重点:threshold在初始化构造函数时默认为16,在初始化数组时,乘以加载因子被二次赋值
            threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
            //初始化数组容量
            table = new Entry[capacity];
            initHashSeedAsNeeded(capacity);
        }
    

    ②hashMap处理空值的方法

    private V putForNullKey(V value) {
    
    		//处理key为null值的hash冲突,采用头插法(null会自动转为0)
            for (Entry<K,V> e = table[0]; e != null; e = e.next) {
                if (e.key == null) {
                    V oldValue = e.value;
                    e.value = value;
                    e.recordAccess(this);
                    return oldValue;
                }
            }
            modCount++;
            addEntry(0, null, value, 0);
            return null;
        }
    

    ③addEntry添加元素

    void addEntry(int hash, K key, V value, int bucketIndex) {
    		//hash扩容(size代表元素个数,如果元素大于threshold【默认是12】,则会进行扩容)
            if ((size >= threshold) && (null != table[bucketIndex])) {
                resize(2 * table.length);
                hash = (null != key) ? hash(key) : 0;
                bucketIndex = indexFor(hash, table.length);
            }
    		//④
            createEntry(hash, key, value, bucketIndex);
        }
    

    void createEntry(int hash, K key, V value, int bucketIndex) {
    
    		//bucketIndex就是put方法中计算出的数组下标i
    		//难点:如果未发生hash冲突,table[bucketIndex]则为空,e也为空,table[bucketIndex]等于最新插入的元素
    		//如果发生了hash冲突,也就是table[bucketIndex]并不为空,table[bucketIndex]就头插到链表中
            Entry<K,V> e = table[bucketIndex];
            table[bucketIndex] = new Entry<>(hash, key, value, e);
            size++;
        }
    

    3.2 用debug分析HashMap是如何get到一个元素的【jdk1.7】

    还是先编写测试用例:

    ps:测试的代码都不复杂,关键是要关注底层是如何实现的

      		HashMap<String,String> hashMap = new HashMap<String, String>(3);
    
            hashMap.put("x","x");
            hashMap.put("y","y");
            hashMap.put("z","z");
            hashMap.get("z");
    

    打上断点:

    image-20200716104509610

    debug运行,强制进入方法内部(Alt+Shift+F7):

    public V get(Object key) {
            if (key == null)     //判空
                return getForNullKey();
            Entry<K,V> entry = getEntry(key);  
    
    		//判空,否则返回value
            return null == entry ? null : entry.getValue();
        }
    
    final Entry<K,V> getEntry(Object key) {
    		//判断数组是否为空
            if (size == 0) {
                return null;
            }
    
    		//判断key是否为空,为空则返回0,否则计算hash值
            int hash = (key == null) ? 0 : hash(key);
            
            //遍历链表,获取Entry对象
            for (Entry<K,V> e = table[indexFor(hash, table.length)];e != null;e = e.next) {
                Object k;
                
                //核心:hash相等并且key相等才能返回entry,否则继续遍历
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            }
            return null;
        }
    

    3.3 用debug分析HashMap是如何扩容的?【jdk1.7】

    编写测试用例:给的初始值为3,根据2的幂计算,HashMap初始化容量为4,扩容阈值为3,也就是在执行 hashMap.put("m","n");时会发生扩容:

    		HashMap<String,String> hashMap = new HashMap<String, String>(3);
    
            hashMap.put("x","x");
            hashMap.put("y","y");
            hashMap.put("z","z");
            hashMap.put("m","n");
    

    打上断点:

    image-20200715204334725

    debug运行,强制进入方法内部(Alt+Shift+F7):

    判断数组是否为空。false

    image-20200715204404918

    。。。(此处省去一些步骤)

    运行到addEntry方法对size和threshold进行判断,此时size为3,满足条件。(ps:除了当前大小大于等于阈值之外,当前元素计算出的数组下标也必须与之前的元素产生hash冲突才能扩容)

    【坑点】:size是元素总个数,而不是数组占用个数,比如只占用了一个数组位置,但是链表长12,还是会扩容,其目的是使得hash分布的更均匀

    resize方法对数组table进行两倍扩容,当前table.length = 4.

    image-20200715204509129

    resize方法:

    image-20200715204719762

     void resize(int newCapacity) {
            Entry[] oldTable = table;
            int oldCapacity = oldTable.length;
            if (oldCapacity == MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return;
            }
    
            Entry[] newTable = new Entry[newCapacity];
            transfer(newTable, initHashSeedAsNeeded(newCapacity));    //将数据移至新数组⑤
            table = newTable;
            threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
        }
    

    ⑤将数据移至新数组

    /**
         * Transfers all entries from current table to newTable.
         */
        void transfer(Entry[] newTable, boolean rehash) {
            int newCapacity = newTable.length;
            //遍历链表
            for (Entry<K,V> e : table) {
                while(null != e) {
                    Entry<K,V> next = e.next;
                    if (rehash) {
                        e.hash = null == e.key ? 0 : hash(e.key);
                    }
                    int i = indexFor(e.hash, newCapacity);   ///重新计算数组下标
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                }
            }
        }
    

    3.4 HashMap 1.7 中多线程下扩容的死循环问题

    问题描述:jdk1.7在多线程并发的情况下会由于链表的头插法导致扩容的死循环问题,在1.8中已经被解决。

    问题代码:

    void transfer(Entry[] newTable, boolean rehash) {
            int newCapacity = newTable.length;
            
            //table是全局变量,多线程的情况下,由于没有任何锁的机制,多个线程可以同时获取到table
            for (Entry<K,V> e : table) {    
            
            //遍历链表
                while(null != e) {
                    Entry<K,V> next = e.next;
                    if (rehash) {
                        e.hash = null == e.key ? 0 : hash(e.key);
                    }
                    //重新计算hash值
                    int i = indexFor(e.hash, newCapacity);
                    //头插法插入链表
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                }
            }
        }
    

    图片描述:假设有A,B,C,D四个元素组成的链表,在扩容的时候,遍历链表A最先被移过去,其次是B,C,D,假设在进行扩容前,同时有两个线程获取到了全局变量table,T1线程扩容进行到了如图所示的步骤,正准备移动D过去。T2线程此时获取到的table的仍然扩容前的指向。所以T2读取到的table可能是A指向B,B同时指向A,这种情况下,遍历链表就会导致死循环。

    			   e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                    
                    一个元素的移动过程(index冲突),newTable[i]是已经移到新table中的数组下标对应的元素,如下图所示,C这个时候就是newTable[i],e
                   就是D,那么过程就是D指向了C,然后把e也就是D元素赋给newTable[i],此时这个链表的头结点就是D。最后一行代码相当与e = e.next。继续遍历链表。
    

    image-20200717145934315

    4,JDK1.8

    1.8相对于1.7有很多改进,比如采用了新的数据结构红黑树,链表改为尾插法等等。相对来说,1.8的代码量较1.7更多,故下文会部分省略代码,只展示程序运行过的步骤。

    4.1 用debug分析第一个元素是如何加入到HashMap中的【jdk1.8】

    切换到jdk1.8,继续debug

    image-20200717152242240

    image-20200717152332868

    计算hash函数:hash(key),1.8中同样允许null值,会自动转换为0

    image-20200717152349422

    jdk1.7中计算hash的方法
    final int hash(Object k) {
            int h = hashSeed;
            if (0 != h && k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h ^= k.hashCode();
            h ^= (h >>> 20) ^ (h >>> 12);
            return h ^ (h >>> 7) ^ (h >>> 4);
        }
    
    jdk1.8中计算hash的方法
    static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
        
    jdk1.7中计算hash值的方法相对比较复杂,主要是因为要尽可能的避免hash冲突,因为链表的遍历是很慢的。但jdk1.8中因为引入了红黑树,即使hash冲突很高,也可以通过转换红黑树来提高查询效率。(所以hash的运算就相对简单,毕竟运算也是要耗费资源的)
    

    核心方法: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;
            
            //初始化扩容 ,resize方法见下文
            if ((tab = table) == null || (n = tab.length) == 0)
           		//n为扩容后的容量,本次情况下为4,上文中HashMap的初始化容量设为3,根据hashMap规则,容量只能为2^n
                n = (tab = resize()).length;
            //&优先级高于=,看了半天没明白啥意思,1.7中将hash转换为index的过程用indexFor方法封装起来了,其实是一样的:h&(length-1)
            //如果当前位置是空的,直接赋值给数组
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
             //这里包括转换为链表或红黑树,下文再分析
            else {
                **************
            }
            //修改次数+1
            ++modCount;
            
            //若当前size+1后的值大于扩容阈值,执行扩容
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }
    
    //hashMap扩容方法
    final Node<K,V>[] resize() {
    		//获取到当前table,table是全局变量
            Node<K,V>[] oldTab = table;
            //计算当前table的长度
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            //获取当前扩容阈值(threshold=capacity*loadFactor)
            int oldThr = threshold;
            //初始化新的容量和扩容阈值
            int newCap, newThr = 0;
            if (oldCap > 0) {
            //若当前容量大于最大容量(10亿多)
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                //左移运算符优先级高于赋值运算符,左移1位相当于乘以2,newCap相当于旧容量2倍扩容
                //另外一个判断条件:当前容量大于默认容量16
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
                         //新的扩容阈值翻倍
                    newThr = oldThr << 1; // double threshold
            }
            //若当前扩容阈值大于0
            else if (oldThr > 0) // initial capacity was placed in threshold
            //将当前扩容阈值赋值给新容量
                newCap = oldThr;
                
            //若当前容量为0且扩容阈值为0,这种情况是在没有给hashmap任何初始值的时候发生的
            else {               // zero initial threshold signifies using defaults
                //默认容量为16
                newCap = DEFAULT_INITIAL_CAPACITY;
                //默认的扩容阈值为默认的负载因子乘以默认初始化容量
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            //若新的扩容阈值为0
            if (newThr == 0) {
            	//计算新的扩容阈值:在新容量小于最大容量且计算后的扩容阈值小于最大容量的情况下,新的扩容阈值为新容量乘以负载因子,否则为最大容量
                float ft = (float)newCap * loadFactor;
                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;
            
            //在为空初始化容量时,并不会进入分支,下文再补充注释
            if (oldTab != null) {
                *******
            }
            //返回新的键值对数组
            return newTab;
        }
    

    ps:1.8中使用Node代替Entry,换了个名,然后hash加上了final修饰

    image-20200717153631434

    image-20200717153646368

    4.2 用debug分析HashMap扩容情况【jdk1.8】

    测试用例如下:HashMap的初始容量给到3,实际容量为4,扩容阈值为3,在添加第四个元素的时候进行扩容

    image-20200727115443539

    进入方法内部:

    image-20200727115750636

    重点关注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;
            //table为空时初始化的扩容操作
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
                
            //若当前数组下标并未有元素,直接赋值
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
                
                //形成链表
            else {
            
                Node<K,V> e; K k;
                //若key冲突,直接替换value(key相同,hash值一定相同)
                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 {
                	//遍历当前table[i]所在的链表
                    for (int binCount = 0; ; ++binCount) {
                    *******
                    }
                }
            }
            ++modCount;
            //当前size为3,加1后大于扩容阈值,进行扩容操作
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }
    

    resize()扩容:

    final Node<K,V>[] resize() {
    		//获取到当前table,table是全局变量
            Node<K,V>[] oldTab = table;
            //计算当前table的长度
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            //获取当前扩容阈值(threshold=capacity*loadFactor)
            int oldThr = threshold;
            //初始化新的容量和扩容阈值
            int newCap, newThr = 0;
            if (oldCap > 0) {
            //若当前容量大于最大容量(10亿多)
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                //左移运算符优先级高于赋值运算符,左移1位相当于乘以2,newCap相当于旧容量2倍扩容
                //另外一个判断条件:当前容量大于默认容量16
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
                         //新的扩容阈值翻倍
                    newThr = oldThr << 1; // double threshold
            }
            //若当前扩容阈值大于0
            else if (oldThr > 0) // initial capacity was placed in threshold
            //将当前扩容阈值赋值给新容量
                newCap = oldThr;
                
            //若当前容量为0且扩容阈值为0,这种情况是在没有给hashmap任何初始值的时候发生的
            else {               // zero initial threshold signifies using defaults
                //默认容量为16
                newCap = DEFAULT_INITIAL_CAPACITY;
                //默认的扩容阈值为默认的负载因子乘以默认初始化容量
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            //若新的扩容阈值为0
            if (newThr == 0) {
            	//计算新的扩容阈值:在新容量小于最大容量且计算后的扩容阈值小于最大容量的情况下,新的扩容阈值为新容量乘以负载因子,否则为最大容量
                float ft = (float)newCap * loadFactor;
                
                //此时新的扩容阈值为6,容量为8
                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;
            
            //上文补充,此时旧数组并不为空 ***************************************************************************//
             if (oldTab != null) {
             	//遍历旧数组,遍历计算下标放入新数组中
                for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
                    //null会直接转化为0,所以不需要计算
                    if ((e = oldTab[j]) != null) {
                    	//旧数组置空
                        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 { // preserve order
                        
                        //为链表节点,需要进行重hash分布(就是数组下标的重新计算,一天天的,就不整个人话)
                            Node<K,V> loHead = null, loTail = null;   //用于数组下标为0的节点
                            Node<K,V> hiHead = null, hiTail = null;   //用于数组下标发生变化的节点
                            Node<K,V> next;
                            do {
                                next = e.next;
                                //将当前元素的hash值与老表的容量进行与运算,相当于计算数组下标,若等于0,则扩容后的下标仍然是0
                                if ((e.hash & oldCap) == 0) {
                                	//若loTail为空,表示该节点为链表上的第一个节点(loTail表示链表尾),将节点赋给loHead
                                    if (loTail == null)
                                        loHead = e;
                                   //若loTail不为空,表示当前节点并非是链表的第一个节点,可将e赋给链表尾loTail的下一个指向,此时表尾lotail后连接的是e
                                    else
                                        loTail.next = e;
                                        
                                    //将e赋给链表尾,1.8中使用了尾插法,而1.7中使用的是头插法
                                    loTail = e;
                                }
                                //处理数组下标非0的节点
                                else {
                                //同理:使用尾插法连接节点
                                    if (hiTail == null)
                                        hiHead = e;
                                    else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);    //这个循环就是遍历链表,直到下一个为null
                            
                            //如果loTail不为空,说明老数组中的数组下标在新数组中也有使用
                            if (loTail != null) {
                            	//将链表尾的下一个指向置为空
                                loTail.next = null;
                                //将链表头赋值给新数组的元素
                                newTab[j] = loHead;
                            }
                            
                            //如果hiTail不为空,说明这是非0的数组下标,
                            if (hiTail != null) {
                            	//将链表尾的下一个指向置为空
                                hiTail.next = null;
                                //新数组下标为原来的数组下标+旧容量(666)
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            //返回新的键值对数组
            return newTab;
        }
    

    4.3 用debug分析链表的形成过程【jdk1.8】

    编写测试用例,(???如何模拟更多的hash冲突???)

    image-20200727143057762

    image-20200727143216152

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
            //table为空时初始化的扩容操作
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
                
            //若当前数组下标并未有元素,直接赋值
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
                
            //形成链表**************************************************
            else {
          
                Node<K,V> e; K k;
                //若key冲突,直接替换value(key相同,hash值一定相同)
                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);
                    
                //排除了key覆盖和红黑树,剩下的就是链表了
                else {
                	//遍历当前table[i]所在的链表
                    for (int binCount = 0; ; ++binCount) {
                    	//若链表当前节点的下一个节点为空,说明已到链表尾,break退出循环
                        if ((e = p.next) == null) {
                        	//退出循环前,把新元素加到链表尾部
                            p.next = newNode(hash, key, value, null);
                            //若链表节点数量大于等于8,转换为红黑树(binCount从0开始计算,到7的时候已经是第8节点了)
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                            break;
                        }
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
             
            }
            ++modCount;
            //当前size为3,加1后大于扩容阈值,进行扩容操作
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }
    

    4.4 用debug分析get元素的过程【jdk1.8】

    image-20200727151534041

    image-20200727151615637

    getNode()

    final Node<K,V> getNode(int hash, Object key) {
            Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
            if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {
                
                //判断第一个节点的hash值和key是否相等,若相等,直接返回,否则进入链表遍历
                if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                    return first;
                //遍历链表
                if ((e = first.next) != null) {
                	//判断链表是否形成了红黑树
                    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;
        }
    

    4.5 用debug分析删除元素的过程【jdk1.8】

    image-20200727152416237

    image-20200727152441206

    removeNode()

    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看得都费劲,p节点是根据hash和key计算出的待删除的节点
            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;
                //若p的hash和key都吻合,直接赋值节点node
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    node = p;
                    
                //说明p所在节点为一个链表
                else if ((e = p.next) != null) {
                	//判断链表是否转换成了红黑树
                    if (p instanceof TreeNode)
                        node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                    //若未转换为红黑树,则遍历链表,直到key和hash都吻合,赋值给node
                    else {
                        do {
                            if (e.hash == hash &&
                                ((k = e.key) == key ||
                                 (key != null && key.equals(k)))) {
                                node = e;
                                break;
                            }
                            p = e;
                        } while ((e = e.next) != null);
                    }
                }
                
                //删除node
                if (node != null && (!matchValue || (v = node.value) == value ||
                                     (value != null && value.equals(v)))) {
                   //判断node是否为红黑树节点
                    if (node instanceof TreeNode)
                        ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                    //判断node节点是否为链表的第一个节点,若是,将当前链表的下一个节点指向赋给数组
                    else if (node == p)
                        tab[index] = node.next;
                    //最后一种情况就是node节点在链表中间,将头节点的下一个节点指向node的下一个节点。
                    else
                        p.next = node.next;
                    ++modCount;
                    --size;
                    afterNodeRemoval(node);
                    //返回node
                    return node;
                }
            }
            return null;
        }
    

    get和remove的思路

    两者大体思路相同,先根据传入的key计算hash,再依次通过:第一个元素是否命中,链表是否为红黑树,遍历链表的思路寻找对应的节点元素删除或返回。

    4.6 关于红黑树。核心就是自平衡!

    红黑树基于二叉查找树实现,在此基础上做了优化。

    二叉查找树又称二叉搜索树,二叉排序树

    关键规则如下:左子树的值=<根节点=<右子树的值,左右子树遵守同样的规则

    二叉查找树的平衡问题:

    image-20200727155549666

    红黑树的核心功能就是自平衡。

    红黑树的规则:

    • 节点为红色或黑色

    • 根节点是黑色

    • 叶子节点(NIL)是黑色

    • 如果一个节点是红色的,则它的子节点必须是黑色的。

    • 任一节点到其子树的叶子节点的路径都包含相同的黑色节点

    preview

    新插入的节点是这样的:

    image-20200727160137762

    若向当前树中插入14,则为:并不会引起红黑树的变化

    preview

    但若插入节点为21:违反了红黑树的红色节点的子节点都为黑色

    img

    与规则发生冲突时,红黑树需要进行调整,调整有两种方式:变色和自旋(自旋又分为左旋和右旋)

    变色:比如新添加一个红色节点到一个红色节点下就会产生变色的情况。

    左旋:当前节点变为左节点,当前节点的右节点变为父节点(把右节点的子树的左节点往左子树挪)

    img

    右旋:当前节点变为右节点,当前节点的左节点变为父节点(把左节点的子树的右节点往右子树挪)img

    4.7 hashMap树化原理

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            else {
                Node<K,V> e; K k;
                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) {
                        if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null);
                            //若链表长度大于8,转换为红黑树
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                            break;
                        }
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            ++modCount;
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }
    

    树化方法 treeifyBin(tab, hash);

    final void treeifyBin(Node<K,V>[] tab, int hash) {
            int n, index; Node<K,V> e;
            
            //若table为空或者tab的长度小于树化最小长度,优先扩容
            if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
                resize();
                
            //获取当前链表的位置
            else if ((e = tab[index = (n - 1) & hash]) != null) {
                TreeNode<K,V> hd = null, tl = null;   //定义红黑树的头结点和尾结点
                //遍历链表,最终结果:hd为表头,tl为表尾
                do {
                    TreeNode<K,V> p = replacementTreeNode(e, null);
                    if (tl == null)
                        hd = p;
                    else {
                        p.prev = tl;
                        tl.next = p;
                    }
                    tl = p;
                } while ((e = e.next) != null);
                //将hd赋给数组
                if ((tab[index] = hd) != null)
                	//树化方法
                    hd.treeify(tab);
            }
        }
    

    treeify

    final void treeify(Node<K,V>[] tab) {
                TreeNode<K,V> root = null;
                //遍历链表,this在第一次循环代表hd
                for (TreeNode<K,V> x = this, next; x != null; x = next) {
                    next = (TreeNode<K,V>)x.next;
                    x.left = x.right = null;
                    //初始化根节点
                    if (root == null) {
                        x.parent = null;
                        x.red = false;
                        root = x;
                    }
                    else {
                        K k = x.key;
                        int h = x.hash;
                        Class<?> kc = null;
                        //遍历根节点
                        for (TreeNode<K,V> p = root;;) {
                            int dir, ph;
                            K pk = p.key;
                            if ((ph = p.hash) > h)
                                dir = -1;   //为p的左子树
                            else if (ph < h)
                                dir = 1;   //为p的右子树
                            else if ((kc == null &&
                                      (kc = comparableClassFor(k)) == null) ||
                                     (dir = compareComparables(kc, k, pk)) == 0)
                                dir = tieBreakOrder(k, pk);
    
                            TreeNode<K,V> xp = p;
                            //判断p的子树是否为空(赋值和判断同时进行,666),若不为空,则在其子树下继续循环。最后到达叶子节点,插入节点
                            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;
                            }
                        }
                    }
                }
                moveRootToFront(tab, root);
            }
    

    本文篇幅已经过长,关于红黑树,之后会专门写一篇文章研究1.8中的实现。

  • 相关阅读:
    80端口被NT kernel & System 占用pid= 4的解决方法
    黑马程序员:装饰类的作用——增强被装饰类的方法的功能(利用组合实现复用)
    黑马程序员——java基础之文件复制
    10进制转换16进制原理及取得16进制最后一位或倒数第二位
    Django-admin源码流程
    Django-内置Admin
    Django-Form 补充
    有时间的时候可以看看
    编辑器KindEditor的使用
    Git的使用
  • 原文地址:https://www.cnblogs.com/noneplus/p/13403063.html
Copyright © 2020-2023  润新知