📖 目录
一、Java集合框架概述
1.1 什么是集合?
在Java中,集合(Collection) 是用来存储和管理多个对象的容器。就像一个可以动态调整大小的"篮子",你可以往里面放东西,也可以拿出来。
1.2 为什么需要集合?
传统数组的局限性:
java
// 数组的缺点示例
String[] names = new String[3]; // ❌ 长度固定,不能动态扩展
names[0] = "张三";
names[1] = "李四";
names[2] = "王五";
// names[3] = "赵六"; // ❌ 报错!数组越界
集合的优势:
java
// 集合的优点示例
ArrayList<String> nameList = new ArrayList<>(); // ✅ 长度可变
nameList.add("张三");
nameList.add("李四");
nameList.add("王五");
nameList.add("赵六"); // ✅ 自动扩容,无需担心长度
System.out.println("集合大小:" + nameList.size()); // 输出:4
1.3 Java集合体系结构
Collection (接口)
├── List (接口) - 有序、可重复
│ ├── ArrayList - 基于数组实现
│ ├── LinkedList - 基于链表实现
│ └── Vector - 线程安全的ArrayList
├── Set (接口) - 无序、不可重复
│ ├── HashSet
│ └── TreeSet
└── Queue (接口) - 队列
└── PriorityQueue
本文重点讲解 List 接口的三个实现类:ArrayList、LinkedList、Vector
二、ArrayList集合详解
2.1 ArrayList的核心特点
| 特点 | 说明 |
|---|---|
| 📦 底层结构 | 基于动态数组实现 |
| 📈 初始容量 | 默认初始容量为 10,自动扩容至 1.5 倍 |
| ✅ 是否有序 | 有序(保证插入顺序) |
| 🔄 是否可重复 | 可重复(允许存储相同元素) |
| ⚡ 查询速度 | 快(支持随机访问,时间复杂度 O(1)) |
| 🐢 增删速度 | 慢(需要移动元素,时间复杂度 O(n)) |
| 🔒 线程安全 | 否(非线程安全) |
2.2 ArrayList的底层原理
2.2.1 为什么用 Object 类型存储?
java
// ArrayList 源码简化版
public class ArrayList<E> {
// 底层使用 Object 数组存储元素
transient Object[] elementData;
// 实际存储的元素个数
private int size;
}
💡 为什么使用 Object[]?
- 通用性:Object 是所有类的父类,可以存储任何类型的对象
- 多态性 :通过泛型
<E>实现类型安全,编译时检查类型 - 灵活性:底层统一用 Object 存储,上层通过泛型控制类型
java
// 示例:泛型的作用
ArrayList<String> strList = new ArrayList<>(); // 只能存字符串
strList.add("Hello");
// strList.add(123); // ❌ 编译错误!类型不匹配
ArrayList<Integer> intList = new ArrayList<>(); // 只能存整数
intList.add(100);
// intList.add("World"); // ❌ 编译错误!
2.2.2 ArrayList 的扩容机制
java
// 扩容原理示例
public class ArrayListExpandDemo {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>(); // 初始容量为 10
// 添加第 1-10 个元素时,不会扩容
for (int i = 1; i <= 10; i++) {
list.add(i);
}
System.out.println("添加10个元素后,容量:10");
// 添加第 11 个元素时,触发扩容
list.add(11); // 容量扩展为 10 * 1.5 = 15
System.out.println("添加11个元素后,容量:15");
// 添加第 16 个元素时,再次扩容
for (int i = 12; i <= 16; i++) {
list.add(i);
}
System.out.println("添加16个元素后,容量:22(15 * 1.5)");
}
}
扩容步骤:
- 创建一个新数组,容量为原来的 1.5 倍
- 将旧数组的元素复制到新数组
- 将引用指向新数组
2.3 ArrayList 常用方法详解
2.3.1 基础操作方法
java
import java.util.ArrayList;
public class ArrayListBasicDemo {
public static void main(String[] args) {
// ========== 1. 创建 ArrayList ==========
ArrayList<String> courseList = new ArrayList<>();
System.out.println("初始集合:" + courseList); // 输出:[]
// ========== 2. 添加元素 add() ==========
courseList.add("Java基础"); // 添加到末尾
courseList.add("数据结构");
courseList.add("算法");
System.out.println("添加元素后:" + courseList);
// 输出:[Java基础, 数据结构, 算法]
// ========== 3. 在指定位置插入元素 add(index, element) ==========
courseList.add(1, "操作系统"); // 在索引1的位置插入,后面元素后移
System.out.println("插入元素后:" + courseList);
// 输出:[Java基础, 操作系统, 数据结构, 算法]
// ========== 4. 获取元素 get(index) ==========
String firstCourse = courseList.get(0); // 获取索引0的元素
System.out.println("第一门课程:" + firstCourse);
// 输出:第一门课程:Java基础
// ========== 5. 获取集合大小 size() ==========
int count = courseList.size();
System.out.println("课程总数:" + count);
// 输出:课程总数:4
// ========== 6. 修改元素 set(index, element) ==========
String oldCourse = courseList.set(2, "计算机网络"); // 替换索引2的元素
System.out.println("被替换的课程:" + oldCourse);
// 输出:被替换的课程:数据结构
System.out.println("修改后:" + courseList);
// 输出:[Java基础, 操作系统, 计算机网络, 算法]
// ========== 7. 判断是否包含元素 contains() ==========
boolean hasJava = courseList.contains("Java基础");
System.out.println("是否包含Java基础:" + hasJava);
// 输出:true
boolean hasAI = courseList.contains("人工智能");
System.out.println("是否包含人工智能:" + hasAI);
// 输出:false
// ========== 8. 判断集合是否为空 isEmpty() ==========
boolean empty = courseList.isEmpty();
System.out.println("集合是否为空:" + empty);
// 输出:false
}
}
2.3.2 删除操作方法
java
import java.util.ArrayList;
public class ArrayListRemoveDemo {
public static void main(String[] args) {
ArrayList<String> fruits = new ArrayList<>();
fruits.add("苹果");
fruits.add("香蕉");
fruits.add("橙子");
fruits.add("香蕉"); // 重复元素
fruits.add("葡萄");
System.out.println("初始水果列表:" + fruits);
// 输出:[苹果, 香蕉, 橙子, 香蕉, 葡萄]
// ========== 1. 根据索引删除 remove(int index) ==========
String removed1 = fruits.remove(2); // 删除索引为2的元素(橙子)
System.out.println("删除的元素:" + removed1);
// 输出:删除的元素:橙子
System.out.println("删除后:" + fruits);
// 输出:[苹果, 香蕉, 香蕉, 葡萄]
// ========== 2. 根据对象删除 remove(Object o) ==========
boolean removed2 = fruits.remove("香蕉"); // 删除第一个匹配的"香蕉"
System.out.println("是否删除成功:" + removed2);
// 输出:true
System.out.println("删除后:" + fruits);
// 输出:[苹果, 香蕉, 葡萄] (只删除了第一个香蕉)
// ========== 3. 清空所有元素 clear() ==========
fruits.clear();
System.out.println("清空后:" + fruits);
// 输出:[]
System.out.println("是否为空:" + fruits.isEmpty());
// 输出:true
}
}
⚠️ 注意事项:
remove(int index)返回被删除的元素remove(Object o)返回 boolean,表示是否删除成功- 删除元素后,后面的元素会自动前移
2.3.3 遍历 ArrayList 的三种方式
java
import java.util.ArrayList;
import java.util.Iterator;
public class ArrayListTraverseDemo {
public static void main(String[] args) {
ArrayList<String> students = new ArrayList<>();
students.add("张三");
students.add("李四");
students.add("王五");
students.add("赵六");
System.out.println("========== 方式1:普通 for 循环 ==========");
// 优点:可以通过索引访问,灵活控制
// 缺点:代码稍显冗长
for (int i = 0; i < students.size(); i++) {
String student = students.get(i); // 通过索引获取元素
System.out.println("第" + (i+1) + "个学生:" + student);
}
System.out.println("\n========== 方式2:增强 for 循环(foreach) ==========");
// 优点:代码简洁,易读
// 缺点:无法获取索引,无法删除元素
for (String student : students) {
System.out.println("学生:" + student);
}
System.out.println("\n========== 方式3:迭代器(Iterator) ==========");
// 优点:可以在遍历时安全删除元素
// 缺点:代码相对复杂
Iterator<String> iterator = students.iterator();
while (iterator.hasNext()) { // 判断是否有下一个元素
String student = iterator.next(); // 获取下一个元素
System.out.println("学生:" + student);
// 如果是"李四",则删除(使用迭代器的remove方法)
if ("李四".equals(student)) {
iterator.remove(); // 安全删除
System.out.println(" ↑ 李四已被删除");
}
}
System.out.println("删除后的列表:" + students);
// 输出:[张三, 王五, 赵六]
}
}
💡 遍历方式选择建议:
- 只读取数据 → 用增强 for 循环(最简洁)
- 需要索引 → 用普通 for 循环
- 需要删除元素 → 用迭代器
三、LinkedList集合详解
3.1 LinkedList的核心特点
| 特点 | 说明 |
|---|---|
| 🔗 底层结构 | 基于双向链表实现 |
| 📍 内部结构 | 每个节点包含:前驱指针、数据、后继指针 |
| ✅ 是否有序 | 有序(保证插入顺序) |
| 🔄 是否可重复 | 可重复 |
| 🐢 查询速度 | 慢(需要遍历链表,时间复杂度 O(n)) |
| ⚡ 增删速度 | 快(只需修改指针,时间复杂度 O(1)) |
| 🔒 线程安全 | 否(非线程安全) |
3.2 LinkedList的底层原理
3.2.1 双向链表结构图解
[头节点] ⇄ [节点1] ⇄ [节点2] ⇄ [节点3] ⇄ [尾节点]
↑ ↑
first last
每个节点的结构:
┌──────────────────┐
│ prev (前驱指针) │ → 指向前一个节点
├──────────────────┤
│ item (数据) │ → 存储实际数据
├──────────────────┤
│ next (后继指针) │ → 指向下一个节点
└──────────────────┘
3.2.2 LinkedList 源码简化版
java
// LinkedList 底层实现(简化版)
public class LinkedList<E> {
// 链表的头节点
transient Node<E> first;
// 链表的尾节点
transient Node<E> last;
// 元素个数
transient int size = 0;
// 内部节点类
private static class Node<E> {
E item; // 存储的数据
Node<E> next; // 指向下一个节点
Node<E> prev; // 指向前一个节点
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
}
3.2.3 LinkedList 添加元素原理
java
// 在尾部添加元素的过程演示
public class LinkedListAddDemo {
public static void main(String[] args) {
/*
* 假设我们要添加三个元素:"A", "B", "C"
*
* 步骤1:添加 "A"
* [A]
* first = A, last = A
*
* 步骤2:添加 "B"
* [A] ⇄ [B]
* first = A, last = B
*
* 步骤3:添加 "C"
* [A] ⇄ [B] ⇄ [C]
* first = A, last = C
*/
LinkedList<String> list = new LinkedList<>();
list.add("A"); // 1. 创建节点A,first=A, last=A
list.add("B"); // 2. 创建节点B,A.next=B, B.prev=A, last=B
list.add("C"); // 3. 创建节点C,B.next=C, C.prev=B, last=C
System.out.println(list); // 输出:[A, B, C]
}
}
3.3 LinkedList 常用方法详解
3.3.1 添加操作
java
import java.util.LinkedList;
public class LinkedListAddDemo {
public static void main(String[] args) {
LinkedList<Integer> numbers = new LinkedList<>();
// ========== 1. 在尾部添加元素 add() 或 addLast() ==========
numbers.add(1); // [1]
numbers.add(2); // [1, 2]
numbers.add(3); // [1, 2, 3]
numbers.addLast(6); // [1, 2, 3, 6] (与add()效果相同)
System.out.println("尾部添加后:" + numbers);
// ========== 2. 在头部添加元素 addFirst() ==========
numbers.addFirst(4); // [4, 1, 2, 3, 6]
numbers.addFirst(5); // [5, 4, 1, 2, 3, 6]
System.out.println("头部添加后:" + numbers);
// ========== 3. 在指定位置插入 add(index, element) ==========
numbers.add(2, 9); // 在索引2的位置插入9
System.out.println("指定位置插入后:" + numbers);
// 输出:[5, 4, 9, 1, 2, 3, 6]
/*
* 💡 性能分析:
* - addFirst() 和 addLast() 时间复杂度:O(1) ⚡ 快
* - add(index, element) 时间复杂度:O(n) 🐢 慢(需要先找到位置)
*/
}
}
3.3.2 获取操作
java
import java.util.LinkedList;
public class LinkedListGetDemo {
public static void main(String[] args) {
LinkedList<String> queue = new LinkedList<>();
queue.add("第1个");
queue.add("第2个");
queue.add("第3个");
queue.add("第4个");
// ========== 1. 获取头部元素 getFirst() ==========
String first = queue.getFirst();
System.out.println("头部元素:" + first); // 输出:第1个
// ========== 2. 获取尾部元素 getLast() ==========
String last = queue.getLast();
System.out.println("尾部元素:" + last); // 输出:第4个
// ========== 3. 根据索引获取 get(index) ==========
String second = queue.get(1);
System.out.println("索引1的元素:" + second); // 输出:第2个
System.out.println("原列表:" + queue);
// 输出:[第1个, 第2个, 第3个, 第4个]
// 注意:get操作不会删除元素,只是查看
}
}
3.3.3 修改操作
java
import java.util.LinkedList;
public class LinkedListSetDemo {
public static void main(String[] args) {
LinkedList<Integer> scores = new LinkedList<>();
scores.add(85);
scores.add(90);
scores.add(78);
scores.add(92);
System.out.println("原始分数:" + scores);
// 输出:[85, 90, 78, 92]
// ========== set(index, element) - 替换指定位置的元素 ==========
Integer oldScore = scores.set(1, 95); // 将索引1的元素从90改为95
System.out.println("被替换的分数:" + oldScore); // 输出:90
System.out.println("修改后的分数:" + scores);
// 输出:[85, 95, 78, 92]
}
}
3.3.4 删除操作
java
import java.util.LinkedList;
public class LinkedListRemoveDemo {
public static void main(String[] args) {
LinkedList<String> tasks = new LinkedList<>();
tasks.add("任务A");
tasks.add("任务B");
tasks.add("任务C");
tasks.add("任务D");
System.out.println("初始任务列表:" + tasks);
// 输出:[任务A, 任务B, 任务C, 任务D]
// ========== 1. 删除头部元素 remove() 或 removeFirst() ==========
String removed1 = tasks.remove(); // 删除并返回头部元素
System.out.println("删除的头部元素:" + removed1); // 输出:任务A
System.out.println("删除后:" + tasks);
// 输出:[任务B, 任务C, 任务D]
// ========== 2. 删除尾部元素 removeLast() ==========
String removed2 = tasks.removeLast(); // 删除并返回尾部元素
System.out.println("删除的尾部元素:" + removed2); // 输出:任务D
System.out.println("删除后:" + tasks);
// 输出:[任务B, 任务C]
// ========== 3. 删除指定位置元素 remove(index) ==========
tasks.add("任务E");
tasks.add("任务F");
System.out.println("当前任务:" + tasks);
// 输出:[任务B, 任务C, 任务E, 任务F]
String removed3 = tasks.remove(2); // 删除索引2的元素
System.out.println("删除的元素:" + removed3); // 输出:任务E
System.out.println("最终任务:" + tasks);
// 输出:[任务B, 任务C, 任务F]
}
}
3.3.5 查询操作
java
import java.util.LinkedList;
public class LinkedListContainsDemo {
public static void main(String[] args) {
LinkedList<Integer> numbers = new LinkedList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(20); // 重复元素
// ========== contains(Object o) - 判断是否包含某元素 ==========
boolean has20 = numbers.contains(20);
System.out.println("是否包含20:" + has20); // 输出:true
boolean has50 = numbers.contains(50);
System.out.println("是否包含50:" + has50); // 输出:false
// ========== size() - 获取链表长度 ==========
System.out.println("链表长度:" + numbers.size()); // 输出:4
}
}
3.3.6 清空操作
java
import java.util.LinkedList;
public class LinkedListClearDemo {
public static void main(String[] args) {
LinkedList<String> cache = new LinkedList<>();
cache.add("数据1");
cache.add("数据2");
cache.add("数据3");
System.out.println("清空前:" + cache);
// 输出:[数据1, 数据2, 数据3]
// ========== clear() - 清空所有元素 ==========
cache.clear();
System.out.println("清空后:" + cache); // 输出:[]
System.out.println("是否为空:" + cache.isEmpty()); // 输出:true
}
}
3.4 LinkedList 作为队列和栈使用
java
import java.util.LinkedList;
public class LinkedListAsQueueAndStack {
public static void main(String[] args) {
// ========== 作为队列(Queue)使用:先进先出(FIFO) ==========
System.out.println("========== 队列演示 ==========");
LinkedList<String> queue = new LinkedList<>();
// 入队(从尾部添加)
queue.offer("顾客1"); // 等同于 addLast()
queue.offer("顾客2");
queue.offer("顾客3");
System.out.println("队列:" + queue);
// 输出:[顾客1, 顾客2, 顾客3]
// 出队(从头部移除)
String served1 = queue.poll(); // 等同于 removeFirst()
System.out.println("服务的顾客:" + served1); // 输出:顾客1
String served2 = queue.poll();
System.out.println("服务的顾客:" + served2); // 输出:顾客2
System.out.println("剩余队列:" + queue);
// 输出:[顾客3]
// ========== 作为栈(Stack)使用:后进先出(LIFO) ==========
System.out.println("\n========== 栈演示 ==========");
LinkedList<String> stack = new LinkedList<>();
// 入栈(从头部添加)
stack.push("盘子1"); // 等同于 addFirst()
stack.push("盘子2");
stack.push("盘子3");
System.out.println("栈:" + stack);
// 输出:[盘子3, 盘子2, 盘子1]
// 出栈(从头部移除)
String popped1 = stack.pop(); // 等同于 removeFirst()
System.out.println("取出的盘子:" + popped1); // 输出:盘子3
String popped2 = stack.pop();
System.out.println("取出的盘子:" + popped2); // 输出:盘子2
System.out.println("剩余栈:" + stack);
// 输出:[盘子1]
}
}
四、Vector集合详解
4.1 Vector的核心特点
| 特点 | 说明 |
|---|---|
| 📦 底层结构 | 基于动态数组实现(与ArrayList相同) |
| 📈 初始容量 | 默认初始容量为 10 |
| 📊 扩容机制 | 扩容至 2倍(ArrayList是1.5倍) |
| ✅ 是否有序 | 有序 |
| 🔄 是否可重复 | 可重复 |
| 🔒 线程安全 | 是(所有方法都加了 synchronized) |
| ⚡ 性能 | 较慢(因为同步开销) |
| 🚫 使用建议 | 不推荐使用(已被 ArrayList 替代) |
4.2 Vector 为什么不推荐使用?
java
// Vector 的方法都加了 synchronized
public class Vector<E> {
// 所有方法都是同步的
public synchronized boolean add(E e) { ... }
public synchronized E get(int index) { ... }
public synchronized E remove(int index) { ... }
}
不推荐的原因:
- 性能低:即使在单线程环境,也有同步开销
- 粒度粗:锁的粒度太大,并发性能差
- 有替代品 :
- 单线程 → 用
ArrayList - 多线程 → 用
Collections.synchronizedList()或CopyOnWriteArrayList
- 单线程 → 用
4.3 Vector 基本用法(了解即可)
java
import java.util.Vector;
import java.util.Enumeration;
public class VectorDemo {
public static void main(String[] args) {
// ========== 1. 创建 Vector ==========
Vector<String> vector = new Vector<>(); // 默认容量10
Vector<Integer> vec2 = new Vector<>(20); // 指定初始容量20
Vector<Double> vec3 = new Vector<>(10, 5); // 初始容量10,每次扩容增加5
// ========== 2. 添加元素 ==========
vector.add("元素1");
vector.add("元素2");
vector.addElement("元素3"); // Vector 特有方法,等同于 add()
System.out.println(vector); // 输出:[元素1, 元素2, 元素3]
// ========== 3. 访问元素 ==========
String item1 = vector.get(0); // 通用方法
String item2 = vector.elementAt(1); // Vector 特有方法
System.out.println("元素:" + item1 + ", " + item2);
// ========== 4. 修改元素 ==========
vector.set(1, "新元素2");
System.out.println(vector); // 输出:[元素1, 新元素2, 元素3]
// ========== 5. 删除元素 ==========
vector.remove(2); // 删除索引2的元素
vector.removeElement("元素1"); // 删除指定元素
System.out.println(vector); // 输出:[新元素2]
// ========== 6. 容量相关 ==========
System.out.println("元素个数:" + vector.size()); // 实际元素数量
System.out.println("容量:" + vector.capacity()); // 数组容量
// ========== 7. 遍历方式(Enumeration - Vector特有) ==========
vector.add("A");
vector.add("B");
vector.add("C");
Enumeration<String> enumeration = vector.elements();
System.out.print("遍历结果:");
while (enumeration.hasMoreElements()) {
System.out.print(enumeration.nextElement() + " ");
}
// 输出:新元素2 A B C
}
}
4.4 多线程场景示例
java
import java.util.Vector;
public class VectorThreadSafeDemo {
public static void main(String[] args) throws InterruptedException {
// Vector 在多线程环境下是安全的
Vector<Integer> sharedVector = new Vector<>();
// 创建两个线程同时添加元素
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
sharedVector.add(i); // 线程安全,不会出现数据错乱
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
// 等待两个线程执行完毕
thread1.join();
thread2.join();
System.out.println("Vector 大小:" + sharedVector.size());
// 输出:2000(确保线程安全)
// ========== 对比:ArrayList 在多线程下不安全 ==========
// 如果用 ArrayList,可能输出 < 2000,甚至抛出异常
}
}
五、List集合对比总结
5.1 三种 List 的核心区别
| 对比项 | ArrayList | LinkedList | Vector |
|---|---|---|---|
| 底层结构 | 数组 | 双向链表 | 数组 |
| 查询速度 | 快 O(1) | 慢 O(n) | 快 O(1) |
| 增删速度 | 慢 O(n) | 快 O(1) | 慢 O(n) |
| 线程安全 | 否 | 否 | 是 |
| 扩容机制 | 1.5倍 | 不需要扩容 | 2倍 |
| 初始容量 | 10 | 无 | 10 |
| 内存占用 | 较小 | 较大(存指针) | 较小 |
| 使用场景 | 频繁查询 | 频繁增删 | 多线程(不推荐) |
5.2 使用场景选择
java
// ========== 场景1:频繁查询,少量增删 → ArrayList ==========
// 例如:学生成绩管理系统
ArrayList<Integer> scores = new ArrayList<>();
scores.add(85);
scores.add(90);
scores.add(78);
// 频繁查询分数
System.out.println("第一个学生分数:" + scores.get(0));
// ========== 场景2:频繁增删,少量查询 → LinkedList ==========
// 例如:任务队列、浏览器历史记录
LinkedList<String> browserHistory = new LinkedList<>();
browserHistory.addFirst("www.baidu.com"); // 快速在头部添加
browserHistory.addFirst("www.google.com");
browserHistory.removeFirst(); // 快速删除头部
// ========== 场景3:需要线程安全 → 不用Vector,用以下方式 ==========
// 推荐方式1:包装 ArrayList
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// 推荐方式2:使用 CopyOnWriteArrayList(读多写少场景)
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
5.3 性能对比测试
java
import java.util.*;
public class ListPerformanceTest {
public static void main(String[] args) {
int count = 100000; // 测试数据量
// ========== 测试1:尾部添加性能 ==========
System.out.println("========== 尾部添加10万个元素 ==========");
testAdd(new ArrayList<>(), "ArrayList", count);
testAdd(new LinkedList<>(), "LinkedList", count);
testAdd(new Vector<>(), "Vector", count);
// ========== 测试2:随机访问性能 ==========
System.out.println("\n========== 随机访问1万次 ==========");
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < count; i++) {
arrayList.add(i);
linkedList.add(i);
}
testGet(arrayList, "ArrayList", 10000);
testGet(linkedList, "LinkedList", 10000);
}
// 测试添加性能
static void testAdd(List<Integer> list, String name, int count) {
long start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
list.add(i);
}
long end = System.currentTimeMillis();
System.out.println(name + " 添加耗时:" + (end - start) + "ms");
}
// 测试访问性能
static void testGet(List<Integer> list, String name, int times) {
Random random = new Random();
long start = System.currentTimeMillis();
for (int i = 0; i < times; i++) {
list.get(random.nextInt(list.size()));
}
long end = System.currentTimeMillis();
System.out.println(name + " 访问耗时:" + (end - start) + "ms");
}
}
/*
* 输出结果(仅供参考):
* ========== 尾部添加10万个元素 ==========
* ArrayList 添加耗时:8ms ⚡ 快
* LinkedList 添加耗时:12ms ⚡ 快
* Vector 添加耗时:15ms 🐢 慢(有同步开销)
*
* ========== 随机访问1万次 ==========
* ArrayList 访问耗时:2ms ⚡ 极快
* LinkedList 访问耗时:2500ms 🐢 极慢(需要遍历)
*/
六、集合与数组的区别
6.1 长度区别
| 对比项 | 数组 | 集合 |
|---|---|---|
| 长度 | 固定,创建后不可改变 | 可变,自动扩容 |
| 声明方式 | int[] arr = new int[5] |
ArrayList<Integer> list = new ArrayList<>() |
java
// ========== 数组长度固定 ==========
int[] numbers = new int[3];
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
// numbers[3] = 40; // ❌ 报错!ArrayIndexOutOfBoundsException
// ========== 集合长度可变 ==========
ArrayList<Integer> numList = new ArrayList<>();
numList.add(10);
numList.add(20);
numList.add(30);
numList.add(40); // ✅ 自动扩容,无问题
numList.add(50);
System.out.println("集合大小:" + numList.size()); // 输出:5
6.2 内容区别
| 对比项 | 数组 | 集合 |
|---|---|---|
| 存储类型 | 可以存基本数据类型 和引用类型 | 只能存引用类型(对象) |
| 示例 | int[] arr 或 String[] arr |
ArrayList<Integer> 或 ArrayList<String> |
java
// ========== 数组可以存基本类型 ==========
int[] ages = {18, 20, 22}; // ✅ 直接存 int
String[] names = {"张三", "李四"}; // ✅ 存 String 对象
// ========== 集合只能存引用类型 ==========
// ArrayList<int> list1 = new ArrayList<>(); // ❌ 错误!不能用 int
ArrayList<Integer> list2 = new ArrayList<>(); // ✅ 必须用 Integer(包装类)
list2.add(18); // 自动装箱:int → Integer
int age = list2.get(0); // 自动拆箱:Integer → int
ArrayList<String> list3 = new ArrayList<>(); // ✅ String 是引用类型
list3.add("Hello");
6.3 元素内容区别
| 对比项 | 数组 | 集合 |
|---|---|---|
| 同一性 | 只能存同一数据类型 | 理论上可以存不同类型(但不推荐) |
| 类型安全 | 编译时检查 | 使用泛型检查 |
java
// ========== 数组只能存同一类型 ==========
String[] students = new String[3];
students[0] = "张三";
students[1] = "李四";
// students[2] = 123; // ❌ 错误!类型不匹配
// ========== 集合使用泛型保证类型安全 ==========
ArrayList<String> list1 = new ArrayList<>();
list1.add("张三");
// list1.add(123); // ❌ 错误!泛型限制只能添加 String
// 不使用泛型(不推荐)
ArrayList list2 = new ArrayList(); // 没有泛型
list2.add("张三"); // ✅ 可以
list2.add(123); // ✅ 也可以(但不安全)
list2.add(true); // ✅ 还可以(完全混乱)
// 问题:取出时需要强制类型转换,容易出错
七、常见面试题精讲
面试题1:ArrayList 底层为什么使用 Object[] 存储?
答案:
- 通用性:Object 是所有类的父类,可以存储任何类型的对象
- 多态性:利用多态特性,底层统一用 Object 存储,上层通过泛型控制类型
- 类型安全 :配合泛型
<E>,在编译时检查类型,运行时通过类型擦除保证兼容性
java
// ArrayList 源码(简化)
public class ArrayList<E> {
transient Object[] elementData; // 使用 Object[]
public boolean add(E e) {
elementData[size++] = e; // 存入时:E → Object(向上转型)
return true;
}
public E get(int index) {
return (E) elementData[index]; // 取出时:Object → E(强制类型转换)
}
}
// 使用示例
ArrayList<String> list = new ArrayList<>();
list.add("Hello"); // 存入:"Hello"(String) → Object
String str = list.get(0); // 取出:Object → "Hello"(String)
面试题2:ArrayList 和 LinkedList 的区别?如何选择?
答案:
| 维度 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 随机访问 | O(1) 快 | O(n) 慢 |
| 头尾增删 | O(n) 慢 | O(1) 快 |
| 内存占用 | 较小 | 较大(存储指针) |
选择建议:
- 查询多,增删少 → ArrayList(如:学生成绩查询)
- 增删多,查询少 → LinkedList(如:任务队列)
- 不确定 → 优先 ArrayList(综合性能更好)
面试题3:ArrayList 的扩容机制是什么?
答案:
- 初始容量 :默认为 10
- 扩容时机:当元素个数超过当前容量时触发
- 扩容大小 :扩容至原来的 1.5 倍
- 扩容步骤 :
- 创建一个新数组,容量为
oldCapacity + (oldCapacity >> 1)(即 1.5 倍) - 使用
Arrays.copyOf()将旧数组元素复制到新数组 - 将引用指向新数组
- 创建一个新数组,容量为
java
// 源码简化
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
}
elementData = Arrays.copyOf(elementData, newCapacity);
}
面试题4:为什么 ArrayList 查询快,增删慢?
答案:
查询快:
- 基于数组实现,支持随机访问
- 通过索引直接计算内存地址:
地址 = 首地址 + 索引 × 元素大小 - 时间复杂度:O(1)
增删慢:
- 插入/删除元素时,需要移动后续所有元素
- 例如:在索引 1 插入元素,索引 1-n 的元素都要后移
- 时间复杂度:O(n)
java
// 删除中间元素的过程
ArrayList: [A, B, C, D, E]
删除索引2的元素C:
1. 找到索引2 → O(1)
2. 删除C
3. 将D、E向前移动 → O(n)
结果: [A, B, D, E]
面试题5:为什么 LinkedList 增删快,查询慢?
答案:
增删快(仅限头尾操作):
- 基于双向链表,只需修改指针
- 头部/尾部增删:直接操作 first/last 指针 → O(1)
- 中间增删:需要先找到位置 → O(n)
查询慢:
- 不支持随机访问,必须从头/尾遍历
- 时间复杂度:O(n)
java
// LinkedList 头部插入
原链表: [A] ⇄ [B] ⇄ [C]
头部插入D:
1. 创建节点D
2. D.next = A
3. A.prev = D
4. first = D
结果: [D] ⇄ [A] ⇄ [B] ⇄ [C]
// 只需修改指针,不需要移动元素 → O(1)
面试题6:ArrayList 是线程安全的吗?如何保证线程安全?
答案:
ArrayList 不是线程安全的。
线程安全的方案:
方案1 :使用 Collections.synchronizedList()
java
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
syncList.add("元素"); // 线程安全
方案2 :使用 CopyOnWriteArrayList(读多写少场景)
java
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
cowList.add("元素"); // 线程安全,写时复制
方案3 :使用 Vector(不推荐,性能差)
java
Vector<String> vector = new Vector<>();
vector.add("元素"); // 线程安全,但性能低
面试题7:ArrayList 和 Vector 的区别?
| 对比项 | ArrayList | Vector |
|---|---|---|
| 线程安全 | 否 | 是(synchronized) |
| 扩容大小 | 1.5倍 | 2倍 |
| 性能 | 快 | 慢(同步开销) |
| 推荐程度 | ✅ 推荐 | ❌ 不推荐(已过时) |
结论 :优先使用 ArrayList,需要线程安全时用 Collections.synchronizedList() 包装。
面试题8:如何在遍历时删除元素?
❌ 错误方式1:增强 for 循环删除
java
ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String item : list) {
if ("B".equals(item)) {
list.remove(item); // ❌ 抛出 ConcurrentModificationException
}
}
❌ 错误方式2:普通 for 循环删除(会漏删)
java
ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "B", "C"));
for (int i = 0; i < list.size(); i++) {
if ("B".equals(list.get(i))) {
list.remove(i); // ❌ 删除后索引变化,会漏删
}
}
✅ 正确方式1:迭代器删除
java
ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("B".equals(item)) {
iterator.remove(); // ✅ 使用迭代器的 remove 方法
}
}
System.out.println(list); // 输出:[A, C]
✅ 正确方式2:倒序遍历删除
java
ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "B", "C"));
for (int i = list.size() - 1; i >= 0; i--) {
if ("B".equals(list.get(i))) {
list.remove(i); // ✅ 从后往前删,不影响前面索引
}
}
System.out.println(list); // 输出:[A, C]
面试题9:ArrayList 的 remove(Object o) 和 remove(int index) 有什么区别?
答案:
java
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(10, 20, 30));
// ========== remove(int index) - 根据索引删除 ==========
list.remove(1); // 删除索引1的元素(20)
System.out.println(list); // 输出:[10, 30]
// ========== remove(Object o) - 根据对象删除 ==========
list = new ArrayList<>(Arrays.asList(10, 20, 30));
list.remove(Integer.valueOf(20)); // 删除值为20的元素
System.out.println(list); // 输出:[10, 30]
// ⚠️ 注意:直接写数字会调用 remove(int index)
list = new ArrayList<>(Arrays.asList(10, 20, 30));
list.remove(2); // 删除索引2的元素(30),而不是值为2的元素
System.out.println(list); // 输出:[10, 20]
面试技巧:
remove(int index):返回被删除的元素remove(Object o):返回 boolean,表示是否删除成功- 删除 Integer 类型时,用
Integer.valueOf()明确意图
面试题10:List 如何去重?
方式1:使用 Set(会改变顺序)
java
ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "A", "C", "B"));
HashSet<String> set = new HashSet<>(list); // 自动去重
ArrayList<String> result = new ArrayList<>(set);
System.out.println(result); // 输出:[A, B, C](顺序可能变化)
方式2:使用 LinkedHashSet(保持顺序)
java
ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "A", "C", "B"));
LinkedHashSet<String> set = new LinkedHashSet<>(list); // 去重且保持顺序
ArrayList<String> result = new ArrayList<>(set);
System.out.println(result); // 输出:[A, B, C](保持插入顺序)
方式3:使用 Stream(Java 8+)
java
ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "A", "C", "B"));
List<String> result = list.stream().distinct().collect(Collectors.toList());
System.out.println(result); // 输出:[A, B, C]
📌 总结
🎯 核心知识点回顾
- ArrayList:基于数组,查询快O(1),增删慢O(n),非线程安全
- LinkedList:基于链表,查询慢O(n),头尾增删快O(1),非线程安全
- Vector:基于数组,线程安全,但性能差,不推荐使用
🚀 使用建议
| 场景 | 推荐选择 |
|---|---|
| 频繁查询、少量增删 | ArrayList |
| 频繁头尾增删 | LinkedList |
| 需要线程安全 | Collections.synchronizedList(ArrayList) |
| 读多写少的并发 | CopyOnWriteArrayList |
💡 面试高频问题
- ArrayList 和 LinkedList 的区别? → 数组 vs 链表
- ArrayList 的扩容机制? → 1.5倍扩容
- 为什么 ArrayList 用 Object[]? → 通用性 + 泛型配合
- 如何在遍历时安全删除元素? → 用迭代器
- ArrayList 线程安全吗? → 不安全,用
Collections.synchronizedList()
🎓 学习建议
- 动手实践:每个示例代码都运行一遍,修改参数观察结果
- 画图理解:ArrayList 的扩容、LinkedList 的指针变化,画图更清晰
- 对比记忆:制作对比表格,强化记忆
- 刷题巩固:LeetCode 上刷链表、数组相关题目
💬 结语:集合是 Java 基础中的重点,掌握 List 的三个实现类,对后续学习 Set、Map 有很大帮助。建议多动手写代码,理论结合实践,才能真正掌握!
📖 推荐阅读 :Java 集合框架源码解析、JDK 官方文档
🔗 相关文章:Java泛型详解、Java多线程与集合