Java Map 集合深度笔记(理论篇)

Map 是 Java 集合框架中双列集合 的核心接口,隶属于 java.util 包,与 Collection 接口(单列集合)并列,专门用于存储 "键(Key)- 值(Value)" 映射关系。相较于单列集合,Map 更侧重 "通过键快速查找值" 的场景,其设计思想和底层原理是 Java 集合体系的核心知识点。

一、Map 接口的核心设计原则

1. 键值对的映射规则

  • 一对一映射 :一个键(Key)只能关联一个值(Value),向 Map 中插入相同键的新值时,会覆盖原有值,并返回被覆盖的旧值(put 方法的返回值特性)。
  • 键的唯一性 :Map 不允许重复的键,"重复" 的判定依赖键对象的 hashCode()equals() 方法 ------ 两个键对象若 equals() 返回 true,且 hashCode() 返回值相同,则视为同一个键。
  • 值的可重复性:值无需满足唯一性,多个不同的键可以映射到同一个值。

2. 空值处理规则

  • HashMapLinkedHashMap:允许键为 null(仅允许一个),允许值为 null(多个);
  • HashtableConcurrentHashMap:不允许键或值为 null(避免空指针,适配多线程场景);
  • TreeMap:键不能为 null(依赖比较器 / 自然排序,null 无法参与比较),值可以为 null

二、Map 核心实现类的底层原理

1. HashMap(JDK 8 及以上)

核心结构
  • 底层为 "数组 + 链表 + 红黑树":
    • 数组(Node [] table):存储键值对的哈希桶,默认初始容量 16(2^4),扩容因子 0.75,扩容后容量为原 2 倍(保证哈希计算的均匀性);
    • 链表:当多个键的哈希值冲突(映射到同一数组下标)时,先以链表形式存储,链表长度阈值为 8;
    • 红黑树:当链表长度 ≥ 8 且数组容量 ≥ 64 时,链表转为红黑树(降低查找时间复杂度:从 O (n) 到 O (logn));若后续删除元素导致树节点数 ≤ 6,红黑树转回链表。
哈希计算与索引定位
  1. 计算键的 hashCode()
  2. 对哈希值进行扰动处理:hash = key.hashCode() ^ (key.hashCode() >>> 16)(高位参与运算,减少哈希冲突);
  3. 计算数组下标:index = hash & (table.length - 1)(等价于取模,效率更高,前提是数组容量为 2 的幂)。
线程安全性
  • 非线程安全:多线程下扩容(resize())可能导致链表成环,插入 / 删除操作可能出现数据丢失;
  • 解决方式:使用 ConcurrentHashMap(JDK 8 后基于 CAS + synchronized 实现,性能优于 Hashtable)。

2. LinkedHashMap

核心特性
  • 继承自 HashMap,底层额外维护一个双向链表,记录键值对的插入顺序 / 访问顺序;
  • 有序性:默认按 "插入顺序" 遍历,也可通过构造器指定 accessOrder = true 实现 "访问顺序"(每次 get/put 访问的元素会移到链表尾部,适用于实现 LRU 缓存)。
底层实现
  • 重写 HashMapNode 节点,新增 beforeafter 指针,用于构建双向链表;
  • 重写 newNode()afterNodeAccess()afterNodeInsertion() 等方法,维护双向链表的节点顺序。

3. TreeMap

核心结构
  • 底层为红黑树(自平衡的二叉查找树),无哈希表结构,因此无需考虑哈希冲突;
  • 有序性:按键的 "自然顺序"(如 Integer 升序、String 字典序)或自定义 Comparator 排序,遍历结果始终有序。
键的要求
  • 键必须实现 Comparable 接口(自然排序),或创建 TreeMap 时指定 Comparator(自定义排序);
  • 若键未实现 Comparable 且无自定义比较器,调用 put 时会抛出 ClassCastException
核心方法
  • ceilingKey(K key):返回大于等于指定键的最小键;
  • floorKey(K key):返回小于等于指定键的最大键;
  • subMap(K fromKey, K toKey):获取键在指定范围的子 Map(视图,原 Map 变更会同步)。

4. Hashtable(古老实现,几乎淘汰)

  • 底层为 "数组 + 链表"(无红黑树优化);
  • 线程安全:所有方法加 synchronized 锁(锁整个对象,并发效率低);
  • 容量:默认初始容量 11(非 2 的幂),扩容因子 0.75,扩容后容量 = 原容量 × 2 + 1;
  • 已被 ConcurrentHashMap 替代,仅兼容旧代码场景。

5. ConcurrentHashMap

JDK 8 核心实现
  • 底层为 "数组 + 链表 + 红黑树",与 HashMap 结构类似;
  • 线程安全:
    • 数组分段锁:不再使用 JDK 7 的分段锁(Segment),改为对数组单个桶(Node)加 synchronized 锁(锁粒度更小,并发效率更高);
    • CAS 操作:对桶的初始化、节点的插入等无冲突操作,使用 CAS 保证原子性,避免加锁。
  • 不允许键 / 值为 null,避免多线程下 null 值的歧义(无法区分 "键不存在" 和 "值为 null")。

三、Map 的遍历方式与性能分析

1. 核心遍历方式

遍历方式 实现方式 优点 缺点
键遍历(keySet ()) for (K key : map.keySet()) { V value = map.get(key); } 代码简洁 需两次哈希查找(keySet 遍历一次,get 一次),效率低
键值对遍历(entrySet ()) for (Map.Entry<K,V> entry : map.entrySet()) { K key = entry.getKey(); V value = entry.getValue(); } 一次遍历获取键值,效率最高 代码稍繁琐
迭代器遍历(Iterator) Iterator<Map.Entry<K,V>> iterator = map.entrySet().iterator(); while (iterator.hasNext()) { ... } 支持遍历中删除元素(iterator.remove ()) 代码繁琐
forEach 遍历(JDK 8+) map.forEach((k, v) -> { ... }); 函数式编程,代码简洁 无法遍历中删除元素(需用 removeIf)

2. 性能对比

  • 最优:entrySet() 遍历(仅一次哈希计算,直接获取键值对);
  • 次优:forEach 遍历(底层基于 entrySet,语法简化);
  • 最差:keySet() + get()(两次哈希查找,哈希冲突时性能更差)。

四、自定义对象作为 Map 键的注意事项

1. 必须重写 hashCode()equals()

  • 原因:Map 依赖这两个方法判断键的唯一性,若不重写,默认使用 Object 类的实现:
    • equals():比较对象地址,导致即使内容相同的两个对象被视为不同键;
    • hashCode():返回对象的内存地址哈希值,导致相同内容的对象哈希值不同,映射到不同数组桶。

2. 重写原则

  • 一致性:若两个对象 equals() 返回 true,则 hashCode() 必须返回相同值;若 equals() 返回 falsehashCode() 尽量返回不同值(减少哈希冲突);
  • 稳定性:hashCode() 的返回值在对象生命周期内不能改变(若键对象的属性参与哈希计算,需保证属性不可变,或避免修改参与计算的属性)。

3. 示例代码

java 复制代码
class Student {
    private String id;
    private String name;

    // 构造器、getter/setter 省略

    // 重写 equals()
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return Objects.equals(id, student.id); // 以唯一标识 id 作为判断依据
    }

    // 重写 hashCode()
    @Override
    public int hashCode() {
        return Objects.hash(id); // 仅基于 id 计算哈希值
    }
}

// 使用自定义对象作为键
Map<Student, Integer> scoreMap = new HashMap<>();
scoreMap.put(new Student("001", "张三"), 90);
scoreMap.put(new Student("001", "张三"), 95); // 覆盖原有值(equals 返回 true)
System.out.println(scoreMap.size()); // 输出 1

五、Map 的扩容机制(以 HashMap 为例)

1. 扩容触发条件

  • 当 Map 中键值对数量(size)≥ 数组容量 × 扩容因子(默认 16 × 0.75 = 12)时,触发扩容;
  • 当链表转红黑树时,若数组容量 < 64,先扩容数组(而非转树),避免小容量下频繁转树。

2. 扩容流程

  1. 创建新数组,容量为原数组的 2 倍;
  2. 遍历原数组的所有节点,重新计算节点在新数组的下标(因容量翻倍,下标仅可能为原下标或原下标 + 原容量);
  3. 将节点迁移到新数组(红黑树节点需拆分,链表节点直接迁移);
  4. 替换原数组为新数组,完成扩容。

六、Map 与 Collection 的核心区别

维度 Map Collection
存储形式 键值对(双列) 单个元素(单列)
核心子接口 HashMap、TreeMap、ConcurrentHashMap 等 List、Set、Queue 等
元素唯一性 键唯一,值可重复 Set 元素唯一,List 元素可重复
遍历方式 键遍历、键值对遍历、值遍历 直接遍历、迭代器遍历
核心方法 put、get、remove(按键操作) add、remove、contains(按元素操作)

七、常见面试考点总结

  1. HashMap JDK 7 与 JDK 8 的区别:JDK 7 为 "数组 + 链表",JDK 8 新增红黑树优化,哈希计算扰动方式简化,扩容迁移逻辑优化;
  2. HashMap 为什么线程不安全:扩容成环、数据丢失、可见性问题;
  3. ConcurrentHashMap 线程安全实现方式:JDK 7 分段锁(Segment),JDK 8 CAS + 桶级 synchronized;
  4. TreeMap 排序原理:基于红黑树的自然排序 / 自定义比较器;
  5. LinkedHashMap 实现 LRU 缓存:通过 accessOrder = true 让访问过的元素移到链表尾部,淘汰链表头部元素。

总结

Map 是 Java 中处理键值对映射的核心工具,不同实现类适配不同场景:

  • 无顺序要求、追求高性能:HashMap
  • 需保持插入 / 访问顺序:LinkedHashMap
  • 需按键排序:TreeMap
  • 多线程并发场景:ConcurrentHashMap
  • 避免使用 Hashtable(性能差、API 老旧)。掌握 Map 的底层原理和使用规范,是优化 Java 程序性能、避免并发问题的关键。
相关推荐
似霰2 小时前
传统 Hal 开发笔记2----传统 HAL 整体架构
java·架构·framework·hal
源码获取_wx:Fegn08952 小时前
基于springboot + vue停车场管理系统
java·vue.js·spring boot·后端·spring·课程设计
灯前目力虽非昔,犹课蝇头二万言。2 小时前
HarmonyOS笔记9:UIAbility之间的切换和数据的传递
笔记·harmonyos
求梦8203 小时前
Java:Windows家庭中文版的Docker下载安装
java·windows·docker
Ccjf酷儿3 小时前
操作系统 李治军 3 内存管理
笔记
TL滕3 小时前
从0开始学算法——第十一天(字符串基础算法)
笔记·学习·算法
Hello_Embed3 小时前
FreeRTOS 入门(二十六):队列创建与读写 API 实战解析
笔记·学习·操作系统·嵌入式·freertos
A Mr Yang3 小时前
JAVA 对比老、新两个列表,找出新增、修改、删除的数据
java·开发语言·spring boot·后端·spring cloud·mybatis
BBB努力学习程序设计3 小时前
Java I/O 流与文件操作完全指南:从基础到现代实践
java