哈喽,各位 Java 学习者!欢迎来到《Java 学习日记》的第十三篇内容~ 前面我们掌握了 String 类的核心用法,今天要正式进入 Java 集合框架的核心 ------List接口,以及它的两个最常用实现类:ArrayList和LinkedList。集合是 Java 中存储多个元素的核心容器,而 List 作为 "有序、可重复" 的集合,是开发中使用频率最高的类型。本文会从 List 的核心特性、ArrayList 与 LinkedList 的底层原理,到实战场景选择和性能优化,帮你彻底掌握 List 的使用!
一、List 接口:有序、可重复的集合核心
1. 为什么需要 List?
数组是固定长度的容器,一旦创建无法扩容,且缺乏便捷的增删改查方法(如添加元素需要手动扩容、删除元素需要手动移动数组)。而 List 作为动态数组,完美解决了这些问题:
- 自动扩容,无需关心底层长度;
- 提供丰富的操作方法(add/remove/get/set 等);
- 支持索引访问,有序且可重复。
2. List 接口的核心特性
| 特性 | 说明 |
|---|---|
| 有序性 | 元素按插入顺序排列,可通过索引(下标)访问 |
| 可重复性 | 允许存储重复元素(甚至 null) |
| 索引访问 | 支持get(int index)、set(int index, E e)等索引操作 |
| 动态扩容 | 底层自动扩容,无需手动管理容量 |
3. List 接口的常用方法(必背)
List 继承自Collection接口,新增了大量索引相关的方法,核心方法如下:
| 方法 | 作用 | 示例 |
|---|---|---|
add(E e) |
末尾添加元素 | list.add("Java") |
add(int index, E e) |
指定索引插入元素 | list.add(0, "Python") |
get(int index) |
获取指定索引元素 | list.get(0) |
set(int index, E e) |
修改指定索引元素 | list.set(0, "C++") |
remove(int index) |
删除指定索引元素 | list.remove(0) |
remove(Object o) |
删除第一个匹配的元素 | list.remove("Java") |
indexOf(Object o) |
获取元素首次出现的索引 | list.indexOf("Java") |
lastIndexOf(Object o) |
获取元素最后出现的索引 | list.lastIndexOf("Java") |
size() |
获取元素个数 | list.size() |
isEmpty() |
判断是否为空 | list.isEmpty() |
clear() |
清空所有元素 | list.clear() |
subList(int from, int to) |
截取 [from, to) 区间的子列表 | list.subList(0, 2) |
实战 1:List 接口基础用法
java
运行
java
import java.util.ArrayList;
import java.util.List;
public class ListBasicDemo {
public static void main(String[] args) {
// 1. 创建List对象(多态:父接口引用指向子类实现)
List<String> list = new ArrayList<>();
// 2. 添加元素
list.add("Java");
list.add("Python");
list.add("Java"); // 允许重复
list.add(null); // 允许null
list.add(1, "C++"); // 指定索引插入
// 3. 遍历元素(三种方式)
// 方式1:普通for循环(按索引)
System.out.println("普通for循环遍历:");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// 方式2:增强for循环
System.out.println("\n增强for循环遍历:");
for (String s : list) {
System.out.println(s);
}
// 方式3:迭代器(支持删除元素,避免并发修改异常)
System.out.println("\n迭代器遍历:");
java.util.Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s == null) {
it.remove(); // 迭代器删除,安全
} else {
System.out.println(s);
}
}
// 4. 其他常用方法
System.out.println("\n元素个数:" + list.size()); // 4
System.out.println("Java首次出现索引:" + list.indexOf("Java")); // 2
System.out.println("修改索引0的元素:");
list.set(0, "Go");
System.out.println(list.get(0)); // Go
}
}
二、ArrayList:基于数组的动态列表(开发首选)
1. 底层原理
ArrayList是 List 接口的数组实现,核心源码简化如下:
java
运行
java
public class ArrayList<E> extends AbstractList<E> implements List<E> {
// 底层存储元素的数组
transient Object[] elementData;
// 数组默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 实际元素个数
private int size;
// 添加元素:数组满了则扩容
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 检查容量,不足则扩容
elementData[size++] = e;
return true;
}
// 扩容逻辑:默认扩容为原容量的1.5倍
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
2. 核心特性
- 底层结构:Object 数组(连续内存空间);
- 初始容量:默认 10(Java 8+,空构造器初始为 {},首次添加元素扩容为 10);
- 扩容规则 :默认扩容为原容量的 1.5 倍(通过
Arrays.copyOf复制数组,耗时); - 访问性能:随机访问(get/set)极快(O (1)),增删慢(尤其是中间位置,O (n));
- 线程安全 :非线程安全(多线程操作需手动同步,如
Collections.synchronizedList)。
3. 性能分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 随机访问(get/set) | O(1) | 数组直接通过索引访问,无遍历 |
| 末尾添加(add (E e)) | O(1) | 无扩容时直接赋值,扩容时 O (n) |
| 指定位置增删(add/remove (int index)) | O(n) | 需要移动后续元素,元素越多越慢 |
| 查找元素(indexOf) | O(n) | 需遍历数组 |
实战 2:ArrayList 的扩容与性能验证
java
运行
java
import java.util.ArrayList;
import java.util.List;
public class ArrayListDemo {
public static void main(String[] args) {
// 1. 指定初始容量(优化:预估元素个数,避免频繁扩容)
List<Integer> list = new ArrayList<>(10000); // 初始容量10000
// 2. 批量添加元素,测试性能
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
list.add(i);
}
long end = System.currentTimeMillis();
System.out.println("添加10万元素耗时:" + (end - start) + "ms"); // 约1ms
// 3. 随机访问(极快)
start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
list.get(i);
}
end = System.currentTimeMillis();
System.out.println("随机访问10万次耗时:" + (end - start) + "ms"); // 约0ms
// 4. 指定位置插入(较慢)
start = System.currentTimeMillis();
list.add(50000, 999999);
end = System.currentTimeMillis();
System.out.println("中间位置插入耗时:" + (end - start) + "ms"); // 约1ms
}
}
三、LinkedList:基于双向链表的列表(高频增删场景)
1. 底层原理
LinkedList是 List 接口的双向链表实现,每个节点包含前驱、后继和元素,核心源码简化如下:
java
运行
java
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E> {
// 链表节点
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;
}
}
// 头节点、尾节点
transient Node<E> first;
transient Node<E> last;
// 元素个数
int size;
// 末尾添加元素:直接修改尾节点
public boolean add(E e) {
linkLast(e);
return true;
}
// 指定位置添加:只需修改节点的前驱后继,无需移动元素
public void add(int index, E element) {
Node<E> x = node(index); // 找到索引对应的节点
Node<E> newNode = new Node<>(x.prev, element, x);
x.prev.next = newNode;
x.prev = newNode;
size++;
}
}
2. 核心特性
- 底层结构:双向链表(非连续内存空间);
- 容量:无固定容量,无需扩容(每个节点动态创建);
- 访问性能:随机访问极慢(O (n)),需遍历链表找节点;
- 增删性能:指定位置增删快(O (1)),只需修改节点引用;
- 额外功能 :实现
Deque接口,可作为队列 / 双端队列使用; - 线程安全:非线程安全。
3. 性能分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 随机访问(get/set) | O(n) | 需遍历链表找到对应节点 |
| 首尾增删(add/removeFirst/Last) | O(1) | 直接修改头尾节点引用 |
| 指定位置增删 | O (n)(查找)+ O (1)(修改) | 查找节点耗时,修改节点极快 |
| 查找元素(indexOf) | O(n) | 需遍历链表 |
实战 3:LinkedList 的队列用法
java
运行
java
import java.util.LinkedList;
import java.util.Queue;
public class LinkedListDemo {
public static void main(String[] args) {
// 1. 作为List使用
LinkedList<String> list = new LinkedList<>();
list.add("A");
list.add("B");
list.addFirst("头节点"); // 链表特有方法
list.addLast("尾节点");
System.out.println("获取头节点:" + list.getFirst()); // 头节点
// 2. 作为队列(Queue)使用(先进先出FIFO)
Queue<String> queue = new LinkedList<>();
queue.offer("1号任务"); // 入队
queue.offer("2号任务");
queue.offer("3号任务");
System.out.println("\n队列遍历:");
while (!queue.isEmpty()) {
String task = queue.poll(); // 出队(移除并返回头元素)
System.out.println("执行任务:" + task);
}
// 3. 随机访问性能对比(LinkedList慢)
LinkedList<Integer> numList = new LinkedList<>();
for (int i = 0; i < 10000; i++) {
numList.add(i);
}
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
numList.get(i); // 遍历链表,极慢
}
long end = System.currentTimeMillis();
System.out.println("\nLinkedList随机访问1万次耗时:" + (end - start) + "ms"); // 约50ms+
}
}
四、ArrayList vs LinkedList:核心区别与场景选择
1. 核心区别对比表
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 随机访问 | 极快(O (1)) | 极慢(O (n)) |
| 首尾增删 | 快(O (1),无扩容) | 极快(O (1)) |
| 中间增删 | 慢(O (n),需移动元素) | 中(O (n) 查找 + O (1) 修改) |
| 内存占用 | 连续内存,有扩容冗余 | 非连续内存,每个节点多存前驱后继(内存开销大) |
| 初始容量 | 默认 10 | 无(按需创建节点) |
| 额外功能 | 仅 List 接口 | 实现 Deque,可作为队列 / 栈 |
2. 场景选择最佳实践
| 场景 | 推荐使用 | 原因 |
|---|---|---|
| 大量随机访问(get/set) | ArrayList | 数组索引访问,性能碾压 LinkedList |
| 大量首尾增删(队列 / 栈) | LinkedList | 无需移动元素,直接修改头尾引用 |
| 大量中间位置增删 | 视情况:元素少→LinkedList元素多→ArrayList | 元素多的时候,LinkedList 查找节点的耗时超过 ArrayList 移动元素的耗时 |
| 元素数量可预估 | ArrayList(指定初始容量) | 避免扩容,提升性能 |
| 元素数量不可预估 | LinkedList | 无需扩容,按需创建节点 |
| 开发无特殊需求 | ArrayList | 综合性能更优,是开发默认选择 |
实战 4:ArrayList vs LinkedList 性能对比
java
运行
java
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class ListPerformanceDemo {
// 测试方法:批量添加元素
public static void testAdd(List<Integer> list, int count) {
long start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
list.add(i);
}
long end = System.currentTimeMillis();
System.out.println(list.getClass().getSimpleName() + "添加" + count + "元素耗时:" + (end - start) + "ms");
}
// 测试方法:随机访问
public static void testGet(List<Integer> list, int count) {
long start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
list.get(i);
}
long end = System.currentTimeMillis();
System.out.println(list.getClass().getSimpleName() + "随机访问" + count + "次耗时:" + (end - start) + "ms");
}
// 测试方法:中间位置插入
public static void testAddMiddle(List<Integer> list, int count) {
// 先添加count个元素
for (int i = 0; i < count; i++) {
list.add(i);
}
long start = System.currentTimeMillis();
// 中间位置插入1000次
for (int i = 0; i < 1000; i++) {
list.add(count / 2, i);
}
long end = System.currentTimeMillis();
System.out.println(list.getClass().getSimpleName() + "中间插入1000次耗时:" + (end - start) + "ms");
}
public static void main(String[] args) {
int count = 10000;
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
// 1. 测试添加
testAdd(arrayList, count); // ArrayList添加10000元素耗时:0ms
testAdd(linkedList, count); // LinkedList添加10000元素耗时:1ms
// 2. 测试随机访问
testGet(arrayList, count); // ArrayList随机访问10000次耗时:0ms
testGet(linkedList, count); // LinkedList随机访问10000次耗时:50ms+
// 3. 测试中间插入
testAddMiddle(arrayList, count); // ArrayList中间插入1000次耗时:1ms
testAddMiddle(linkedList, count); // LinkedList中间插入1000次耗时:10ms+
}
}
五、List 使用的高频坑点与优化技巧
1. 高频坑点
坑点 1:ArrayList 的扩容性能损耗
-
问题 :频繁扩容会触发
Arrays.copyOf,复制数组耗时; -
解决 :预估元素个数,创建时指定初始容量:
java
运行
java// 优化前:默认容量10,需多次扩容 List<String> list = new ArrayList<>(); // 优化后:指定初始容量,避免扩容 List<String> list = new ArrayList<>(100);
坑点 2:增强 for 循环中删除元素(并发修改异常)
-
问题 :增强 for 循环遍历中调用
list.remove()会抛出ConcurrentModificationException; -
解决 :使用迭代器删除:
java
运行
java// 错误示例 for (String s : list) { if (s.equals("Java")) { list.remove(s); // 抛出异常 } } // 正确示例 Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if (s.equals("Java")) { it.remove(); // 迭代器删除,安全 } }
坑点 3:LinkedList 的随机访问滥用
- 问题:误以为 LinkedList 增删快,就所有场景都用,结果随机访问性能极差;
- 解决:优先用 ArrayList,仅在大量首尾增删时用 LinkedList。
2. 优化技巧
技巧 1:批量添加元素(addAll)
java
运行
java
// 批量添加比循环单个add更快(减少扩容检查次数)
List<String> list = new ArrayList<>(100);
List<String> data = Arrays.asList("A", "B", "C", "D");
list.addAll(data);
技巧 2:子列表(subList)的使用注意
-
subList返回的是原列表的视图,修改子列表会影响原列表; -
子列表使用完后及时赋值为 null,避免持有原列表引用导致内存泄漏: java
运行
javaList<String> subList = list.subList(0, 2); subList.set(0, "X"); // 原列表也会被修改 subList = null; // 释放引用
技巧 3:空列表处理(Collections.emptyList)
-
避免返回 null,返回空列表更安全: java
运行
java// 错误:返回null,调用方需判空 public List<String> getEmptyList() { return null; } // 正确:返回空列表,调用方可直接遍历 public List<String> getEmptyList() { return Collections.emptyList(); }
六、综合实战:List 实现学生成绩管理系统
java
运行
java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Scanner;
// 学生类
class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
// getter/setter
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getScore() { return score; }
public void setScore(int score) { this.score = score; }
@Override
public String toString() {
return "姓名:" + name + ",成绩:" + score;
}
}
// 成绩管理系统
public class StudentScoreManager {
private List<Student> studentList;
public StudentScoreManager() {
// 指定初始容量,优化性能
studentList = new ArrayList<>(100);
}
// 添加学生
public void addStudent(Student student) {
studentList.add(student);
System.out.println("添加成功:" + student);
}
// 删除学生(按姓名)
public void removeStudent(String name) {
Iterator<Student> it = studentList.iterator();
boolean removed = false;
while (it.hasNext()) {
Student s = it.next();
if (s.getName().equals(name)) {
it.remove();
removed = true;
System.out.println("删除成功:" + s);
break;
}
}
if (!removed) {
System.out.println("未找到学生:" + name);
}
}
// 查询所有学生
public void queryAllStudents() {
if (studentList.isEmpty()) {
System.out.println("暂无学生信息");
return;
}
System.out.println("\n所有学生信息:");
for (Student s : studentList) {
System.out.println(s);
}
}
// 修改学生成绩
public void updateScore(String name, int newScore) {
boolean updated = false;
for (Student s : studentList) {
if (s.getName().equals(name)) {
s.setScore(newScore);
updated = true;
System.out.println("修改成功:" + s);
break;
}
}
if (!updated) {
System.out.println("未找到学生:" + name);
}
}
public static void main(String[] args) {
StudentScoreManager manager = new StudentScoreManager();
Scanner scanner = new Scanner(System.in);
// 模拟操作
manager.addStudent(new Student("张三", 90));
manager.addStudent(new Student("李四", 85));
manager.addStudent(new Student("王五", 78));
manager.queryAllStudents();
manager.updateScore("李四", 95);
manager.removeStudent("王五");
manager.queryAllStudents();
scanner.close();
}
}
总结
关键点回顾
- List 核心特性:有序、可重复、支持索引访问,是开发中最常用的集合类型;
- ArrayList:数组实现,随机访问极快,是默认选择,使用时建议指定初始容量;
- LinkedList:双向链表实现,首尾增删极快,适合队列 / 栈场景,避免随机访问;
- 核心坑点:增强 for 循环删除元素会抛异常(用迭代器)、ArrayList 频繁扩容(指定初始容量);
- 场景选择:优先用 ArrayList,仅在大量首尾增删时用 LinkedList。
List 是 Java 集合框架的基础,掌握 ArrayList 和 LinkedList 的底层原理和场景选择,能让你的代码既简洁又高效。下一篇【Day14】,我们会讲解 "集合框架(二):Set 接口(HashSet vs TreeSet)实战",帮你掌握 "无序、不可重复" 的集合类型!如果今天的内容对你有帮助,欢迎点赞 + 收藏 + 关注,有任何问题都可以在评论区留言,咱们一起讨论~ 明天见!🚀