前言
在程序开发中,我们经常需要保存和处理一组数据,例如购物车中的商品、文章标签、排队等待执行的任务、学生姓名与成绩的对应关系。
数组虽然也能保存多个元素,但它的长度固定,增删元素不够灵活。java 集合框架就像一套准备好的“数据收纳工具箱”:不同的容器有不同的摆放规则,也适用于不同的业务场景。
java 集合框架主要分为两条路线:
collection:保存一个个独立元素,下面包括list、set、queue。map:保存键值对,例如“用户名 → 用户信息”。
需要特别注意:map 属于集合框架的一部分,但它不是 collection 的子接口。
一、为什么需要集合框架
1.1 数组的局限
数组适合保存数量比较固定的数据:
string[] names = new string[3]; names[0] = "张三"; names[1] = "李四"; names[2] = "王五";
但是,当数据数量不断变化时,数组会显得不够灵活:
- 数组长度创建后不能自动改变。
- 中间插入或删除元素比较麻烦。
- 查找、去重、排序等操作通常需要自己编写代码。
例如,一个购物车可能随时添加或删除商品;一个班级的签到名单也可能不断变化。这时,集合比数组更适合。
1.2 集合框架解决了什么问题
java 集合框架提供了统一的接口和常用实现类,使我们可以直接完成以下操作:
list.add("java");
list.remove("mysql");
set.contains("redis");
map.get("张三");
queue.poll();
可以把集合框架理解为不同用途的容器:
| 容器 | 形象理解 | 典型用途 |
|---|---|---|
| list | 可重复的排号座位表 | 购物车、学生名单 |
| set | 不允许重复入场的名单 | 标签去重、用户名检测 |
| queue | 排队窗口 | 打印任务、消息处理 |
| deque | 两头都能进出的通道 | 栈、撤销操作 |
| map | 按编号查资料的柜子 | 用户 id 查询、配置表 |
二、java 集合框架的整体结构
2.1 两条核心支线
java 集合框架的基础结构可以简化为:
iterable
|
collection
├── list
│ ├── arraylist
│ └── linkedlist
│
├── set
│ ├── hashset
│ ├── linkedhashset
│ └── treeset
│
└── queue
├── priorityqueue
└── deque
├── arraydeque
└── linkedlist
map
├── hashmap
├── linkedhashmap
└── treemap
其中:
list关注元素的排列位置。set关注元素不能重复。queue关注元素按照某种规则依次处理。map关注通过唯一的键查找对应的值。
2.2 接口与实现类
实际开发中,通常使用接口声明变量,再选择具体实现类创建对象:
list<string> list = new arraylist<>(); set<string> set = new hashset<>(); queue<string> queue = new arraydeque<>(); map<string, integer> map = new hashmap<>();
这种写法可以理解为:
- 左边决定“我需要什么功能”
- 右边决定“底层使用什么结构实现”
例如:
list<string> list = new arraylist<>();
表示当前业务需要的是“列表”功能,至于底层先使用动态数组实现。以后需要更换实现类时,业务代码受到的影响会更小。
三、list:有序、可重复的列表
3.1 list 的特点
list 表示一个有顺序的集合,元素可以重复,并且可以通过下标访问。
list<string> courses = new arraylist<>();
courses.add("java");
courses.add("mysql");
courses.add("java");
system.out.println(courses); // [java, mysql, java]
system.out.println(courses.get(1)); // mysql
这里第二次添加 "java" 不会被拦截,因为 list 允许重复元素。
适用场景:
- 购物车商品列表。
- 用户浏览记录。
- 按顺序展示的文章评论。
- 考试成绩列表。
list 官方定义强调了两个核心能力:元素具有顺序,并且可以按整数下标访问;它通常允许重复元素。(oracle docs)
3.2 arraylist:动态数组
arraylist 底层可以理解为一个能够自动扩容的数组。
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 底层由一个个相互连接的节点组成,每个节点知道前一个节点和后一个节点。
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 的核心特点是:不允许保存重复元素。
set<string> tags = new hashset<>();
tags.add("java");
tags.add("mysql");
tags.add("java");
system.out.println(tags.size()); // 2
第二次添加 "java" 时,集合不会重复保存它。
适用场景:
- 对文章标签去重。
- 判断用户名是否已经存在。
- 统计不重复的访问用户。
- 保存不能重复的编号。
需要特别注意:
set 的核心定义是“不重复”,而不是“所有实现类都无序”。
不同实现类对顺序的处理方式不同:
| 实现类 | 是否去重 | 顺序特点 |
|---|---|---|
hashset | 是 | 不保证遍历顺序 |
linkedhashset | 是 | 保留插入顺序 |
treeset | 是 | 按自然顺序或比较器排序 |
set 接口规定元素不能重复;hashset 不保证遍历顺序,而 linkedhashset 与 treeset 分别提供插入顺序和排序能力。(oracle docs)
4.2 hashset:快速去重
hashset 底层基于 hashmap 实现,适合快速判断某个元素是否存在。
set<string> uniquetags = new hashset<>();
uniquetags.add("java");
uniquetags.add("redis");
uniquetags.add("java");
system.out.println(uniquetags.contains("redis")); // true
system.out.println(uniquetags.size()); // 2
适用场景:
- 单词去重。
- 用户 id 去重。
- 快速判断数据是否已经处理过。
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。
set<string> tags = new linkedhashset<>();
tags.add("java");
tags.add("mysql");
tags.add("java");
tags.add("redis");
system.out.println(tags); // [java, mysql, redis]
适用场景:
- 对用户提交的数据去重,但保持展示顺序。
- 对搜索关键词去重,同时保留原始输入顺序。
4.4 treeset:去重并排序
treeset 会在去重的同时,对元素进行排序。
set<integer> scores = new treeset<>(); scores.add(90); scores.add(75); scores.add(88); scores.add(75); system.out.println(scores); // [75, 88, 90]
适用场景:
- 保存不重复且需要排序的分数。
- 保存有序编号。
- 查询某个范围内的数据。
treeset 基于 treemap,按照元素的自然顺序或指定比较器进行排序,其基础操作具有 o(log n) 的时间成本。(oracle docs)
五、queue 与 deque:等待处理的数据队列
5.1 queue:先进先出的排队结构
queue 表示队列,通常遵循先进先出原则,即先来的元素先处理。
queue<string> printqueue = new arraydeque<>();
printqueue.offer("文档a");
printqueue.offer("文档b");
printqueue.offer("文档c");
system.out.println(printqueue.poll()); // 文档a
system.out.println(printqueue.poll()); // 文档b
可以把队列理解为食堂排队:
入队:新同学站到队尾 出队:最前面的同学先打饭离开
适用场景:
- 打印任务队列。
- 消息消费队列。
- 请求排队处理。
- 广度优先搜索。
queue 常用方法有两组:
| 操作 | 失败时抛异常 | 失败时返回特殊值 |
|---|---|---|
| 插入元素 | add(e) | offer(e) |
| 删除队首 | remove() | poll() |
| 查看队首 | element() | peek() |
日常使用中,offer()、poll() 和 peek() 往往更安全,因为队列为空时不会直接抛出异常。queue 通常采用先进先出规则,但 priorityqueue 等实现类可以使用不同的出队规则。(oracle docs)
5.2 deque:两端都能操作的队列
deque 是双端队列,元素可以从头部加入或删除,也可以从尾部加入或删除。
deque<string> deque = new arraydeque<>();
deque.addfirst("b");
deque.addfirst("a");
deque.addlast("c");
system.out.println(deque); // [a, b, c]
deque 既可以模拟普通队列,也可以模拟栈。
作为队列使用
queue<string> queue = new arraydeque<>();
queue.offer("任务a");
queue.offer("任务b");
system.out.println(queue.poll()); // 任务a
作为栈使用
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 会按照优先级决定谁先出队。
queue<integer> queue = new priorityqueue<>(); queue.offer(80); queue.offer(100); queue.offer(60); system.out.println(queue.poll()); // 60
默认情况下,数字越小,越先出队。
如果希望“优先级数字越大,任务越重要”,可以提供比较器:
queue<integer> queue = new priorityqueue<>(comparator.reverseorder()); queue.offer(80); queue.offer(100); queue.offer(60); system.out.println(queue.poll()); // 100
适用场景:
- 线上故障优先处理。
- 医院急诊排队。
- 考试成绩排名处理。
- 定时任务调度。
需要注意:priorityqueue 只保证每次 poll() 取出的元素符合优先级规则,直接遍历整个队列时,不保证输出结果已经完全排好序。(oracle docs)
六、map:通过键查找值
6.1 map 的特点
map 保存的是键值对:
键 key → 值 value
例如:
学号 → 学生姓名 商品编号 → 商品详情 用户名 → 登录信息 配置名称 → 配置值
示例:
map<string, integer> scores = new hashmap<>();
scores.put("张三", 90);
scores.put("李四", 86);
system.out.println(scores.get("张三")); // 90
map 中的键不能重复。相同的键再次调用 put() 时,新值会替换旧值:
scores.put("张三", 95);
system.out.println(scores.get("张三")); // 95
可以把 map 理解为快递柜:
柜门编号是 key 柜门里面的包裹是 value 同一个柜门编号只能对应一个当前位置上的包裹
map 的官方定义是键到值的映射,一个键最多只能映射到一个值。(oracle docs)
6.2 hashmap:最常用的键值容器
hashmap 是日常开发中非常常见的 map 实现。
map<string, string> users = new hashmap<>();
users.put("1001", "张三");
users.put("1002", "李四");
system.out.println(users.get("1001")); // 张三
适用场景:
- 用户 id 查询用户信息。
- 商品编号查询商品详情。
- 单词出现次数统计。
- 缓存临时数据。
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")); // 2
hashmap 不保证遍历顺序;当哈希分布良好时,其 get() 与 put() 等基本操作通常具有常数时间性能。(oracle docs)
6.3 linkedhashmap:按插入顺序遍历
如果希望键值对按照加入顺序输出,可以使用 linkedhashmap。
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 会按照键的自然顺序或比较器规则进行排序。
map<integer, string> ranking = new treemap<>();
ranking.put(3, "铜牌");
ranking.put(1, "金牌");
ranking.put(2, "银牌");
system.out.println(ranking); // {1=金牌, 2=银牌, 3=铜牌}
适用场景:
- 按编号排序的数据。
- 按日期排序的记录。
- 需要范围查询的数据。
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 最常见的默认选择
在普通业务开发中,可以先记住以下组合:
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 及以上版本。
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;
/**
* java 集合框架综合测试:jdk 8+ 可运行。
*/
public class collectionframeworkdemo {
public static void main(string[] args) {
testlist();
testset();
testqueueanddeque();
testpriorityqueue();
testmap();
}
/**
* 测试 list:
* 1. 元素有顺序
* 2. 元素可以重复
* 3. 可以通过下标访问和修改元素
*/
private static void testlist() {
system.out.println("===== 1. list:有序、可重复、可按下标访问 =====");
list<string> shoppingcart = new arraylist<>();
shoppingcart.add("java书");
shoppingcart.add("键盘");
shoppingcart.add("java书"); // list 允许重复
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();
}
/**
* 测试 set:
* 1. 自动去重
* 2. 不同实现类具有不同的顺序特点
*/
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();
}
/**
* 测试 queue 和 deque:
* 1. queue 按先进先出处理元素
* 2. deque 可以作为栈使用
*/
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();
}
/**
* 测试 priorityqueue:
* 数值越大的任务,优先级越高,越先处理。
*/
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();
}
/**
* 测试 map:
* 1. key 不能重复
* 2. 相同 key 再次 put 会覆盖旧 value
* 3. 不同 map 实现类的顺序特点不同
*/
private static void testmap() {
system.out.println("===== 5. map:通过唯一键查找值 =====");
map<string, integer> scoremap = new hashmap<>();
scoremap.put("张三", 90);
scoremap.put("李四", 86);
scoremap.put("张三", 95); // 相同 key 会替换旧 value
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 + ")";
}
}
}
运行后可以观察到:
list 中可以同时保存两个“java书” set 会自动去除重复标签 queue 会按照 文档a → 文档b → 文档c 的顺序处理 deque 作为栈时,最后访问的页面最先弹出 priorityqueue 会先处理“修复线上故障” map 中张三的成绩会从 90 被更新为 95 linkedhashmap 会保持 first、second、third 的插入顺序 treemap 会按照 1、2、3 的键顺序输出
需要注意:
hashset 和 hashmap 的遍历输出顺序不固定, 不要在代码中依赖它们的打印顺序。
九、常见易混知识点
9.1 collection 与 collections 不一样
collection 是集合接口体系的根接口之一:
collection<string> collection;
collections 是工具类,提供排序、反转、查找最大值等静态方法:
collections.sort(list); collections.reverse(list); collections.max(list);
可以简单记忆为:
collection 是容器体系 collections 是操作容器的工具箱
9.2 map 不属于 collection 子接口
虽然 map 也属于 java 集合框架,但它保存的是键值对,而 collection 保存的是单个元素。
list<string> list = new arraylist<>(); map<string, integer> map = new hashmap<>();
它们属于两条不同的体系。
总结
java 集合框架的核心不是记住大量类名,而是理解不同容器解决的问题:
list:保存有顺序、允许重复的数据。set:保存不能重复的数据。queue:保存等待处理的数据。deque:支持两端操作,也可以模拟栈。map:通过唯一键快速查找对应值。
在常见业务开发中,可以优先掌握:
arraylist hashset arraydeque hashmap
当业务进一步要求保持插入顺序、自动排序或优先级处理时,再选择:
linkedhashset treeset priorityqueue linkedhashmap treemap
理解“需要什么能力,再选择什么容器”,比单纯背诵集合类更加重要。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论