首页 / Java 入门教程 / Map 与 HashMap

Java 入门教程

Map 与 HashMap

本教程共 100 篇 · 第 78 篇 · 更新于 2026-08-05 · 约 5 分钟阅读

JavaJava 入门教程MapHashMap键值对

78. Map 与 HashMap

本节目标:认清 Map 是「键→值」配对容器、键不能重复,熟练用 HashMap 的增删查改与三种遍历视图,学完能正确用 Map 存查数据。

Map 存的是「配对」

前面学的 List、Set 都装单个元素,Map 不一样,它装的是「键 → 值」这种配对,像通讯录里「名字 → 号码」。

Map<String, Integer> scores = new HashMap<>();
scores.put("张三", 90);
scores.put("李四", 85);
System.out.println(scores.get("张三")); // 90
```java

Map 有个铁律:**键不能重复**。你往同一个键上再 put 一次,新值会覆盖旧值,而不是变成两条。

```java
scores.put("张三", 95); // 覆盖,张三现在 95
System.out.println(scores.get("张三")); // 95
Note

取一个不存在的键,HashMap 返回 null。所以 get 返回 null 有两种可能:键不存在,或者键对应的值本来就是 null。需要区分时先 containsKey 判断。

HashMap 是最常用的实现

HashMap 是 Map 的默认实现,底层也是哈希表,所以查找极快、不保序。日常 90% 的键值场景用它就够了。

Map<String, String> env = new HashMap<>();
env.put("JAVA_HOME", "/opt/java");
env.put("PATH", "/usr/bin");
boolean has = env.containsKey("JAVA_HOME"); // 是否含某个键
String v = env.getOrDefault("TEMP", "/tmp"); // 没有就给默认值
env.remove("PATH"); // 按键删
int n = env.size();  // 键值对个数
```bash

`v = scores.getOrDefault("王五", 0)`。注意 `get` 和 `getOrDefault` 的区别:后者在键缺失时返回你给的默认值,前者返回 null,写统计类逻辑时 `getOrDefault` 顺手。

## 三种遍历视图

Map 不能直接用 for-each 遍历,因为它既不是单列也不是数组。它提供三个「视图」让你间接遍历:

```java
// 1. 遍历所有键
for (String key : scores.keySet()) {
    System.out.println(key);
}

// 2. 遍历所有值
for (Integer val : scores.values()) {
    System.out.println(val);
}

// 3. 遍历键值对(最常用)
for (var entry : scores.entrySet()) {
    System.out.println(entry.getKey() + " => " + entry.getValue());
}
Tip

要同时拿到键和值,永远优先用 entrySet(),比先 keySet() 再挨个 get(key) 快——后者等于每个键又查了一次表。

键的约束和 Set 一样

Map 判断两个键是否「相同」,用的也是 equals + hashCode 两条约定,和 HashSet 判重一模一样。所以把自定义对象当键时,同样要保证这两个方法基于业务字段正确重写,而且放进 Map 后别再改参与 hashCode 的字段,否则就找不回来了。

record City(String code) {} // 自动生成 equals/hashCode,适合当键
Map<City, Integer> pop = new HashMap<>();
pop.put(new City("BJ"), 2189);
```bash

HashMap 允许一个 `null` 键和多个 `null` 值,但 TreeMap(下一章)不允许 null 键。写代码时如果键可能为 null,心里要有数。

## 一个能跑的完整例子

下面这段统计词语出现次数,是 Map 最经典的应用:

```java
import java.util.*;

public class WordCount {
    public static void main(String[] args) {
        List<String> words = List.of("a", "b", "a", "c", "b", "a");
        Map<String, Integer> freq = new HashMap<>();
        for (String w : words) {
            freq.put(w, freq.getOrDefault(w, 0) + 1); // 没有就从 0 起加
        }
        System.out.println(freq); // {a=3, b=2, c=1}
    }
}

freq.getOrDefault(w, 0) + 1 这行是关键:第一次见到词给 0 再加 1,之后每次累加,一行就完成了计数。

Map 不是 Collection 家族的

回头看第 72 章那张家族图:Collection 是单列,Map 是单独一脉。所以 Map 没有实现 Collection 接口,它不能像 List/Set 那样直接 for-each(必须用 entrySet 等视图)。这是很多初学者困惑的点——为什么 Map 看起来像集合,却不能用集合的遍历方式。记住它「独立成派」即可。

一个键对应多个值怎么办

Map 规定一个键只能对应一个值,但现实里常要「一个学生对应多门成绩」。做法是把值设成集合:Map<String, List<Integer>>

Map<String, List<Integer>> grades = new HashMap<>();
grades.computeIfAbsent("张三", k -> new ArrayList<>()).add(90);
grades.computeIfAbsent("张三", k -> new ArrayList<>()).add(85);
```bash

`computeIfAbsent` 的意思是:键不存在就先用后面的函数造一个值放进去,存在就直接返回。这行代码一行就完成了「没有就建列表、有就往里加」,是 Map 多值写法的利器。

## 遍历时删键也要用迭代器

81 章说过,遍历集合时改结构会触发 fail-fast。Map 同理:你不能边 for-each 边 `map.remove(key)`,得用 `entrySet().iterator().remove()`。

```java
Iterator<Map.Entry<String, Integer>> it = scores.entrySet().iterator();
while (it.hasNext()) {
    if (it.next().getValue() < 60) {
        it.remove(); // 安全删掉不及格的
    }
}
Warning

直接 for (var e : map.entrySet()) { map.remove(e.getKey()); } 会抛 ConcurrentModificationException。要删就走迭代器的 remove,这是 Map 遍历删除的唯一安全通道。

容量和负载因子(了解即可)

HashMap 构造时可以给初始容量和负载因子(默认 0.75)。容量不够时会扩容并重排,和 ArrayList 扩容类似。数据量大、能预估规模时,给个初始容量能减少扩容次数。日常用默认即可,不必纠结。

put 返回的旧值,别浪费

put 方法其实有返回值:如果键之前存在,它返回被覆盖掉的旧值;如果是新键,返回 null。这个返回值在很多场景有用,比如「记录某个键上一次是什么」。

Integer old = scores.put("张三", 95);
System.out.println(old); // 第一次放打印 null,否则打印被覆盖的旧分
```bash

> [!TIP]
> 想判断「是不是第一次放这个键」,直接看 put 的返回值是不是 null 就行,不用先 containsKey 再 put,少一次查表。

## 键用什么类型最稳

Map 的键最好用「不可变且正确实现 equals/hashCode」的类型,最稳的就是 `String`、`Integer` 这些 JDK 自带的。自己写的类当键时,强烈建议用 `record`(自动生成正确契约且不可变)。一旦键对象在放进 Map 后被改了参与 hashCode 的字段,它就「失踪」了——再也查不到,这是 Map 最阴间的 bug 之一,务必警惕。

## 小提醒:value 不要求唯一

Map 只保证 key 唯一,value 完全可以重复。比如统计每个学生考了哪几门,value 都是分数,重复很正常。别误以为 Map 连 value 也去重,那是 Set 的活。记住:唯一性只约束在 key 上,value 随你放。所以「去重」永远发生在 key 维度,value 维度想去重得另想办法,比如把 value 也丢进一个 Set 里单独处理。

另外,Map 本身没有实现 Collection,所以你不能把它直接当成集合去调 `removeIf` 之类的方法;要批量操作,得通过 `keySet`/`values`/`entrySet` 这三个视图,它们才是真正的 Collection,能调用集合的所有方法。

## 小结

Map 是键值配对容器,键不能重复、会覆盖;HashMap 是最常用的实现,基于哈希表、查找快但不保序。增删查改用 put/get/containsKey/remove,遍历优先用 entrySet。键同样依赖 equals/hashCode 约定。下一章看 LinkedHashMap、TreeMap,以及该绕开的 Hashtable。