Java List 集合详解课件
一、课程信息
学习目标
- 理解 List 集合的核心特性(有序、可重复、索引访问)
- 掌握 ArrayList/LinkedList 的使用场景与区别
- 熟练操作 List 的增删改查,避免常见错误
- 能通过 List 解决实际问题(如学生管理、购物车)
二、课程导入:数组的痛点与 List 的诞生
❌ 数组的三大痛点
String[] fruits = new String[3]; // 1. 长度固定
fruits[0] = "苹果";
fruits[1] = "香蕉";
// 插入橘子到中间?需手动搬移元素(2. 增删效率低)
// 存储混合类型?Object[]不安全(3. 类型不统一)
✅ List 的解决方案
List<String> fruits = new ArrayList<>(); // 动态扩容
fruits.add("苹果");
fruits.add(1, "橘子"); // 自动扩容+元素移动
// 泛型保证类型安全,只能存String
📌 核心对比:数组 vs List
特性 | 数组 | List(如 ArrayList) |
---|---|---|
长度 | 固定 | 动态扩展(按需扩容) |
增删效率 | 低(需手动移动元素) | 高(ArrayList 尾部快,中间慢) |
类型安全 | 基本类型 / 对象数组,需强转 | 泛型支持,编译期检查 |
常用操作 | 索引访问 | 支持更多 API(如 addAll, indexOf) |
三、List 基础:核心特性与常用方法
1. List 的三大特性(结合案例)
案例:学生名单管理
List<String> students = new ArrayList<>();
students.add("张三"); // 有序:存入顺序=取出顺序
students.add("李四");
students.add("张三"); // 可重复:允许元素重复
System.out.println(students.get(1)); // 索引访问:李四
2. 常用方法速查表(带案例)
方法名 | 作用 | 案例(以 students 为例) |
---|---|---|
add(E e) | 尾部添加元素 | students.add("王五") |
add(int index, E e) | 插入元素到指定位置 | students.add(1, "赵六") (索引从 0 开始) |
get(int index) | 获取指定索引的元素 | System.out.println(students.get(0)) |
set(int index, E e) | 修改指定索引的元素 | students.set(2, "钱七") |
remove(int index) | 删除指定索引的元素 | students.remove(1) (赵六被删除) |
size() | 获取元素个数 | System.out.println(students.size()) |
contains(Object o) | 判断是否包含元素 | students.contains("张三") → true |
indexOf(Object o) | 获取元素首次出现的索引 | students.indexOf("张三") → 0 |
3. 遍历方式对比(重点!)
案例:打印所有学生
// 方式1:for循环(最常用)
for (int i = 0; i < students.size(); i++) {
System.out.println(students.get(i));
}
// 方式2:增强for(简洁,无法获取索引)
for (String s : students) {
System.out.println(s);
}
// 方式3:迭代器(可删除元素)
Iterator<String> it = students.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.equals("张三")) {
it.remove(); // 安全删除,避免并发异常
}
}
四、ArrayList:动态数组的实现
1. 内存分配与扩容机制(图解)
初始化:new ArrayList<>()
默认容量 10(JDK1.8)。
扩容过程:当元素填满时,扩容为原容量的 1.5 倍(比如 10→15→22...)。
案例:添加 11 个元素时的扩容(黑板画图):
ArrayList<Integer> nums = new ArrayList<>();
for (int i=0; i<11; i++) {
nums.add(i); // 第11次添加触发扩容
}
2. 增删性能对比(插入位置影响效率)
案例:尾部插入 vs 头部插入
List<String> list = new ArrayList<>();
long start = System.currentTimeMillis();
for (int i=0; i<100000; i++) {
list.add(0, "a"); // 头部插入,每次移动所有元素
}
System.out.println("头部插入耗时:" + (System.currentTimeMillis()-start));
// 输出:约3000ms(尾部插入仅需1ms)
3. 适用场景:适合频繁查询、尾部增删的场景
案例:日志系统(按时间顺序添加,很少修改中间元素)。
五、LinkedList:链表的实现
1. 双向链表结构(图解节点)
每个节点包含prev
、next
、value
,头尾节点的prev
/next
为 null。
案例:添加 "苹果"→"香蕉"→"橘子",画出链表结构:
null ← [苹果] ↔ [香蕉] ↔ [橘子] → null
2. 增删优势(中间操作快)
案例:在中间插入元素(索引 1)
LinkedList<String> list = new LinkedList<>();
list.add("A");
list.add("B");
list.add(1, "C"); // 仅需修改前后节点的指针,无需移动元素
3. 特有方法(链表专属)
LinkedList<String> queue = new LinkedList<>();
queue.addFirst("加急"); // 头部插入(链表优势)
queue.addLast("普通"); // 尾部插入(等价于add())
System.out.println(queue.getFirst()); // 加急
4. 适用场景:适合频繁中间增删、队列 / 栈场景
案例:聊天消息队列(频繁头部 / 尾部操作)。
六、ArrayList vs LinkedList:对比与选择
操作 | ArrayList | LinkedList |
---|---|---|
随机访问 | O (1)(直接索引) | O (n)(从头遍历) |
尾部增删 | O (1)(偶尔扩容) | O(1) |
中间增删 | O (n)(移动元素) | O (1)(改指针) |
内存占用 | 连续内存,存储效率高 | 离散内存,每个节点有额外指针 |
常用场景 | 数组型操作(查询为主) | 链表型操作(增删为主) |
口诀:
“查询多用 ArrayList,增删多用 LinkedList,
头尾操作都能行,中间插入 Linked 快!”
七、初学者必避的 5 大陷阱
陷阱 1:索引越界(ArrayIndexOutOfBoundsException)
案例:
List<Integer> list = new ArrayList<>();
list.add(1);
System.out.println(list.get(1)); // 报错!索引最大0
解决:先list.size()
判断索引范围。
陷阱 2:并发修改异常(ConcurrentModificationException)
错误代码:
for (String s : students) { // 增强for内部用迭代器
if (s.equals("张三")) {
students.remove(s); // 直接修改集合,触发fail-fast机制
}
}
正确做法:用迭代器的remove()
方法(见遍历方式 3)。
陷阱 3:泛型擦除导致的类型不安全(JDK1.5 前的坑)
错误代码(无泛型):
List list = new ArrayList();
list.add("字符串");
int num = (Integer) list.get(0); // 运行时报ClassCastException
正确:永远使用泛型List<String> list = new ArrayList<>();
。
陷阱 4:remove(Object)
与remove(int)
的重载歧义
案例:
List<Integer> list = Arrays.asList(1,2,3);
list.remove(2); // 意图删除元素2?No!实际删除索引2(元素3)
解决:删除元素时,优先用list.remove((Integer)2)
明确类型。
陷阱 5:clear()
vs 置null
List<String> list = new ArrayList<>();
list.add("a");
list = null; // ① 引用置null,堆中对象等待GC
list.clear(); // ② 清空元素,集合仍可复用
八、综合案例:学生管理系统
需求:
- 添加学生(姓名、学号)
2. 根据学号删除学生
3. 修改学生姓名
4. 查询所有学生
实现代码(带注释):
class Student {
String name;
int id;
public Student(String name, int id) {
this.name = name;
this.id = id;
}
@Override
public String toString() {
return "学号:" + id + ",姓名:" + name;
}
}
public class StudentManager {
private List<Student> students = new ArrayList<>();
// 添加学生(去重:学号唯一)
public void addStudent(Student stu) {
for (Student s : students) {
if (s.id == stu.id) {
System.out.println("学号已存在!");
return;
}
}
students.add(stu);
System.out.println("添加成功!");
}
// 根据学号删除学生
public void deleteStudent(int id) {
for (Iterator<Student> it = students.iterator(); it.hasNext(); ) {
Student s = it.next();
if (s.id == id) {
it.remove();
System.out.println("删除成功!");
return;
}
}
System.out.println("未找到该学号!");
}
// 测试
public static void main(String[] args) {
StudentManager manager = new StudentManager();
manager.addStudent(new Student("张三", 1001));
manager.addStudent(new Student("李四", 1002));
manager.deleteStudent(1001); // 输出:删除成功!
System.out.println(manager.students); // 输出:[学号:1002,姓名:李四]
}
}
九、课堂练习
练习 1:购物车功能(ArrayList 实现)
需求:
- 添加商品(名称、价格)
- 删除指定名称的商品
- 计算总价格
- 打印所有商品(格式:编号 - 名称 - 价格)
提示:用List<Map<String, Object>>
或自定义Product
类。
练习 2:链表实现栈结构(LinkedList 特有方法)
LinkedList<String> stack = new LinkedList<>();
stack.push("A"); // 入栈(头部添加)
stack.pop(); // 出栈(头部删除)
十、课程总结
知识图谱:
List接口 → 有序、可重复、索引
↳ ArrayList:动态数组,查询快
↳ LinkedList:双向链表,增删快
核心方法:add/get/set/remove/indexOf
避坑指南:索引越界、并发修改、泛型缺失
口诀记忆:
“List 有序能重复,索引访问是基础,
Array 查询快如飞,Linked 增删不挪位,
泛型一定不能少,迭代删除用 it.remove!”
十一、课后作业(必做 + 选做)
必做 1:用 List 实现班级成绩统计
- 功能:输入 10 个学生成绩,统计平均分、最高分、最低分
- 提示:
ArrayList<Double>
存储成绩,Collections.max(list)
求最高分
必做 2:链表去重(保留首次出现的元素)
List<Integer> list = Arrays.asList(1, 2, 1, 3, 2);
// 输出:[1, 2, 3]
选做:分析 ArrayList 扩容源码(JDK8)
// 找到grow方法,分析扩容逻辑(1.5倍扩容)
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍
// ...
}