摘要: 本文深入讲解Java集合框架中的Map和Queue两大核心接口。Map作为键值对映射容器,涵盖HashMap、LinkedHashMap和TreeMap的实现原理与使用场景;Queue作为先进先出队列,介绍LinkedList、ArrayDeque和PriorityQueue的实践应用。文章通过丰富代码示例展示遍历方式、键唯一性原理、选择指南及常见陷阱,帮助开发者掌握这两种在日常开发中高频使用的数据结构,提升编程效率与代码质量。
关键词: Java集合框架, Map映射, Queue队列, HashMap, 数据结构
上一篇文章我们认识了 List 和 Set,它们都是“单个元素”的容器。你往里面放一个东西,它给你存起来;你取出来的时候,得到的是那个东西本身。
但现实中的数据结构,还有一种非常常见的模式:“根据某个东西,找到另一个东西”。
比如:
- 根据“学号”找到“学生对象”
- 根据“用户名”找到“用户信息”
- 根据“商品ID”找到“商品详情”
- 根据“单词”找到“它的释义”
这种“键 → 值”的映射关系,在程序世界里无处不在。在 Java 中,表达这种关系的数据结构就是 Map(映射)。
此外,还有一种非常基础的数据结构叫 Queue(队列)——它就像排队买票一样,先到先得,先进先出(FIFO)。
今天,我们就来深入学习 Map 和 Queue,它们是集合框架中另外两个极其重要的成员。
1. Map 接口 —— 键值对的“字典”
Map 和 Collection 是平级的两大接口体系。Map 不继承 Collection,它有自己的结构。
Map 的特点:
- 存储键值对(Key-Value Pair)。
- 键(Key)是唯一的,不能重复(通过 equals() 和 hashCode() 判断)。
- 值(Value)可以重复。
- 每个键最多映射到一个值。
我们最常用的三个 Map 实现是:HashMap、LinkedHashMap 和 TreeMap。
1.1 HashMap —— 基于哈希表的映射
HashMap 是最常用的 Map 实现。它基于哈希表,查找、插入、删除的平均时间复杂度为 O(1)。
import java.util.HashMap;
import java.util.Map;
public class HashMapDemo {
public static void main(String[] args) {
// 创建 HashMap,键是 String(学号),值是 Student 对象
Map<String, Student> studentMap = new HashMap<>();
// 1. 添加键值对
Student s1 = new Student("张三", 20, "S001");
Student s2 = new Student("李四", 21, "S002");
Student s3 = new Student("王五", 19, "S003");
studentMap.put("S001", s1);
studentMap.put("S002", s2);
studentMap.put("S003", s3);
// 2. 根据键获取值
Student found = studentMap.get("S002");
System.out.println("学号 S002 的学生:" + found.getName()); // 李四
// 如果键不存在,get() 返回 null
Student notFound = studentMap.get("S999");
System.out.println("S999:" + notFound); // null
// 3. 获取带默认值的(JDK 8+)
Student defaultStudent = studentMap.getOrDefault("S999", new Student("默认", 0, "DEFAULT"));
System.out.println("默认学生:" + defaultStudent.getName());
// 4. 检查是否包含某个键或值
System.out.println("是否包含键 S001:" + studentMap.containsKey("S001")); // true
System.out.println("是否包含学生张三:" + studentMap.containsValue(s1)); // true
// 5. 替换值
Student newS2 = new Student("李四改名", 22, "S002");
studentMap.put("S002", newS2); // 相同的键会覆盖旧值
// 6. 删除键值对
studentMap.remove("S003");
// 7. 获取大小
System.out.println("Map 大小:" + studentMap.size()); // 2
}
}
1.2 LinkedHashMap —— 保留插入顺序的 HashMap
LinkedHashMap 是 HashMap 的子类。它在哈希表的基础上增加了一个双向链表,维护了键值对的插入顺序(或访问顺序,取决于构造参数)。
Map<String, String> linkedMap = new LinkedHashMap<>();
linkedMap.put("apple", "苹果");
linkedMap.put("banana", "香蕉");
linkedMap.put("orange", "橘子");
// 遍历顺序与插入顺序一致:apple → banana → orange
for (Map.Entry<String, String> entry : linkedMap.entrySet()) {
System.out.println(entry.getKey() + " = " + entry.getValue());
}
用途:当你需要保留插入顺序时(比如实现 LRU 缓存),LinkedHashMap 很合适。
1.3 TreeMap —— 基于红黑树的有序映射
TreeMap 基于红黑树实现,它会根据键的自然顺序(或自定义比较器)对键进行排序。
import java.util.Map;
import java.util.TreeMap;
public class TreeMapDemo {
public static void main(String[] args) {
// 键按字典序升序排列
Map<String, Integer> scores = new TreeMap<>();
scores.put("张三", 85);
scores.put("李四", 92);
scores.put("王五", 78);
scores.put("阿强", 88);
// 遍历时会按键的字典序输出:阿强、李四、王五、张三
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// TreeMap 提供了额外的导航方法
System.out.println("第一个键:" + ((TreeMap<String, Integer>) scores).firstKey()); // 阿强
System.out.println("最后一个键:" + ((TreeMap<String, Integer>) scores).lastKey()); // 张三
System.out.println("大于等于李四的键:" + ((TreeMap<String, Integer>) scores).ceilingKey("李四")); // 李四
}
}
TreeMap 的用途:当需要按键排序时(比如按时间戳排序的日志、按名称排序的字典)。
2. 遍历 Map —— 三种方式
Map 不直接实现 Iterable,所以不能直接用增强 for 遍历。它提供了三种“视图”来遍历:
① 遍历键(keySet())
for (String key : studentMap.keySet()) {
Student student = studentMap.get(key);
System.out.println(key + " → " + student.getName());
}
② 遍历值(values())
for (Student student : studentMap.values()) {
System.out.println(student.getName());
}
③ 遍历键值对(entrySet())—— 最常用,效率最高
for (Map.Entry<String, Student> entry : studentMap.entrySet()) {
String key = entry.getKey();
Student value = entry.getValue();
System.out.println(key + " → " + value.getName());
}
entrySet() 返回的是 Map.Entry 对象集合,每个 Entry 包含一个键和一个值。这种方式在遍历时不需要二次查找(get(key)),性能最好。
JDK 8+ 的 forEach 方法(Lambda)
studentMap.forEach((key, value) -> {
System.out.println(key + " → " + value.getName());
});
这是最简洁的遍历方式,适合简单的遍历操作。
3. Map 中键的唯一性 —— hashCode() 和 equals()
和 HashSet 一样,HashMap 和 LinkedHashMap 依赖键的 hashCode() 和 equals() 来判断键是否相同。
如果你用自定义类作为 Map 的键,必须正确重写 hashCode() 和 equals(),否则相同的对象可能会被视为不同的键,导致数据错乱。
// 自定义类作为键(必须重写 hashCode 和 equals)
public class ProductKey {
private String category;
private String id;
public ProductKey(String category, String id) {
this.category = category;
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ProductKey that = (ProductKey) o;
return category.equals(that.category) && id.equals(that.id);
}
@Override
public int hashCode() {
return 31 * category.hashCode() + id.hashCode();
}
}
💡 对于 String、Integer 等 JDK 类,它们已经正确重写了这两个方法,所以可以直接作为键使用。
4. Map 的选择指南
| 最快的查找速度,不关心顺序 | HashMap |
| 需要保留插入顺序 | LinkedHashMap |
| 需要按键排序(升序/自定义) | TreeMap |
| 需要线程安全(不推荐,用 ConcurrentHashMap) | ConcurrentHashMap(我们后面会讲) |
在日常开发中,HashMap 的使用频率最高,约占 90% 以上。
5. Queue 接口 —— 先进先出的“队列”
Queue 是 Collection 的子接口,代表一个队列(Queue)——一种先进先出(FIFO)的数据结构。就像排队买票:先来的人先买到,后来的人排在后面。
Queue 提供了两类操作:
- 添加:offer()(推荐,失败返回 false) vs add()(失败抛异常)
- 取出并移除:poll()(推荐,队列空返回 null) vs remove()(空抛异常)
- 查看但不移除:peek()(推荐,空返回 null) vs element()(空抛异常)
最常用的 Queue 实现是 LinkedList(它实现了 Queue 接口)和 ArrayDeque。
5.1 基本用法
import java.util.LinkedList;
import java.util.Queue;
public class QueueDemo {
public static void main(String[] args) {
// LinkedList 实现了 Queue 接口
Queue<String> queue = new LinkedList<>();
// 添加元素
queue.offer("第一个人");
queue.offer("第二个人");
queue.offer("第三个人");
// 查看队首元素(不移除)
System.out.println("队首:" + queue.peek()); // 第一个人
// 出队:取出并移除队首
System.out.println(queue.poll()); // 第一个人
System.out.println(queue.poll()); // 第二个人
System.out.println(queue.poll()); // 第三个人
// 队列为空时,poll() 返回 null
System.out.println(queue.poll()); // null
}
}
5.2 ArrayDeque —— 更高效的队列/栈
ArrayDeque 是一个基于动态数组的双端队列(Deque),它比 LinkedList 在队列操作上性能更好,且不支持 null 元素。
import java.util.ArrayDeque;
import java.util.Deque;
public class ArrayDequeDemo {
public static void main(String[] args) {
// 作为队列使用(FIFO)
Deque<String> queue = new ArrayDeque<>();
queue.offer("A");
queue.offer("B");
queue.offer("C");
System.out.println(queue.poll()); // A
// 作为栈使用(LIFO)
Deque<String> stack = new ArrayDeque<>();
stack.push("X"); // 入栈
stack.push("Y");
stack.push("Z");
System.out.println(stack.pop()); // Z(后进先出)
}
}
ArrayDeque 在作为队列或栈使用时,性能优于 LinkedList 和 Stack,是推荐的首选。
5.3 PriorityQueue —— 带优先级的队列
PriorityQueue 不是 FIFO 队列,而是一个优先队列——它根据元素的优先级(自然顺序或自定义比较器)决定出队顺序。优先级最高的元素最先出队。
import java.util.PriorityQueue;
import java.util.Queue;
public class PriorityQueueDemo {
public static void main(String[] args) {
// 默认是自然升序(最小堆)
Queue<Integer> pq = new PriorityQueue<>();
pq.offer(30);
pq.offer(10);
pq.offer(50);
pq.offer(20);
// 出队顺序:10, 20, 30, 50(从小到大)
while (!pq.isEmpty()) {
System.out.print(pq.poll() + " "); // 10 20 30 50
}
}
}
PriorityQueue 非常适合任务调度、Dijkstra 算法、求 Top K 等场景。
6. Deque 接口 —— 双端队列
Deque 是 Queue 的子接口,表示双端队列(Double Ended Queue)——你可以从两端添加和移除元素。它既可以作为队列使用,也可以作为栈使用。
Deque 的主要方法:
| 添加 | offerFirst() / addFirst() | offerLast() / addLast() |
| 取出并移除 | pollFirst() / removeFirst() | pollLast() / removeLast() |
| 查看 | peekFirst() / getFirst() | peekLast() / getLast() |
LinkedList 和 ArrayDeque 都实现了 Deque 接口。
Deque<String> deque = new ArrayDeque<>();
deque.offerFirst("头部元素");
deque.offerLast("尾部元素");
System.out.println(deque.pollFirst()); // 头部元素
System.out.println(deque.pollLast()); // 尾部元素
7. JDK 21 的 SequencedMap
在 JDK 21 中,Map 体系也迎来了一个重要的新成员:SequencedMap 接口。它为有序 Map(如 LinkedHashMap 和 TreeMap)提供了统一的方法来访问“第一个”和“最后一个”键值对。
import java.util.LinkedHashMap;
import java.util.SequencedMap;
public class SequencedMapDemo {
public static void main(String[] args) {
SequencedMap<String, Integer> map = new LinkedHashMap<>();
map.put("苹果", 5);
map.put("香蕉", 3);
map.put("橘子", 8);
// 获取第一个和最后一个键值对
System.out.println(map.firstEntry()); // 苹果=5
System.out.println(map.lastEntry()); // 橘子=8
// 反转顺序
SequencedMap<String, Integer> reversed = map.reversed();
reversed.forEach((k, v) -> System.out.println(k + "=" + v));
// 橘子=8, 香蕉=3, 苹果=5
}
}
SequencedMap 与 SequencedCollection、SequencedSet 共同构成了 JDK 21 对“有序集合”的统一支持。
8. 综合示例 —— 用 Map 重构学生管理系统
我们来把学生管理系统中“查找学生”的功能用 Map 重构一下。
之前的做法:用 List 存储学生,查找时需要遍历整个列表。
// 旧方式:遍历查找
public Student findStudentById(String id) {
for (Student s : students) {
if (s.getStudentId().equals(id)) {
return s;
}
}
return null;
}
重构后:用 Map<String, Student> 存储,学号作为键,学生对象作为值。
// SchoolManager 中的改动
private Map<String, Student> studentMap = new HashMap<>();
private Map<String, Teacher> teacherMap = new HashMap<>();
private Map<String, Course> courseMap = new HashMap<>();
// 添加学生时,同时放入 Map
public void addStudent(Student s) {
studentMap.put(s.getStudentId(), s);
}
// 查找学生 —— O(1) 时间!
public Student findStudentById(String id) {
return studentMap.get(id);
}
// 查看所有学生
public void listStudents() {
for (Student s : studentMap.values()) {
System.out.println(s);
}
}
这样改完之后,查找效率从 O(n) 变成了 O(1),而且代码更简洁。Map 的 values() 方法可以直接获取所有学生对象,keySet() 可以获取所有学号。
💡 设计建议:在需要频繁根据某个唯一标识查找对象的场景中,用 Map 存储是标配做法。你甚至可以让 students 列表和 studentMap 同时存在——列表用来保持顺序,Map 用来加速查找。
9. 常见陷阱与注意事项
1. 用可变对象作为 Map 的键
如果键对象是可变的,并且修改后影响了 hashCode() 或 equals() 的返回值,那这个键就“丢失”了——你再也找不到对应的值。
// 错误示例
Map<List<String>, String> map = new HashMap<>();
List<String> key = new ArrayList<>();
key.add("A");
map.put(key, "value");
key.add("B"); // 修改了键!
System.out.println(map.get(key)); // 可能返回 null,因为 hashCode 变了
解决方案:使用不可变对象作为键(如 String、Integer,或者你自己的不可变类)。
2. HashMap 中的 null 值
- HashMap 允许一个 null 键和任意个 null 值。
- TreeMap 不允许 null 键(因为无法排序),但值可以是 null。
- ConcurrentHashMap 不允许 null 键或值。
3. 遍历时修改 Map
和集合一样,在遍历 Map 时(无论是 keySet()、values() 还是 entrySet()),不能直接调用 put() 或 remove(),否则会抛出 ConcurrentModificationException。可以使用迭代器的 remove() 方法,或使用 removeIf()。
// 安全删除:使用 entrySet 的 removeIf
studentMap.entrySet().removeIf(entry -> entry.getValue().getAge() < 18);
4. 选择合适的初始容量
如果你能预估 Map 的大小,在构造时指定初始容量可以减少扩容开销,提升性能。
// 预计存储 1000 个元素
Map<String, Student> map = new HashMap<>(1000);
10. 今天的总结
今天我们学习了 Map 和 Queue 两个重要的集合类型:
Map(映射)
- 存储键值对,键唯一。核心实现:HashMap(最快)、LinkedHashMap(保留插入顺序)、TreeMap(按键排序)。
- 遍历方式:keySet()、values()、entrySet()(最常用)。
- 键的相等性依赖 hashCode() 和 equals(),自定义键必须重写。
- JDK 21 新增 SequencedMap,支持有序访问。
Queue(队列)
- FIFO 数据结构,核心实现:LinkedList、ArrayDeque(推荐)、PriorityQueue(按优先级出队)。
- Deque 双端队列,可作为队列或栈使用。
- 推荐使用 offer()、poll()、peek() 系列方法,它们在队列空时返回 null 而非抛异常。
Map 和 Queue 是日常开发中使用频率极高的数据结构。掌握它们,你就能优雅地处理“映射关系”和“排队任务”这两类常见场景。
动手试试
下一篇文章,我们将继续“集合框架”的话题,重点讲集合的实用技巧——比如 Collections 工具类、线程安全问题、JDK 21 的集合工厂方法、以及更深层的性能优化。我们还会把 Map 的各种高级用法(如 computeIfAbsent、merge 等)一并讲完。
我们下一篇见。😃
📌 获取本系列示例代码请访问 GitCode。
网硕互联帮助中心

评论前必须登录!
注册