HashMap详解笔记

HashMap 详解

一、概述

HashMap 是 Java 集合框架中 java.util 包下的重要实现类,基于哈希表实现,用于存储键值对(Key-Value)映射。它允许 null 键和 null 值,是非线程安全的集合,其迭代器是快速失败(fail-fast)的。

核心特点:

  • 存储键值对,键唯一(通过 equals() 判断)

  • 无序(存储顺序与插入顺序不一致)

  • 非线程安全

  • 可以有null值

  • 查询、插入、删除效率高(平均 O (1) 时间复杂度)

二、数据结构

1. 整体结构

HashMap 的底层数据结构在 JDK 1.7 和 JDK 1.8 中有差异:

  • JDK 1.7:数组 + 链表(拉链法解决哈希冲突)

  • JDK 1.8:数组 + 链表 + 红黑树(当链表长度超过阈值时转为红黑树,优化查询效率)

2. 各部分作用

  • 数组(哈希表):也称为桶(Bucket),是 HashMap 的主体,默认初始容量为 16,元素类型为 Node<K,V>(JDK 1.8 中链表节点)或 TreeNode<K,V>(红黑树节点)。

  • 链表:解决哈希冲突(不同 key 计算出相同哈希值时,将元素以链表形式存储在同一桶中)。

  • 红黑树:当链表长度超过 8 且数组容量 ≥ 64 时,链表会转为红黑树(平衡二叉树),将查询时间复杂度从 O (n) 优化为 O (log n)。

三、核心参数

参数作用默认值
initialCapacity初始容量(数组长度),必须是 2 的幂次16
loadFactor负载因子(扩容阈值因子)0.75f
threshold扩容阈值(capacity * loadFactor),当元素数量超过此值时触发扩容12(16*0.75)
size实际存储的键值对数量0
DEFAULT_INITIAL_CAPACITY默认初始容量16
MAXIMUM_CAPACITY最大容量(2^30)1073741824
TREEIFY_THRESHOLD链表转红黑树的阈值8
UNTREEIFY_THRESHOLD红黑树转链表的阈值(删除元素时触发)6
MIN_TREEIFY_CAPACITY链表转红黑树的最小数组容量(避免频繁扩容)64

四、常用方法详解

1. put(K key, V value)

作用:向 HashMap 中添加键值对,若 key 已存在则覆盖旧值。

流程

  1. 计算 key 的哈希值(通过hash(key)方法:

    static final int hash(Object key) {
        int h;
        // 对 key 的 hashCode 进行高位异或低位,减少哈希冲突
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
  2. 根据哈希值计算数组索引(桶位置):index = (n - 1) & hash(n 为数组长度,利用位运算替代取模,效率更高)。

  3. 检查桶位置:

    • 若桶为空,直接创建新节点放入。

    • 若桶不为空:

      • 若头节点 key 与插入 key 相同(equals() 判断),覆盖旧值。

      • 若头节点是红黑树节点,调用红黑树的插入方法。

      • 若头节点是链表节点,遍历链表:

        • 找到相同 key 则覆盖旧值。

        • 未找到则在链表尾部插入新节点,插入后若链表长度 ≥ 8,且数组容量 ≥ 64,则将链表转为红黑树。

  4. 插入后若 size > threshold,触发扩容(resize())。

2. get(Object key)

作用:根据 key 获取对应 value,若 key 不存在则返回 null。

流程

  1. 计算 key 的哈希值(同 put 方法)。

  2. 根据哈希值计算数组索引,定位到目标桶。

  3. 检查桶中元素:

    • 若桶为空,返回 null。

    • 若桶中是红黑树,调用红黑树的查找方法。

    • 若桶中是链表,遍历链表,通过 equals() 匹配 key,找到则返回对应 value,否则返回 null。

3. containsKey(Object key)

作用:判断 HashMap 中是否包含指定 key。

流程:与 get 方法类似,区别在于找到 key 后返回 true,未找到则返回 false(不关心 value)。

4. getOrDefault(Object key, V defaultValue)

作用:根据指定的key获取对应的value,若key不存在则返回预设的默认值。

流程:调用get(key)获取value,判断value以及key的存在性,使用containKey是因为get(key)返回null有可能是key不存在也可能是key存在但是value为null,获取到value的话存到变量v中,不存在则返回defaultValue。

        V v;
        return (((v = get(key)) != null) || containsKey(key))
            ? v
            : defaultValue;

5. 其他常用方法

  • remove(Object key):删除指定 key 对应的键值对,返回被删除的 value(若 key 不存在返回 null)。

  • size():返回键值对数量。

  • isEmpty():判断是否为空(size == 0 时返回 true)。

  • clear():清空所有键值对。

  • keySet():返回所有 key 的集合(Set<K>)。

  • values():返回所有 value 的集合(Collection<V>)。

五、扩容机制(resize()

1. 触发条件

当 HashMap 中键值对数量 size > thresholdcapacity * loadFactor)时,触发扩容。

2. 扩容流程

  1. 计算新容量:新容量 = 旧容量 * 2(保证容量始终是 2 的幂次)。

  2. 计算新阈值:新阈值 = 新容量 * loadFactor。

  3. 创建新数组(容量为新容量)。

  4. 将旧数组中的元素迁移到新数组:

    • JDK 1.7:重新计算所有元素的哈希值和索引,再迁移(效率低)。

    • JDK 1.8:优化迁移逻辑:

      • 对于链表节点:通过

        (hash & oldCap) == 0

        判断索引是否变化(无需重新计算哈希):

        • 若为 0,索引不变,仍在原位置。

        • 若为 1,新索引 = 旧索引 + 旧容量。

      • 对于红黑树节点:拆分红黑树为两个链表(或红黑树),再迁移。

  5. 替换旧数组为新数组,更新容量和阈值。

六、常见面试题

1. HashMap 与 Hashtable 的区别?

对比项HashMapHashtable
线程安全非线程安全线程安全(方法加 synchronized
null 键值允许 null 键和 null 值不允许 null 键和 null 值
初始容量16,扩容为 2 倍11,扩容为 2 倍 + 1
效率高(无同步开销)低(同步开销大)
父类继承 AbstractMap继承 Dictionary

2. JDK 1.7 与 JDK 1.8 中 HashMap 的区别?

对比项JDK 1.7JDK 1.8
数据结构数组 + 链表数组 + 链表 + 红黑树
链表插入方式头插法(可能导致扩容时链表环化)尾插法(避免环化)
扩容时哈希计算重新计算哈希值利用 (hash & oldCap) 判断索引
红黑树优化有(链表转红黑树提升查询效率)
冲突解决拉链法拉链法 + 红黑树

3. 为什么 HashMap 的初始容量是 16?

  • 16 是 2 的幂次,通过 (n - 1) & hash 计算索引时,n - 1 的二进制全为 1(如 15 是 1111),能保证哈希值的低位全参与运算,减少哈希冲突。

  • 16 是平衡初始空间和扩容频率的折中值(太小易频繁扩容,太大浪费空间)。

4. 为什么负载因子是 0.75?

  • 负载因子是「空间」与「时间」的平衡:

    • 太小(如 0.5):哈希冲突少,但扩容频繁,浪费空间。

    • 太大(如 1.0):空间利用率高,但哈希冲突多,查询效率低。

  • 0.75 是基于概率统计的最优值(哈希冲突概率较低,且空间利用率合理)。

5. HashMap 为什么线程不安全?

  • JDK 1.7 中,扩容时链表头插法可能导致链表环化,引发死循环。

  • 多线程并发修改时,可能出现数据覆盖(如同时 put 相同 key 导致值丢失)。

  • 解决方案:使用 ConcurrentHashMap(线程安全,效率高于 Hashtable)。

6. 哈希冲突的解决方法有哪些?

  • 拉链法(HashMap 采用):将哈希值相同的元素以链表 / 红黑树形式存储在同一桶中。

  • 开放地址法:当哈希冲突时,通过线性探测、二次探测等方式寻找下一个空桶。

  • 再哈希法:使用多个哈希函数,若第一个函数冲突则使用第二个。

7. 红黑树的转换条件是什么?

  • 链表转红黑树:当链表长度 ≥ TREEIFY_THRESHOLD(8),且数组容量 ≥ MIN_TREEIFY_CAPACITY(64)。

  • 红黑树转链表:当红黑树节点数量 ≤ UNTREEIFY_THRESHOLD(6)。

8. HashMap 的 key 可以是自定义对象吗?需要注意什么?

  • 可以,但必须重写hashCode()和equals()

    方法:

    • hashCode():保证相同对象返回相同哈希值,不同对象尽量返回不同哈希值(减少冲突)。

    • equals():保证相同对象返回 true(需满足自反性、对称性、传递性)。

  • 注意:key 应尽量不可变(如 String、Integer),否则修改后可能导致无法找到对应 value。

七、总结

HashMap 是 Java 中最常用的键值对存储工具,其核心是通过哈希表实现高效的增删改查。JDK 1.8 引入红黑树优化长链表查询,使性能更稳定。理解其数据结构、扩容机制和线程安全性,对日常开发和面试都至关重要。在并发场景中,应优先使用 ConcurrentHashMap 替代 HashMap

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值