📌 学习目标:深入理解Java集合框架的设计哲学,掌握List、Set、Map三大体系的核心实现原理与性能差异,学会在真实业务场景中做出最优选型决策。
🔑 重点 :ArrayList vs LinkedList 底层结构差异与适用边界 、HashSet/LinkedHashSet/TreeSet 去重机制与排序策略 、HashMap 扩容机制与线程安全问题 、Stream API 函数式遍历 、Collections工具类与并发集合简介。
一、集合框架全景图:为什么需要集合?
在Java中,数组的长度是固定的,无法动态扩容。当我们需要存储数量不确定的数据时,集合框架(Java Collections Framework)应运而生。它提供了一套标准化的数据结构,让我们能够高效地增删改查数据。
1.1 集合框架的继承体系
Java集合框架的核心位于 java.util 包下,主要分为两大阵营:
-
Collection 接口家族 :存储单个元素的集合
List:有序、可重复Set:无序、不可重复Queue:队列(FIFO)
-
Map 接口家族:存储键值对(Key-Value)
Iterable │ Collection Map / │ \ / \ List Set Queue HashMap / \ / | \ / \ / \ ArrayList LinkedList HashSet LinkedHashSet TreeMap TreeSet PriorityQueue LinkedHashMap
💡 设计思想:接口定义规范,实现类提供具体的数据结构。这种"面向接口编程"的设计让我们可以轻松切换底层实现,而无需修改业务代码。
二、List:有序列表的两种灵魂
List 是有序集合,允许元素重复,支持通过索引(下标)访问元素。
2.1 ArrayList:数组的"动态升级版"
底层结构 :基于Object数组实现,初始容量默认为10,扩容时增长为原来的1.5倍。
时间复杂度分析:
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 查询(get) | O(1) | 直接通过数组索引访问 |
| 尾部插入(add) | 均摊 O(1) | 偶尔触发扩容,需要数组拷贝 |
| 中间插入/删除 | O(n) | 需要移动后续所有元素 |
适用场景:读多写少、频繁随机访问、数据量已知或可预估。
java
import java.util.ArrayList;
import java.util.List;
public class ArrayListDemo {
public static void main(String[] args) {
// 推荐:指定初始容量,避免频繁扩容
List<String> books = new ArrayList<>(100);
books.add("Java核心技术");
books.add("Effective Java");
books.add("深入理解JVM");
books.add("Spring实战");
// 随机访问 --- O(1)
System.out.println("第2本书:" + books.get(1));
// 批量添加
List<String> moreBooks = List.of("Redis设计与实现", "MySQL必知必会");
books.addAll(moreBooks);
// 遍历方式1:for-each(最常用)
System.out.println("\n=== 书单 ===");
for (String book : books) {
System.out.println("📖 " + book);
}
// 遍历方式2:Lambda表达式(Java 8+)
System.out.println("\n=== Lambda遍历 ===");
books.forEach(book -> System.out.println("📚 " + book));
// 遍历方式3:Stream过滤
System.out.println("\n=== 过滤出含Java的书 ===");
books.stream()
.filter(b -> b.contains("Java"))
.forEach(System.out::println);
}
}
输出:
第2本书:Effective Java
=== 书单 ===
📖 Java核心技术
📖 Effective Java
📖 深入理解JVM
📖 Spring实战
📖 Redis设计与实现
📖 MySQL必知必会
=== Lambda遍历 ===
📚 Java核心技术
📚 Effective Java
📚 深入理解JVM
📚 Spring实战
📚 Redis设计与实现
📚 MySQL必知必会
=== 过滤出含Java的书 ===
Java核心技术
Effective Java
2.2 LinkedList:链表的"双向魅力"
底层结构 :基于双向链表实现,每个节点包含:前驱指针、数据、后继指针。
时间复杂度分析:
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 查询(get) | O(n) | 需要从头/尾遍历到目标位置 |
| 头部/尾部插入删除 | O(1) | 只需修改指针指向 |
| 中间插入/删除 | O(n) | 先遍历到位置,再修改指针 |
适用场景:频繁在头部/尾部操作、实现队列/栈结构、数据量较小。
java
import java.util.LinkedList;
import java.util.List;
public class LinkedListDemo {
public static void main(String[] args) {
// LinkedList 同时实现了 List 和 Deque 接口
LinkedList<String> queue = new LinkedList<>();
// 模拟消息队列:生产者入队
queue.offer("订单消息-001");
queue.offer("订单消息-002");
queue.offer("订单消息-003");
System.out.println("队列内容:" + queue);
// 消费者出队 --- 先进先出
while (!queue.isEmpty()) {
String msg = queue.poll(); // 取出并移除队首
System.out.println("处理消息:" + msg);
}
// 作为栈使用 --- 后进先出
LinkedList<String> stack = new LinkedList<>();
stack.push("第一层");
stack.push("第二层");
stack.push("第三层");
System.out.println("\n=== 栈弹出 ===");
while (!stack.isEmpty()) {
System.out.println("弹出:" + stack.pop());
}
}
}
输出:
队列内容:[订单消息-001, 订单消息-002, 订单消息-003]
处理消息:订单消息-001
处理消息:订单消息-002
处理消息:订单消息-003
=== 栈弹出 ===
弹出:第三层
弹出:第二层
弹出:第一层
2.3 ArrayList vs LinkedList 选型指南
┌─────────────────────────────────────────────────────────────┐
│ 场景 │ 推荐选择 │
├─────────────────────────────────────────────────────────────┤
│ 频繁随机访问(get/indexOf) │ ✅ ArrayList │
│ 频繁在头部/尾部插入删除 │ ✅ LinkedList │
│ 数据量很大且内存敏感 │ ❌ ArrayList(扩容浪费内存) │
│ 需要实现队列/栈 │ ✅ LinkedList(天然支持) │
│ 遍历为主,偶尔增删 │ ✅ ArrayList │
│ 已知数据量,一次性初始化 │ ✅ ArrayList(指定容量) │
└─────────────────────────────────────────────────────────────┘
⚠️ 注意:现代JVM对ArrayList的优化已经非常成熟,除非明确需要队列/栈特性,否则大多数场景优先选择ArrayList。
三、Set:去重集合的三种性格
Set 的核心特性是不允许重复元素 。判断是否重复的标准是 equals() 和 hashCode() 方法。
3.1 HashSet:最快的去重方案
底层结构 :基于 HashMap 实现,元素作为Key存储,Value是一个固定的Object占位符。
核心机制:
- 添加元素时,计算
hashCode()定位桶位置 - 如果桶内有元素,调用
equals()判断是否重复 - 不重复则链式存储(JDK 8+ 链表长度>8时转为红黑树)
特点 :无序、去重、O(1) 增删查、非线程安全。
java
import java.util.HashSet;
import java.util.Set;
public class HashSetDemo {
public static void main(String[] args) {
Set<String> tags = new HashSet<>();
// 添加标签
tags.add("Java");
tags.add("Spring");
tags.add("MySQL");
tags.add("Java"); // 重复,自动忽略
System.out.println("标签集合:" + tags);
System.out.println("数量:" + tags.size()); // 3,不是4
// 判断是否存在
System.out.println("包含Java?" + tags.contains("Java")); // true
// 批量操作
Set<String> newTags = Set.of("Redis", "Java", "Kafka");
// 交集
Set<String> intersection = new HashSet<>(tags);
intersection.retainAll(newTags);
System.out.println("交集:" + intersection);
// 并集
Set<String> union = new HashSet<>(tags);
union.addAll(newTags);
System.out.println("并集:" + union);
}
}
输出:
标签集合:[Java, Spring, MySQL]
数量:3
包含Java?true
交集:[Java]
并集:[Java, Spring, MySQL, Redis, Kafka]
3.2 LinkedHashSet:记住你来时的顺序
底层结构:HashSet + 双向链表,在HashSet的基础上维护了一个插入顺序链表。
特点 :按插入顺序遍历、去重、比HashSet略慢(维护链表有额外开销)。
java
import java.util.LinkedHashSet;
import java.util.Set;
public class LinkedHashSetDemo {
public static void main(String[] args) {
// 需要保持插入顺序时使用
Set<String> history = new LinkedHashSet<>();
history.add("首页");
history.add("商品列表");
history.add("商品详情");
history.add("购物车");
history.add("结算页");
System.out.println("浏览历史(按访问顺序):");
history.forEach(page -> System.out.println("→ " + page));
// 模拟后退:移除最后访问的页面
// LinkedHashSet 没有直接移除最后一个的方法,但维护了顺序
}
}
3.3 TreeSet:自动排序的集合
底层结构 :基于 红黑树(自平衡二叉搜索树)实现。
特点 :元素按自然顺序 或自定义比较器排序、去重、O(log n) 增删查。
java
import java.util.Comparator;
import java.util.TreeSet;
public class TreeSetDemo {
public static void main(String[] args) {
// 自然排序(String按字典序)
TreeSet<String> words = new TreeSet<>();
words.add("banana");
words.add("apple");
words.add("cherry");
words.add("date");
System.out.println("自然排序:" + words);
// 自定义排序:按字符串长度降序
TreeSet<String> byLength = new TreeSet<>(Comparator.comparingInt(String::length).reversed());
byLength.add("hi");
byLength.add("hello");
byLength.add("hey");
byLength.add("goodbye");
System.out.println("按长度降序:" + byLength);
// 范围查询:TreeSet的强大功能
TreeSet<Integer> scores = new TreeSet<>();
scores.add(85);
scores.add(92);
scores.add(78);
scores.add(95);
scores.add(88);
System.out.println("\n成绩区间查询:");
System.out.println("大于等于90的:" + scores.tailSet(90)); // [92, 95]
System.out.println("小于90的:" + scores.headSet(90)); // [78, 85, 88]
System.out.println("80到90之间的:" + scores.subSet(80, 90)); // [85, 88]
}
}
输出:
自然排序:[apple, banana, cherry, date]
按长度降序:[goodbye, hello, hey, hi]
成绩区间查询:
大于等于90的:[92, 95]
小于90的:[78, 85, 88]
80到90之间的:[85, 88]
3.4 Set 选型速查表
| 实现类 | 有序性 | 去重方式 | 时间复杂度 | 适用场景 |
|---|---|---|---|---|
| HashSet | 无序 | hashCode+equals | O(1) | 仅需去重,不关心顺序 |
| LinkedHashSet | 插入顺序 | hashCode+equals | O(1) | 需要去重且保留插入顺序 |
| TreeSet | 排序顺序 | compareTo/compare | O(log n) | 需要去重且需要排序/范围查询 |
四、Map:键值对的智慧
Map 存储的是键值对(Key-Value),Key 唯一,Value 可重复。它是Java中使用最频繁的集合类型之一。
4.1 HashMap:最经典的键值对存储
底层结构:数组 + 链表/红黑树(JDK 8+)。
核心参数:
- 初始容量:默认16,必须是2的幂次方(方便位运算取模)
- 加载因子:默认0.75,即当元素数量 > 容量 × 0.75 时触发扩容
- 扩容机制:容量翻倍,所有元素重新计算hash位置(rehash)
hash冲突解决:
- 链表长度 < 8:使用链表
- 链表长度 ≥ 8 且数组长度 ≥ 64:链表转为红黑树
- 红黑树节点 < 6:退化为链表
java
import java.util.HashMap;
import java.util.Map;
public class HashMapDemo {
public static void main(String[] args) {
// 学生成绩表:学号 -> 成绩
Map<String, Integer> scores = new HashMap<>(16, 0.75f);
scores.put("2024001", 85);
scores.put("2024002", 92);
scores.put("2024003", 78);
scores.put("2024004", 95);
// 查询
System.out.println("学号2024002的成绩:" + scores.get("2024002"));
// 不存在时返回默认值
System.out.println("学号2024999的成绩:" + scores.getOrDefault("2024999", 0));
// 遍历方式1:EntrySet(效率最高)
System.out.println("\n=== 成绩明细 ===");
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
System.out.printf("学号:%s,成绩:%d%n", entry.getKey(), entry.getValue());
}
// 遍历方式2:Java 8 Lambda
System.out.println("\n=== Lambda遍历 ===");
scores.forEach((id, score) ->
System.out.printf("学号:%s,成绩:%d%n", id, score)
);
// 遍历方式3:KeySet遍历
System.out.println("\n=== 所有学号 ===");
scores.keySet().forEach(System.out::println);
// 计算操作:不存在则放入,存在则更新
scores.merge("2024001", 5, Integer::sum); // 85 + 5 = 90
System.out.println("\n更新后2024001的成绩:" + scores.get("2024001"));
}
}
输出:
学号2024002的成绩:92
学号2024999的成绩:0
=== 成绩明细 ===
学号:2024001,成绩:85
学号:2024002,成绩:92
学号:2024003,成绩:78
学号:2024004,成绩:95
=== Lambda遍历 ===
学号:2024001,成绩:85
学号:2024002,成绩:92
学号:2024003,成绩:78
学号:2024004,成绩:95
=== 所有学号 ===
2024001
2024002
2024003
2024004
更新后2024001的成绩:90
4.2 LinkedHashMap:有序的HashMap
特点:继承自HashMap,额外维护了一个双向链表记录插入顺序(或访问顺序)。
java
import java.util.LinkedHashMap;
import java.util.Map;
public class LinkedHashMapDemo {
public static void main(String[] args) {
// accessOrder = true:按访问顺序排序(LRU缓存的基础)
Map<String, String> cache = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
// 当容量超过3时,移除最久未访问的元素
return size() > 3;
}
};
cache.put("A", "数据A");
cache.put("B", "数据B");
cache.put("C", "数据C");
System.out.println("初始缓存:" + cache.keySet());
// 访问A,A变为最近使用
cache.get("A");
System.out.println("访问A后:" + cache.keySet());
// 添加D,超出容量,移除最久未使用的B
cache.put("D", "数据D");
System.out.println("添加D后:" + cache.keySet());
}
}
输出:
初始缓存:[A, B, C]
访问A后:[B, C, A]
添加D后:[C, A, D]
4.3 TreeMap:排序的键值对
特点:基于红黑树,Key按自然顺序或自定义比较器排序,支持范围查询。
java
import java.util.TreeMap;
import java.util.Map;
public class TreeMapDemo {
public static void main(String[] args) {
// 按价格排序的商品目录
TreeMap<Double, String> menu = new TreeMap<>();
menu.put(12.0, "宫保鸡丁");
menu.put(8.0, "酸辣土豆丝");
menu.put(28.0, "水煮鱼");
menu.put(18.0, "糖醋排骨");
menu.put(6.0, "米饭");
System.out.println("=== 菜单(按价格升序)===");
menu.forEach((price, dish) ->
System.out.printf("¥%.1f --- %s%n", price, dish)
);
// 价格区间查询
System.out.println("\n10元以下的菜:" + menu.headMap(10.0));
System.out.println("15元以上的菜:" + menu.tailMap(15.0));
System.out.println("10-20元的菜:" + menu.subMap(10.0, 20.0));
// 最接近某价格的菜
System.out.println("\n最接近15元的菜:" + menu.ceilingEntry(15.0)); // ≥15的最小值
System.out.println("最接近15元的菜(向下):" + menu.floorEntry(15.0)); // ≤15的最大值
}
}
输出:
=== 菜单(按价格升序)===
¥6.0 --- 米饭
¥8.0 --- 酸辣土豆丝
¥12.0 --- 宫保鸡丁
¥18.0 --- 糖醋排骨
¥28.0 --- 水煮鱼
10元以下的菜:{6.0=米饭, 8.0=酸辣土豆丝}
15元以上的菜:{18.0=糖醋排骨, 28.0=水煮鱼}
10-20元的菜:{12.0=宫保鸡丁, 18.0=糖醋排骨}
最接近15元的菜:18.0=糖醋排骨
最接近15元的菜(向下):12.0=宫保鸡丁
4.4 Map 选型速查表
| 实现类 | Key有序性 | 时间复杂度 | 特殊能力 | 适用场景 |
|---|---|---|---|---|
| HashMap | 无序 | O(1) | 无 | 通用键值存储 |
| LinkedHashMap | 插入/访问顺序 | O(1) | LRU缓存 | 需要顺序或缓存 |
| TreeMap | 排序顺序 | O(log n) | 范围查询 | 需要排序或区间查找 |
| ConcurrentHashMap | 无序 | O(1) | 线程安全 | 并发环境 |
五、现代遍历方式:Stream API
Java 8 引入的 Stream API 让集合操作更加函数式和声明式。
java
import java.util.*;
import java.util.stream.Collectors;
public class StreamDemo {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 过滤 + 映射 + 收集
List<Integer> evenSquares = numbers.stream()
.filter(n -> n % 2 == 0) // 过滤偶数
.map(n -> n * n) // 平方
.collect(Collectors.toList()); // 收集为List
System.out.println("偶数的平方:" + evenSquares);
// 分组
List<String> names = List.of("Alice", "Bob", "Anna", "Bill", "Amy");
Map<Character, List<String>> grouped = names.stream()
.collect(Collectors.groupingBy(s -> s.charAt(0)));
System.out.println("\n按首字母分组:" + grouped);
// 统计
IntSummaryStatistics stats = numbers.stream()
.mapToInt(Integer::intValue)
.summaryStatistics();
System.out.println("\n统计信息:");
System.out.println("数量:" + stats.getCount());
System.out.println("总和:" + stats.getSum());
System.out.println("平均:" + stats.getAverage());
System.out.println("最大:" + stats.getMax());
System.out.println("最小:" + stats.getMin());
}
}
输出:
偶数的平方:[4, 16, 36, 64, 100]
按首字母分组:{A=[Alice, Anna, Amy], B=[Bob, Bill]}
统计信息:
数量:10
总和:55
平均:5.5
最大:10
最小:1
六、实战:电商订单系统
综合运用集合框架实现一个简化的电商订单管理系统。
java
import java.util.*;
import java.util.stream.Collectors;
// 商品类
class Product {
private String id;
private String name;
private double price;
private String category;
public Product(String id, String name, double price, String category) {
this.id = id; this.name = name; this.price = price; this.category = category;
}
public String getId() { return id; }
public String getName() { return name; }
public double getPrice() { return price; }
public String getCategory() { return category; }
@Override public String toString() { return name + "(¥" + price + ")"; }
}
// 订单项
class OrderItem {
private Product product;
private int quantity;
public OrderItem(Product product, int quantity) {
this.product = product; this.quantity = quantity;
}
public double getSubtotal() { return product.getPrice() * quantity; }
public Product getProduct() { return product; }
public int getQuantity() { return quantity; }
}
// 订单
class Order {
private String orderId;
private List<OrderItem> items;
private long createTime;
public Order(String orderId) {
this.orderId = orderId;
this.items = new ArrayList<>();
this.createTime = System.currentTimeMillis();
}
public void addItem(Product product, int quantity) {
items.add(new OrderItem(product, quantity));
}
public double getTotal() {
return items.stream().mapToDouble(OrderItem::getSubtotal).sum();
}
public String getOrderId() { return orderId; }
public List<OrderItem> getItems() { return items; }
}
// 订单管理系统
public class OrderSystem {
public static void main(String[] args) {
// 商品库:使用HashMap快速查找
Map<String, Product> productMap = new HashMap<>();
productMap.put("P001", new Product("P001", "iPhone 15", 5999.0, "手机"));
productMap.put("P002", new Product("P002", "MacBook Pro", 14999.0, "电脑"));
productMap.put("P003", new Product("P003", "AirPods Pro", 1899.0, "配件"));
productMap.put("P004", new Product("P004", "iPad Air", 4799.0, "平板"));
productMap.put("P005", new Product("P005", "小米14", 3999.0, "手机"));
// 创建订单
List<Order> orders = new ArrayList<>();
Order order1 = new Order("O20240522001");
order1.addItem(productMap.get("P001"), 1);
order1.addItem(productMap.get("P003"), 2);
orders.add(order1);
Order order2 = new Order("O20240522002");
order2.addItem(productMap.get("P002"), 1);
order2.addItem(productMap.get("P004"), 1);
orders.add(order2);
Order order3 = new Order("O20240522003");
order3.addItem(productMap.get("P005"), 2);
orders.add(order3);
// 统计:按品类汇总销售额
System.out.println("=== 品类销售统计 ===");
Map<String, Double> categorySales = new HashMap<>();
for (Order order : orders) {
for (OrderItem item : order.getItems()) {
String category = item.getProduct().getCategory();
categorySales.merge(category, item.getSubtotal(), Double::sum);
}
}
categorySales.entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.forEach(e -> System.out.printf("%s:¥%.2f%n", e.getKey(), e.getValue()));
// 统计:找出最畅销的商品
System.out.println("\n=== 商品销量排行 ===");
Map<String, Integer> productSales = new HashMap<>();
for (Order order : orders) {
for (OrderItem item : order.getItems()) {
productSales.merge(item.getProduct().getName(), item.getQuantity(), Integer::sum);
}
}
productSales.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.forEach(e -> System.out.printf("%s:%d件%n", e.getKey(), e.getValue()));
// 订单总金额
System.out.println("\n=== 订单汇总 ===");
double totalRevenue = orders.stream().mapToDouble(Order::getTotal).sum();
System.out.printf("总订单数:%d%n", orders.size());
System.out.printf("总销售额:¥%.2f%n", totalRevenue);
System.out.printf("平均客单价:¥%.2f%n", totalRevenue / orders.size());
}
}
输出:
=== 品类销售统计 ===
电脑:¥14999.00
手机:¥13997.00
配件:¥3798.00
平板:¥4799.00
=== 商品销量排行 ===
AirPods Pro:2件
小米14:2件
iPhone 15:1件
MacBook Pro:1件
iPad Air:1件
=== 订单汇总 ===
总订单数:3
总销售额:¥37593.00
平均客单价:¥12531.00
七、避坑指南:集合使用常见陷阱
7.1 并发修改异常
java
List<String> list = new ArrayList<>(List.of("A", "B", "C"));
// ❌ 错误:遍历时直接删除
for (String s : list) {
if (s.equals("B")) list.remove(s); // 抛出 ConcurrentModificationException
}
// ✅ 正确:使用迭代器的remove方法
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("B")) it.remove();
}
// ✅ 更优雅:使用removeIf(Java 8+)
list.removeIf(s -> s.equals("B"));
7.2 HashMap的Key可变性问题
java
// ❌ 错误:使用可变对象作为Key
Map<List<String>, String> badMap = new HashMap<>();
List<String> key = new ArrayList<>(List.of("a", "b"));
badMap.put(key, "value");
key.add("c"); // 修改了Key!
// 此时再也找不到这个Entry了,因为hashCode变了
// ✅ 正确:使用不可变对象作为Key
Map<String, String> goodMap = new HashMap<>();
7.3 自动装箱的性能陷阱
java
// ❌ 低效:频繁自动装箱
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
list.add(i); // int -> Integer 装箱
}
// ✅ 高效:使用基本类型集合(第三方库如fastutil,或Java原生的IntStream)
IntStream.range(0, 1000000).boxed().collect(Collectors.toList());
八、总结与选型速查
| 需求 | 推荐集合 | 理由 |
|---|---|---|
| 有序列表,读多写少 | ArrayList |
随机访问O(1) |
| 队列/栈,头尾操作 | LinkedList |
双端操作O(1) |
| 快速去重 | HashSet |
增删查O(1) |
| 去重+保留顺序 | LinkedHashSet |
插入顺序 |
| 去重+排序 | TreeSet |
自然排序 |
| 通用键值存储 | HashMap |
增删查O(1) |
| 键值存储+顺序 | LinkedHashMap |
LRU缓存 |
| 键值存储+排序 | TreeMap |
范围查询 |
| 并发环境 | ConcurrentHashMap |
线程安全 |
| 不可变集合 | List.of() / Map.of() |
安全、高效 |