集合框架五 -- 基于jdk1.8及以后的HashMap

之前已经说过了1.7的HashMap源码,接下来来学习1.8 及以后的源码,注意,本文使用的是jdk13的源码

1. 定义

1.1 继承和实现

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

和1.7相比没有什么变化

1.2 常量变量

相比于1.7主要是HashMap的结构由Entry变成了Node,同时多了链表转红黑树的阙值

	// 默认初始容量。容量必须是2的倍数
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    // 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // 默认加载因子,当实际使用容量/容量大于这个值时,进行扩容
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

	// 链表转红黑树阙值
	static final int TREEIFY_THRESHOLD = 8;

	// 红黑树转链表阈值
	static final int UNTREEIFY_THRESHOLD = 6;

    // treeifyBin方法中判断,只有当table数组长度大于64时,才会触发链表转红黑树
    // 小于的时候还是优先扩容
    static final int MIN_TREEIFY_CAPACITY = 64;

    static final Entry<?,?>[] EMPTY_TABLE = {};
    
    // 存储键值对对应的Node数组
    transient Node<K,V>[] table;

    // 键值对的数量
    transient int size;

    // 等于加载因子乘以数组长度,表示一个阙值,当size(实际使用容量)超过了就会扩容
    int threshold;
	
	// 加载因子
	final float loadFactor;

1.3 初始化

	// 无参构造,初始化
 	public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

	// 自定义初始容量初始化,调用HashMap(int, float)方法初始化
 	public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
	
	// 自定义初始容量和加载因子初始化
	 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        // 这里的tableSizeFor()方法在jdk8和jdk13中的实现方式有点不同,同时和1.7也有不同,1.7是赋值为初始容量
        this.threshold = tableSizeFor(initialCapacity);
    }

	// 根据map初始化
	public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
1.3.1 扩容阙值初始化方法:tableSizeFor()

由于jdk8和jdk13用的方法目的是一样的,但是方法不太一样,所以我们这里单独拎出来说,

  • 首先来看jdk8的:这里的实现方法最后的目的jdk7的还是比较相似的,类似于Integer的highestOneBit()方法,这个方法是为了获取二进制最左一位,往右补零的int型结果。
    因为这里的cap是大于0的,所以直接用的无符号右移。目的是将当前容量的二进制值的0都变成1,并将这个值转成int型,最后再将这个int型+1,相当于获取二进制最左一位,再向左移一位再往右补零的int型结果。
	//jdk8的实现
	// 获取传入容量的最左一位的int值加一,如传入10则为16,传入15则为16,传入17则为32
	static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
  • 接着我们看jdk13的:jdk13没有像jdk8那样用无符号位移而是使用了Integer的numberOfLeadingZeros()方法,其目的也是获取与当前二进制长度相同的最大的二进制的int型值,如果5则为7,9则为15。
    最后再将这个int型+1,相当于获取二进制最左一位,再向左移一位再往右补零的int型结果。
	static final int tableSizeFor(int cap) {
		// 相当于将cap的二进制值右边的0都变成1,和jdk8的实现有点类似
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
	// 返回无符号整型i的最高非零位前面的0的个数,包括符号位在内;
	// 比如i=10,则返回28,i=7则返回29
	public static int numberOfLeadingZeros(int i) {
        // HD, Count leading 0's
        if (i <= 0)
            return i == 0 ? 32 : 0;
        int n = 31;
        if (i >= 1 << 16) { n -= 16; i >>>= 16; }
        if (i >= 1 <<  8) { n -=  8; i >>>=  8; }
        if (i >= 1 <<  4) { n -=  4; i >>>=  4; }
        if (i >= 1 <<  2) { n -=  2; i >>>=  2; }
        return n - (i >>> 1);
    }

1.4 Map最重要的内容:静态内部类Node

1.7时为Entry,这里变成了Node,其实还是差不多的概念都实现了Map.entry,Node中的hash多了final关键字

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

1.5 整个HashMap的图就像如下的结构:

首先一个结论:

  • 链表:插入复杂度O(1),查找复杂度O(n)
  • 红黑树:插入复杂度O(logn),查找复杂度O(logn)

在1.8之前,HashMap的结构都是如下所示,当hash发生了冲突的时候,就会将新数据以链表的形式存储。虽然插入直接使用头插,插入复杂度O(1);当链表较短时候,查找数据时对性能并没有什么影响,如果链表一长,查找起来就很影响性能了。结构如下图:
在这里插入图片描述
在Java8中,如果链表长度到达了8个,就会转化为红黑树,提高了查找的性能,但每次插入新的数据,都得维护红黑树的结构,复杂度为O(logn)。这样算是对查找和插入元素时性能的一个权衡。结构如下图:
在这里插入图片描述

2. 插入

2.1 put(K key, V value):

假如我执行了如下方法:

Map<String, String> map = new HashMap<String, String>();
map.put("111", "111");

具体做了什么呢?(链表转红黑树暂时不做讲解)

2.1.1 首先第一行,调用无参构造。(此时阙值没有初始化)

查看IDEA的debug模式:
在这里插入图片描述
假如我传入一个初始容量进行初始化,阙值是什么?

 Map<String, String> map = new HashMap<String, String>(8);

在这里插入图片描述

可以看到是大于等于传入容量的最接近的2的幂次。

2.1.2 然后第二行put方法做了什么:
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

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赋值给tab,table长度赋值为n;如果table为空或长度为0,调用resize()方法创建哈希桶
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // case1:通过hash值来确认在hash桶中的位置,如果该位置为空,则该K、V作为该位置的头节点
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 该位置已有节点,即发生hash冲突
    else {
        Node<K,V> e; K k;
        // case2:如果新添加的值的Key与原节点相同,将p赋值给e
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // case3:如果新添加的值与头节点不同,并且该头节点已经是红黑树的结构,则调用putTreeVal方法
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // case4:如果新添加的值与头节点不同,并且该头节点仍然是链表的状态
        else {
        	// 遍历链表
            for (int binCount = 0; ; ++binCount) {
            	// 如果p的下一个节点为null,贼将新的值放到链表尾部
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 如果此时链表长度大于链表转红黑树的阙值8,ze 调用treeifyBin方法将该链表转为红黑树 
                    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;
            }
        }
        // 如果e不为空,说明有重复的节点,因为e为空,说明执行的都是插入操作
        // 将重复的节点的value赋为新的value,跳出方法,返回旧值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 子类(LinkedHashMap)实现
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 说明是一个新的节点,更新fail-fast
    ++modCount;
    // 如果长度超过阙值,扩容
    if (++size > threshold)
        resize();
    // 子类(LinkedHashMap)实现
    afterNodeInsertion(evict);
    return null;
}

总结一下put的流程就是:

  1. 判断hash桶是否为空,为空调用resize方法创建
  2. 即case1:通过hash计算来判断新插入的节点在桶中的位置,如果该位置为空,则直接调用newNode创建节点,放在该位置
  3. 如果通过hash计算发生了hash冲突:
    • case2:如果添加的节点与该位置头节点相同,则直接将该节点值赋给e
    • case3:如果添加的节点与头节点不同,并且该头节点已变成红黑树结构,则调用putTreeVal方法,将该节点更新到红黑树,如果是插入返回null,更新返回更新的值给e
    • case4:如果该头节点仍为链表的结构,循环遍历链表,一旦找出相同的节点就跳出循环,在此过程中已经给e赋值;如果没有找到相同节点,将需要插入的节点插入到链表尾部,如果链表长度超过8,就调用treeifyBin方法将链表转为红黑树
    • 如果是更新的节点,就将之前拿到的需要更新的节点e的value更新,并将旧值返回
  4. 如果是新添加的元素,计数器+1,如果长度超过阙值,调用resize方法扩容

IDEA的debug模式:
在这里插入图片描述

2.2 resize()方法,扩容方法:

final Node<K,V>[] resize() {
     Node<K,V>[] oldTab = table; //将当前hash桶数组,设为老的hash桶数组
     int oldCap = (oldTab == null) ? 0 : oldTab.length; //老的hash桶容量
     int oldThr = threshold;	// 老的扩容阀值设置
     int newCap, newThr = 0;	// 新hash桶的容量,新hash桶的扩容阀值都初始化为0
     // 接下来这段if else目的主要是给新的hash桶设置容量和扩容阙值
     // case1:
     if (oldCap > 0) {	// 如果老hash桶容量大于0,说明已经存在元素
         if (oldCap >= MAXIMUM_CAPACITY) { // 如果hash桶元素个数大于等于限定的最大容量(2的30次方)
             // 扩容阀值设置为int最大值(2的31次方 -1 ),因为oldCap再乘2就溢出了。
             threshold = Integer.MAX_VALUE;	
             return oldTab;	// 返回老的hash桶
         }

        /*
         * 如果hash桶数组元素个数在正常范围内,那么新的数组容量为老的数组容量的2倍(左移1位相当于乘以2)
         * 如果扩容之后的新容量小于最大容量  并且老的数组容量大于等于默认初始化容量(16),那么新数组的扩容阀值设置为老阀值的2倍。
         * (老的数组容量大于16意味着:要么构造函数指定了一个大于16的初始化容量值,要么已经经历过了至少一次扩容)
         */
         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                  oldCap >= DEFAULT_INITIAL_CAPACITY)
             newThr = oldThr << 1; // double threshold
     }

     // case2:
     // 运行到这个else if  说明老数组没有任何元素
     // 如果老数组的扩容阀值大于0,那么设置新数组的容量为该阀值
     // 这一步也就意味着构造该map的时候,指定了初始化容量。
     else if (oldThr > 0) // initial capacity was placed in threshold
         newCap = oldThr;
     else {               // zero initial threshold signifies using defaults
         // 能运行到这里的话,说明是调用无参构造函数创建的该map,并且第一次添加元素
         newCap = DEFAULT_INITIAL_CAPACITY;	// 设置新数组容量 为 16
         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 设置新数组扩容阀值为 16*0.75 = 12。0.75为负载因子(当元素个数达到容量了4分之3,那么扩容)
     }

     // 如果新扩容阀值为0 (case2的情况),设置一下新的扩容阙值
     if (newThr == 0) {
         float ft = (float)newCap * loadFactor;
         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                   (int)ft : Integer.MAX_VALUE);  // 参见:PS2
     }
     threshold = newThr; // 设置map的扩容阀值为 新的阀值
     @SuppressWarnings({"rawtypes","unchecked"})
     // 创建新的hash桶数组(对于第一次添加元素,那么这个数组就是第一个数组;对于存在oldTab的时候,那么这个数组就是要需要扩容到的新数组)
     Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
     // 将该map的table属性指向到该新hash桶数组
     table = newTab;	
     // 如果老hash桶数组不为空,说明是扩容操作,那么涉及到元素的转移操作
     if (oldTab != null) {	
         for (int j = 0; j < oldCap; ++j) { // 遍历老hash桶数组
             Node<K,V> e;
             if ((e = oldTab[j]) != null) { // 如果当前位置元素不为空,那么需要转移该元素到新数组
                 oldTab[j] = null; // 释放掉老数组对于要转移走的元素的引用(主要为了使得数组可被回收)
                 if (e.next == null) // 如果元素没有有下一个节点,说明该元素不存在hash冲突
                     // case3
                     // 把元素存储到新的数组中,存储到数组的哪个位置需要根据hash值和数组长度来进行取模
                     // 【hash值 % 数组长度】 = 【hash值 & 数组长度-1】
                     // 这种与运算求模的方式要求  数组长度必须是2的N次方,
                     // 但是可以通过构造函数随意指定初始化容量呀,如果指定了17,15这种,岂不是出问题了就?
                     // 没关系,最终会通过tableSizeFor方法将用户指定的转化为大于其并且最相近的2的N次方。
                     // 15 -> 16、17-> 32        
                     newTab[e.hash & (newCap - 1)] = e;

                 // 如果该元素有下一个节点,那么说明该位置上存在一个链表了(hash相同的多个元素以链表的方式存储到了老数组的这个位置上了)
                 // 例如:数组长度为16,那么hash值为1(1%16=1)的和hash值为17(17%16=1)的两个元素都是会存储在数组的第2个位置上(对应数组下标为1),
                 // 当数组扩容为32(1%32=1)时,hash值为1的还应该存储在新数组的第二个位置上,但是hash值为17(17%32=17)的就应该存储在新数组的第18个位置上了。
                 // 所以,数组扩容后,所有元素都需要重新计算在新数组中的位置。
                 // 如果该节点为TreeNode类型,说明这个节点上的数据已经转为红黑树
                 else if (e instanceof TreeNode)  
                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);  // 此处单独展开讨论
                 // 说明该节点还没有转换为红黑树仍为链表形式
                 else { // preserve order
                     Node<K,V> loHead = null, loTail = null;  // 按命名来翻译的话,应该叫低位首尾节点
                     Node<K,V> hiHead = null, hiTail = null;  // 按命名来翻译的话,应该叫高位首尾节点
                     // 以上的低位指的是新数组的 0  到 oldCap-1 、高位指定的是oldCap 到 newCap - 1
                     // 其中oldCap即旧数组长度,newCap即新数组长度
                     Node<K,V> next;
                     // 遍历链表
                     do {  
                         next = e.next;
                         // 这一步拿元素的hash值和老数组的长度做与运算
                         // case3里曾说到,数组的长度一定是2的N次方(例如16),如果hash值和该长度做与运算,
                         // 那么该hash值可参与计算的有效二进制位就是和长度二进制对等的后几位,如果结果为0,
                         // 说明hash值中参与计算的对等的二进制位的最高位一定为0(即比如oldCap=16,说明hash值从后往前数第五位为0).
                         // 因为数组长度的二进制有效最高位是1(例如16对应的二进制是10000),
                         // 只有*..0**** 和 10000 进行与运算结果才为00000(*..表示不确定的多个二进制位)。
                         // 又因为定位下标时的取模运算是以hash值和长度减1进行与运算,所以下标 = (*..0**** & 1111) 也= (*..0**** & 11111) 。
                         // 	- 其中1111是15的二进制、11111是16*2-1 也就是31的二进制(2倍扩容)。
                         // 	- 比如4的二进制是0 0100,无论跟1111还是1 1111进行与都是0 0100,所以还在原位置
                         //		- 而18的二进制是1 0010,跟1111求与是0 0010而跟1 1111求与就是1 0010,在不同位置
                         // 所以该hash值再和新数组的长度取摸的话mod值也不会放生变化,
                         // 也就是说该元素的在新数组的位置和在老数组的位置是相同的,所以该元素可以放置在低位链表中。
                         // 举个反推例子:
                         // 现在hash桶数组长度为l=16,l-1的二进制为1111;某个key的hash值为k=*..00101,
                         // 根据寻址的运算k & (l-1),得到的结果为5,说明该key需要被存放在table[4]中
                         // 现在hash桶长度扩容到l=32;那么l-1的二进制变成了11111,此时k & (l-1)得到的结果仍为5
                         // 而只有满足在hash桶数组长度左移一位的最高位处,该key的值为0,才能满足在扩容后与计算的结果与之前一样
                         // 否则假如说key的hash值k=*..10101,则扩容前应存放在table[4],而扩容后存放在table[21]
                         // 当key满足如上所述的条件时,(e.hash & oldCap)一定是等于0的
                         // 所以当满足(e.hash & oldCap) == 0 时,扩容前后该key在数组中位置不回放生变化
                         // 这个判断的本质时将原链表一分为二,一部分存放在原来的位置,一部分存放到原位置加上原数组长度的位置上去
                         if ((e.hash & oldCap) == 0) {  
                             // case4
                             if (loTail == null) // 如果没有尾,说明链表为空
                                 loHead = e; // 链表为空时,头节点指向该元素
                             else
                                 loTail.next = e; // 如果有尾,那么链表不为空,把该元素挂到链表的最后。
                             loTail = e; // 把尾节点设置为当前元素
                         }

                         // 如果与运算结果不为0,说明hash值大于老数组长度(例如hash值为17)
                         // 此时该元素应该放置到新数组的高位位置上
                         // 例:老数组长度16,那么新数组长度为32,hash为17的应该放置在数组的第17个位置上,也就是下标为16,
                         // 那么下标为16已经属于高位了,低位是[0-15],高位是[16-31]
                         else {  // 以下逻辑同case4
                             if (hiTail == null)
                                 hiHead = e;
                             else
                                 hiTail.next = e;
                             hiTail = e;
                         }
                     } while ((e = next) != null);
                     // 低位的元素组成的链表还是放置在原来的位置
                     if (loTail != null) { 
                         loTail.next = null;
                         newTab[j] = loHead;
                     }
                     // 高位的元素组成的链表放置的位置只是在原有位置上偏移了老数组的长度个位置。
                     if (hiTail != null) {  
                         hiTail.next = null;
                         // 例:hash为 17 在老数组放置在0下标,在新数组放置在16下标;
                         //    hash为 18 在老数组放置在1下标,在新数组放置在17下标; 
                         newTab[j + oldCap] = hiHead;                   
                     }
                 }
             }
         }
     }
     return newTab; // 返回新数组
}

resize方法可以主要分为2部分:

  1. 首先是将hash桶数组扩容,分为三种情况:
    • 桶容量大于0,说明是扩容,在原长度没有超过最大长度的情况下,将数组长度和加载因子乘以2
    • 桶容量等于0,但是扩容因子大于0,说明初始化时赋值了
    • 都为0,说明是创建新桶
  2. 原hash桶中的元素转移工作,也是分为三种情况:
    • 第一种是原数组节点位置没有下一个节点,说明该位置只有这么一个节点,直接hash计算一下并设值就行
    • 第二种是原数组节点已经转变为红黑树,这里我们暂时不讨论
    • 第三种是原数组节点为链表,这里主要讲链表中元素分为了2类,一部分放在原节点中,一部分放在了原节点index+原数组长度位置处。
      • 例如原长度为16,新长度为32
      • 4扩容后还在index为3的位置,20扩容后则会在index为19的位置
      • 因为16-1 = 1111,32-1=11111
      • 4&1111和4&11111都等于4
      • 20&1111=4,20&11111=20

3. 删除

3.1 remove(Object key):

假如我执行了如下代码:

Map<String, String > map = new HashMap<>();
map.put("111", "111");
map.put("222", "222");
map.remove("111");

最后的remove方法做了哪些事情?首先来看remove源码:

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

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;
    // 如果hash桶不为空,并且通过hash计算出hash桶中该位置存在数据,将p设置为该处的头节点
    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; // node需要删除的节点
        // case1:如果该头节点p就是要删除的节点,直接得到节点node
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        // 头节点不匹配
        else if ((e = p.next) != null) {
        	// case2:假如头节点已经变成红黑树,调用getTreeNode方法拿到节点node
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else { // case3:该节点仍为链表的结构,遍历链表,找到需要删除的node,跳出循环;循环的过程中,p也一直在往后遍历
                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)))) {
            // 如果是红黑树,调用红黑树的删除方法
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            // 如果待删除节点是头节点,将下一个节点设置为头节点
            else if (node == p)
                tab[index] = node.next;
            // 如果是链表,将需要删除的节点的前驱节点的next设为需要删除的节点的next
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

总的来说,删除的步骤就是:

  1. 先通过hash计算需要删除的key的桶中的位置
  2. 然后得到该位置的节点,根据该头节点的情况用不同的方式获得需要删除的节点node,然后再根据该节点情况执行不同的方式删除:
    • case1:是头节点,则直接将next设为头节点
    • case2:如果是红黑树,调用removeTreeNode方法删除
    • case3:如果是链表,则将node的前驱节点p的next节点设置为node的next节点

4. 获取

get方法的步骤和remove方法的获取node有点相似,这里就不重复介绍了。

5. jdk8及以后相比于1.7做了哪些修改:

5.1 数据结构的修改:

数组+链表 --> 数组+链表+红黑树这个就不做细说了

5.2 初始化时,扩容阙值赋值不同:

  • 1.7是将初始容量赋值给扩容阙值
  • 而1.8是将大于等于初始容量最接近的2的幂次赋值给扩容阙值

5.3 hash算法的变化:

  • 1.8的hash算法:
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
  • 1.7的hash算法:
	final int hash(Object k) {
        int h = hashSeed;//默认为0
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

5.4 扩容方法的修改:

  1. 在判断是否扩容时,1.8是判断容量是否超过扩容阙值,而1.7是判断容量是否超过扩容阙值同时判断该节点是否为空
  2. JDK1.8中resize()方法在表为空时,创建表,在表不为空时,扩容;而JDK1.7中resize()方法负责扩容,inflateTable()负责创建表
  3. 1.8中没有区分键为null的情况,而1.7版本中对于键为null的情况调用putForNullKey()方法。但是两个版本中如果键为null,那么调用hash()方法得到的都将是0,所以键为null的元素都始终位于哈希表中第一个桶中,这一点两个版本是相同的。
  4. 当1.8中的桶中元素处于链表的情况,遍历的同时最后如果没有匹配的,直接将节点添加到链表了尾部,而1.7在遍历的同时没有添加数据,而是另外调用了addEntry()方法。
  5. addEntry中默认将新加的节点作为链表的头节点,而1.8中会将新加的结点添加到链表末尾
  6. 1.7中是通过更改hashSeed值修改节点的hash值从而达到rehash时的分散;而1.8中键的hash值不会改变,rehash时根据hash&cap==0将链表一分为二,一部分在原位置,一部分在原位置+原长度的位置
  7. 1.8rehash时保证原链表的顺序,而1.7中rehash时将改变链表的顺序

6. 为什么从1.7的头插法改为尾插法?

头插法的问题:

  • 破坏了链表元素的插入顺序:由于头插法是将新插入的元素插入到链表的头部,这样就导致链表的顺序与元素插入的顺序相反,不利于一些需要按照插入顺序遍历的场景。
  • 容易引起链表环形问题:由于头插法需要修改链表头,这会导致在并发环境下,多个线程同时修改链表头,可能会引起链表环形问题,使得链表无法正确遍历或者出现死循环的情况。
  • 不利于链表长度的平衡:在长时间运行的情况下,头插法可能会导致链表的长度不平衡,即某些链表的长度会很长,而某些链表的长度很短,这会降低HashMap的性能。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值