zoukankan      html  css  js  c++  java
  • 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中的实现。

  • 相关阅读:
    「UVA12293」 Box Game
    「CF803C」 Maximal GCD
    「CF525D」Arthur and Walls
    「CF442C」 Artem and Array
    LeetCode lcci 16.03 交点
    LeetCode 1305 两棵二叉搜索树中的所有元素
    LeetCode 1040 移动石子直到连续 II
    LeetCode 664 奇怪的打印机
    iOS UIPageViewController系统方法崩溃修复
    LeetCode 334 递增的三元子序列
  • 原文地址:https://www.cnblogs.com/noneplus/p/13403063.html
Copyright © 2011-2022 走看看