作为一名 Java 后端工程师,在面试中被问到 Java 集合那是家常便饭。特别是 List 和 Set,简直是面试官的必考点。但很多同学对它们的理解仅仅停留在“List 有序可重复,Set 无序不重复”这种浅显的层面,一旦深入问一些底层原理、使用场景、以及容易踩坑的地方,就很容易露怯。本文就来帮你彻底搞懂 List 和 Set,让你在面试中游刃有余。
List:有序可重复,但背后的故事你了解吗?
场景重现:微信朋友圈点赞
想象一下微信朋友圈的点赞功能。点赞列表就是一个典型的 List 应用场景,用户点赞的顺序会被记录下来,并且同一个用户可以多次点赞(虽然实际情况不太可能)。
底层原理:ArrayList vs LinkedList
List 接口最常用的两个实现类是 ArrayList 和 LinkedList。它们的底层实现完全不同,这决定了它们各自的特性和适用场景。
ArrayList:
- 底层基于动态数组实现。可以把它想象成一个可以自动扩容的数组。
- 优点:随机访问效率高(通过下标直接定位元素),因为数组在内存中是连续存储的。
- 缺点:插入和删除元素效率低(特别是头部或中间位置),因为需要移动元素。
- 扩容机制:当元素数量超过容量时,会创建一个更大的数组,并将原数组中的元素复制过去。默认扩容为原来的 1.5 倍。
LinkedList:

- 底层基于双向链表实现。可以把它想象成一串互相连接的珠子。
- 优点:插入和删除元素效率高(只需要修改指针),不需要移动元素。
- 缺点:随机访问效率低(需要从头或尾部遍历链表),因为链表在内存中不是连续存储的。
代码示例:ArrayList 的常见操作
import java.util.ArrayList;
import java.util.List;
public class ArrayListExample {
public static void main(String[] args) {
// 创建 ArrayList
List<String> names = new ArrayList<>();
// 添加元素
names.add("张三");
names.add("李四");
names.add("王五");
names.add("张三"); // 允许重复元素
// 获取元素
String name = names.get(0); // 获取第一个元素
System.out.println("第一个元素:" + name);
// 修改元素
names.set(0, "赵六"); // 将第一个元素修改为赵六
System.out.println("修改后的第一个元素:" + names.get(0));
// 删除元素
names.remove(2); // 删除第三个元素
// 遍历元素
for (String n : names) {
System.out.println(n);
}
}
}
实战避坑:ArrayList 的线程安全问题
ArrayList 不是线程安全的。在多线程环境下,多个线程同时修改 ArrayList 可能会导致数据不一致。可以使用 Collections.synchronizedList() 方法将 ArrayList 包装成线程安全的 List,或者使用 CopyOnWriteArrayList。CopyOnWriteArrayList 的特点是读写分离,写操作会复制一个新的数组,避免读写冲突,但写操作的开销较大。
Set:无序不重复,靠什么保证唯一性?
场景重现:抽奖系统
设想一个抽奖系统,每个用户只能中奖一次。中奖用户列表就可以使用 Set 来存储,保证每个用户只会被记录一次。
底层原理:HashSet vs TreeSet
Set 接口常用的实现类有 HashSet 和 TreeSet。它们对“无序”和“不重复”的实现方式不同。
HashSet:

- 底层基于
HashMap实现。HashSet中的元素作为HashMap的 key 存储,value是一个固定的Object对象。 - 无序性:元素的存储位置取决于元素的哈希值,所以看起来是无序的。
- 唯一性:通过
hashCode()和equals()方法来保证元素的唯一性。如果两个元素的hashCode()值不同,则认为是不同的元素;如果hashCode()值相同,则继续使用equals()方法进行比较,如果equals()方法返回true,则认为是相同的元素,否则认为是不同的元素。
- 底层基于
TreeSet:
- 底层基于红黑树(一种自平衡的二叉搜索树)实现。
- 有序性:元素按照自然顺序或自定义比较器进行排序。
- 唯一性:通过比较器(自然顺序或自定义比较器)来保证元素的唯一性。如果两个元素的比较结果为 0,则认为是相同的元素。
代码示例:HashSet 的常见操作
import java.util.HashSet;
import java.util.Set;
public class HashSetExample {
public static void main(String[] args) {
// 创建 HashSet
Set<String> uniqueNames = new HashSet<>();
// 添加元素
uniqueNames.add("张三");
uniqueNames.add("李四");
uniqueNames.add("王五");
uniqueNames.add("张三"); // 重复元素,不会被添加
// 打印元素
System.out.println(uniqueNames); // 输出结果:[张三, 李四, 王五] (顺序不保证)
// 检查元素是否存在
boolean contains = uniqueNames.contains("李四");
System.out.println("是否包含李四:" + contains);
// 删除元素
uniqueNames.remove("李四");
// 遍历元素
for (String name : uniqueNames) {
System.out.println(name);
}
}
}
实战避坑:重写 hashCode() 和 equals() 方法
如果自定义对象需要存储到 HashSet 中,务必重写 hashCode() 和 equals() 方法,以保证对象的唯一性。重写时需要遵循以下原则:
- 如果两个对象相等(
equals()返回true),则它们的hashCode()值必须相等。 - 如果两个对象不相等(
equals()返回false),则它们的hashCode()值最好不相等(可以提高性能)。
可以使用 IDE 自动生成 hashCode() 和 equals() 方法,并根据对象的关键属性进行比较。
List + Set 组合使用:更灵活的应用场景
List 和 Set 可以结合使用,解决更复杂的问题。例如,可以使用 LinkedHashSet 来实现一个既保证元素唯一性,又保持插入顺序的集合。
在实际项目中,根据具体的业务需求选择合适的 List 和 Set 实现类,才能充分发挥它们的优势,提高代码的性能和可维护性。 掌握 Java 集合 的核心知识,是成为一名优秀的 Java 后端工程师的必备技能。
冠军资讯
代码一只喵