Java 集合框架与数据结构
本文系统讲解 Java 集合框架与常用数据结构,覆盖 Collection、List、Set、Map、Queue、泛型、迭代遍历、去重、排序和常见实现类的使用场景。文章结合业务案例说明不同集合的特点,帮助读者在实际开发中正确选择数据容器。
一、基础与总览
1. 为什么要学习 Java 集合?
在 Java 开发中,集合几乎无处不在。
比如:
- 查询数据库后,需要用
List接收多条数据 - 需要根据用户 ID 快速找到用户信息,可以用
Map - 需要去重,可以用
Set - 需要排队处理任务,可以用
Queue - 需要线程安全的数据结构,可以用
ConcurrentHashMap
简单来说:
数组适合存放固定数量的数据,而集合适合存放动态变化的数据。
数组的长度一旦创建就固定了:
int[] arr = new int[3];
如果后面想放第 4 个元素,就需要创建新数组再复制数据,比较麻烦。
而集合可以动态扩容:
List<String> list = new ArrayList<>();
list.add("张三");
list.add("李四");
list.add("王五");
集合的核心价值就是:
帮我们更方便、更高效地管理一组对象。
2. Java 集合体系总览
Java 集合主要分为两大类:
Collection体系:单列集合,每个元素是一个独立对象Map体系:双列集合,每个元素是一个键值对key-value
1. Collection 体系
Collection 是单列集合的根接口。
它下面主要有三大分支:
Collection
├── List
│ ├── ArrayList
│ ├── LinkedList
│ └── Vector
│
├── Set
│ ├── HashSet
│ ├── LinkedHashSet
│ └── TreeSet
│
└── Queue
├── LinkedList
├── PriorityQueue
└── ArrayDeque
2. Map 体系
Map 是键值对集合的根接口。
Map
├── HashMap
├── LinkedHashMap
├── TreeMap
├── Hashtable
└── ConcurrentHashMap
3. Collection 接口
1. Collection 是什么?
Collection 表示一组对象。
它定义了集合最基础的操作,例如:
boolean add(E e); // 添加元素
boolean remove(Object o); // 删除元素
boolean contains(Object o);// 判断是否包含元素
int size(); // 获取元素个数
boolean isEmpty(); // 判断是否为空
void clear(); // 清空集合
Iterator<E> iterator(); // 获取迭代器
2. 示例
Collection<String> collection = new ArrayList<>();
collection.add("Java");
collection.add("MySQL");
collection.add("Redis");
System.out.println(collection.size());
System.out.println(collection.contains("Java"));
collection.remove("Redis");
for (String item : collection) {
System.out.println(item);
}
注意:
实际开发中,我们很少直接使用 Collection 类型创建对象,更多是使用它的子接口,比如 List、Set、Queue。
二、List 体系
1. List 集合
1. List 的特点
List 是有序、可重复的集合。
这里的“有序”指的是:
元素按照添加顺序保存。
例如:
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
遍历时通常也是:
A B C
List 允许重复元素:
list.add("A");
list.add("A");
最终集合中可以有两个 A。
2. List 常用方法
void add(int index, E element); // 在指定位置添加元素
E get(int index); // 根据下标获取元素
E set(int index, E element); // 修改指定位置元素
E remove(int index); // 删除指定位置元素
int indexOf(Object o); // 查找元素第一次出现的位置
示例:
List<String> list = new ArrayList<>();
list.add("Java");
list.add("MySQL");
list.add("Redis");
System.out.println(list.get(0)); // Java
list.set(1, "Oracle");
list.remove(2);
System.out.println(list);
2. ArrayList 深入理解
1. ArrayList 是什么?
ArrayList 是最常用的 List 实现类。
底层是数组。
可以简单理解为:
ArrayList = 可以自动扩容的数组。
2. ArrayList 的特点
| 特点 | 说明 |
|---|---|
| 底层结构 | 数组 |
| 是否有序 | 有序 |
| 是否允许重复 | 允许 |
| 是否允许 null | 允许 |
| 查询速度 | 快 |
| 增删速度 | 中间位置较慢 |
| 线程安全 | 不安全 |
3. 为什么 ArrayList 查询快?
因为它底层是数组。
数组支持通过下标直接访问元素:
list.get(3);
底层大致相当于:
elementData[3];
数组通过下标定位元素非常快,时间复杂度是:
O(1)
4. 为什么 ArrayList 中间插入和删除慢?
假设有一个数组:
[A, B, C, D, E]
如果要在 B 后面插入 X:
[A, B, X, C, D, E]
那么 C、D、E 都要往后移动。
删除也类似。
所以在中间位置插入或删除元素时,ArrayList 需要移动大量元素,效率较低。
5. ArrayList 扩容机制
ArrayList 底层数组容量不够时,会自动扩容。
常见理解方式:
原数组容量不够
↓
创建一个更大的新数组
↓
把旧数组元素复制过去
↓
新元素加入新数组
在 JDK 8 中,ArrayList 默认第一次添加元素时会创建容量为 10 的数组,后续扩容大约是原容量的 1.5 倍。
示例:
10 → 15 → 22 → 33 → 49 ...
注意:
扩容本身是有成本的,因为需要复制数组。所以如果你提前知道大概数据量,可以指定初始容量。
List<String> list = new ArrayList<>(1000);
这可以减少扩容次数,提高性能。
6. ArrayList 使用场景
适合:
- 查询多
- 遍历多
- 尾部添加多
- 中间插入删除少
不适合:
- 频繁在中间插入
- 频繁在中间删除
- 多线程并发修改
3. LinkedList 深入理解
1. LinkedList 是什么?
LinkedList 也是 List 的实现类。
但它底层不是数组,而是双向链表。
可以简单理解为:
LinkedList = 一节一节连起来的数据结构。
每个节点大致包含三部分:
前一个节点地址 | 当前数据 | 后一个节点地址
2. LinkedList 的特点
| 特点 | 说明 |
|---|---|
| 底层结构 | 双向链表 |
| 是否有序 | 有序 |
| 是否允许重复 | 允许 |
| 查询速度 | 较慢 |
| 头尾增删 | 快 |
| 中间查找 | 慢 |
| 线程安全 | 不安全 |
3. 为什么 LinkedList 查询慢?
数组可以通过下标直接定位:
array[100]
但是链表不能直接定位第 100 个节点。
它需要从头或尾一个一个找。
所以查询效率通常是:
O(n)
4. 为什么 LinkedList 头尾增删快?
因为链表添加或删除节点,只需要修改节点之间的引用关系。
例如删除节点 B:
A <-> B <-> C
删除后:
A <-> C
只需要把 A 和 C 连接起来即可。
5. LinkedList 常用方法
LinkedList<String> list = new LinkedList<>();
list.addFirst("A");
list.addLast("B");
System.out.println(list.getFirst());
System.out.println(list.getLast());
list.removeFirst();
list.removeLast();
6. LinkedList 使用场景
适合:
- 频繁在头部或尾部插入删除
- 实现队列
- 实现栈
不适合:
- 频繁随机访问元素
- 大量根据下标查询
4. ArrayList 和 LinkedList 对比
| 对比项 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 数组 | 双向链表 |
| 查询 | 快,O(1) | 慢,O(n) |
| 尾部添加 | 快 | 快 |
| 中间插入 | 慢,需要移动元素 | 找到位置后快,但查找位置慢 |
| 删除元素 | 慢,需要移动元素 | 找到位置后快 |
| 内存占用 | 较少 | 较多,因为每个节点要存前后引用 |
| 使用频率 | 非常高 | 相对较低 |
实际开发建议:
大多数情况下优先使用 ArrayList。
只有在明确需要频繁头尾操作,或者需要队列/栈结构时,再考虑 LinkedList 或 ArrayDeque。
三、Set 体系
1. Set 集合
1. Set 的特点
Set 是无重复元素的集合。
它的核心特点是:
不允许重复
示例:
Set<String> set = new HashSet<>();
set.add("Java");
set.add("Java");
set.add("MySQL");
System.out.println(set);
结果中只会有一个 Java。
2. Set 常见实现类
| 实现类 | 特点 |
|---|---|
| HashSet | 无序、不重复、查询快 |
| LinkedHashSet | 按添加顺序保存、不重复 |
| TreeSet | 自动排序、不重复 |
2. HashSet 深入理解
1. HashSet 是什么?
HashSet 是最常用的 Set 实现类。
它底层其实是基于 HashMap 实现的。
可以简单理解为:
HashSet 只使用了 HashMap 的 key,不关心 value。
例如:
Set<String> set = new HashSet<>();
set.add("Java");
底层大致类似:
map.put("Java", 一个固定对象);
2. HashSet 如何判断重复?
HashSet 判断两个对象是否重复,主要依赖两个方法:
hashCode()
equals()
判断过程可以简单理解为:
先比较 hashCode
如果 hashCode 不同,认为不是同一个对象
如果 hashCode 相同,再用 equals 比较
如果 equals 为 true,认为重复
3. 示例:自定义对象去重失败
class Student {
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
Set<Student> set = new HashSet<>();
set.add(new Student("张三", 18));
set.add(new Student("张三", 18));
System.out.println(set.size());
结果可能是:
2
为什么?
因为两个 new Student("张三", 18) 是两个不同对象。
如果没有重写 equals() 和 hashCode(),默认比较的是对象地址。
4. 正确做法:重写 equals 和 hashCode
import java.util.Objects;
class Student {
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
这样再放入 HashSet:
Set<Student> set = new HashSet<>();
set.add(new Student("张三", 18));
set.add(new Student("张三", 18));
System.out.println(set.size()); // 1
5. HashSet 使用场景
适合:
- 去重
- 判断元素是否存在
- 不关心顺序
例如:
List<String> names = Arrays.asList("张三", "李四", "张三", "王五");
Set<String> uniqueNames = new HashSet<>(names);
System.out.println(uniqueNames);
3. LinkedHashSet
1. LinkedHashSet 是什么?
LinkedHashSet 是 HashSet 的子类。
它既能去重,又能保留添加顺序。
Set<String> set = new LinkedHashSet<>();
set.add("B");
set.add("A");
set.add("C");
set.add("A");
System.out.println(set);
输出顺序通常是:
[B, A, C]
2. 使用场景
适合:
- 需要去重
- 又要保留原始添加顺序
例如:
List<String> list = Arrays.asList("A", "B", "A", "C", "B");
List<String> result = new ArrayList<>(new LinkedHashSet<>(list));
System.out.println(result); // [A, B, C]
这是一种常见的“List 去重并保序”的写法。
4. TreeSet
1. TreeSet 是什么?
TreeSet 是可以自动排序的 Set。
它底层基于红黑树实现。
特点:
不重复 + 自动排序
示例:
Set<Integer> set = new TreeSet<>();
set.add(3);
set.add(1);
set.add(2);
System.out.println(set);
输出:
[1, 2, 3]
2. TreeSet 排序规则
TreeSet 有两种排序方式:
- 自然排序:元素类实现
Comparable - 比较器排序:创建 TreeSet 时传入
Comparator
3. 自然排序 Comparable
class Student implements Comparable<Student> {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Student other) {
return this.age - other.age;
}
@Override
public String toString() {
return name + ":" + age;
}
}
使用:
Set<Student> set = new TreeSet<>();
set.add(new Student("张三", 20));
set.add(new Student("李四", 18));
set.add(new Student("王五", 22));
System.out.println(set);
4. 比较器排序 Comparator
Set<Student> set = new TreeSet<>((s1, s2) -> s1.getAge() - s2.getAge());
更推荐写法:
Set<Student> set = new TreeSet<>(Comparator.comparingInt(Student::getAge));
如果年龄相同,还想按姓名排序:
Set<Student> set = new TreeSet<>(
Comparator.comparingInt(Student::getAge)
.thenComparing(Student::getName)
);
5. TreeSet 注意点
TreeSet 判断重复不是依赖 equals(),而是依赖比较结果。
如果比较结果是 0,TreeSet 就认为两个元素重复。
例如只按年龄比较:
Comparator.comparingInt(Student::getAge)
那么两个年龄相同的学生会被认为重复,即使名字不同。
所以排序规则一定要写完整。
四、Map 体系
1. Map 集合
1. Map 是什么?
Map 是键值对集合。
一个元素由两部分组成:
key -> value
例如:
1001 -> 张三
1002 -> 李四
1003 -> 王五
在 Java 中:
Map<Integer, String> map = new HashMap<>();
map.put(1001, "张三");
map.put(1002, "李四");
map.put(1003, "王五");
2. Map 的特点
- key 不允许重复
- value 可以重复
- 一个 key 对应一个 value
- 如果 key 重复,新的 value 会覆盖旧的 value
示例:
Map<String, String> map = new HashMap<>();
map.put("name", "张三");
map.put("name", "李四");
System.out.println(map.get("name")); // 李四
3. Map 常用方法
V put(K key, V value); // 添加或修改
V get(Object key); // 根据 key 获取 value
V remove(Object key); // 根据 key 删除
boolean containsKey(Object key);// 是否包含 key
boolean containsValue(Object value);// 是否包含 value
Set<K> keySet(); // 获取所有 key
Collection<V> values(); // 获取所有 value
Set<Map.Entry<K,V>> entrySet(); // 获取所有键值对
2. HashMap 深入理解
1. HashMap 是什么?
HashMap 是最常用的 Map 实现类。
它的核心特点是:
根据 key 快速存取 value
示例:
Map<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("张三", 90);
scoreMap.put("李四", 85);
scoreMap.put("王五", 95);
System.out.println(scoreMap.get("张三"));
2. HashMap 底层结构
在 JDK 8 之后,HashMap 底层结构可以理解为:
数组 + 链表 + 红黑树
存储过程简化理解
当执行:
map.put("name", "张三");
大致流程是:
1. 计算 key 的 hash 值
2. 根据 hash 值计算数组下标
3. 如果该位置为空,直接放入
4. 如果该位置已有元素,说明发生 hash 冲突
5. 冲突时用链表或红黑树保存多个元素
3. 什么是 hash 冲突?
HashMap 底层有一个数组。
不同的 key 经过 hash 计算后,有可能落到同一个数组位置。
这就叫 hash 冲突。
例如:
key1 -> 下标 3
key2 -> 下标 3
它们都想放到数组下标 3 的位置。
HashMap 的解决方式是:
数组位置上挂链表
链表过长后转成红黑树
4. 为什么链表会转红黑树?
如果很多元素都落在同一个数组位置上,链表会变得很长。
链表查询需要一个一个找,效率是:
O(n)
红黑树查询效率更高,通常是:
O(log n)
所以 JDK 8 以后,当链表达到一定长度并且数组容量足够时,链表会转成红黑树。
常见面试点:
链表长度 >= 8 且数组容量 >= 64 时,链表转红黑树。
如果数组容量还比较小,HashMap 会优先扩容,而不是直接树化。
5. HashMap 扩容机制
HashMap 默认初始容量通常是 16,负载因子是 0.75。
扩容阈值:
容量 × 负载因子
默认情况下:
16 × 0.75 = 12
也就是说,当元素数量超过 12 时,HashMap 会扩容。
扩容通常变为原来的 2 倍:
16 → 32 → 64 → 128 ...
6. 为什么 HashMap 容量通常是 2 的幂?
HashMap 根据 hash 值计算下标时,常用类似下面的方式:
index = (n - 1) & hash;
其中 n 是数组长度。
当 n 是 2 的幂时,使用位运算可以更高效,并且能让元素分布更均匀。
7. HashMap 的 key 为什么要重写 equals 和 hashCode?
如果用自定义对象作为 HashMap 的 key,必须正确重写:
equals()
hashCode()
否则可能出现“看起来相同的 key,却取不到 value”的问题。
错误示例:
class User {
String name;
public User(String name) {
this.name = name;
}
}
Map<User, String> map = new HashMap<>();
map.put(new User("张三"), "管理员");
System.out.println(map.get(new User("张三"))); // null
原因:
两个 new User("张三") 是两个不同对象,默认比较地址。
正确做法是重写 equals() 和 hashCode()。
3. LinkedHashMap
1. LinkedHashMap 是什么?
LinkedHashMap 是 HashMap 的子类。
它在 HashMap 的基础上维护了一个双向链表,所以可以保留顺序。
默认保留插入顺序。
Map<String, Integer> map = new LinkedHashMap<>();
map.put("B", 2);
map.put("A", 1);
map.put("C", 3);
System.out.println(map.keySet());
输出:
[B, A, C]
2. 使用场景
适合:
- 需要 key-value 存储
- 又希望保留插入顺序
例如:
Map<String, Object> result = new LinkedHashMap<>();
result.put("code", 200);
result.put("message", "success");
result.put("data", null);
这样输出 JSON 时字段顺序更可控。
4. TreeMap
1. TreeMap 是什么?
TreeMap 是可以根据 key 自动排序的 Map。
底层基于红黑树。
示例:
Map<Integer, String> map = new TreeMap<>();
map.put(3, "C");
map.put(1, "A");
map.put(2, "B");
System.out.println(map);
输出:
{1=A, 2=B, 3=C}
2. TreeMap 使用场景
适合:
- 需要按 key 排序
- 需要范围查询
例如:
TreeMap<Integer, String> map = new TreeMap<>();
map.put(90, "优秀");
map.put(60, "及格");
map.put(0, "不及格");
System.out.println(map.ceilingEntry(70));
System.out.println(map.floorEntry(70));
5. HashMap、Hashtable、ConcurrentHashMap 对比
1. Hashtable
Hashtable 是比较老的类。
特点:
- 线程安全
- 方法上使用
synchronized - 性能较低
- 不允许 null key 和 null value
现在实际开发中一般不推荐使用。
2. HashMap
特点:
- 非线程安全
- 性能高
- 允许一个 null key
- 允许多个 null value
适合普通单线程场景。
3. ConcurrentHashMap
特点:
- 线程安全
- 并发性能比 Hashtable 更好
- 不允许 null key 和 null value
适合多线程并发场景。
4. 对比表
| 类 | 线程安全 | 是否允许 null | 性能 | 使用建议 |
|---|---|---|---|---|
| HashMap | 不安全 | 允许 | 高 | 单线程常用 |
| Hashtable | 安全 | 不允许 | 低 | 不推荐 |
| ConcurrentHashMap | 安全 | 不允许 | 高 | 多线程推荐 |
五、Queue 与双端队列
1. Queue 队列
1. Queue 是什么?
Queue 表示队列。
队列的特点通常是:
先进先出 FIFO
就像排队买票:
先来的人先买
2. Queue 常用方法
| 方法 | 异常版 | 安全版 |
|---|---|---|
| 添加元素 | add(e) | offer(e) |
| 获取并删除队头 | remove() | poll() |
| 获取但不删除队头 | element() | peek() |
更推荐使用安全版:
offer()
poll()
peek()
示例:
Queue<String> queue = new LinkedList<>();
queue.offer("A");
queue.offer("B");
queue.offer("C");
System.out.println(queue.poll()); // A
System.out.println(queue.poll()); // B
System.out.println(queue.peek()); // C
2. Deque 双端队列
1. Deque 是什么?
Deque 是双端队列。
它允许从队头和队尾两端添加、删除元素。
常用实现类:
ArrayDeque
LinkedList
2. 使用 ArrayDeque 实现栈
栈的特点:
后进先出 LIFO
示例:
Deque<String> stack = new ArrayDeque<>();
stack.push("A");
stack.push("B");
stack.push("C");
System.out.println(stack.pop()); // C
System.out.println(stack.pop()); // B
现在如果需要栈结构,更推荐使用 ArrayDeque,而不是老的 Stack 类。
六、遍历、工具与高级特性
1. 集合遍历方式
1. 普通 for 循环
适合 List,因为 List 有下标。
List<String> list = Arrays.asList("A", "B", "C");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
2. 增强 for 循环
适合大多数集合。
for (String item : list) {
System.out.println(item);
}
本质上是使用迭代器。
3. Iterator 迭代器
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
System.out.println(item);
}
4. Lambda 遍历
list.forEach(item -> System.out.println(item));
可以简写为:
list.forEach(System.out::println);
5. Map 遍历方式
方式一:遍历 keySet
Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
for (String key : map.keySet()) {
Integer value = map.get(key);
System.out.println(key + "=" + value);
}
方式二:遍历 entrySet
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
推荐使用 entrySet(),因为它可以同时拿到 key 和 value,效率更好。
方式三:Lambda
map.forEach((key, value) -> {
System.out.println(key + "=" + value);
});
2. 集合删除元素的常见坑
1. 错误写法:增强 for 中直接 remove
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for (String item : list) {
if ("B".equals(item)) {
list.remove(item);
}
}
这可能会抛出:
ConcurrentModificationException
原因:
增强 for 底层使用的是 Iterator,但你直接调用了集合本身的 remove(),破坏了迭代器的预期。
2. 正确写法一:使用 Iterator 删除
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("B".equals(item)) {
iterator.remove();
}
}
3. 正确写法二:removeIf
list.removeIf(item -> "B".equals(item));
这是实际开发中更简洁的写法。
3. 泛型 Generics
1. 为什么需要泛型?
没有泛型时,集合中可以放任何对象。
List list = new ArrayList();
list.add("Java");
list.add(123);
取出来时需要强制转换:
String s = (String) list.get(0);
如果类型转换错误,运行时才会报错。
泛型可以在编译期限制类型。
List<String> list = new ArrayList<>();
list.add("Java");
// list.add(123); // 编译错误
2. 泛型的好处
- 类型安全
- 减少强制类型转换
- 代码更清晰
- 编译期提前发现错误
3. 常见泛型写法
List<String> list = new ArrayList<>();
Map<String, Integer> map = new HashMap<>();
Set<Long> ids = new HashSet<>();
4. 通配符 ?
上界通配符
List<? extends Number>
表示:
可以接收 Number 或 Number 的子类集合
例如:
List<Integer> integers = new ArrayList<>();
List<? extends Number> numbers = integers;
适合读取。
下界通配符
List<? super Integer>
表示:
可以接收 Integer 或 Integer 的父类集合
适合写入。
5. PECS 原则
PECS 是泛型中一个重要原则:
Producer Extends, Consumer Super
意思是:
- 如果一个集合主要用于生产数据,也就是读取,用
extends - 如果一个集合主要用于消费数据,也就是写入,用
super
示例:
public void readNumbers(List<? extends Number> list) {
Number number = list.get(0);
}
public void writeIntegers(List<? super Integer> list) {
list.add(1);
}
4. Collections 工具类
Collections 是 Java 提供的集合工具类。
常用方法:
Collections.sort(list); // 排序
Collections.reverse(list); // 反转
Collections.shuffle(list); // 打乱
Collections.max(list); // 最大值
Collections.min(list); // 最小值
Collections.unmodifiableList(list); // 不可修改集合
示例:
List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 2));
Collections.sort(list);
System.out.println(list); // [1, 2, 3]
Collections.reverse(list);
System.out.println(list); // [3, 2, 1]
自定义排序:
List<Student> students = new ArrayList<>();
students.sort(Comparator.comparingInt(Student::getAge));
5. Arrays 工具类
Arrays 是数组工具类。
常用方法:
Arrays.asList();
Arrays.sort();
Arrays.toString();
Arrays.copyOf();
示例:
int[] arr = {3, 1, 2};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
Arrays.asList 的坑
List<String> list = Arrays.asList("A", "B", "C");
list.add("D");
会报错:
UnsupportedOperationException
原因:
Arrays.asList() 返回的不是普通 ArrayList,而是数组内部的固定长度 List。
正确写法:
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
list.add("D");
6. Stream 与集合
1. Stream 是什么?
Stream 是 Java 8 引入的一种处理集合数据的方式。
它可以让代码更简洁,更像是在描述“我要做什么”。
2. 常见操作
filter 过滤
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> result = nums.stream()
.filter(n -> n > 3)
.collect(Collectors.toList());
System.out.println(result); // [4, 5]
map 转换
List<String> names = Arrays.asList("zhangsan", "lisi");
List<String> upperNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
distinct 去重
List<Integer> result = nums.stream()
.distinct()
.collect(Collectors.toList());
sorted 排序
List<Integer> result = nums.stream()
.sorted()
.collect(Collectors.toList());
collect 收集
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());
joining 拼接字符串
当流中的元素本身就是字符串,或者可以先映射成字符串时,可以使用 Collectors.joining() 把它们拼成一个结果。
List<String> tags = Arrays.asList("Java", "Spring", "Redis");
String text = tags.stream()
.collect(Collectors.joining(", "));
System.out.println(text); // Java, Spring, Redis
也可以指定前缀和后缀:
String text = tags.stream()
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(text); // [Java, Spring, Redis]
它特别适合和 map、filter 组合使用,例如先筛选、再转换、最后拼接成展示文本。
groupingBy 分组
Map<Integer, List<Student>> groupMap = students.stream()
.collect(Collectors.groupingBy(Student::getAge));
toMap 转 Map
Map<Long, Student> studentMap = students.stream()
.collect(Collectors.toMap(Student::getId, student -> student));
如果 key 可能重复,需要指定合并规则:
Map<String, Student> map = students.stream()
.collect(Collectors.toMap(
Student::getName,
student -> student,
(oldValue, newValue) -> newValue
));
7. 集合的线程安全问题
1. 为什么会有线程安全问题?
例如多个线程同时操作同一个 ArrayList:
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(() -> list.add(1)).start();
}
可能出现:
- 数据丢失
- 数组越界
- 结果数量不正确
- 并发修改异常
因为 ArrayList 本身不是线程安全的。
2. 常见线程安全集合
| 场景 | 推荐 |
|---|---|
| 线程安全 List | CopyOnWriteArrayList |
| 线程安全 Set | CopyOnWriteArraySet |
| 线程安全 Map | ConcurrentHashMap |
| 阻塞队列 | ArrayBlockingQueue / LinkedBlockingQueue |
3. Collections.synchronizedList
List<String> list = Collections.synchronizedList(new ArrayList<>());
这种方式可以让集合变成线程安全,但并发性能一般。
4. CopyOnWriteArrayList
适合读多写少的场景。
写操作时会复制一份新数组,写完后再替换旧数组。
优点:
- 读操作不加锁
- 遍历安全
缺点:
- 写操作成本高
- 占用更多内存
5. ConcurrentHashMap
适合高并发 Map 场景。
常用示例:
Map<String, Integer> map = new ConcurrentHashMap<>();
map.put("A", 1);
七、实战与总结
1. 常见集合选择建议
1. 需要有序、可重复
选择:
ArrayList
2. 需要去重,不关心顺序
选择:
HashSet
3. 需要去重,并保留添加顺序
选择:
LinkedHashSet
4. 需要去重,并自动排序
选择:
TreeSet
5. 需要 key-value 存储
选择:
HashMap
6. 需要 key-value,并保留插入顺序
选择:
LinkedHashMap
7. 需要 key 自动排序
选择:
TreeMap
8. 多线程环境下使用 Map
选择:
ConcurrentHashMap
9. 栈或双端队列
选择:
ArrayDeque
2. 实际开发案例
案例一:List 去重
不要求顺序
List<String> list = Arrays.asList("A", "B", "A", "C");
Set<String> set = new HashSet<>(list);
System.out.println(set);
要求保留原顺序
List<String> list = Arrays.asList("A", "B", "A", "C");
List<String> result = new ArrayList<>(new LinkedHashSet<>(list));
System.out.println(result);
案例二:根据 ID 查询用户
Map<Long, User> userMap = new HashMap<>();
for (User user : users) {
userMap.put(user.getId(), user);
}
User user = userMap.get(1001L);
如果不用 Map,每次查找都要遍历 List,效率比较低。
案例三:统计单词出现次数
List<String> words = Arrays.asList("java", "mysql", "java", "redis", "java");
Map<String, Integer> countMap = new HashMap<>();
for (String word : words) {
countMap.put(word, countMap.getOrDefault(word, 0) + 1);
}
System.out.println(countMap);
案例四:按部门分组员工
Map<String, List<Employee>> groupMap = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
案例五:取出年龄大于 18 的用户
List<User> adults = users.stream()
.filter(user -> user.getAge() > 18)
.collect(Collectors.toList());
3. 总结
Java 集合的学习可以抓住一条主线:
List:有序、可重复
Set:去重
Map:键值对
Queue:队列
再进一步理解各个实现类的底层结构:
ArrayList:数组
LinkedList:链表
HashSet:HashMap 的 key
HashMap:数组 + 链表 + 红黑树
TreeSet / TreeMap:红黑树
ConcurrentHashMap:并发安全 Map
学习集合时,不要只背 API,而要多问自己:
- 它底层是什么结构?
- 为什么查询快或慢?
- 为什么能去重?
- 为什么要重写 equals 和 hashCode?
- 当前业务场景应该选哪个集合?
掌握这些之后,Java 集合就不只是“会用”,而是真正理解了。