Java 集合框架入门:List、Set、Queue 与 Map
前言
在程序开发中,我们经常需要保存和处理一组数据,例如购物车中的商品、文章标签、排队等待执行的任务、学生姓名与成绩的对应关系。
数组虽然也能保存多个元素,但它的长度固定,增删元素不够灵活。Java 集合框架就像一套准备好的“数据收纳工具箱”:不同的容器有不同的摆放规则,也适用于不同的业务场景。
Java 集合框架主要分为两条路线:
Collection:保存一个个独立元素,下面包括 List、Set、Queue。
Map:保存键值对,例如“用户名 → 用户信息”。
需要特别注意:Map 属于集合框架的一部分,但它不是 Collection 的子接口。
一、为什么需要集合框架
1.1 数组的局限
数组适合保存数量比较固定的数据:
1 2 3 4
| String[] names = new String[3]; names[0] = "张三"; names[1] = "李四"; names[2] = "王五";
|
但是,当数据数量不断变化时,数组会显得不够灵活:
- 数组长度创建后不能自动改变。
- 中间插入或删除元素比较麻烦。
- 查找、去重、排序等操作通常需要自己编写代码。
例如,一个购物车可能随时添加或删除商品;一个班级的签到名单也可能不断变化。这时,集合比数组更适合。
1.2 集合框架解决了什么问题
Java 集合框架提供了统一的接口和常用实现类,使我们可以直接完成以下操作:
1 2 3 4 5
| list.add("Java"); list.remove("MySQL"); set.contains("Redis"); map.get("张三"); queue.poll();
|
可以把集合框架理解为不同用途的容器:
| 容器 |
形象理解 |
典型用途 |
List |
可重复的排号座位表 |
购物车、学生名单 |
Set |
不允许重复入场的名单 |
标签去重、用户名检测 |
Queue |
排队窗口 |
打印任务、消息处理 |
Deque |
两头都能进出的通道 |
栈、撤销操作 |
Map |
按编号查资料的柜子 |
用户 ID 查询、配置表 |
二、Java 集合框架的整体结构
2.1 两条核心支线
Java 集合框架的基础结构可以简化为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| Iterable | Collection ├── List │ ├── ArrayList │ └── LinkedList │ ├── Set │ ├── HashSet │ ├── LinkedHashSet │ └── TreeSet │ └── Queue ├── PriorityQueue └── Deque ├── ArrayDeque └── LinkedList
Map ├── HashMap ├── LinkedHashMap └── TreeMap
|
其中:
List 关注元素的排列位置。
Set 关注元素不能重复。
Queue 关注元素按照某种规则依次处理。
Map 关注通过唯一的键查找对应的值。(二哥的Java进阶之路)
2.2 接口与实现类
实际开发中,通常使用接口声明变量,再选择具体实现类创建对象:
1 2 3 4
| List<String> list = new ArrayList<>(); Set<String> set = new HashSet<>(); Queue<String> queue = new ArrayDeque<>(); Map<String, Integer> map = new HashMap<>();
|
这种写法可以理解为:
- 左边决定“我需要什么功能”
- 右边决定“底层使用什么结构实现”
例如:
1
| List<String> list = new ArrayList<>();
|
表示当前业务需要的是“列表”功能,至于底层先使用动态数组实现。以后需要更换实现类时,业务代码受到的影响会更小。
三、List:有序、可重复的列表
3.1 List 的特点
List 表示一个有顺序的集合,元素可以重复,并且可以通过下标访问。
1 2 3 4 5 6 7 8
| List<String> courses = new ArrayList<>();
courses.add("Java"); courses.add("MySQL"); courses.add("Java");
System.out.println(courses); System.out.println(courses.get(1));
|
这里第二次添加 "Java" 不会被拦截,因为 List 允许重复元素。
适用场景:
- 购物车商品列表。
- 用户浏览记录。
- 按顺序展示的文章评论。
- 考试成绩列表。
List 官方定义强调了两个核心能力:元素具有顺序,并且可以按整数下标访问;它通常允许重复元素。(Oracle Docs)
3.2 ArrayList:动态数组
ArrayList 底层可以理解为一个能够自动扩容的数组。
1 2 3 4 5 6 7
| List<String> cart = new ArrayList<>();
cart.add("键盘"); cart.add("鼠标"); cart.add("显示器");
System.out.println(cart.get(1));
|
它的特点如下:
| 操作 |
特点 |
| 按下标查询 |
快,适合频繁读取 |
| 尾部添加元素 |
通常较快 |
| 中间插入或删除 |
需要移动后面的元素 |
| 内存结构 |
元素连续存放,额外开销较少 |
可以把 ArrayList 想象成一排电影院座位。找到第 10 个座位很快,但如果要在中间增加一个座位,后面的人可能都要往后移动。
适用场景:
- 查询操作多的列表。
- 商品展示列表。
- 查询结果集合。
- 一般业务中的默认
List 实现。
ArrayList 是 List 的可调整大小数组实现,并实现了随机访问能力。(Oracle Docs)
3.3 LinkedList:双向链表
LinkedList 底层由一个个相互连接的节点组成,每个节点知道前一个节点和后一个节点。
1 2 3 4 5 6 7
| List<String> history = new LinkedList<>();
history.add("首页"); history.add("课程页"); history.add("订单页");
System.out.println(history);
|
它的特点如下:
| 操作 |
特点 |
| 按下标查询 |
需要沿节点寻找,效率较低 |
| 首尾添加、删除 |
比较适合 |
| 中间插入、删除 |
找到位置后修改连接关系较方便 |
| 内存结构 |
每个节点需要保存前后引用 |
可以把 LinkedList 想象成一列手拉手的小火车。增加或移除一节车厢时,只需要调整相邻连接;但要直接找到第 100 节车厢,就需要一路数过去。
需要注意:不能简单理解为“LinkedList 任意位置增删都一定快”。如果需要先通过下标找到中间位置,寻找节点本身仍然需要遍历。
LinkedList 同时实现了 List 和 Deque,按下标访问时会从头部或尾部开始遍历。(Oracle Docs)
四、Set:元素不能重复的集合
4.1 Set 的特点
Set 的核心特点是:不允许保存重复元素。
1 2 3 4 5 6 7
| Set<String> tags = new HashSet<>();
tags.add("Java"); tags.add("MySQL"); tags.add("Java");
System.out.println(tags.size());
|
第二次添加 "Java" 时,集合不会重复保存它。
适用场景:
- 对文章标签去重。
- 判断用户名是否已经存在。
- 统计不重复的访问用户。
- 保存不能重复的编号。
需要特别注意:
Set 的核心定义是“不重复”,而不是“所有实现类都无序”。
不同实现类对顺序的处理方式不同:
| 实现类 |
是否去重 |
顺序特点 |
HashSet |
是 |
不保证遍历顺序 |
LinkedHashSet |
是 |
保留插入顺序 |
TreeSet |
是 |
按自然顺序或比较器排序 |
Set 接口规定元素不能重复;HashSet 不保证遍历顺序,而 LinkedHashSet 与 TreeSet 分别提供插入顺序和排序能力。(Oracle Docs)
4.2 HashSet:快速去重
HashSet 底层基于 HashMap 实现,适合快速判断某个元素是否存在。
1 2 3 4 5 6 7 8
| Set<String> uniqueTags = new HashSet<>();
uniqueTags.add("Java"); uniqueTags.add("Redis"); uniqueTags.add("Java");
System.out.println(uniqueTags.contains("Redis")); System.out.println(uniqueTags.size());
|
适用场景:
- 单词去重。
- 用户 ID 去重。
- 快速判断数据是否已经处理过。
1 2 3 4
| List<String> words = Arrays.asList("Java", "MySQL", "Java", "Redis"); Set<String> uniqueWords = new HashSet<>(words);
System.out.println(uniqueWords);
|
当哈希分布良好时,HashSet 的 add、remove、contains 和 size 等基本操作通常具有常数时间性能。(Oracle Docs)
4.3 LinkedHashSet:去重并保留原顺序
如果既想去重,又想保留元素首次出现的顺序,可以使用 LinkedHashSet。
1 2 3 4 5 6 7 8
| Set<String> tags = new LinkedHashSet<>();
tags.add("Java"); tags.add("MySQL"); tags.add("Java"); tags.add("Redis");
System.out.println(tags);
|
适用场景:
- 对用户提交的数据去重,但保持展示顺序。
- 对搜索关键词去重,同时保留原始输入顺序。
4.4 TreeSet:去重并排序
TreeSet 会在去重的同时,对元素进行排序。
1 2 3 4 5 6 7 8
| Set<Integer> scores = new TreeSet<>();
scores.add(90); scores.add(75); scores.add(88); scores.add(75);
System.out.println(scores);
|
适用场景:
- 保存不重复且需要排序的分数。
- 保存有序编号。
- 查询某个范围内的数据。
TreeSet 基于 TreeMap,按照元素的自然顺序或指定比较器进行排序,其基础操作具有 O(log n) 的时间成本。(Oracle Docs)
五、Queue 与 Deque:等待处理的数据队列
5.1 Queue:先进先出的排队结构
Queue 表示队列,通常遵循先进先出原则,即先来的元素先处理。
1 2 3 4 5 6 7 8
| Queue<String> printQueue = new ArrayDeque<>();
printQueue.offer("文档A"); printQueue.offer("文档B"); printQueue.offer("文档C");
System.out.println(printQueue.poll()); System.out.println(printQueue.poll());
|
可以把队列理解为食堂排队:
1 2
| 入队:新同学站到队尾 出队:最前面的同学先打饭离开
|
适用场景:
- 打印任务队列。
- 消息消费队列。
- 请求排队处理。
- 广度优先搜索。
Queue 常用方法有两组:
| 操作 |
失败时抛异常 |
失败时返回特殊值 |
| 插入元素 |
add(e) |
offer(e) |
| 删除队首 |
remove() |
poll() |
| 查看队首 |
element() |
peek() |
日常使用中,offer()、poll() 和 peek() 往往更安全,因为队列为空时不会直接抛出异常。Queue 通常采用先进先出规则,但 PriorityQueue 等实现类可以使用不同的出队规则。(Oracle Docs)
5.2 Deque:两端都能操作的队列
Deque 是双端队列,元素可以从头部加入或删除,也可以从尾部加入或删除。
1 2 3 4 5 6 7
| Deque<String> deque = new ArrayDeque<>();
deque.addFirst("B"); deque.addFirst("A"); deque.addLast("C");
System.out.println(deque);
|
Deque 既可以模拟普通队列,也可以模拟栈。
作为队列使用
1 2 3 4 5 6
| Queue<String> queue = new ArrayDeque<>();
queue.offer("任务A"); queue.offer("任务B");
System.out.println(queue.poll());
|
作为栈使用
1 2 3 4 5 6 7 8
| Deque<String> stack = new ArrayDeque<>();
stack.push("首页"); stack.push("商品页"); stack.push("订单页");
System.out.println(stack.pop()); System.out.println(stack.pop());
|
适用场景:
- 浏览器返回记录。
- 编辑器撤销功能。
- 深度优先搜索。
- 普通业务队列。
虽然 ArrayDeque 底层使用数组实现,但它不是 List,不能通过 get(0) 这样的下标方式访问元素。官方文档还指出,ArrayDeque 用作栈时通常比 Stack 更快,用作队列时通常比 LinkedList 更快。(Oracle Docs)
5.3 PriorityQueue:优先处理重要任务
普通队列按照先来后到处理任务,而 PriorityQueue 会按照优先级决定谁先出队。
1 2 3 4 5 6 7
| Queue<Integer> queue = new PriorityQueue<>();
queue.offer(80); queue.offer(100); queue.offer(60);
System.out.println(queue.poll());
|
默认情况下,数字越小,越先出队。
如果希望“优先级数字越大,任务越重要”,可以提供比较器:
1 2 3 4 5 6 7
| Queue<Integer> queue = new PriorityQueue<>(Comparator.reverseOrder());
queue.offer(80); queue.offer(100); queue.offer(60);
System.out.println(queue.poll());
|
适用场景:
- 线上故障优先处理。
- 医院急诊排队。
- 考试成绩排名处理。
- 定时任务调度。
需要注意:PriorityQueue 只保证每次 poll() 取出的元素符合优先级规则,直接遍历整个队列时,不保证输出结果已经完全排好序。(Oracle Docs)
六、Map:通过键查找值
6.1 Map 的特点
Map 保存的是键值对:
例如:
1 2 3 4
| 学号 → 学生姓名 商品编号 → 商品详情 用户名 → 登录信息 配置名称 → 配置值
|
示例:
1 2 3 4 5 6
| Map<String, Integer> scores = new HashMap<>();
scores.put("张三", 90); scores.put("李四", 86);
System.out.println(scores.get("张三"));
|
Map 中的键不能重复。相同的键再次调用 put() 时,新值会替换旧值:
1 2 3
| scores.put("张三", 95);
System.out.println(scores.get("张三"));
|
可以把 Map 理解为快递柜:
1 2 3
| 柜门编号是 key 柜门里面的包裹是 value 同一个柜门编号只能对应一个当前位置上的包裹
|
Map 的官方定义是键到值的映射,一个键最多只能映射到一个值。(Oracle Docs)
6.2 HashMap:最常用的键值容器
HashMap 是日常开发中非常常见的 Map 实现。
1 2 3 4 5 6
| Map<String, String> users = new HashMap<>();
users.put("1001", "张三"); users.put("1002", "李四");
System.out.println(users.get("1001"));
|
适用场景:
- 用户 ID 查询用户信息。
- 商品编号查询商品详情。
- 单词出现次数统计。
- 缓存临时数据。
1 2 3 4 5 6 7
| Map<String, Integer> wordCount = new HashMap<>();
wordCount.put("Java", 1); wordCount.put("MySQL", 1); wordCount.put("Java", wordCount.get("Java") + 1);
System.out.println(wordCount.get("Java"));
|
HashMap 不保证遍历顺序;当哈希分布良好时,其 get() 与 put() 等基本操作通常具有常数时间性能。(Oracle Docs)
6.3 LinkedHashMap:按插入顺序遍历
如果希望键值对按照加入顺序输出,可以使用 LinkedHashMap。
1 2 3 4 5 6 7
| Map<String, Integer> steps = new LinkedHashMap<>();
steps.put("登录", 1); steps.put("选择商品", 2); steps.put("提交订单", 3);
System.out.println(steps.keySet());
|
适用场景:
- 需要固定展示顺序的配置项。
- 需要记录访问顺序的数据。
- 需要实现简单 LRU 缓存的场景。
LinkedHashMap 在哈希表基础上维护了链表,因此拥有明确的遍历顺序,默认通常是插入顺序。(Oracle Docs)
6.4 TreeMap:按照键排序
TreeMap 会按照键的自然顺序或比较器规则进行排序。
1 2 3 4 5 6 7
| Map<Integer, String> ranking = new TreeMap<>();
ranking.put(3, "铜牌"); ranking.put(1, "金牌"); ranking.put(2, "银牌");
System.out.println(ranking);
|
适用场景:
- 按编号排序的数据。
- 按日期排序的记录。
- 需要范围查询的数据。
TreeMap 基于红黑树实现,其 put()、get()、remove() 和 containsKey() 等操作具有 O(log n) 的时间成本。(Oracle Docs)
七、常用集合如何选择
7.1 选型思路
选择集合时,不要先背实现类,而应该先问业务需要什么能力:
| 业务需求 |
推荐接口 |
常用实现类 |
| 数据允许重复,并且需要下标访问 |
List |
ArrayList |
| 数据允许重复,经常在首尾操作 |
List / Deque |
LinkedList 或 ArrayDeque |
| 数据不能重复,不关心顺序 |
Set |
HashSet |
| 数据不能重复,同时保留插入顺序 |
Set |
LinkedHashSet |
| 数据不能重复,同时自动排序 |
Set |
TreeSet |
| 按先来后到处理任务 |
Queue |
ArrayDeque |
| 按优先级处理任务 |
Queue |
PriorityQueue |
| 根据唯一键快速查询值 |
Map |
HashMap |
| 根据键查询值,并保留插入顺序 |
Map |
LinkedHashMap |
| 根据键查询值,并按键排序 |
Map |
TreeMap |
7.2 最常见的默认选择
在普通业务开发中,可以先记住以下组合:
1 2 3 4
| List<String> list = new ArrayList<>(); Set<String> set = new HashSet<>(); Queue<String> queue = new ArrayDeque<>(); Map<String, String> map = new HashMap<>();
|
当业务明确提出“需要保持顺序”“需要自动排序”“需要优先级处理”等要求时,再更换为对应实现类。
八、测试代码
下面的完整代码演示以下内容:
ArrayList 的增删改查与重复元素。
LinkedList 作为普通列表使用。
HashSet、LinkedHashSet、TreeSet 的区别。
ArrayDeque 作为队列和栈使用。
PriorityQueue 按任务优先级出队。
HashMap、LinkedHashMap、TreeMap 的区别。
代码兼容 JDK 8 及以上版本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
| import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.PriorityQueue; import java.util.Queue; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet;
public class CollectionFrameworkDemo {
public static void main(String[] args) { testList(); testSet(); testQueueAndDeque(); testPriorityQueue(); testMap(); }
private static void testList() { System.out.println("===== 1. List:有序、可重复、可按下标访问 =====");
List<String> shoppingCart = new ArrayList<>();
shoppingCart.add("Java书"); shoppingCart.add("键盘"); shoppingCart.add("Java书"); shoppingCart.add(1, "鼠标");
System.out.println("ArrayList 初始内容:" + shoppingCart); System.out.println("下标为 1 的商品:" + shoppingCart.get(1));
shoppingCart.set(1, "无线鼠标"); shoppingCart.remove("键盘");
System.out.println("修改和删除后:" + shoppingCart);
List<String> recentPages = new LinkedList<>(); recentPages.add("首页"); recentPages.add("课程页"); recentPages.add("订单页");
System.out.println("LinkedList 作为普通列表:" + recentPages); System.out.println(); }
private static void testSet() { System.out.println("===== 2. Set:元素不重复,具体实现决定顺序 =====");
List<String> tags = Arrays.asList( "Java", "MySQL", "Java", "Redis", "MySQL" );
Set<String> hashSet = new HashSet<>(tags); System.out.println("HashSet 去重结果(顺序不保证):" + hashSet);
Set<String> linkedHashSet = new LinkedHashSet<>(tags); System.out.println("LinkedHashSet 去重并保留插入顺序:" + linkedHashSet);
Set<String> treeSet = new TreeSet<>(tags); System.out.println("TreeSet 去重并按自然顺序排序:" + treeSet);
System.out.println(); }
private static void testQueueAndDeque() { System.out.println("===== 3. Queue / Deque:排队处理与栈操作 =====");
Queue<String> printQueue = new ArrayDeque<>();
printQueue.offer("文档A"); printQueue.offer("文档B"); printQueue.offer("文档C");
System.out.println("队首但不移除 peek():" + printQueue.peek());
while (!printQueue.isEmpty()) { System.out.println("打印完成:" + printQueue.poll()); }
System.out.println("空队列 poll() 返回:" + printQueue.poll());
Deque<String> browserBackStack = new ArrayDeque<>();
browserBackStack.push("首页"); browserBackStack.push("集合框架文章"); browserBackStack.push("ArrayList 文章");
System.out.println("当前页面 peek():" + browserBackStack.peek()); System.out.println("返回上一页,弹出:" + browserBackStack.pop()); System.out.println("返回后所在页面:" + browserBackStack.peek());
System.out.println(); }
private static void testPriorityQueue() { System.out.println("===== 4. PriorityQueue:按优先级处理任务 =====");
Queue<Task> tasks = new PriorityQueue<>( Comparator.comparingInt(Task::getPriority).reversed() );
tasks.offer(new Task("修复线上故障", 100)); tasks.offer(new Task("回复普通邮件", 10)); tasks.offer(new Task("发布版本", 80));
while (!tasks.isEmpty()) { System.out.println("处理任务:" + tasks.poll()); }
System.out.println(); }
private static void testMap() { System.out.println("===== 5. Map:通过唯一键查找值 =====");
Map<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("张三", 90); scoreMap.put("李四", 86); scoreMap.put("张三", 95);
System.out.println("张三最新成绩:" + scoreMap.get("张三")); System.out.println("王五成绩,不存在时给默认值:" + scoreMap.getOrDefault("王五", 0));
for (Map.Entry<String, Integer> entry : scoreMap.entrySet()) { System.out.println( "学生=" + entry.getKey() + ",成绩=" + entry.getValue() ); }
Map<String, Integer> insertionOrdered = new LinkedHashMap<>();
insertionOrdered.put("first", 1); insertionOrdered.put("second", 2); insertionOrdered.put("third", 3);
System.out.println("LinkedHashMap 插入顺序:" + insertionOrdered.keySet());
Map<Integer, String> ranking = new TreeMap<>();
ranking.put(3, "铜牌"); ranking.put(1, "金牌"); ranking.put(2, "银牌");
System.out.println("TreeMap 按 key 排序:" + ranking); }
private static class Task { private final String name; private final int priority;
private Task(String name, int priority) { this.name = name; this.priority = priority; }
private int getPriority() { return priority; }
@Override public String toString() { return name + "(优先级=" + priority + ")"; } } }
|
运行后可以观察到:
1 2 3 4 5 6 7 8
| List 中可以同时保存两个“Java书” Set 会自动去除重复标签 Queue 会按照 文档A → 文档B → 文档C 的顺序处理 Deque 作为栈时,最后访问的页面最先弹出 PriorityQueue 会先处理“修复线上故障” Map 中张三的成绩会从 90 被更新为 95 LinkedHashMap 会保持 first、second、third 的插入顺序 TreeMap 会按照 1、2、3 的键顺序输出
|
需要注意:
1 2
| HashSet 和 HashMap 的遍历输出顺序不固定, 不要在代码中依赖它们的打印顺序。
|
九、常见易混知识点
9.1 Collection 与 Collections 不一样
Collection 是集合接口体系的根接口之一:
1
| Collection<String> collection;
|
Collections 是工具类,提供排序、反转、查找最大值等静态方法:
1 2 3
| Collections.sort(list); Collections.reverse(list); Collections.max(list);
|
可以简单记忆为:
1 2
| Collection 是容器体系 Collections 是操作容器的工具箱
|
9.2 Map 不属于 Collection 子接口
虽然 Map 也属于 Java 集合框架,但它保存的是键值对,而 Collection 保存的是单个元素。
1 2
| List<String> list = new ArrayList<>(); Map<String, Integer> map = new HashMap<>();
|
它们属于两条不同的体系。
总结
Java 集合框架的核心不是记住大量类名,而是理解不同容器解决的问题:
List:保存有顺序、允许重复的数据。
Set:保存不能重复的数据。
Queue:保存等待处理的数据。
Deque:支持两端操作,也可以模拟栈。
Map:通过唯一键快速查找对应值。
在常见业务开发中,可以优先掌握:
1 2 3 4
| ArrayList HashSet ArrayDeque HashMap
|
当业务进一步要求保持插入顺序、自动排序或优先级处理时,再选择:
1 2 3 4 5
| LinkedHashSet TreeSet PriorityQueue LinkedHashMap TreeMap
|
理解“需要什么能力,再选择什么容器”,比单纯背诵集合类更加重要。