最近跟两个正在找工作的同学聊天,说起集合,都是面试的重灾区,必问的选项,而且在实际的面试中并不会单独提问某一个问题,而是围绕核心知识连环炮提问。所以背面试题治标不治本,还是得读一读源码。谁让这是个面试造火箭,工作拧螺丝的市场氛围,就连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中的实现。