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 程序性能、避免并发问题的关键。
相关推荐
一只小小Java5 分钟前
Java面试场景高频题
java·开发语言·面试
沛沛老爹6 分钟前
Web开发者快速上手AI Agent:基于Function Calling的12306自动订票系统实战
java·人工智能·agent·web转型
CRUD酱9 分钟前
后端使用POI解析.xlsx文件(附源码)
java·后端
亓才孓9 分钟前
多态:编译时看左边,运行时看右边
java·开发语言
小白探索世界欧耶!~9 分钟前
用iframe实现单个系统页面在多个系统中复用
开发语言·前端·javascript·vue.js·经验分享·笔记·iframe
2501_9418024821 分钟前
从缓存更新到数据一致性的互联网工程语法实践与多语言探索
java·后端·spring
拆房老料33 分钟前
文档预览开源选型对比:BaseMetas FileView 与 KK FileView,谁更适合你的系统?
java·开源·java-rocketmq·开源软件
Frank_refuel35 分钟前
C++之内存管理
java·数据结构·c++
551只玄猫40 分钟前
新编大学德语1第三版笔记 第3课Studentenleben
笔记·德语·外语·德语a1·德语笔记·自学德语·新编大学德语
钱多多_qdd43 分钟前
springboot注解(五)
java·spring boot·后端