漫谈 HashMap

Intro

基本知识

  1. Map 接口实现类(Java 1.2+)
    • 存储 K-V 键值对
    • 无序,无下标,Key 唯一
  2. 线程不安全
  3. 允许一个 key 和多个 value 的取值为 null

相关知识

  1. 👉 哈希及 Java 实现
    1. 哈希函数
    2. 哈希冲突
  2. 👉 浅谈 equals() 和 hashCode()
    1. 作用
    2. 默认实现
    3. 重写

1、底层结构

1.1、版本区别

Java 1.8 对 HashMap 的底层结构做了改动,

在原先的基础上引入了红黑树。

  • Java 1.7-数组 + 位桶(链地址法)

    image

  • Java 1.8+数组 + 位桶,红黑树

    • 链表查询元素的时间复杂度是 O(n),若哈希冲突频繁会导致链表过长,效率低。
    • 红黑树查询元素的时间复杂度是 O(logn)

    image

1.2、红黑树

1.2.1、树化 & 退化

树化条件:需同时满足两个条件

  1. 位桶结点数 >= 树化阈值(默认 8)
  2. 哈希表结点数 >= 最小树化容量(默认 64,根据泊松分布得出)

退化条件位桶结点数 <= 退化阈值(默认 6)

删除结点、扩容(易忽略)时可能触发退化。

退化阈值肯定不超过树化阈值,那么阈值的大小有什么影响?

  1. 过大:与树化阈值接近,反复的树化和退化影响性能。
  2. 过小:结点数少的时候,红黑树性能反而不高,应当尽早退化。

1.2.2、为什么是红黑树

为何采用红黑树,而不是其它树。

  1. 二叉排序树(BST):可能产生线性结构。

    1. 在极端情况下,添加的元素都比根节点大或者小,导致一侧子树线性增长
    2. 此时的结构无异于链表,时间复杂度是 O(n)
  2. 自平衡二叉查找树(AVL):与红黑树都是平衡二叉搜索树,区别在于增删结点时的旋转操作。

    AVL 树 红黑树
    说明 严格平衡,平均查询效率更快,但需要更多旋转次数 需要最多 2 次旋转
    查找时间复杂度 O(logn) O(logn)
    旋转时间复杂度 O(logn) O(1)
    适用场景 查找密集型任务 增删密集型任务

2、成员结构

2.1、位桶 (bucket)

位桶(bucket):哈希表的基本存储单元。

  1. Java 1.7 之前称为 Entry<K, V>,采用头插法
  2. Java 1.8 之后称为 Node<K, V>,采用尾插法

2.1.1、源码

HashMap 的静态内部类

成员变量

  1. 哈希值

  2. 结点的键(关键码)

  3. 结点的值

  4. 后继结点

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        
        // 构造方法
        // getKey()、getValue()
        // toString()、hashCode()、equals()
        // setValue()
    }
    

2.1.2、TODO

  1. HashMap 1.7 并发扩容
  2. 红黑树结点

2.2、成员变量

HashMap 的所有成员变量均为 default

外部只能通过特定接口进行操作(封装)。

概念区分

容量 vs 大小

Hint:结点数量 = 键值对数量

  1. 容量(capacity):哈希表的数组长度最多可容纳的结点数量)。
    1. 默认容量 1<<416),根据泊松分布得出。
    2. 为了保证 HashMap 的性能,容量始终是 2 的幂
  2. 大小(size)实际存储的结点数量。
    1. 哈希表大小:哈希表中的结点数量。
    2. 位桶大小:链表(或红黑树)中的结点数量。

扩容 vs 树化

  1. 含义
    1. 扩容:扩充哈希表的数组长度,从而能存储更多链表。
    2. 树化:针对现有的链表,在符合条件的情况下转换为红黑树。
  2. 当哈希表存储的结点数增加时,如何从扩容、树化中抉择呢?
    1. Java 为了避免二者的冲突,要求最小树化容量 > 4 * 树化阈值
    2. 优先选择扩容,使链表长度降低

2.2.1、重要参数(❗)

含义 说明 默认值
table 哈希表 Node<K, V> 类型的数组 -
DEFAULT_INITIAL_CAPACITY 默认初始容量 使用无参构造方法时,数组默认的长度 1<<4
(i.e. 16
size - 哈希表结点数 -
threshold 扩容阈值 size > 扩容阈值时,哈希表会扩容并重新映射
扩容阈值 = 哈希表容量 * 加载因子
12
loadFactor 加载因子 哈希表扩容之前允许的最大存储度量,需要在时间和空间成本之间折衷(过大导致链表太长,过小导致频繁扩容) 0.75f
TREEIFY_THRESHOLD 树化阈值 树化的条件之一:位桶结点数 >= 树化阈值
(参考 putVal() 源码)
8
UNTREEIFY_THRESHOLD 退化阈值 退化条件:位桶结点数 < 退化阈值 6
MIN_TREEIFY_CAPACITY 最小树化容量 树化的条件之一:数组长度 >= 最小树化容量
(参考 treeifyBin() 源码)
64

2.2.2、其它参数

含义 说明
MAXIMUM_CAPACITY 最大容量 1<<30
modCount 修改数 记录哈希表的修改次数,用于多线程下的 fail-fast 机制
entrySet 包含所有 K-V 的 Set 集合
keySet 包含所有 K 的 Set 集合 继承自 AbstractMap 类
values 包含所有 V 的 Collection 集合 继承自 AbstractMap 类

2.3、构造方法

HashMap 有 4 个构造方法,可指定不同参数。

哈希表(table)在构造方法中尚未初始化,首次添加元素的时候才初始化

① 初始容量、加载因子

指定期望容量、加载因子

  1. 初始容量:必须大于 0,小于最大容量(2 ^ 30)。

    1. 此处的 initialCapacity 仅代表期望容量
    2. 不一定会用作哈希表数组的初始长度。
  2. 加载因子:必须是大于 0 的合法浮点数。

  3. 扩容阈值:tableSizeFor()

    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;
        this.threshold = tableSizeFor(initialCapacity);
    }
    

② 初始容量

指定初始容量

调用首个构造方法,默认加载因子 0.75f

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

③ 无参

默认加载因子 0.75f

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

④ Map

传入一个 Map 集合

  1. 指定默认加载因子 0.75f

  2. 调用 putMapEntries(...),批量添加元素(深拷贝)。

    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
    

putMapEntries(...)

提供给 HashMap 的构造方法putAll(...) 使用。

参数

  1. m:Map 集合
  2. evict:false 表示初始化时调用。
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        // 当前哈希表为空 -> 初始化扩容阈值
        if (table == null) { // pre-size
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        // 当前哈希表非空 -> 扩容
        else if (s > threshold)	// 此构造方法没有赋扩容阈值,此时 threshold == 0
            resize();
        // 深拷贝
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}

3、确定存储位置

3.1、hash() 扰动函数

Hint:对象的 hashcode 不会直接用于哈希表。

  • 分析
    1. 哈希表容量(数组长度)是有限的。
    2. 对象的 hashCode() 通常超出了哈希表长度范围。
  • 对策:扰动函数 hash()
    • HashMap 定义的静态方法。
    • 用于进一步处理 hashCode(),得到合适的哈希值

Java 1.7

(扰动函数)4 次位运算 + 5 次异或运算

作用:防止低位不变、高位变化时,造成的哈希冲突。

static final int hash(Object k) {
   int h = 0;
   h ^= k.hashCode(); 
   h ^= (h >>> 20) ^ (h >>> 12);
   return h ^ (h >>> 7) ^ (h >>> 4);
}

Java 1.8

(优化的扰动函数)1 次位运算 + 1 次异或运算

  1. 所有 bit 都能参与运算。
  2. 减少处理次数,提高性能。

做法高 16 位与低 16 位进行异或运算

  1. 将对象的 hashCode 右移 16 位,丢弃低 16 位(此时高 16 位全为 0)

  2. 将位运算结果(高 16 位)与对象 hashCode 做异或运算。

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    

3.2、indexfor() 确定下标

indexfor():计算元素在哈希表中的存储位置(数组下标)。

Java 1.7 单独定义了该方法,

Java 1.8+ 将代码逻辑移到 put() 中。

static int indexFor(int h, int length) {
    // 二进制与运算
    return h & (length-1);	// 等价于 h % length(十进制取余运算)
}

Hint:对于计算机,二进制运算速度比十进制快。

分析:为什么 h & (length-1) 等价于 h % length

  • 哈希表容量(length)始终是 2 的幂(二进制的低位有多个 0)。

  • h 对 length 的取余结果 <= length - 1,即无法整除的部分

  • h 和 length - 1 的与运算,代表 h 低位无法整除 length 的部分,即余数。

    image

4、添加元素 put()

Hint

  1. 哈希表(table)在构造方法中尚未初始化,首次添加元素的时候才初始化
  2. HashMap 没有专门提供修改元素的方法,可以通过 put() 覆盖旧值

思维导图

源码共分为四个阶段。

Hint:若方法返回值非 null,说明哈希表中已存在相同 key 的结点。

  1. 初始扩容:判断哈希表为空或长度为 0,进行扩容。

  2. 存储位置:计算元素的存储位置(数组下标)。

    1. 位置为空:直接赋值进入第四阶段
    2. 位置非空:发生了哈希冲突,进入第三阶段
  3. 拉链法

    1. 判断首个结点与待插入元素的 hash 和 key 是否相等,是则跳到 4
    2. 判断首个结点是否是红黑树,是则调用 putTreeVal() 添加红黑树结点(之后进入第四阶段)。
    3. 双指针遍历链表
      1. 判断是否到表尾,是则尾插入、判断树化条件(之后进入第四阶段)。
      2. 判断当前结点与待插入元素的的 hash 和 key 是否相等,是则跳到 4
    4. 存在结点与待插入元素的 hash 和 key 相等,覆盖 value 值并返回旧值(方法结束)。
  4. 扩容:判断哈希表结点数超过扩容阈值,进行扩容。

    image

put()

调用 hash() 扰动哈希值,

调用 putVal() 添加元素。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

putVal()

/*
	hash		 -	扰动后的哈希值,用于计算存储位置(数组下标)
	key			 -	元素的键
	value		 -	元素的值
	onlyIfAbsent -	当哈希表中对应下标位置为空才添加
	evict		 -	false 表示初始化时调用。
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
	// -----------------------(一)初始扩容-----------------------
    /*
    	tab:当前哈希表
    	p:数组下标为 i 的元素
    	n:哈希表结点数
    	i:元素存储位置(数组下标),即 indexfor() 计算结果
    */
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 判断哈希表为空或长度为0 -> 扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // -----------------------(二)存储位置-----------------------
    // 计算存储位置,即 indexfor()
    // 判断存储位置为空 -> 直接赋值
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // -----------------------(三)拉链法-----------------------
    else {	// 位置不为空(哈希冲突)
        /*
        	e:若非空,表示当前链表(或红黑树中)与待添加元素的key相同的结点
        	k:指针e的key
        */
        Node<K,V> e; K k;
        // 链表首个结点与待插入元素的hash和key相同 -> 确定e
        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 {	// 双指针遍历链表(p和e)
            /*
            	binCount:遍历次数,最多不超过链表长度
            	p:最初指向数组下标i
            	e:表示当前指向的结点,最初指向 p.next
            */
            for (int binCount = 0; ; ++binCount) {
                // 遍历到表尾 -> 插入新结点(Java 1.8 尾插法)
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // binCount + 1 表示添加新结点后的链表长度(包括数组下标 i 的元素)
                    // 判断当前链表长度超过树化阈值 -> 树化
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 当前结点的hash和key与待插入结点相同 -> 确定 e,结束遍历
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                // 更新指针
                p = e;
            }
        }
        // 说明链表中存在相同的key -> 覆盖value值
        if (e != null) { // existing mapping for key
            /*
            	e.value:旧value值
            	value:新value值
            */
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);	// 在HashMap中是空实现
            // 返回旧值(方法结束)
            return oldValue;
        }
    }
    // 修改数+1
    ++modCount;
    // -----------------------(四)扩容-----------------------
    // 判断哈希表结点数超过扩容阈值 -> 扩容
    if (++size > threshold)
        resize();
    
    afterNodeInsertion(evict);	// 在HashMap中是空实现
    return null;
}

HashMap 中的模板方法模式

HashMap 中定义并提供空实现,LinkedHashMap 子类重写了具体逻辑。

  1. afterNodeAccess:将被访问到的结点移动到链表尾部(LRU 算法)

  2. afterNodeInsertion:链表长度超过容量时,移除链表头个元素(LRU 算法)

  3. afterNodeRemoval:用于移除双向链表中的结点。

    // Callbacks to allow LinkedHashMap post-actions
    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }
    

putIfAbsent()

(Java 1.8+)底层调用 putVal() 方法,

指定 onlyIfAbsent = true

@Override
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}

5、扩容 resize()

扩容:哈希表的数组长度和扩容阈值变为原来的 2 倍,并再次散列。

  1. 版本区别
    1. Java 1.7-:扩容后,链表结点顺序倒置。(HashMap 1.7 并发扩容
    2. Java 1.8+:会保持原有顺序。
  2. 触发场景
    1. 初始化:哈希表为空或长度为 0,进行初始化。
    2. 扩容:哈希表的结点数超过扩容阈值,扩容 2 倍。

思路

源码主要分为两个阶段。

注意:扩容指的是数组长度,

  1. 确定新容量和扩容阈值
    1. 判断数组长度是否已达上限,是则不再扩容(方法结束)。
    2. 判断数组已初始化:确定新容量和扩容阈值为原来的 2 倍跳到 5)。
    3. 数组未初始化:判断扩容阈值是否已赋值,从而决定初始化时的容量和扩容阈值。
      • 大于 0:说明调用的是有参构造方法(指定了期望容量和加载因子),将当前扩容阈值(oldThr)作为初始容量(跳到 4)。
      • 小于等于 0:说明调用的是空参构造方法使用默认初始容量 16 和默认扩容阈值跳到 5)。
    4. 针对调用有参构造方法的情况,基于实际初始容量计算扩容阈值(newThr)。
    5. 更新扩容阈值。
  2. 创建新数组,重新散列
    1. 若原哈希表数组尚未初始化,则直接返回初始化完成的新数组(方法结束)。
    2. 遍历原数组的每个位置(空/单个结点/链表/红黑树)
      1. 用指针保存当前遍历到的数组位置,将其空间释放。
      2. 若是单个结点,则直接计算新存储位置并保存到新数组
      3. 若是红黑树,则分割树节点并插入新数组。
      4. 若是链表,分成两段(称为 low 区、high 区),分别插入到新数组的下标 i偏移量 oldCap + i

resize()

final Node<K,V>[] resize() {
    /*
    	oldTab:当前哈希表
    	oldCap:当前数组长度
    	oldThr:当前扩容阈值
    	
    	newCap:新数组长度
    	newThr:新扩容阈值
    */
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // -----------------------(一)确定新容量和扩容阈值-----------------------
    // ----------数组已初始化-----------
    if (oldCap > 0) {
        // 数组长度已达最大容量 -> 不再扩容,返回当前哈希表
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 扩容:新容量2倍,扩容阈值2倍
        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;
    // 空参构造方法进入else块
    else {               // zero initial threshold signifies using defaults
        // 初始容量16,扩容阈值12
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 数组未初始化,且扩容阈值大于0(即上面的else if块的情况)
    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 = newTab;
    if (oldTab != null) {
        // 遍历原数组的每个位置(空/单个结点/链表/红黑树)
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;	// 指向数组中每个链表(红黑树)的首个结点
            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
                    /*
                    	扩容后的数组长度是原来 2 倍,而结点存储位置 = hash % 数组长度
                    	因此,针对原哈希表的所有链表(或红黑树)上的结点,新存储位置有两种散列情况:
                    	1. 相同位置:称为低位区(low)
                    	2. 扩充位置:称为高位区(high)
                    	
                    	loHead和loTail分别代表low区的头尾指针,lhiHead和hiTail分别代表high区的头尾指针,
                    */
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    // 指向原链表当前遍历的结点
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // oldCap 二进制始终是【... 1000】
                        if ((e.hash & oldCap) == 0) {	// 说明 hash取余结果 < oldCap,放在低位
                            // 判断 low 区没有元素 - > 直接插入
                            if (loTail == null)
                                loHead = e;
                            // 判断 low 区已形成链表 -> 尾插到loTail
                            else
                                loTail.next = e;
                            // 更新low区尾指针
                            loTail = e;
                        }
                        else {	// 放在高位,逻辑同上
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 将low区放到新数组的相同位置
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 将high区放到新数组的扩充位置
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

关于 low 和 high

扩容后的数组长度是原来 2 倍,相当于多出一段与原来相等长度的存储空间。

针对旧哈希表的同一条链表,会被拆分成两条链表,分别移到新哈希表的两段等长空间。

  • Java 1.7-:重新计算每个结点在新哈希表中的位置(i = hash & (newCap - 1)
  • Java 1.8+:判断每个结点是否满足 (hash & oldCap) == 0,从而对应两段等长空间。

以下分析 Java 1.8 算法的原理。

示例

以 oldCap = 16(0010 0000)为例,

其不同二进制值如下。

n n - 1
old 0010 0000(旧容量) 0001 1111(旧容量 - 1)
new 0100 0000(新容量) 0011 1111(新容量 - 1)

以实际哈希值 h 为例,观察二进制与运算的结果。

  1. 示例一:225(1001 0101)

    1001 0101(示例 1) 与运算结果 说明
    0001 1111(oldCap - 1) 0001 0101 旧下标
    0011 1111(newCap - 1) 0001 0101 新下标 = 旧下标
    0010 0000(oldCap) 0000 0000 低位是 0,最高位和 newCap - 1 最高位相同
  2. 示例二:173(1001 0101)

    1010 1101(示例 2) 与运算结果 说明
    0001 1111(oldCap - 1) 0000 1101 旧下标
    0011 1111(newCap - 1) 0010 1101 新下标 = 旧下标 + 偏移量
    0010 0000(oldCap) 0010 0000 低位是 0,最高位和 newCap - 1 结果最高位相同

分析

分析结果

  1. h & oldCap - 1:旧下标。
  2. h & newCap - 1新下标 = 旧下标 (+ 偏移量)
    • 是否添加偏移量,取决于 newCap - 1 的最高位取值。
    • 低位信息代表偏移量,可以由后续统一添加。
  3. h & oldCap:最高位是 0 或 1,低位始终是 0。

结论:新下标的结果取决于 newCap - 1 最高位(恰好也是 oldCap 最高位)。

  1. Java 1.7-:每个结点都计算偏移量。
  2. Java 1.8+先判断是否需要偏移,再为偏移部分统一添加偏移量

6、查询 get()

根据 key 获取 value,

若 key 不存在则返回 null。

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

getNode()

  1. 计算存储位置(数组下标),即 indexfor() 算法逻辑。

  2. 判断下标为 indexfor() 的元素是否为待查找元素,是则返回。

  3. 判断是红黑树结点,调用 getTreeNode() 获取并返回红黑树结点。

  4. 双指针遍历链表,直到找到待查询元素(若无,则返回 null)。

    final Node<K,V> getNode(int hash, Object key) {
        /*
        	tab:当前哈希表
        	first:根据hash计算的数组下标位置的元素(链表的首个结点)
        	e:双指针算法
        	n:哈希表结点数
        	i:元素存储位置(数组下标),即 indexfor() 计算结果
        */
        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) {
            // 链表首个结点恰好是待查找结点 -> 返回
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 进入链表/红黑树寻找
            if ((e = first.next) != null) {
                // 判断是红黑树结点 -> 调用getTreeNode()获取
                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;
    }
    

getOrDefault()

(Java 1.8+)调用 getNode() 并指定默认值,

如果找不到目标 key 则返回默认值。

public V getOrDefault(Object key, V defaultValue) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}

7、删除 remove()

HashMap 有两个 remove(),分别来自重写、重载。

  1. 重写:删除 key 对应的结点,且要求 key 的 value 值与指定 value 值相等才删除。

  2. 重载:删除 key 对应的结点。

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

参数

  1. matchValue:value 值与指定 value 值相等才删除。
  2. movable:移除红黑树结点时允许移动其它结点。
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
        /*
    	tab:当前哈希表
    	p:下标为index的数组元素
    	e:双指针算法
    	n:哈希表结点数
    	index:元素存储位置(数组下标),即 indexfor() 计算结果
    */
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        /*
        node:存储待删除结点
        e:双指针算法
        k/v:当前指向结点的键/值
        */
        Node<K,V> node = null, e; K k; V v;
         // 链表首个结点恰好是待删除结点 -> 确定node
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            // 判断红黑树结点 -> 遍历红黑树找到node
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            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);
            }
        }
        // 找到待删除结点、满足删除value的条件
        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;
            // 删除表中结点(此时p指向node的前一个结点)
            else
                p.next = node.next;
            
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

8、迭代器

  1. HashIterator
    • KeyIterator
    • ValueIterator
    • EntryIterator
  2. KeySet、Values、EntrySet 的 Iterator(Collection 接口定义)

9、对比 Hashtable

Hashtable HashMap
线程安全
K/V 为 null 不允许 允许一个 key、多个 value
容量 默认 16,始终为 2 的幂 默认 11
哈希值 对象 hashCode() 对象 hashCode() + 扰动函数 hash()
存储位置 取余 %(十进制) &(二进制)
扩容 2 倍 + 1 2 倍

参考

posted @ 2022-09-29 15:37  Jaywee  阅读(9)  评论(0编辑  收藏  举报

👇