【Day13】集合框架(一):List 接口(ArrayList vs LinkedList)实战

哈喽,各位 Java 学习者!欢迎来到《Java 学习日记》的第十三篇内容~ 前面我们掌握了 String 类的核心用法,今天要正式进入 Java 集合框架的核心 ------List接口,以及它的两个最常用实现类:ArrayListLinkedList。集合是 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. 核心特性

  1. 底层结构:Object 数组(连续内存空间);
  2. 初始容量:默认 10(Java 8+,空构造器初始为 {},首次添加元素扩容为 10);
  3. 扩容规则 :默认扩容为原容量的 1.5 倍(通过Arrays.copyOf复制数组,耗时);
  4. 访问性能:随机访问(get/set)极快(O (1)),增删慢(尤其是中间位置,O (n));
  5. 线程安全 :非线程安全(多线程操作需手动同步,如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. 核心特性

  1. 底层结构:双向链表(非连续内存空间);
  2. 容量:无固定容量,无需扩容(每个节点动态创建);
  3. 访问性能:随机访问极慢(O (n)),需遍历链表找节点;
  4. 增删性能:指定位置增删快(O (1)),只需修改节点引用;
  5. 额外功能 :实现Deque接口,可作为队列 / 双端队列使用;
  6. 线程安全:非线程安全。

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

    运行

    java 复制代码
    List<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();
    }
}

总结

关键点回顾

  1. List 核心特性:有序、可重复、支持索引访问,是开发中最常用的集合类型;
  2. ArrayList:数组实现,随机访问极快,是默认选择,使用时建议指定初始容量;
  3. LinkedList:双向链表实现,首尾增删极快,适合队列 / 栈场景,避免随机访问;
  4. 核心坑点:增强 for 循环删除元素会抛异常(用迭代器)、ArrayList 频繁扩容(指定初始容量);
  5. 场景选择:优先用 ArrayList,仅在大量首尾增删时用 LinkedList。

List 是 Java 集合框架的基础,掌握 ArrayList 和 LinkedList 的底层原理和场景选择,能让你的代码既简洁又高效。下一篇【Day14】,我们会讲解 "集合框架(二):Set 接口(HashSet vs TreeSet)实战",帮你掌握 "无序、不可重复" 的集合类型!如果今天的内容对你有帮助,欢迎点赞 + 收藏 + 关注,有任何问题都可以在评论区留言,咱们一起讨论~ 明天见!🚀

相关推荐
眠りたいです2 小时前
Docker:镜像的运行实体-Docker Container
java·运维·c++·docker·容器·eureka
Filotimo_2 小时前
在java后端开发中,ES的用处
java·elasticsearch·jenkins
华仔啊2 小时前
都在用 Java8 或 Java17,那 Java9 到 16 呢?他们真的没用吗?
java·后端
WizLC2 小时前
【后端】面向对象编程是什么(附加几个通用小实例项目)
java·服务器·后端·python·设计语言
刘个Java2 小时前
手搓遥控器通过上云api执行航线
java·redis·spring cloud·docker
wanghowie2 小时前
01.09 Java基础篇|算法与数据结构实战
java·数据结构·算法
回吐泡泡oO2 小时前
ElasticSearch添加登录校验(仅供参考)
java·elasticsearch·jenkins
@小张在努力2 小时前
Javascript中的闭包
开发语言·javascript·ecmascript
muyouking112 小时前
Rust Nightly 切换指南:解锁前沿特性的钥匙
开发语言·后端·rust