云计算百科
云计算领域专业知识百科平台

30. 【Java】集合框架(中):Map 与 Queue

摘要: 本文深入讲解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 的主要方法:

操作队首(First)队尾(Last)
添加 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 是日常开发中使用频率极高的数据结构。掌握它们,你就能优雅地处理“映射关系”和“排队任务”这两类常见场景。


动手试试

  • 创建一个 HashMap<String, Integer>,存储几个人的姓名和年龄。用 entrySet() 遍历,找出年龄最大的人并打印。
  • 写一个方法,统计一个字符串中每个字符出现的次数,返回 Map<Character, Integer>。然后对结果按字符排序输出(提示:用 TreeMap)。
  • 用 PriorityQueue 实现一个“任务调度器”:任务有优先级(1-5,数字越小优先级越高),每次出队执行优先级最高的任务。
  • 把学生管理系统的 SchoolManager 重构,用 Map 替代 List 来存储学生、老师、课程。修改所有相关方法,确保功能不变。
  • 阅读 HashMap 的源码(在 IDEA 中按 Ctrl+点击 进入),看看 put() 和 get() 是如何实现的。不需要全看懂,先感受一下哈希表的基本原理。
  • 下一篇文章,我们将继续“集合框架”的话题,重点讲集合的实用技巧——比如 Collections 工具类、线程安全问题、JDK 21 的集合工厂方法、以及更深层的性能优化。我们还会把 Map 的各种高级用法(如 computeIfAbsent、merge 等)一并讲完。

    我们下一篇见。😃

    📌 获取本系列示例代码请访问 GitCode。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 30. 【Java】集合框架(中):Map 与 Queue
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!