集合框架是业务代码的「容器」;Spring 里 List/DTO、Map 缓存、Set 去重无处不在。
1. 集合体系概览
概念
Collection
├── List 有序、可重复
├── Set 不重复
└── Queue 队列
Map 键值对,非 Collection 子接口
常用实现:ArrayList、LinkedList、HashSet、HashMap。
原理
接口定义契约,实现类决定时间复杂度与迭代顺序。编程时面向接口声明:List<String> list = new ArrayList<>();
完整示例
package com.example.demo;
import java.util.*;
public class CollectionOverview {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Set<String> set = new HashSet<>();
Map<String, Integer> map = new HashMap<>();
list.add("a"); list.add("a");
set.add("a"); set.add("a");
map.put("a", 1); map.put("b", 2);
System.out.println(list.size()); // 2
System.out.println(set.size()); // 1
System.out.println(map.get("a"));
}
}
常见坑
- 仍用原始类型
List list— 失去泛型检查(见泛型节)。 - 在 foreach 里直接
list.remove()— 用Iterator.remove()或removeIf。
2. List:ArrayList vs LinkedList
概念
- ArrayList:动态数组,随机访问 O(1),中间插入删除 O(n)。
- LinkedList:双向链表,头尾操作方便,随机访问 O(n)。
原理
绝大多数业务读多写少、按下标访问 → 默认 ArrayList。队列/Deque 场景可用 ArrayDeque(往往优于 LinkedList)。
完整示例
package com.example.demo;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class ListDemo {
public static void main(String[] args) {
List<Integer> nums = new ArrayList<>();
nums.add(10);
nums.add(20);
nums.set(0, 15);
System.out.println(nums.get(1));
List<Integer> dequeAsList = new LinkedList<>();
((LinkedList<Integer>) dequeAsList).addFirst(1);
((LinkedList<Integer>) dequeAsList).addLast(2);
}
}
练习
合并两个已排序 List<Integer> 为一个排序列表(可先分别排序再双指针 merge)。
完整例子
package com.example.demo;
import java.util.ArrayList;
import java.util.List;
public class MergeSortedLists {
static List<Integer> merge(List<Integer> a, List<Integer> b) {
List<Integer> out = new ArrayList<>();
int i = 0, j = 0;
while (i < a.size() && j < b.size()) {
if (a.get(i) <= b.get(j)) out.add(a.get(i++));
else out.add(b.get(j++));
}
while (i < a.size()) out.add(a.get(i++));
while (j < b.size()) out.add(b.get(j++));
return out;
}
public static void main(String[] args) {
System.out.println(merge(List.of(1, 3, 5), List.of(2, 4, 6)));
}
}
常见坑
Arrays.asList(arr)返回固定大小列表,不能 add/remove。List.of(...)(9+)不可变,不能 add。
3. Set:HashSet 基础
概念
HashSet:基于 HashMap,元素唯一,无序(迭代顺序不稳定)。
LinkedHashSet:插入顺序。TreeSet:排序,元素需 Comparable 或传入 Comparator。
原理
依赖元素的 hashCode 与 equals:相等对象必须相同 hash(不等 hash 也可能碰撞,靠 equals 链)。
完整示例
package com.example.demo;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class SetDemo {
static class Tag {
final String name;
Tag(String name) { this.name = name; }
@Override public boolean equals(Object o) {
if (!(o instanceof Tag t)) return false;
return Objects.equals(name, t.name);
}
@Override public int hashCode() { return Objects.hash(name); }
}
public static void main(String[] args) {
Set<Tag> tags = new HashSet<>();
tags.add(new Tag("java"));
tags.add(new Tag("java"));
System.out.println(tags.size()); // 1
}
}
练习
去重:List<String> 去重保序 — 用 LinkedHashSet 或循环 + Set 判断。
完整例子
package com.example.demo;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
public class DedupList {
static List<String> dedup(List<String> input) {
return new ArrayList<>(new LinkedHashSet<>(input));
}
public static void main(String[] args) {
System.out.println(dedup(List.of("a", "b", "a", "c", "b"))); // [a, b, c]
}
}
常见坑
- 未重写
equals/hashCode的自定义类放进HashSet会「重复」插入。 - 修改已放入 Set 的、参与 hash 的字段 — 破坏集合不变性,禁止。
4. Map:HashMap 基础
概念
键值对;键唯一。HashMap 平均 O(1) get/put(JDK 8+ 链表+红黑树)。
原理
键的 hashCode → 桶下标;冲突链/树;equals 判同键。
null 键:HashMap 允许一个 null key(ConcurrentHashMap 不允许)。
完整示例
package com.example.demo;
import java.util.HashMap;
import java.util.Map;
public class MapDemo {
public static void main(String[] args) {
Map<Long, String> users = new HashMap<>();
users.put(1L, "alice");
users.put(2L, "bob");
if (users.containsKey(1L)) {
System.out.println(users.get(1L));
}
for (Map.Entry<Long, String> e : users.entrySet()) {
System.out.println(e.getKey() + " -> " + e.getValue());
}
users.putIfAbsent(3L, "carol");
users.merge(1L, " (vip)", (oldV, append) -> oldV + append);
}
}
练习
词频统计:字符串 "java is fun and java is popular" 拆词,Map<String,Integer> 计数。
Map cache:Map<Long, User> 模拟按 id 查用户,命中打 log、未命中「加载」再放入。
完整例子
package com.example.demo.exercise;
import java.util.HashMap;
import java.util.Map;
public class MapExercises {
record User(Long id, String name) {}
static Map<String, Integer> wordCount(String sentence) {
Map<String, Integer> map = new HashMap<>();
for (String w : sentence.toLowerCase().split("\\s+")) {
map.merge(w, 1, Integer::sum);
}
return map;
}
static final Map<Long, User> cache = new HashMap<>();
static User findById(Long id) {
User hit = cache.get(id);
if (hit != null) {
System.out.println("cache hit");
return hit;
}
System.out.println("load from db");
User loaded = new User(id, "user-" + id);
cache.put(id, loaded);
return loaded;
}
public static void main(String[] args) {
System.out.println(wordCount("java is fun and java is popular"));
findById(1L);
findById(1L); // 第二次应 hit
}
}
常见坑
map.get(key)不存在返回null,与「值为 null」混淆 — 用getOrDefault或containsKey。- 多线程共享
HashMap需ConcurrentHashMap,否则数据错乱。
5. equals 与 hashCode 契约
概念
若 a.equals(b),则 a.hashCode() == b.hashCode()。
重写 equals 必须重写 hashCode(IDE 可生成)。
原理
HashMap/HashSet 先用 hash 定位,再用 equals 精确比较。违反契约会导致「丢数据」「查不到」。
完整示例
package com.example.demo;
import java.util.Objects;
public class User {
private final Long id;
private String name;
public User(Long id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User user)) return false;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
练习
故意只写 equals 不写 hashCode,把两个 equal 的 User 放入 HashSet,观察 size。
完整例子
package com.example.demo;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class BrokenHashCodeDemo {
static class User {
final Long id;
User(Long id) { this.id = id; }
@Override public boolean equals(Object o) {
return o instanceof User u && Objects.equals(id, u.id);
}
// 故意不重写 hashCode → 默认用对象身份
}
public static void main(String[] args) {
Set<User> set = new HashSet<>();
set.add(new User(1L));
set.add(new User(1L));
System.out.println(set.size()); // 常为 2(equals 为 true 但 hash 不同)
}
}
常见坑
- 用可变字段参与
equals/hashCode(如name会变)导致 Set/Map 行为诡异 — 常用不可变 id。 - 继承体系下 equals 对称性破坏 — 考虑
getClass()比较或组合模式。
6. 泛型入门(Generics)
概念
编译期类型参数:List<String> 只能放 String;避免强转与部分 ClassCastException。
原理
类型擦除:运行时泛型信息大多被擦掉,List<String> 与 List<Integer> 运行时都是 List。
因此不能 new T()、不能 instanceof List<String>(可 instanceof List<?>)。
完整示例
package com.example.demo;
import java.util.ArrayList;
import java.util.List;
public class GenericsDemo {
static <T> T first(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
static void printAll(List<?> list) { // 通配符:只读场景
for (Object o : list) {
System.out.println(o);
}
}
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("alice");
String s = first(names);
List<Integer> nums = List.of(1, 2, 3);
printAll(nums);
}
}
练习
写泛型方法 swap(List<T> list, int i, int j)。
完整例子
package com.example.demo;
import java.util.ArrayList;
import java.util.List;
public class GenericSwap {
static <T> void swap(List<T> list, int i, int j) {
T tmp = list.get(i);
list.set(i, list.get(j));
list.set(j, tmp);
}
public static void main(String[] args) {
List<String> names = new ArrayList<>(List.of("a", "b", "c"));
swap(names, 0, 2);
System.out.println(names); // [c, b, a]
}
}
常见坑
- 原始类型与泛型混用产生 heap pollution 警告。
List<Object>不能接收List<String>(不变性)— 用List<? extends Number>等通配符(PECS 原则,进阶再学)。
7. 不可变集合(Java 9+)
概念
List.of()、Set.of()、Map.of() 创建不可变集合,适合常量配置。
完整示例
package com.example.demo;
import java.util.List;
import java.util.Map;
public class ImmutableCollections {
static final List<String> ROLES = List.of("ADMIN", "USER");
static final Map<String, Integer> ERROR_CODES = Map.of(
"NOT_FOUND", 404,
"BAD_REQUEST", 400
);
}
常见坑
List.of(a, b, c)不允许 null 元素。- 超过 10 个键值对用
Map.ofEntries。
本章小结
| 结构 | 选用 |
|---|---|
| 默认列表 | ArrayList |
| 去重 | HashSet / LinkedHashSet |
| 键值 | HashMap |
| 自定义键 | 必写 equals + hashCode |