Java 集合实战使用手册
一、集合框架全景图
Iterable
│
Collection
┌─────┼──────────┐
List Set Queue
│ │ │
┌──────┤ ┌──┴───┐ ┌──┴──────┐
ArrayList LinkedList │ PriorityQueue Deque
HashSet TreeSet │
│ ArrayDeque
LinkedHashSet
Map
┌────┼────┐
HashMap TreeMap Hashtable
│
LinkedHashMap
─── 线程安全版本 ───
ConcurrentHashMap
CopyOnWriteArrayList
CopyOnWriteArraySet
ConcurrentLinkedQueue
BlockingQueue (接口)
├── ArrayBlockingQueue
├── LinkedBlockingQueue
└── PriorityBlockingQueue
一句话记忆:Collection 管”一组对象”,Map 管”键值对”。选集合就是选数据结构。
二、场景选型指南
2.1 List 选型
| 场景 | 推荐 | 原因 |
|---|---|---|
| 90% 的业务场景 | ArrayList | 连续内存,随机访问 O(1),CPU 缓存友好 |
| 频繁在头部插入/删除 | LinkedList | 头部操作 O(1),但实际场景极少 |
| 多线程读多写少 | CopyOnWriteArrayList | 读无锁,写时复制 |
| 需要线程安全 + 高并发写 | Collections.synchronizedList 或并发队列 | 根据具体场景选择 |
⚠️ 真相:99% 的场景直接用 ArrayList。LinkedList 在实际项目中几乎不用,因为即使是中间插入,ArrayList 的数组拷贝也比 LinkedList 的指针遍历快(CPU 缓存行优势)。
2.2 Set 选型
| 场景 | 推荐 | 原因 |
|---|---|---|
| 去重(不关心顺序) | HashSet | O(1) 查找 |
| 去重 + 保持插入顺序 | LinkedHashSet | 维护插入顺序的链表 |
| 去重 + 排序 | TreeSet | 红黑树,自动排序 |
| 多线程去重 | ConcurrentHashMap.newKeySet() | JDK8+ 推荐方式 |
2.3 Map 选型
| 场景 | 推荐 | 原因 |
|---|---|---|
| 通用键值映射 | HashMap | O(1) 读写,最常用 |
| 需要按 key 排序 | TreeMap | 红黑树,key 有序 |
| 需要保持插入顺序 | LinkedHashMap | 维护双向链表 |
| 实现 LRU 缓存 | LinkedHashMap(accessOrder=true) | 自带 LRU 支持 |
| 高并发场景 | ConcurrentHashMap | 分段锁/CAS,线程安全 |
| 枚举作为 key | EnumMap | 数组实现,极致性能 |
⚠️ 永远不要用 Hashtable 和 Vector,它们是 JDK1.0 的遗留类,所有方法加 synchronized,性能差,已被淘汰。
2.4 Queue 选型
| 场景 | 推荐 | 原因 |
|---|---|---|
| 普通队列/栈 | ArrayDeque | 比 LinkedList 和 Stack 都快 |
| 优先级排序 | PriorityQueue | 最小堆实现 |
| 生产者-消费者模式 | LinkedBlockingQueue | 阻塞队列,线程安全 |
| 有界阻塞队列 | ArrayBlockingQueue | 固定容量,防 OOM |
| 延迟任务 | DelayQueue | 元素到期才能取出 |
三、企业级使用实战
3.1 ArrayList:业务数据处理
// 场景:从数据库查出用户列表,进行过滤和转换
public List<UserVO> getActiveUsers(Long departmentId) {
List<UserPO> users = userMapper.selectByDepartment(departmentId);
// ✅ 预估容量,避免扩容开销
List<UserVO> result = new ArrayList<>(users.size());
for (UserPO user : users) {
if (user.getStatus() == UserStatus.ACTIVE) {
result.add(convertToVO(user));
}
}
return result;
}
// ✅ Stream 写法(更简洁,企业中更常见)
public List<UserVO> getActiveUsersStream(Long departmentId) {
return userMapper.selectByDepartment(departmentId).stream()
.filter(u -> u.getStatus() == UserStatus.ACTIVE)
.map(this::convertToVO)
.collect(Collectors.toList()); // JDK16+ 可用 .toList()
}
3.2 HashMap:业务数据分组与缓存
// 场景:将订单按用户 ID 分组
public Map<Long, List<Order>> groupOrdersByUser(List<Order> orders) {
// ✅ 方式一:手动分组
Map<Long, List<Order>> map = new HashMap<>(orders.size() * 4 / 3 + 1);
// 为什么是 size * 4/3 + 1?因为 HashMap 默认负载因子 0.75
// 预设容量 = 期望元素数 / 0.75,避免 rehash
for (Order order : orders) {
map.computeIfAbsent(order.getUserId(), k -> new ArrayList<>())
.add(order);
}
return map;
// ✅ 方式二:Stream(推荐)
// return orders.stream().collect(Collectors.groupingBy(Order::getUserId));
}
// 场景:本地缓存(简单场景,不需要 Redis/Caffeine 时)
public class LocalConfigCache {
// ✅ ConcurrentHashMap 保证线程安全
private final Map<String, String> cache = new ConcurrentHashMap<>(256);
public String getConfig(String key) {
return cache.computeIfAbsent(key, k -> {
// 从数据库加载配置
return configMapper.selectByKey(k);
});
}
public void refresh(String key) {
cache.remove(key);
}
}
3.3 LinkedHashMap:实现 LRU 缓存
// 场景:用 LinkedHashMap 实现一个简单的 LRU 缓存
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int maxCapacity;
public LRUCache(int maxCapacity) {
// initialCapacity: 容量
// loadFactor: 0.75(默认)
// accessOrder: true —— 关键!按访问顺序排列,最近访问的在尾部
super(maxCapacity * 4 / 3 + 1, 0.75f, true);
this.maxCapacity = maxCapacity;
}
// 当 put 新元素后,如果超出容量,自动移除最老的元素(链表头部)
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxCapacity;
}
}
// 使用
LRUCache<String, Object> cache = new LRUCache<>(1000);
cache.put("user:1001", userObj);
Object val = cache.get("user:1001"); // 访问后,这个元素移到尾部,不会被淘汰
⚠️ 这个 LRU 是非线程安全的。多线程环境下请用
Collections.synchronizedMap()包装,或者直接用 Caffeine。
3.4 ConcurrentHashMap:并发计数器
// 场景:统计接口调用次数(高并发)
public class ApiCounter {
private final ConcurrentHashMap<String, LongAdder> counterMap = new ConcurrentHashMap<>();
// ✅ 原子性累加,无锁竞争
public void increment(String apiPath) {
counterMap.computeIfAbsent(apiPath, k -> new LongAdder()).increment();
}
public long getCount(String apiPath) {
LongAdder adder = counterMap.get(apiPath);
return adder == null ? 0 : adder.sum();
}
public Map<String, Long> getAllCounts() {
Map<String, Long> result = new HashMap<>(counterMap.size() * 4 / 3 + 1);
counterMap.forEach((k, v) -> result.put(k, v.sum()));
return result;
}
}
⚠️ 不要用
AtomicLong,高并发下 CAS 自旋严重。LongAdder通过分段累加大幅降低竞争。
3.5 PriorityQueue:任务优先级调度
// 场景:处理工单,优先级高的先处理
public class TicketScheduler {
// 按优先级排序,数字越小优先级越高
private final PriorityQueue<Ticket> queue = new PriorityQueue<>(
Comparator.comparingInt(Ticket::getPriority)
.thenComparing(Ticket::getCreateTime) // 同优先级按时间排
);
public void submit(Ticket ticket) {
queue.offer(ticket);
}
public Ticket takeNext() {
return queue.poll(); // 取出优先级最高的
}
}
3.6 ArrayDeque:替代 Stack
// ⚠️ 永远不要用 java.util.Stack(继承自 Vector,全方法加锁,还是 LIFO 接口设计错误)
// ✅ 用 ArrayDeque 作为栈
Deque<String> stack = new ArrayDeque<>();
stack.push("first");
stack.push("second");
String top = stack.pop(); // "second"
// ✅ 用 ArrayDeque 作为队列
Deque<String> queue = new ArrayDeque<>();
queue.offer("first");
queue.offer("second");
String head = queue.poll(); // "first"
四、常见陷阱与注意事项
4.1 ConcurrentModificationException
// ❌ 错误:遍历时直接删除
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : list) {
if ("b".equals(s)) {
list.remove(s); // 💥 ConcurrentModificationException
}
}
// ✅ 方案一:Iterator 删除
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if ("b".equals(it.next())) {
it.remove(); // 安全
}
}
// ✅ 方案二:removeIf(JDK8+,推荐)
list.removeIf("b"::equals);
// ✅ 方案三:Stream 过滤生成新集合
list = list.stream()
.filter(s -> !"b".equals(s))
.collect(Collectors.toList());
4.2 Arrays.asList 的坑
// ⚠️ Arrays.asList 返回的是固定大小的 List,不能 add/remove
List<String> list = Arrays.asList("a", "b", "c");
list.add("d"); // 💥 UnsupportedOperationException
list.remove("a"); // 💥 UnsupportedOperationException
list.set(0, "x"); // ✅ 这个可以,因为只是替换元素
// ✅ 正确方式:包装成真正的 ArrayList
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
// ✅ JDK9+ 推荐:List.of(注意返回的是不可变集合)
List<String> immutable = List.of("a", "b", "c");
// immutable.add("d"); // 💥 也不行
// ✅ JDK9+ 可变集合
List<String> mutable = new ArrayList<>(List.of("a", "b", "c"));
4.3 subList 的内存泄漏
// ⚠️ subList 返回的是原 List 的视图,不是新 List
List<Integer> bigList = new ArrayList<>(IntStream.range(0, 1000000)
.boxed().collect(Collectors.toList()));
// 这个 subList 持有对 bigList 的引用,bigList 无法被 GC
List<Integer> sub = bigList.subList(0, 10);
// ✅ 正确方式:拷贝一份
List<Integer> sub = new ArrayList<>(bigList.subList(0, 10));
// 现在 bigList 可以被 GC 了
4.4 HashMap 的 null 陷阱
// HashMap 允许 null key 和 null value
Map<String, String> map = new HashMap<>();
map.put(null, "value"); // ✅ 可以
map.put("key", null); // ✅ 可以
// ConcurrentHashMap 不允许 null key 和 null value
Map<String, String> cmap = new ConcurrentHashMap<>();
cmap.put(null, "value"); // 💥 NullPointerException
cmap.put("key", null); // 💥 NullPointerException
// ⚠️ TreeMap 不允许 null key(因为要比较大小),但允许 null value
4.5 equals 和 hashCode 的契约
// ⚠️ 如果你把自定义对象作为 HashMap 的 key 或放入 HashSet
// 必须同时重写 equals() 和 hashCode()
public class User {
private Long id;
private String name;
// ❌ 只重写 equals 不重写 hashCode:
// 两个相等的对象可能落在不同的 bucket,导致 HashMap 找不到
// ✅ 正确做法:同时重写
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
// ✅ 实际项目中直接用 Lombok:@EqualsAndHashCode(of = "id")
}
4.6 集合判空的正确姿势
// ❌ 容易 NPE
if (list.size() == 0) { ... }
if (list.isEmpty()) { ... } // list 为 null 时一样 NPE
// ✅ 防御性判空
if (list == null || list.isEmpty()) { ... }
// ✅ 使用工具类(企业中最常见)
// Spring
if (CollectionUtils.isEmpty(list)) { ... }
// Apache Commons
if (CollectionUtils.isEmpty(list)) { ... }
// Hutool
if (CollUtil.isEmpty(list)) { ... }
4.7 返回空集合而非 null
// ❌ 返回 null,调用方必须判空
public List<User> findUsers() {
if (noData) return null; // 调用方忘了判空就 NPE
}
// ✅ 返回空集合
public List<User> findUsers() {
if (noData) return Collections.emptyList(); // 不可变空集合,不占内存
// ...
}
// ✅ 返回空 Map
public Map<String, Object> getConfig() {
if (noConfig) return Collections.emptyMap();
// ...
}
五、集合与周边工具的配合使用
5.1 集合 + Stream API
// 实际业务中最常见的 Stream 操作组合
List<Order> orders = orderService.getOrders(userId);
// 过滤 + 排序 + 分页
List<OrderVO> result = orders.stream()
.filter(o -> o.getStatus() != OrderStatus.CANCELLED)
.sorted(Comparator.comparing(Order::getCreateTime).reversed())
.skip(pageNum * pageSize)
.limit(pageSize)
.map(this::toOrderVO)
.collect(Collectors.toList());
// 分组统计
Map<OrderStatus, Long> statusCount = orders.stream()
.collect(Collectors.groupingBy(Order::getStatus, Collectors.counting()));
// 提取 ID 集合(超高频操作)
Set<Long> userIds = orders.stream()
.map(Order::getUserId)
.collect(Collectors.toSet());
// toMap(注意 key 冲突处理)
Map<Long, Order> orderMap = orders.stream()
.collect(Collectors.toMap(
Order::getId,
Function.identity(),
(v1, v2) -> v1 // ⚠️ key 冲突时保留第一个,不写这个参数 key 冲突会抛异常
));
5.2 集合 + Collections 工具类
// 不可变集合
List<String> immutable = Collections.unmodifiableList(list);
// JDK9+: List.of(), Map.of(), Set.of()
// JDK10+: List.copyOf(), Map.copyOf()
// 线程安全包装
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, Object> syncMap = Collections.synchronizedMap(new HashMap<>());
// ⚠️ 遍历时仍需手动加锁,高并发场景直接用 ConcurrentHashMap
// 排序
Collections.sort(list); // 自然排序
Collections.sort(list, Comparator.reverseOrder()); // 倒序
list.sort(Comparator.comparing(User::getAge)); // JDK8+ 推荐用 List.sort
// 二分查找(前提:list 已排序)
int index = Collections.binarySearch(list, target);
// 打乱顺序(洗牌)
Collections.shuffle(list);
// 填充
Collections.fill(list, defaultValue);
5.3 集合 + Guava(Google 工具库)
// ✅ 不可变集合(性能更优,编译期安全)
ImmutableList<String> list = ImmutableList.of("a", "b", "c");
ImmutableMap<String, Integer> map = ImmutableMap.of("a", 1, "b", 2);
// ✅ Multimap:一个 key 对应多个 value
ListMultimap<String, String> multimap = ArrayListMultimap.create();
multimap.put("fruit", "apple");
multimap.put("fruit", "banana");
List<String> fruits = multimap.get("fruit"); // [apple, banana]
// 省去了 Map<String, List<String>> 的繁琐操作
// ✅ BiMap:双向映射
BiMap<String, Integer> biMap = HashBiMap.create();
biMap.put("one", 1);
biMap.inverse().get(1); // "one"
// ✅ Table:二维 Map(行+列→值)
Table<String, String, Integer> table = HashBasedTable.create();
table.put("张三", "数学", 90);
table.put("张三", "英语", 85);
table.row("张三"); // {数学=90, 英语=85}
// ✅ Sets/Lists 工具
Sets.difference(set1, set2); // 差集
Sets.intersection(set1, set2); // 交集
Sets.union(set1, set2); // 并集
Lists.partition(bigList, 100); // 按 100 个一组分批
5.4 集合 + 序列化(JSON)
// Jackson(Spring Boot 默认)
ObjectMapper mapper = new ObjectMapper();
// 集合 → JSON
String json = mapper.writeValueAsString(userList);
// JSON → 集合(注意泛型擦除问题)
// ❌ 错误:泛型信息丢失,反序列化后元素是 LinkedHashMap
List<User> users = mapper.readValue(json, List.class);
// ✅ 正确:使用 TypeReference 保留泛型
List<User> users = mapper.readValue(json, new TypeReference<List<User>>() {});
// ✅ 正确:使用 JavaType
JavaType type = mapper.getTypeFactory()
.constructCollectionType(List.class, User.class);
List<User> users = mapper.readValue(json, type);
六、集合使用速查表
| 操作 | 推荐写法 | 说明 |
|---|---|---|
| 创建并初始化 | new ArrayList<>(List.of(1,2,3)) | JDK9+ |
| 预设容量 | new ArrayList<>(expectedSize) | 避免扩容 |
| 判空 | CollectionUtils.isEmpty(list) | Spring 工具类 |
| 遍历删除 | list.removeIf(predicate) | JDK8+ |
| 集合转数组 | list.toArray(new String[0]) | 传空数组性能更优 |
| 数组转集合 | new ArrayList<>(Arrays.asList(arr)) | 可变集合 |
| Map 遍历 | map.forEach((k, v) -> ...) | JDK8+ |
| 安全取值 | map.getOrDefault(key, defaultVal) | 避免 null |
| 计算/缓存 | map.computeIfAbsent(key, k -> ...) | 惰性计算 |
| 合并值 | map.merge(key, val, Integer::sum) | 计数场景 |
| 不可变集合 | Collections.unmodifiableList(list) | 或 JDK9 List.of() |
| 线程安全 Map | ConcurrentHashMap | 别用 Hashtable |
| 线程安全 List | CopyOnWriteArrayList | 读多写少 |
| 栈 | new ArrayDeque<>() | 别用 Stack |