Java HashMap

98 阅读7分钟

Java HashMap

  • HashMap 是基于 Hash Table 哈希表(又叫散列表)实现的 key-value 键值对存储结构,实现了 java.util.Map 映射接口
  • 键唯一,值可重复,允许使用 null 键和 null 值
  • 由于基于哈希表实现(底层实现基于哈希表,通过计算 key 键的 hashcode 哈希值来确定键值对的存储位置,即利用哈希函数将 key 键映射到数组的某个位置),具有高效的增删查操作特性,查找、插入和删除操作的时间复杂度均为 O(1),空间复杂度为 O(n),其中 n 为键值对的数量
  • 内部结构:数组 + 链表 + 红黑树,使用数组(称为 bucket 桶,用于存放 Entry 对象形式的键值对,数组的长度即为容量)存储数据,当发生哈希冲突(哈希碰撞)时,冲突的元素以链表形式存储(将相同哈希值的元素连起来,解决哈希冲突,jdk 1.8 及以后引入红黑树优化性能,长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是比较快的),若链表长度 >= 8 且数组容量 >= 64,则链表会转换为红黑树(树化)以提高查询效率,当红黑树节点数 <= 6 时转换回(退化)链表结构
  • 无序性:不保证元素的插入顺序,也不保证顺序恒定不变,需有序可改用 LinkedHashMap(默认按插入顺序排列,可选按访问顺序排列)
  • 非线程安全:可以改用 ConcurrentHashMap 或 SynchronizedMap 保证线程安全(虽然 Hashtable 也是线程安全的,但已被弃用,不推荐使用)

操作元素

  • 使用 put 方法插入键值对:计算哈希值 -> 定位桶 -> 处理冲突(链表/红黑树) -> 插入或更新
  • 使用 get 方法根据键获取对应的值:通过哈希值快速定位桶,遍历链表或红黑树查找键
  • 使用 remove 方法根据键删除对应的键值对:类似查找操作,移除目标节点并调整链表/红黑树结构
  • 使用 entrySet 遍历键值对

初始容量和加载因子(装载因子、装载系数)

  • 可以根据实际需求情况通过构造函数传入指定合理的初始容量(注意这个容量通常是 2 的次幂,扩容方式也是 2 的次幂)和加载因子以优化性能
  • Initial Capacity 初始容量:指的是哈希表在创建时的容量(桶的数量),DEFAULT_INITIAL_CAPACITY 长度默认 16
  • Load Factor 加载因子:指的是哈希表在其容量自动增加之前允许能够填充达到多满的程度,DEFAULT_LOAD_FACTOR 阈值默认 0.75f(即默认情况下当元素数量达到 12 时触发扩容)
  • 扩容机制:当 HashMap 中元素数量超过 容量 × 加载因子 时则会触发扩容(容量翻倍,扩容并不是在原数组基础上扩大容量,而是需要申请一个长度为原来 2 倍的新数组,重新 Rehash 散列分布计算所有元素位置并迁移至新数组)

链表和红黑树

  • 链表:链表结构简单,插入和删除操作效率较高,时间复杂度为 O(1),不过查找操作需要从头节点开始逐个遍历,效率较低,时间复杂度为 O(n),其中 n 是链表的长度,不过当链表长度较短时,查找操作的性能损耗并不显著
  • 红黑树:红黑树是一种自平衡的二叉查找树(红黑树的节点需要额外存储父节点、左右子节点和颜色标识信息,内存空间占用是普通链表节点的 2 倍),查找、插入和删除操作的时间复杂度均为 O(log n),其中 n 是树的节点数
  • 过早转换:如果链表长度较短,过早使用红黑树反而会增加不必要的内存空间开销(避免红黑树的复杂结构带来的额外开销)
  • 过晚转换:当链表长度较长时,时间复杂度 O(n) 的链表查找操作性能显著下降,而时间复杂度 O(log n) 的红黑树的优势开始显现,所以为了提高查找效率,才需要将其及时转换为红黑树结构
  • 综合考量:当链表长度较短时,两者的性能差异并不显著,链表的简单结构和较低的内存开销反而更高效(因为红黑树的节点结构更复杂,维护成本更高,仅当链表足够长时,红黑树带来的时间收益才能弥补平衡其空间成本),当链表长度达到一定长度后,红黑树的性能优势开始显现,因此进行转换,红黑树是为了应对极端的哈希冲突情况(比如用户自定义了比较低效的 hashCode 方法),确保在罕见的极端场景下仍然能保证查询效率

为什么加载因子默认是 0.75

  • 采用 0.75 是为了达到时间和空间的平衡,0.75 是一个折中值(实践验证值),在减少哈希冲突和避免频繁扩容之间找到平衡,既保证较高的空间利用率,又能维持较好的查询性能
  • 空间效率:加载因子越高(比如 0.8),内存空间浪费越少,但冲突概率显著增加,查询效率下降(因为需要处理更多的碰撞,链表或红黑树结构导致复杂化)
  • 时间效率:加载因子越低(比如 0.7),虽然冲突概率减少,但空间利用率低且扩容频率增加

链表转成红黑树的限制为何是 8

  • 在哈希函数设计合理(哈希函数分布均匀)的情况下,发生哈希碰撞 8 次(单个哈希槽中元素数量达到 8,即链表长度达到 8)的概率非常小(约为百万分之六),这意味着链表长度达到 8 的概率极低,也就是说在正常状况下链表几乎不会转化为红黑树(选择 8 作为阈值可确保只有在极端情况下才会触发转换成红黑树),从而避免了红黑树带来的不必要的内存空间开销
  • 如果阈值设置过低,可能会导致频繁的结构转换,增加开销,如果阈值设置过高,则可能无法及时充分利用红黑树的性能优势
  • 总结来说,链表转成红黑树的阈值为 8,是通过综合考虑哈希冲突概率、空间复杂度和极端情况下的性能优化等多方面因素得出的,既防止了极端哈希冲突导致的性能下降问题,又避免了红黑树带来的额外空间开销,为了有效地保证 HashMap 在各种情况下的高效运行
  • 另外除了链表长度达到 8 以外,链表转换为红黑树还需要满足另一个条件,即 HashMap 的总容量(桶数组大小)必须 >= 64,因为扩容(容量翻倍)的开销可能比转换为红黑树要更小,避免在小容量情况下因红黑树的复杂结构而导致的性能下降

红黑树转成链表的限制为何是 6

  • 链表转换红黑树需要消耗性能,若倘若当链表长度在 8 附近波动时(比如 7 -> 8 -> 7),会导致频繁的树化和退化结构转换操作,反而大大的降低了效率,影响稳定性
  • 通过设定阈值的滞后区间(和 8 之间设置 2 的差值 6),避免不必要的频繁转换而而导致的性能抖动,从而减少性能损耗