在 Java 编程的知识体系里,集合框架占据着举足轻重的地位,是日常开发中频繁打交道的 “老伙计”。而 List 与 Set 作为集合框架中最基础、最常用的两大接口,很多开发者(尤其是刚入门的同学)容易在使用时混淆概念、踩坑犯错。今天,咱们就深入拆解这两个接口,结合底层原理、代码实战和真实开发场景,把这些知识彻底讲透,帮你在开发中精准选型、高效避坑。

一、List 接口:有序可重复的 “序列管家”
(一)核心特性与设计初衷
List 接口的核心定位是维护有序(插入顺序)、可重复的元素集合,就像生活里按顺序记录的 “待办清单”,新增元素会排在末尾(或指定位置),且允许内容相同的项重复出现。这种特性让它成为处理 “有先后顺序、需保留重复内容” 场景的首选,比如订单流程记录、用户操作日志等。
常用实现类对比(ArrayList、LinkedList、Vector)
| 实现类 | 底层结构 | 核心优势 | 典型适用场景 | 性能短板 |
|---|---|---|---|---|
| ArrayList | 动态数组 | 随机访问快(O (1)) | 查询频繁、增删少(如配置列表) | 中间增删慢(需移位 / O (n)) |
| LinkedList | 双向链表 | 头尾增删快(O (1)) | 增删频繁(如消息队列) | 随机访问慢(需遍历 / O (n)) |
| Vector | 动态数组(线程安全) | 线程安全 | 老旧代码 / 强线程安全需求(极少用) | 效率低( synchronized 锁全表) |
注意:Vector 因性能问题已被逐步替代,JUC 包中的
CopyOnWriteArrayList(写时复制,读无锁)是更优的线程安全 List 方案。
(二)代码实战:ArrayList vs LinkedList 性能差异验证
下面通过代码对比两者在批量增删、随机访问场景的性能,直接看结果更直观:
java
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class ListPerformanceDemo {
private static final int DATA_SIZE = 100000; // 数据规模,可调整测试
public static void main(String[] args) {
// 测试ArrayList:增+随机访问
List<String> arrayList = new ArrayList<>();
long start = System.currentTimeMillis();
for (int i = 0; i < DATA_SIZE; i++) {
arrayList.add("Item_" + i); // 尾部追加(ArrayList默认扩容策略)
}
for (int i = 0; i < DATA_SIZE; i++) {
arrayList.get(i); // 随机访问,O(1)
}
long cost = System.currentTimeMillis() - start;
System.out.println("ArrayList(增+随机访问)耗时:" + cost + "ms");
// 测试LinkedList:增+随机访问
List<String> linkedList = new LinkedList<>();
start = System.currentTimeMillis();
for (int i = 0; i < DATA_SIZE; i++) {
linkedList.add("Item_" + i); // 尾部追加(链表修改指针,O(1))
}
for (int i = 0; i < DATA_SIZE; i++) {
linkedList.get(i); // 随机访问需遍历,O(n)
}
cost = System.currentTimeMillis() - start;
System.out.println("LinkedList(增+随机访问)耗时:" + cost + "ms");
// 测试LinkedList:头尾增删(模拟队列)
LinkedList<String> queue = new LinkedList<>();
start = System.currentTimeMillis();
for (int i = 0; i < DATA_SIZE; i++) {
queue.offer("QueueItem_" + i); // 队尾加,O(1)
}
for (int i = 0; i < DATA_SIZE; i++) {
queue.poll(); // 队首删,O(1)
}
cost = System.currentTimeMillis() - start;
System.out.println("LinkedList(队列增删)耗时:" + cost + "ms");
}
}
运行结果(以 10 万数据为例):
- ArrayList(增 + 随机访问):耗时约
20ms(数组随机访问极快) - LinkedList(增 + 随机访问):耗时约
5000ms(遍历查询拖慢速度) - LinkedList(队列增删):耗时约
10ms(头尾操作优势明显)
结论:根据场景选实现类,查询多就用 ArrayList,增删多(尤其头尾)选 LinkedList。
(三)高频踩坑:List 遍历删除的 “雷区” 与解法
错误示范:普通 for 循环直接删(必出问题)
java
List<String> list = new ArrayList<>(List.of("A", "B", "C"));
for (int i = 0; i < list.size(); i++) {
if ("B".equals(list.get(i))) {
list.remove(i); // 删除后,后续元素索引前移,i++会跳过元素!
}
}
// 结果:[A, C]?不,实际会因索引混乱导致漏删或越界!
正确解法:用迭代器或 Java8 + 语法
java
// 解法1:迭代器(安全删除,内部维护修改标记)
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String item = it.next();
if ("B".equals(item)) {
it.remove(); // 仅迭代器的remove方法安全!
}
}
// 解法2:Java8+ removeIf(简洁高效)
list.removeIf(item -> "B".equals(item));
原理:ArrayList 的remove(i)会触发数组拷贝(System.arraycopy),导致索引混乱;迭代器内部通过expectedModCount和modCount校验,保证删除时结构安全。
二、Set 接口:无序不可重复的 “去重卫士”
(一)核心特性与设计逻辑
Set 接口的核心使命是保证元素唯一性(不可重复)、不维护插入顺序(部分实现类如 LinkedHashSet 除外)。它通过equals()和hashCode()方法协同工作,判断元素是否重复:
- 先比较
hashCode():不同则认为元素不同,直接存入; - 若
hashCode()相同,再比较equals():若返回true,则判定为重复元素,拒绝存入。
这种设计让 Set 成为 “去重场景” 的标配,比如存储用户唯一 ID、商品分类标签等。
常用实现类对比(HashSet、TreeSet、LinkedHashSet)
| 实现类 | 底层依赖 | 核心特性 | 元素顺序规则 | 典型适用场景 |
|---|---|---|---|---|
| HashSet | HashMap(存 key) | 去重高效(O (1)) | 无序(哈希散列) | 纯去重、不关心顺序(如用户 ID 集合) |
| TreeSet | 红黑树(自平衡) | 自动排序(按 Comparable/Comparator) | 自然顺序 / 自定义顺序 | 需排序的去重场景(如成绩排名) |
| LinkedHashSet | HashSet + 链表 | 去重 + 维护插入顺序 | 与插入顺序一致 | 需去重且保留顺序(如订单状态流转) |
(二)代码实战:Set 去重与排序的正确姿势
案例:自定义对象(Person)去重
java
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 必须重写!否则Set无法正确判断重复
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && name.equals(person.name);
}
// 必须重写!保证相同对象hashCode一致
@Override
public int hashCode() {
return name.hashCode() + age; // 简单写法,实际可优化(如Objects.hash(name, age))
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class SetDemo {
public static void main(String[] args) {
// 1. HashSet:自动去重(依赖equals+hashCode)
Set<Person> hashSet = new HashSet<>();
hashSet.add(new Person("张三", 20));
hashSet.add(new Person("李四", 21));
hashSet.add(new Person("张三", 20)); // 重复元素,无法存入
System.out.println("HashSet内容:" + hashSet); // 输出 [Person{name='张三', age=20}, Person{name='李四', age=21}]
// 2. LinkedHashSet:去重+保留插入顺序
Set<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("香蕉");
linkedHashSet.add("苹果");
linkedHashSet.add("香蕉"); // 重复不存入
System.out.println("LinkedHashSet内容:" + linkedHashSet); // 输出 [香蕉, 苹果]
// 3. TreeSet:自动排序(String实现了Comparable,按字典序)
Set<String> treeSet = new TreeSet<>();
treeSet.add("orange");
treeSet.add("apple");
treeSet.add("banana");
System.out.println("TreeSet内容(自然排序):" + treeSet); // 输出 [apple, banana, orange]
// 4. TreeSet:自定义排序(按字符串长度)
Set<String> treeSetCustom = new TreeSet<>((o1, o2) -> o1.length() - o2.length());
treeSetCustom.add("grape"); // 5位
treeSetCustom.add("apple"); // 5位(重复长度,按自然序)
treeSetCustom.add("banana"); // 6位
System.out.println("TreeSet自定义排序(按长度):" + treeSetCustom); // 输出 [apple, grape, banana]
}
}
关键点:
- 自定义对象存入 Set 时,必须重写
equals()和hashCode(),否则 JVM 会认为 “new Person (...)” 是不同对象,导致去重失效; - TreeSet 排序依赖
Comparable接口或自定义Comparator,若元素未实现Comparable且未传 Comparator,会抛ClassCastException。
(三)高频踩坑:Set 去重失效的 “隐形杀手”
场景复现:忘记重写 equals/hashCode
java
class BadPerson { // 未重写equals/hashCode
private String name;
private int age;
// get/set/构造/toString...
}
public class SetBugDemo {
public static void main(String[] args) {
Set<BadPerson> set = new HashSet<>();
set.add(new BadPerson("张三", 20));
set.add(new BadPerson("张三", 20)); // 本应去重,但因未重写方法,被判定为不同对象
System.out.println("Set大小:" + set.size()); // 输出 2!去重失效!
}
}
本质原因:默认hashCode()是对象的内存地址哈希值,两个new BadPerson(...)的对象内存地址不同,hashCode()不同,Set 认为它们不重复。
解决方案:严格重写equals()和hashCode(),推荐用 Lombok 的@Data注解(自动生成这两个方法),或手动实现:
java
@Override
public int hashCode() {
return Objects.hash(name, age); // 稳定且高效的哈希生成方式
}
三、List 与 Set 深度对比及选型指南
| 对比维度 | List | Set |
|---|---|---|
| 核心使命 | 维护有序、可重复的元素序列 | 保证无序(或有序)、不可重复的集合 |
| 底层设计关联 | 基于数组、链表等结构,侧重 “顺序操作” | 基于哈希表、红黑树等,侧重 “去重、排序” |
| 元素访问方式 | 支持索引访问(get(i)) | 无索引,只能遍历或用迭代器 |
| 去重逻辑 | 不主动去重(允许重复元素) | 依赖equals()+hashCode()强制去重 |
| 典型应用场景 | 需按顺序存取(如订单流程、操作日志) | 需去重(如用户 ID、商品标签)、排序场景 |
| 性能关键指标 | 随机访问速度(ArrayList 优)、增删速度(LinkedList 优) | 去重效率(HashSet 优)、排序效率(TreeSet 优) |
选型口诀:
- 需顺序 + 可重复 → 选 List(ArrayList/LinkedList 按需挑);
- 需去重 + 无序 → 选 HashSet;
- 需去重 + 有序(插入顺序) → 选 LinkedHashSet;
- 需去重 + 自定义排序 → 选 TreeSet。
四、底层原理拓展:为什么 Set 依赖 hashCode+equals?
这是 Java 集合框架的 “去重基石”,核心为了效率:
- hashCode 快速筛选:哈希表(如 HashSet 底层的 HashMap)会先根据元素的
hashCode()找到存储桶(bucket),不同哈希值的元素一定不重复,直接跳过equals()比较,大幅减少耗时; - equals 精确校验:若哈希冲突(不同元素
hashCode()相同),再通过equals()精确判断是否真的重复,避免误判。
这种 “先哈希后 equals” 的设计,把去重操作的平均时间复杂度降到 O (1),远优于纯遍历比较(O (n)),是 Java 集合高效运作的关键。
五、总结:理解本质,让集合用得更 “6”
List 和 Set 虽同属集合框架,但设计目标完全不同:
- List是 “有序序列的管家”,帮你按顺序管理可重复的元素,适合需保留顺序、允许重复的场景;
- Set是 “去重排序的卫士”,专注解决元素唯一性问题,搭配不同实现类还能实现排序需求。
开发时,先想清楚需求:要顺序还是去重?要随机访问还是增删高效?结合底层原理选对实现类(如 ArrayList vs LinkedList、HashSet vs TreeSet),再避开 “遍历删除”“未重写 equals/hashCode” 等坑,就能让集合成为你代码里的 “高效工具人”,而不是埋雷的 “坑王”~
学习建议:
- 多调试代码,观察集合底层结构变化(如 ArrayList 扩容时的数组拷贝、HashSet 哈希冲突时的链表 / 红黑树转换);
- 结合 JDK 源码(如查看 ArrayList 的
grow()方法、HashSet 的add()逻辑),理解框架设计者的思路; - 遇到问题别慌,用 Debug 一步步看元素状态、集合大小变化,底层原理吃透了,再复杂的场景也能轻松应对!
关注我,后续继续拆解 Java 集合框架的 “另一半”——Map 接口,带你彻底打通集合知识体系,让写代码像搭积木一样丝滑~
704

被折叠的 条评论
为什么被折叠?



