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

摘要: 本文深入讲解Java集合框架中的Map和Queue两大核心接口。Map作为键值对映射容器,涵盖HashMap、LinkedHashMap和TreeMap的实现原理与使用场景;Queue作为先进先出队列,介绍LinkedList、ArrayDeque和PriorityQueue的实践应用。文章通过丰富代码示例展示遍历方式、键唯一性原理、选择指南及常见陷阱,帮助开发者掌握这两种在日常开发中高频使用的数据结构,提升编程效率与代码质量。

关键词: Java集合框架, Map映射, Queue队列, HashMap, 数据结构


上一篇文章我们认识了 ListSet,它们都是“单个元素”的容器。你往里面放一个东西,它给你存起来;你取出来的时候,得到的是那个东西本身。

但现实中的数据结构,还有一种非常常见的模式:“根据某个东西,找到另一个东西”

比如:

  • 根据“学号”找到“学生对象”
  • 根据“用户名”找到“用户信息”
  • 根据“商品ID”找到“商品详情”
  • 根据“单词”找到“它的释义”

这种“键 → 值”的映射关系,在程序世界里无处不在。在 Java 中,表达这种关系的数据结构就是 Map(映射)。

此外,还有一种非常基础的数据结构叫 Queue(队列)——它就像排队买票一样,先到先得,先进先出(FIFO)。

今天,我们就来深入学习 MapQueue,它们是集合框架中另外两个极其重要的成员。


1. Map 接口 —— 键值对的“字典”

MapCollection 是平级的两大接口体系。Map 不继承 Collection,它有自己的结构。

Map 的特点:

  • 存储键值对(Key-Value Pair)
  • 键(Key)是唯一的,不能重复(通过 equals()hashCode() 判断)。
  • 值(Value)可以重复
  • 每个键最多映射到一个值。

我们最常用的三个 Map 实现是:HashMapLinkedHashMapTreeMap

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

LinkedHashMapHashMap 的子类。它在哈希表的基础上增加了一个双向链表,维护了键值对的插入顺序(或访问顺序,取决于构造参数)。

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 一样,HashMapLinkedHashMap 依赖键的 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();
    }
}

💡 对于 StringInteger 等 JDK 类,它们已经正确重写了这两个方法,所以可以直接作为键使用。


4. Map 的选择指南

需求推荐
最快的查找速度,不关心顺序HashMap
需要保留插入顺序LinkedHashMap
需要按键排序(升序/自定义)TreeMap
需要线程安全(不推荐,用 ConcurrentHashMapConcurrentHashMap(我们后面会讲)

在日常开发中,HashMap 的使用频率最高,约占 90% 以上。


5. Queue 接口 —— 先进先出的“队列”

QueueCollection 的子接口,代表一个队列(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 在作为队列或栈使用时,性能优于 LinkedListStack,是推荐的首选。

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 接口 —— 双端队列

DequeQueue 的子接口,表示双端队列(Double Ended Queue)——你可以从两端添加和移除元素。它既可以作为队列使用,也可以作为栈使用。

Deque 的主要方法:

操作队首(First)队尾(Last)
添加offerFirst() / addFirst()offerLast() / addLast()
取出并移除pollFirst() / removeFirst()pollLast() / removeLast()
查看peekFirst() / getFirst()peekLast() / getLast()

LinkedListArrayDeque 都实现了 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(如 LinkedHashMapTreeMap)提供了统一的方法来访问“第一个”和“最后一个”键值对。

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
    }
}

SequencedMapSequencedCollectionSequencedSet 共同构成了 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),而且代码更简洁。Mapvalues() 方法可以直接获取所有学生对象,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 变了

解决方案:使用不可变对象作为键(如 StringInteger,或者你自己的不可变类)。

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. 今天的总结

今天我们学习了 MapQueue 两个重要的集合类型:

Map(映射)

  • 存储键值对,键唯一。核心实现:HashMap(最快)、LinkedHashMap(保留插入顺序)、TreeMap(按键排序)。
  • 遍历方式:keySet()values()entrySet()(最常用)。
  • 键的相等性依赖 hashCode()equals(),自定义键必须重写。
  • JDK 21 新增 SequencedMap,支持有序访问。

Queue(队列)

  • FIFO 数据结构,核心实现:LinkedListArrayDeque(推荐)、PriorityQueue(按优先级出队)。
  • Deque 双端队列,可作为队列或栈使用。
  • 推荐使用 offer()poll()peek() 系列方法,它们在队列空时返回 null 而非抛异常。

MapQueue 是日常开发中使用频率极高的数据结构。掌握它们,你就能优雅地处理“映射关系”和“排队任务”这两类常见场景。


动手试试

  1. 创建一个 HashMap<String, Integer>,存储几个人的姓名和年龄。用 entrySet() 遍历,找出年龄最大的人并打印。
  2. 写一个方法,统计一个字符串中每个字符出现的次数,返回 Map<Character, Integer>。然后对结果按字符排序输出(提示:用 TreeMap)。
  3. PriorityQueue 实现一个“任务调度器”:任务有优先级(1-5,数字越小优先级越高),每次出队执行优先级最高的任务。
  4. 把学生管理系统的 SchoolManager 重构,用 Map 替代 List 来存储学生、老师、课程。修改所有相关方法,确保功能不变。
  5. 阅读 HashMap 的源码(在 IDEA 中按 Ctrl+点击 进入),看看 put()get() 是如何实现的。不需要全看懂,先感受一下哈希表的基本原理。

下一篇文章,我们将继续“集合框架”的话题,重点讲集合的实用技巧——比如 Collections 工具类、线程安全问题、JDK 21 的集合工厂方法、以及更深层的性能优化。我们还会把 Map 的各种高级用法(如 computeIfAbsentmerge 等)一并讲完。

我们下一篇见。😃

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值