手撕 JDK1.8 HashMap源码(下)

手撕 JDK1.8 HashMap源码(下)

手撕源码

1. 核心常量与属性

常量

// table初始容量大小 默认值是16 且容量一定是2的次数
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

// table最大长度 2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
	
// 负载因子大小 默认值是0.75 当哈希表使用了75%就会扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 树化条件1 树化阈值 链表长度达到8
static final int TREEIFY_THRESHOLD = 8;

// 树化条件2 table的长度达到64 满足该条件且满足树化条件1 链表才会树化
static final int MIN_TREEIFY_CAPACITY = 64;
	
// 树降级为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;

Node类

// put到Map里面的数据都会被封装为一个Node<K,V>存放到散列表当中
static class Node<K,V> implements Map.Entry<K,V> {
    
    
        final int hash; // K.hash()经过一次扰动
        final K key;    
        V value;	
        Node<K,V> next; // hash碰撞 链表的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;
        }
		
        // equals()
        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;
        }
    }

属性

// 哈希表table 由Node数组构建 只有第一次put操作时才会初始化table 默认长度是16
transient Node<K,V>[] table;
	
// 每一个Node构成的一个Set集合
transient Set<Map.Entry<K,V>> entrySet;
	
// 当前哈希表元素个数
transient int size;
	
// 当前哈希表结构修改次数 只有增删元素才会改变结构 替换元素不会使modCount变化
transient int modCount;
	
// 扩容阈值 当哈希表中的元素个数超过(数组长度 * 负载因子)时 就会扩容
int threshold;
	
// 负载因子 默认值是0.75
final float loadFactor;

2. 构造方法

双参

// 传入一个初始化大小和负载因子
public HashMap(int initialCapacity, float loadFactor) {
    
    
    // 这3个if就是对参数进行校验
    if (initialCapacity < 0) // 如果为负数则抛出异常
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    // 如果大于最大值 则还原为最大值
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 如果<=0或者非数 则抛出异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor; // 赋值
    // 传进来的initialCapacity可能是5,7这些值 需要利用tableSizeFor()确保大小是合法的 往下看该方法解析
    this.threshold = tableSizeFor(initialCapacity);
    /* 					  ↓
    作用:通过进制计算 返回一个大于等于当前值cap的一个数字 并且这个数字一定是2的次方。详见下方对于cap为10的计算
    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;
    }
    cap = 10;
   	n = 10 - 1 => 9
    n >>> x 表示n在2进制中右移x位
    00001001 | 00000100 => 00001101
    00001101 | 00000011 => 00001111
    00001111 | 00000000 => 00001111
    00001111 | 00000000 => 00001111
    00001111 | 00000000 => 00001111
    00001111(2) => 15(10)
    return 15 + 1 => 16
    */
}

单参

// 传入初始化大小
public HashMap(int initialCapacity) {
    
    
    // 这里的this其实是套娃了上面的双参构造方法
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

无参

// 最常用的构造方法 无参 将负载因子初始化为默认的0.75
public HashMap() {
    
    
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

Map为参数

// 将一个Map中数据复制到新的Map中
public HashMap(Map<? extends K, ? extends V> m) {
    
    
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

3. put() => putVal()

put()

public V put(K key, V value) {
    
    
     	// 通过hash()计算key的哈希值 再把hash,key,value传给putval()方法
        return putVal(hash(key), key, value, false, true);
    	/*              ↓
    	这个hash()就是我们上面提到的扰动函数
    	作用:让key的hash值的高16位也参与路由运算。
    	static final int hash(Object key) {
    		int h;
    		// 当key为null时 hash值为0 放到table下标为0的位置 不为null则对key的hashCode进行^异或运算 详见下方
    		return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
		}
		^ 异或:相同返回0,不同返回1
    	h = 0b 0010 0101 1010 1100 0011 1111 0010 1110
    	0b 0010 0101 1010 1100 0011 1111 0010 1110
    	^
    	0b 0000 0000 0000 0000 0010 0101 1010 1100 右移16位
    	=> 0010 0101 1010 1100 0001 1010 1000 0010
    	一般我们的length会比较小 但是hashcode范围是很大的 这样的话key的hashCode的高位就没有参数到运算中,容易造成哈希冲突
    	如果要让高16位也参与进路由寻址 需要进行一些运算,在这里也就是^异或运算 让高16位的特征也能参与到低16位中
    	让高16位也参与运算,可以使哈希值更加散列,可以减小哈希冲突。
		*/
}

putvalue()

我们可以根据图片来理解代码:

image-20220416230120921

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    
    
    // tab 引用当前的hashMap的散列表 
    // p 表示散列表当前位置的元素
    // n 表示散列表数组的长度
    // i 表示路由寻址得到的结果(下标)
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 成员属性table赋值给局部变量tab判断是否为null 或者是将tab的长度赋值给n判断是否为0 表示当前hashMap中还没有创建table
    if ((tab = table) == null || (n = tab.length) == 0)
        // 延迟初始化:第一次调用putval()方法 才会调用resize()扩容方法 创建散列表
        // 有时候我们把hashMap new出来之后 并不往里面放数据 如果我new的时候就把最占内存的散列表创建出来 那就会浪费内存了
        // 调用resize()扩容算法 创建散列表 再把新扩容的长度赋值给n 
        n = (tab = resize()).length;
    // 计算hash值的下标 (n - 1) & hash => 路由算法
    // &运算 对二进制进行运算(会计算的快一些)其实就是取余%操作 hash % (n - 1) 得到下标赋值给i
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 情况1.如果当前寻址找到的桶位为null 则new一个新链表结点 k,v=>node 直接扔到下标为i的散列表tab
        tab[i] = newNode(hash, key, value, null);
    else {
    
     // 如果值已经存在
        // e:不为null的话,则找到了一个与当前要插入的key-value一致的key的元素node
        // k:临时的一个key
        Node<K,V> e; K k;
        // 情况2.表示桶位中的元素 与你当前插入的元素key完全一致,表示后续需要进行覆盖操作
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p; // 将p赋值给e
        // 情况3.此时已经树化
        else if (p instanceof TreeNode)
            // 按照红黑树的结构去存 如果存储时发生了覆盖 则赋值给e
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 情况4.此时还是链表结构 而且链表的头元素与我们要插入的key不一致
        else {
    
    
            // 遍历链表里的所有数据
            for (int binCount = 0; ; ++binCount) {
    
    
                // 当迭代到null时 说明已在链表的末尾 且在该链表也没有找到一个与你插入的key一致的node元素
                if ((e = p.next) == null) {
    
    
                    // 将该结点添加到p.next位置 也就是链表的末尾
                    p.next = newNode(hash, key, value, null);
                    // 此时需要进行判断 未插入p时的结点数是否已经达到了8个
                    // 如果已经达到了8个 那么插入p.next时链表长度就变成了9 > 8 就需要对链表进行树化操作
                    // 循环从0开始 binCount >= TREEIFY_THRESHOLD - 1 => 7 此时链表已经有8个元素
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash); // 树化操作 后期会更新红黑树的文章
                    break;
                }
                // 此时e.hash也就是p.next.hash与当前插入的hash相同 且key一致 说明找到了一个相同元素 则break出外面操作 
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e; // 一步一步往后迭代
            }
        }
        // 条件成立说明 找到了一个与你插入元素key完全一致的数据 需要执行覆盖操作 e赋上了值就代表要覆盖 e不赋值就代表存上了
        if (e != null) {
    
     // existing mapping for key:密钥的现有映射
            V oldValue = e.value; // 先取出旧值
            // !onlyIfAbsent = true
            if (!onlyIfAbsent || oldValue == null)
                e.value = value; // 然后把现在要存的值赋给e.value 新值替换旧值
            afterNodeAccess(e); // 访问后回调
            return oldValue; // 把旧的值返回 不执行后面的操作
        }
    }
    ++modCount; // 表示散列表被修改的次数 插入新元素才会++ 替换在上面直接return了 不++
    // ++size后 大于扩容阈值 触发扩容算法
    if (++size > threshold)
        resize(); // 扩容算法
    afterNodeInsertion(evict); // 插入后回调
    return null;
}

put()方法总结

先通过hash()扰动计算出哈希值,然后调用putVal()方法:

  1. 延迟初始化:先判断哈希表是否创建过,如果未创建则创建table
  2. 通过路由算法(n - 1) & hash寻址得到当前元素的下标
  3. 插入元素
    • ① 当前桶位为空,直接放
    • ② 当前位置有元素,且哈希值相同,key一致,则进行赋值,后续进行value的覆盖操作
    • ③ 当前位置已被树化,如果存储时发生了覆盖,则赋值,否则插入到树中
    • ④ 当前位置是链表结构,遍历整个链表,如果找到哈希值相同,且key一致,后续进行value的覆盖操作,否则插入到链表尾部,并判断当前链表长度是否达到了树化标准(链表长度达到8 并且数组长度达到64(这个判断条件在树化的treeifyBin()方法中))
  4. 覆盖value操作
  5. 插入元素后 更新变量,++modCount,判断++size是否超过当前大小,如果超过则resize()扩容。

4. resize() 扩容(核心)

// 为什么需要扩容?为了解决哈希冲突导致的链化影响查询效率问题,扩容会缓解该问题。
final Node<K,V>[] resize() {
    
    
    // oldTab:引用扩容前的哈希表
    Node<K,V>[] oldTab = table;
    // oldCap:表示扩容之前table数组的长度 如果是null 则长度为0
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // oldThr:扩容之前的扩容阈值(数组长度),触发本次的扩容阈值
    int oldThr = threshold;
    // newCap:扩容之后达到的大小
    // newThr:扩容之后,下次再次触发扩容的条件
    int newCap, newThr = 0;
    // 条件成立 说明hashMap中的散列表已经初始化过了,是一次正常扩容
    if (oldCap > 0) {
    
    
        // 扩容之前的table数组大小已经达到最大阈值后,则不扩容,且设置扩容条件为 int 最大值
        if (oldCap >= MAXIMUM_CAPACITY) {
    
    
            // 生成这个int最大值就很难达到这个扩容阈值了 但这种情况很少见
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // oldCap左移一位 实现数值翻倍 并赋值给newCap,newCap小于数组最大值限制 且 扩容之前的阈值 >= 16
        // 这种情况下,则下一次扩容的阈值 等于当前阈值翻倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold:双倍阈值
    }
    
    // oldCap == 0 说明hashMap中的散列表是null
    // 1.new HashMap(initCap, loadFactor);
    // 2.new HashMap(initCap);
    // 3.new HashMap(map); 并且这个map有数据
    // 在这3中构造方法情况下,oldThr有值
    else if (oldThr > 0) // initial capacity was placed in threshold:初始容量设置为阈值
        // 第一次构建 直接赋值给newCap
        newCap = oldThr;
    // oldCap == 0,oldThr == 0
    // new HashMap();
    // 只有调用的是无参构造方法时
    else {
    
    				 // zero initial threshold signifies using defaults:零初始阈值表示使用默认值
        newCap = DEFAULT_INITIAL_CAPACITY; // 16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 0.75 * 16 => 12
    }
    
    // newThr为零时,通过newCap和loadFactor计算出一个newThr
    if (newThr == 0) {
    
    
        float ft = (float)newCap * loadFactor;
        // 判断newCap和ft是否合法 如果合法则将计算出的ft转为int 不合法则赋予int最大值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr; // 更新下一次的扩容阈值
    
    @SuppressWarnings({
    
    "rawtypes","unchecked"}) // 注解作用:抑制编译器产生的多类型警告信息
    // 根据newCap创建出一个更长的数组 也有可能是第一次创建数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab; // 将新创建的node赋值给table
    // 说明,hashMap本次扩容之前 table不为null
    // 扩容操作
    if (oldTab != null) {
    
    
        // 将每个旧哈希表中的数据都移动到新的散列表中
        // 遍历旧哈希表中的每一个桶,重新计算桶里元素在新哈希表中的位置
        for (int j = 0; j < oldCap; ++j) {
    
    
            Node<K,V> e; // 当前node节点
            // 说明当前桶位中有数据 但是数据具体是单个数据/链表/红黑树 暂时还未知
            if ((e = oldTab[j]) != null) {
    
    
                // 原来的数据赋值为null 方便JVM GC时回收内存
                oldTab[j] = null;
                // 情况1.说明该桶中未发生过哈希碰撞 只有一个元素
                if (e.next == null)
                    // 根据新length通过路由寻址算法得到新下标 插入到新位置
                    newTab[e.hash & (newCap - 1)] = e;
                // 情况2.当前节点已树化
                else if (e instanceof TreeNode)
                    // 红黑树处理 暂且跳过 后期会更新红黑树的文章
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // 情况3.当前节点为链表 下方有图
                else {
    
     // preserve order:维持秩序
                    // 低位链表:存放在扩容之后的数组的下标位置 与当前数组的下标一致
                    Node<K,V> loHead = null, loTail = null;
                    // 高位链表:存放在扩容之后的数组的下标位置为 当前数组下标位置 + 扩容之前数组的长度
                    Node<K,V> hiHead = null, hiTail = null;
                    
                    Node<K,V> next;
                    do {
    
    
                        next = e.next;
                        /*
                        	假设当前oldCap 也就是当前哈希表长度为16 => 0b 10000 
                        	且当前结点存放在下标为15的位置 则该节点哈希值的后4位一定是1111 第5位可能是0也可能是1
                        	e.hash -> ... 0 1111
                        	e.hash -> ... 1 1111
                        	e.hash & oldCap == 0 说明该节点的下标为15 即放在原下标位置(低位链表) 
                        	e.hash & oldCap == 16 => != 0 说明该节点的下标为31 即放在新下标位置(高位链表)
                        */
                        // hash值 & 原长度 == 0 说明元素应该存至低位链表
                        if ((e.hash & oldCap) == 0) {
    
    
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 存至高位链表
                        else {
    
    
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 低位链表有数据
                    if (loTail != null) {
    
    
                        loTail.next = null; // 将末尾置为空
                        newTab[j] = loHead; // 放到原位置
                    }
                    // 高位链表有数据
                    if (hiTail != null) {
    
    
                        hiTail.next = null; // 将末尾置为空
                        newTab[j + oldCap] = hiHead; // 放到 原位置 + 扩容之前的table长度
                    }
                }
            }
        }
    return newTab; // 返回新散列表
}

扩容—链表处理

低位链表:存放在扩容之后的数组的下标位置 与当前数组的下标一致(蓝色)

高位链表:存放在扩容之后的数组的下标位置为 当前数组下标位置 + 扩容之前数组的长度(绿色)

image-20220417143713934

resize()扩容方法总结

  1. 获取原哈希表的数据 并进行验证和计算后 得到新值newCap(哈希表的新长度),newThr(哈希表的新扩容阈值)
  2. 根据newCap创建出新的哈希表
  3. 扩容操作
    • ① 该桶中只有一个元素,直接通过路由寻址算法得到新下标 插入到新位置
    • ② 当前节点已树化,由树化操作寻址插入
    • ③ 当前节点为链表结构,分离链表(低位、高位),根据位运算判断当前节点应存至低位/高位
  4. 存放操作

5. get() => getNode()

get()

public V get(Object key) {
    
    
    Node<K,V> e;
    // put经过hash(),取时肯定也要hash() 如果key为空则返回空 否则返回value
    return (e = getNode(hash(key), key)) == null ? null : e.value;
} //               ↓

getNode()

final Node<K,V> getNode(int hash, Object key) {
    
    
    // tab:引用当前hashMap的散列表
    // first:桶位中的头元素
    // e:临时node元素
    // n:table数组长度
    // k:key
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 如果哈希表的table存在 且桶中的第一个元素存在 但并不知道该元素是单个/链表/红黑树
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
    
    
        // 情况1.定位的桶位首元素 即为我们要get的数据 直接把该元素返回
        if (first.hash == hash && // always check first node:始终检查第一个节点
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 当前桶位不是单个元素 可能是链表/红黑树
        if ((e = first.next) != null) {
    
    
            // 情况2.桶位是红黑树结构 在树中寻找
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 情况3.桶位是链表结构 遍历链表寻找
            do {
    
    
                // 找到
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null; // 没有数据直接返回空
}

6. remove() => removeNode()

remove()

单参

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

双参

// 根据key和value删除
@Override
public boolean remove(Object key, Object value) {
    
    
    return removeNode(hash(key), key, value, true, true) != null;
}

removeNode()

final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    
    
					// matchValue参数 如果为true则需要对value也进行比较 参照上面的两个构造方法 只传key时 该参数为false
    // tab:引用当前hashMap中的散列表
    // p:当前node元素
    // n:表示散列表数组长度
    // index:表示寻址结果
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    // 判断table中是否有元素 如果有则计算出当前元素位置并赋值给p	
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
    
    
        // node:查找到的结果
        // e:当前Node的下一个元素
        Node<K,V> node = null, e; K k; V v;
        // 进行查找操作 并删除
        // 情况1.当前桶位中的元素 即为你要删除的元素
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        // p.next不为空 当前桶位可能是链表/红黑树
        else if ((e = p.next) != null) {
    
    
            // 情况2.当前桶位已是红黑树
            if (p instanceof TreeNode)
                // 红黑树的查找操作
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            // 情况3.当前桶位为链表
            else {
    
    
                // 遍历整个链表 查找元素
                do {
    
    
                    // 找到 赋值给node
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
    
    
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // 对比查找到的key和value是否与传进来的匹配
        // 删除操作
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
    
    
            // 情况1.如果node是红黑树结构 则用树的操作删除
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            // 情况2.当前桶位即为要删除的元素
            else if (node == p)
                // 直接将当前结点指向next 即删除
                tab[index] = node.next;
            // 情况3.将当前元素p的下一个元素 设置成 要删除元素的 下一个元素
            else
                p.next = node.next;
            ++modCount; // 更新修改次数
            --size; // 更新元素个数
            afterNodeRemoval(node);
            return node; // 返回结果
        }
    }
    return null; // 如果查询不到 则返回null
}

7. replace()

三参

@Override
public boolean replace(K key, V oldValue, V newValue) {
    
    
    Node<K,V> e; V v;
    // 根据key找到node结点 判断找到的value跟传进来的oldValue是否一致 如果一致则用新的newValue替换value
    if ((e = getNode(hash(key), key)) != null &&
        ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
    
    
        e.value = newValue;
        afterNodeAccess(e);
        return true;
    }
    return false;
}

双参

@Override
public V replace(K key, V value) {
    
    
    Node<K,V> e;
    // 根据key找到node结点 做替换 把旧值返回
    if ((e = getNode(hash(key), key)) != null) {
    
    
        V oldValue = e.value;
        e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }
    return null;
}
据key找到node结点 判断找到的value跟传进来的oldValue是否一致 如果一致则用新的newValue替换value
    if ((e = getNode(hash(key), key)) != null &&
        ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
    
    
        e.value = newValue;
        afterNodeAccess(e);
        return true;
    }
    return false;
}

文章参考:b站小刘讲源码视频

猜你喜欢

转载自blog.csdn.net/weixin_53407527/article/details/124248997