Map

HashMAP

类继承

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    ...
}
  1. 同 ArrayList 和 LinkedList 一样也实现了 Serializable(支持序列化)、Cloneable(支持克隆)
  2. Map:定义了一些同一的行为
  3. AbstractMap:一些默认实现,避免重复编写功能

类成员

类属性

// 默认容量(数组长度),2^4 = 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

// 最大容量(数组长度),2^30 = 1,073,741,824
static final int MAXIMUM_CAPACITY = 1 << 30; 

// 保存元素的数组(首次使用时才初始化)
transient Node<K,V>[] table;

// 元素数量,每次插入、删除时更新(不是数组长度,应该 <= 数组长度)
transient int size;

// 扩容阈值,当 size >= threshold 数组发生扩容
int threshold;

// 加载因子(用于计算扩容阈值,threshold = 数组长度 * loadFactor)
final float loadFactor;

// 默认的加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f; 

// Entry<K,V> 是每个元素,Set<Map.Entry<K,V>> 保存了所有的元素
transient Set<Map.Entry<K,V>> entrySet;

// 修改次数,同 size 每次插入、删除时更新
transient int modCount;

// 链表和红黑树的转换相关
// 1)链表转红黑树:链表长度 >= TREEIFY_THRESHOLD && 数组长度 >= MIN_TREEIFY_CAPACITY
// 2)红黑树退化为链表:扩容后只要红黑树的节点总数量 <= UNTREEIFY_THRESHOLD 就退化为链表(不管数组长度)
static final int TREEIFY_THRESHOLD = 8; // 链表长度 >= 8
static final int MIN_TREEIFY_CAPACITY = 64; // 数组长度 >= 64
static final int UNTREEIFY_THRESHOLD = 6; // 红黑树节点 <= 6

内部类

1. 数据结构类

内部类 作用
Node<K,V> 基础键值对节点,用于存储链表结构的数据(hash, key, value, next)。
TreeNode<K,V> 红黑树节点(继承自 Node),在桶链表过长时(≥8)转为红黑树优化查询。

2. 视图类(View Collections)

内部类 作用
KeySet 返回所有键的集合(map.keySet()),支持遍历和删除。
Values 返回所有值的集合(map.values()),仅支持遍历。
EntrySet 返回所有键值对的集合(map.entrySet()),支持遍历和修改。

特点

  • 均继承自 AbstractSetAbstractCollection,提供轻量级的数据视图。
  • 操作(如 remove())会直接反映到底层 HashMap

3. 迭代器类(Iterators)

内部类 作用
HashIterator 基础迭代器,提供 nextNode() 方法遍历所有节点(链表或树)。
KeyIterator 键迭代器(继承 HashIterator),用于 keySet().iterator()
ValueIterator 值迭代器(继承 HashIterator),用于 values().iterator()
EntryIterator 键值对迭代器(继承 HashIterator),用于 entrySet().iterator()

快速失败机制
所有迭代器均通过检查 modCount 实现 ConcurrentModificationException


4. 并行迭代器(Spliterators)

内部类 作用
HashMapSpliterator 基础并行分割器,支持分片遍历(用于 Stream 并行操作)。
KeySpliterator 键分割器(继承 HashMapSpliterator),用于 keySet().spliterator()
ValueSpliterator 值分割器(继承 HashMapSpliterator),用于 values().spliterator()
EntrySpliterator 键值对分割器(继承 HashMapSpliterator),用于 entrySet().spliterator()

特点

  • 支持 trySplit() 分割任务,优化多线程并行处理。
  • 用于 Stream API 的并行流(如 map.keySet().parallelStream())。

5. 工具类(Utilities)

内部类 作用
UnsafeHolder (JDK 内部使用)通过 Unsafe 操作实现低级别内存访问优化。

构造方法

无参构造:不会立即创建数组

public HashMap() {
    // 0.75,初始化加载因子为默认的值,其他属性也都是默认值,不会创建数组(添加元素时创建)
    this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

指定容量:不会立即创建数组

会根据传入的容量调整为 >= initialCapacity 的最小 2 的幂

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR); // 会自动计算合适的容量
}

指定容量和加载因子:不会立即创建数组

public HashMap(int initialCapacity, float loadFactor) {
  
    // 保证传入的 initialCapacity 不能超过最大值,也不能小于 0
    ...
    
    this.loadFactor = loadFactor;
  
    // 内部使用 >>>(使用无符号右移运算,目的是算出 >= initialCapacity 的 2 的幂的值出来)
  	// 虽然这里赋值给扩容阈值 threshold,但是后面在扩容时以这个值作为数组的长度(其实这里是计算数组的初始长度),然后根据数组长度和加载因子重新算 threshold
    this.threshold = tableSizeFor(initialCapacity);
}

// 因为 HashMap 的数组是懒加载方式创建的,所以会在扩容时真正初始化
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold; // 构造方法中暂存的初始容量(2的幂)
    int newCap, newThr = 0;

    if (oldCap == 0) { // 首次初始化
        newCap = oldThr; // 使用构造方法计算的容量(2的幂)
        newThr = (int)(newCap * loadFactor); // 计算真正的扩容阈值
    }
    // ...后续扩容逻辑
    threshold = newThr; // 更新阈值
    return new Node[newCap]; // 创建新数组
}

指定一个 map:会创建数组

public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // 默认的加载因子
    putMapEntries(m, false);
}

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        if (table == null) { // 未初始化
            float ft = ((float)s / loadFactor) + 1.0F; // 计算数组所需的最小容量
            int t = (ft > MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : (int)ft;
            if (t > threshold)
                threshold = tableSizeFor(t); // 保证容量是 2 的幂
        }
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            putVal(hash(e.getKey()), e.getKey(), e.getValue(), false, evict);
        }
    }
}

float ft = ((float)s / loadFactor) + 1.0F
如果 s = 6loadFactor = 0.75,则 6 / 0.75 = 8
表示哈希表至少需要容量 8,才能在不扩容的情况下容纳 6 个元素(8 * 0.75 = 6
+1 的原因:添加一个缓冲值,避免浮点计算误差导致容量不足,例如:若 s / loadFactor = 7.999,直接取整会得到 7,但实际需要 8

添加元素

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; // 定义几个变量
  
  	// 如果数组为空或长度是0就扩容(同时 n 和 tab 已经赋值了:n 是 数组长度,tab 是数组)
    if ((tab = table) == null || (n = tab.length) == 0) 
        n = (tab = resize()).length;
  
  	// n-1:是数组最后一个下标(n 是长度)
  	// & 运算:是当超出数组时把元素放在哪一格(拉链法,前提必须数组长度是2的幂才可以使用 & 运算)
  	// 也给 p 赋值了,就是新元素应该插入的位置的元素(可能这里已经有元素了,也可能没有元素)
    if ((p = tab[i = (n - 1) & hash]) == null) // 如果为空,就说明这个位置没有元素,直接插入新元素 
        tab[i] = newNode(hash, key, value, null); 
    else { // 如果数组这个位置已经有元素了
        Node<K,V> e; K k;
      
      	// hash 相同,equals 也相同,覆盖元素(修改元素)
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
      	
      	// 不是修改元素,此时位置也有元素,两种可能:1 当前元素是链表;2:当前元素是红黑树
        else if (p instanceof TreeNode) // 如果是数节点,按照红黑树的方式插入元素
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
          	// 如果是链表,链表的方式插入元素,这个 for 循环就是在遍历链表
            for (int binCount = 0; ; ++binCount) { // binCount 会是链表的索引(索引不是很合适,当前第几个元素)
              	// 是否达到链表尾部(尾插法)
                if ((e = p.next) == null) { // p 是当前下标的元素(此时是链表)
                    p.next = newNode(hash, key, value, null); // 新元素入队(尾插法)
                    if (binCount >= TREEIFY_THRESHOLD - 1) // 当前 binCount 是否 >= 7,加上的新元素就是 8
                        treeifyBin(tab, hash); // 链表元素数量已经达到 8 了,方法里面会再次判断如果数组长度达到 64 就会转为红黑树
                    break;
                }
              
              	// 如果不是链表尾部,检查 e 的 key 和新元素的 key 是否相同
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break; // 如果相同,退出循坏(后续会覆盖值)
                p = e; // 如果不同,继续遍历链表(继续循环)
            }
        }
        if (e != null) { // e 有值,新元素和链表中已存在的元素 key 相同,覆盖值
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount; // 修改次数+1
    if (++size > threshold)  // 是否需要扩容
        resize();
    afterNodeInsertion(evict);
    return null;
}

扩容

final Node<K,V>[] resize() {
    // 1. 保存旧哈希表
    Node<K,V>[] oldTab = table;
    // 旧容量:如果表为空则为0,否则取当前长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 旧扩容阈值
    int oldThr = threshold;
  
    int newCap, newThr = 0; // 新的容量扩容阈值
    
    // -------------------------------
    // 2. 计算新容量和阈值(分4种情况)
    // -------------------------------
    // 情况1:旧容量 > 0(说明已经初始化过)
    if (oldCap > 0) {
        // 如果旧容量已经达到最大值(2^30),不再扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 新容量 = 旧容量 * 2
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 新阈值 = 旧阈值 * 2
            newThr = oldThr << 1;
    }
    // 情况2:旧容量=0但旧阈值>0(发生在带初始容量的构造方法)
    else if (oldThr > 0)
        // 新容量 = 旧阈值(构造方法中暂存的初始容量)
        newCap = oldThr;
    // 情况3:旧容量=0且旧阈值=0(默认无参构造)
    else {
        // 新容量 = 默认初始容量(16)
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 新阈值 = 默认负载因子(0.75)* 默认容量(16)= 12
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    
    // 情况4:如果新阈值未计算(如情况2未设置newThr)
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr; // 更新全局阈值
    
    // -------------------------------
    // 3. 创建新哈希表并迁移数据
    // -------------------------------
    @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; // 清空旧桶(帮助GC)
                // 情况1:桶中只有一个节点(直接重新哈希)
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 情况2:桶中是红黑树(拆分树)
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // 情况3:桶中是链表(拆分为高低位链表,原来的链表最多拆出两个链表,使用高低位异或运算效率更高)
                else {
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 判断节点属于低位还是高位
                        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;
                    }
                    // 高位链表放在新索引位置(原索引 + oldCap)
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

链表转红黑树

有个前提走到这里链表元素已经达到 8 了,这个方法是决定是否要将链表转为红黑树

final void treeifyBin(Node<K,V>[] tab, int hash) {
  	// n: 数组长度,index: 目标桶索引,e: 当前链表节点
    int n, index; Node<K,V> e;
  
  	// 数组为空或数组长度小于 64,不会转为红黑树,直接扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) { // 数组长度 >= 64,转红黑树
        TreeNode<K,V> hd = null, tl = null;
      
      	// 遍历链表,这个循环时把链表的每个节点转成树节点
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null); // 链表的 node 转 树节点
            if (tl == null)  // 维护树节点的链表关系
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab); // 树节点的链表准备好了,真正开始树化。后续逻辑后续再深入吧(红黑树目前不是特别了解~)
    }
}

红黑树退化为链表

  1. java.util.HashMap#remove(java.lang.Object)
  2. java.util.HashMap#removeNode
  3. java.util.HashMap.TreeNode#removeTreeNode
  4. java.util.HashMap.TreeNode#untreeify

总结

  1. 基于哈希表实现(数组+链表/红黑树),如果是链表采用尾插法
  2. 元素无序,不允许重复,键值都可以为 null,只能一个 null 键,线程不安全
  3. 默认初始容量16(必须2的幂),最大容量 2^30,默认加载因子 0.75
  4. 链表转红黑树:数组长度 >= 64 && 链表长度 >= 8,查询从 O(n) 优化为 O(log n)
  5. 红黑树退化为链表:树节点 <= 6(不关心数组长度)
  6. size > thresholdthreshold = capacity * loadFactor)时触发扩容,扩容操作如下:
    • 新容量 = 旧容量 × 2(保持 2 的幂)
    • 重新哈希(高效迁移:单节点直接定位,链表拆分为高低位链表)
  7. 作为 key 的对象需要重写 hashCode 和 equals 方法
  8. 遍历时 entrySet()keySet() + get() 效率更高(减少哈希计算)

其他 MAP

特性 HashMap Hashtable LinkedHashMap TreeMap
线程安全 是(全表锁)
允许 null 键值 仅值可为 null
顺序 无序 无序 插入顺序/访问顺序 按键排序
底层结构 哈希表 哈希表 哈希表 + 双向链表 红黑树
  • HashTable 和 HashMap 的区别基本就是 HashTable 的方法都加了 synchronized 来保证线程安全
  • TreeMap 和 HashMap 区别就是底层结构不一样,TreeMap 纯粹用红黑树实现
    • 既然用红黑树,key 就不能为空
    • 既然用红黑树,就一定是有顺序的(红黑树是近似平衡的二叉搜索树,都是搜索树了一定要有顺序)
    • 既然所有的 key 都要用顺序,key 就不能是 null 了

插入数据时,元素所在的数组下标是根据 hash 值确定的,所以就不会有顺序(数组某个元素是有顺序的,因为红黑树和链表本来就有顺序)
LinkedHashMap 继承自 HashMap,为什么 LinkedHashMap 会有是有顺序的?

因为 LinkedHashMap 扩展了 HashMap 的 Entry

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after; // 双向链表指针
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}
posted @ 2023-05-24 17:06  CyrusHuang  阅读(42)  评论(0)    收藏  举报