第17章 集合框架:Map

第17章 集合框架:Map

上一节学习了 List 与 Set,本节学习双列集合 Map------以"键值对"形式存储数据,是 Java 中使用频率最高的集合之一(如缓存、统计、对象属性映射)。

一、什么是 Map

Map 存储键值对(Key-Value),每个键唯一,一个键对应一个值:

复制代码
键(key)    值(value)
"name"  -> "张三"
"age"   -> 18
"city"  -> "北京"
  • 键不能重复,值可以重复
  • 通过键来获取值(像字典查单词)
  • 键和值可以是任何引用类型

为什么需要 Map?List/数组只能按下标找元素,而现实数据往往是"按名字找内容"(学号找学生、商品名找价格)。List 要遍历比对,Map 直接按键取数:

java 复制代码
// List 找"李四"的分数:要遍历(O(n))
List<String> names = Arrays.asList("张三", "李四");
List<Integer> scores = Arrays.asList(85, 92);
for (int i = 0; i < names.size(); i++) {
    if (names.get(i).equals("李四")) System.out.println(scores.get(i));   // 92
}

// Map 直接按键取(O(1))
Map<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("张三", 85);
scoreMap.put("李四", 92);
System.out.println(scoreMap.get("李四"));    // 92

二、Map 的常用实现

实现 特点
HashMap 最常用,无序,基于哈希表,键可存一个 null
LinkedHashMap 保持插入顺序
TreeMap 按键自动排序(键须可比较)
Hashtable 线程安全但效率低,已不推荐

开发中用 HashMap 就够了:保序用 LinkedHashMap,排序用 TreeMap,多线程并发用 ConcurrentHashMap(见扩展知识)。

三、HashMap 基本操作

java 复制代码
import java.util.HashMap;
import java.util.Map;

Map<String, Integer> scores = new HashMap<>();

// 添加 / 修改:键不存在是添加,键已存在是覆盖(更新值)
scores.put("张三", 85);
scores.put("李四", 92);
scores.put("王五", 78);
scores.put("张三", 90);        // 张三的值被更新为 90

// 获取
System.out.println(scores.get("张三"));      // 90
System.out.println(scores.get("赵六"));      // null:键不存在

// 判断
System.out.println(scores.containsKey("李四"));  // true
System.out.println(scores.containsValue(78));    // true

// 删除
scores.remove("王五");

// 大小
System.out.println(scores.size());   // 2

// 取默认值:键不存在时返回默认值,不返回 null
int score = scores.getOrDefault("赵六", 0);   // 0

几个"返回值有讲究"的方法,面试常问:

java 复制代码
// put 返回被覆盖的旧值;键不存在时返回 null
Map<String, Integer> m = new HashMap<>();
System.out.println(m.put("a", 1));      // null:第一次放,没有旧值
System.out.println(m.put("a", 2));      // 1:返回被覆盖的旧值
System.out.println(m.get("a"));         // 2

// remove 返回被删掉的值;putAll 批量合并;clear 清空
Map<String, Integer> other = new HashMap<>();
other.put("b", 3);
m.putAll(other);
System.out.println(m.remove("a"));      // 2

如果你用的是 JDK 11 或 17,创建少量固定键值对还有更快的写法------Map.of 一行搞定,生成的是不可变 Map(JDK 8 没有这个方法,只能手动 put):

java 复制代码
// 需 JDK 11/17 才能运行(JDK 8 没有 Map.of)
Map<String, Integer> quick = Map.of("张三", 90, "李四", 92);
System.out.println(quick);   // {张三=90, 李四=92}

四、遍历 Map 的四种方式

java 复制代码
Map<String, Integer> scores = new HashMap<>();
scores.put("张三", 90);
scores.put("李四", 92);

// 方式一:遍历键值对(最常用、效率最高)
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
    System.out.println(entry.getKey() + " -> " + entry.getValue());
}

// 方式二:遍历键,再取对应值
for (String key : scores.keySet()) {
    System.out.println(key + " -> " + scores.get(key));
}

// 方式三:只遍历值
for (Integer value : scores.values()) {
    System.out.println(value);
}

// 方式四:Lambda(JDK 8+)
scores.forEach((key, value) -> System.out.println(key + " -> " + value));

推荐方式一和方式四:一次遍历拿全键和值,避免二次查询;方式二每次 get(key) 都重新算哈希,数据量大时更慢。

五、HashMap 的底层原理(面试高频)

HashMap 的核心是"数组 + 链表 + 红黑树":

  1. 每个元素根据键的 hashCode() 计算哈希值,确定在数组中的桶位置
  2. 多个键的哈希冲突(落在同一个桶)时,用链表 串联(JDK 8 后,链表超过 8 个转为红黑树提升查询效率)
  3. 数组默认初始容量 16,负载因子 0.75------元素达到 16×0.75=12 时扩容为原来的 2 倍

为什么查询快?理想情况下每个桶一个元素,时间复杂度 O(1),直接根据哈希定位,不用遍历(哈希定位细节见扩展知识 1)。

自定义类作为键时,必须重写 hashCode 和 equals(与 HashSet 同理):

java 复制代码
Map<Student, String> map = new HashMap<>();
map.put(new Student("张三", 18), "A班");
map.get(new Student("张三", 18));    // 没重写 hashCode/equals 时返回 null!

六、Map 的经典应用

1. 统计单词出现次数

java 复制代码
String text = "apple banana apple orange banana apple";
Map<String, Integer> count = new HashMap<>();

for (String word : text.split(" ")) {
    count.put(word, count.getOrDefault(word, 0) + 1);
}

System.out.println(count);   // {banana=2, orange=1, apple=3}

2. 分组统计

java 复制代码
// 统计各分数段人数
Map<String, Integer> group = new HashMap<>();
int[] scores = {85, 92, 45, 78, 60, 99};

for (int s : scores) {
    String level = s >= 90 ? "优秀" : s >= 60 ? "及格" : "不及格";
    group.put(level, group.getOrDefault(level, 0) + 1);
}
System.out.println(group);   // {及格=3, 优秀=2, 不及格=1}

3. 两数之和(LeetCode 经典)

java 复制代码
// 在数组中找两个数,使其和等于 target,返回下标
int[] nums = {2, 7, 11, 15};
int target = 9;

Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
    int need = target - nums[i];
    if (map.containsKey(need)) {
        System.out.println(map.get(need) + ", " + i);   // 0, 1
        break;
    }
    map.put(nums[i], i);
}

用 Map 把时间复杂度从 O(n²) 降到 O(n),是"空间换时间"的经典思路;类似的还有 computeIfAbsent(JDK 8+)做缓存:键不存在才计算、存在直接用缓存值。

七、LinkedHashMap 与 TreeMap

java 复制代码
// LinkedHashMap:保持插入顺序
Map<String, Integer> lhm = new LinkedHashMap<>();
lhm.put("c", 3); lhm.put("a", 1); lhm.put("b", 2);
System.out.println(lhm.keySet());   // [c, a, b]:插入顺序

// TreeMap:按键排序
Map<String, Integer> tm = new TreeMap<>();
tm.put("banana", 1); tm.put("apple", 2); tm.put("cherry", 3);
System.out.println(tm.keySet());    // [apple, banana, cherry]:字典序

TreeMap 和 TreeSet 一样支持传入自定义比较器(如按键长度排序)。

八、扩展知识

1. HashMap 的 put / get 完整流程

put(key, value) 的流程(JDK 8/11/17 一致):

  1. 计算 hash = key.hashCode() ^ (hashCode >>> 16)(高低位混合,减少冲突)
  2. 计算桶下标 index = (数组长度 - 1) & hash
  3. 桶为空 → 直接放入
  4. 桶不为空 → 用 equals 逐个比较 key,相同就覆盖 value 并返回旧值
  5. 没找到 → 追加到链表尾部(链表长度 ≥ 8 且数组长度 ≥ 64 时转成红黑树)
  6. 元素总数超过 容量 × 0.75 → 扩容为 2 倍,所有元素重新散列

get(key) 的流程: 算 hash → 算桶下标 → 桶为空返回 null → 第一个节点 key 相同直接返回 → 否则用 equals 逐个找。定位全靠 (长度 - 1) & hash 一个位运算,这就是 HashMap 快的原因。

2. ConcurrentHashMap 初识

HashMap 线程不安全:多个线程同时 put 时,可能互相覆盖、丢数据。多线程要用什么?

方案 说明
Hashtable 所有方法加锁(锁整张表),安全但慢,已不推荐
Collections.synchronizedMap 同样锁整张表
ConcurrentHashMap JDK 8 起采用"数组 + 链表 + 红黑树" + CAS + synchronized 锁单个桶,并发度高,推荐
java 复制代码
import java.util.concurrent.ConcurrentHashMap;

// 多线程环境下用 ConcurrentHashMap
Map<String, Integer> safe = new ConcurrentHashMap<>();
safe.put("a", 1);
System.out.println(safe.get("a"));   // 1

注意:ConcurrentHashMap 的迭代器是弱一致的,遍历时被其他线程修改不会抛异常(和 List 的 fail-fast 相反)。单线程用 HashMap,多线程共享用 ConcurrentHashMap。

3. Map.of 详解(需 JDK 11/17)

java 复制代码
// 需 JDK 11/17 才能运行(JDK 8 没有 Map.of)
Map<String, Integer> m = Map.of("a", 1, "b", 2);
System.out.println(m);   // {a=1, b=2}

// ❌ m.put("c", 3);      // UnsupportedOperationException:不可变
// ❌ Map.of("a", null);  // NullPointerException:键值都不能为 null
// 最多直接写 10 对键值;再多用 Map.ofEntries(...)

Map.of 适合"常量配置"这种固定不变的数据:代码简洁、天然防篡改。

九、易错点

1. 可变对象当 key,修改后查找失败

java 复制代码
// ❌ 把可变对象当 key:修改内容后 hashCode 变了,原来的键"找不到了"
class Person {
    String name;
    Person(String name) { this.name = name; }
    @Override public int hashCode() { return name.hashCode(); }   // 重写:按 name 算哈希
    @Override public boolean equals(Object o) { return o instanceof Person
            && name.equals(((Person) o).name); }
}

Map<Person, String> map = new HashMap<>();
Person p = new Person("张三");
map.put(p, "A班");
p.name = "李四";                  // 修改了 key 的内容!
System.out.println(map.get(p));   // null:hashCode 变了,定位不到原桶

// ✅ 用不可变类型(String、Integer 等)当 key
Map<String, String> safe = new HashMap<>();
safe.put("张三", "A班");
System.out.println(safe.get("张三"));   // A班

2. 遍历时删除 → 抛异常

java 复制代码
Map<String, Integer> scores = new HashMap<>();
scores.put("a", 1); scores.put("b", 2); scores.put("c", 3);

// ❌ 遍历 keySet 时直接 remove:抛 ConcurrentModificationException
for (String key : scores.keySet()) {
    if (key.equals("a")) scores.remove(key);
}

// ✅ 迭代器的 remove()
Iterator<String> it = scores.keySet().iterator();
while (it.hasNext()) {
    if (it.next().equals("a")) it.remove();
}
System.out.println(scores);   // {b=2, c=3}

3. HashMap 线程不安全

java 复制代码
// ❌ 多线程同时往 HashMap 里 put:可能丢失数据、覆盖写
// 10 个线程各 put 1000 个不同 key,最终 size 常常 < 10000

// ✅ 换成 ConcurrentHashMap:结果稳定
Map<String, Integer> safe = new ConcurrentHashMap<>();

单线程内 HashMap 完全够用;一旦多个线程共享同一个 Map 并写入,立刻换 ConcurrentHashMap。

4. put 相同 key 会覆盖旧值

java 复制代码
Map<String, Integer> map = new HashMap<>();
map.put("张三", 85);

// ❌ 想"只在不存在时添加",却把 85 覆盖成了 90
map.put("张三", 90);
System.out.println(map.get("张三"));   // 90

// ✅ 用 putIfAbsent(JDK 8+):键已存在就不动它
Map<String, Integer> map2 = new HashMap<>();
map2.put("张三", 85);
map2.putIfAbsent("张三", 90);
System.out.println(map2.get("张三"));  // 85:没有被覆盖

5. Map.of 不能存 null、不能修改(需 JDK 11/17)

java 复制代码
// 需 JDK 11/17 才能运行
// ❌ Map.of("a", null);  // NullPointerException
// ❌ Map.of("a", 1).put("b", 2);   // UnsupportedOperationException

// ✅ 需要 null 值 → 用 HashMap
Map<String, String> hm = new HashMap<>();
hm.put("a", null);
System.out.println(hm.get("a"));   // null

十、小结

  • 增删改查:put / get / remove / containsKey;put 返回旧值,remove 返回被删的值
  • 遍历推荐 entrySet 或 forEach,避免 keySet + get 二次查询
  • HashMap 底层是数组+链表+红黑树,查询 O(1);put/get 靠 hash 定位、equals 找键
  • 自定义类当 key 必须重写 hashCode + equals,且 key 不要被修改
  • 多线程用 ConcurrentHashMap;JDK 11/17 的 Map.of 不可变、不能 null

下一节学习泛型。

下一篇:第18章 泛型入门

相关推荐
伯远医学43 分钟前
RIP-seq 高分文献案例分享
java·前端·javascript·人工智能·算法·eclipse·html
吃杠碰小鸡44 分钟前
VsCode中使用Java
java·vscode
lq_bog1 小时前
spring事务失效
java·数据库·spring
API快乐传递者1 小时前
淘宝京东数据分析实战指南:从指标体系到决策落地的全链路方法论
java·大数据·数据分析
vancece1 小时前
JS实现堆 & 215.数组中的第K个最大元素
java·前端·javascript
就叫_这个吧1 小时前
java springcloud熔断降级组件sentinel基础应用及nacos持久化处理
java·spring cloud·nacos·sentinel
SimonKing1 小时前
Apache Fory JSON或许可以替代FastJson
java·后端·程序员
程序员良辰1 小时前
【TongWeb7的M11】 首次登录密码修改 & 开启控制台远程访问
java·中间件·tomcat
吴声子夜歌1 小时前
Java面试——设计模式(四)
java·设计模式·面试