Java集合List详解:从入门到精通


📖 目录


一、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[]?

  1. 通用性:Object 是所有类的父类,可以存储任何类型的对象
  2. 多态性 :通过泛型 <E> 实现类型安全,编译时检查类型
  3. 灵活性:底层统一用 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. 创建一个新数组,容量为原来的 1.5 倍
  2. 将旧数组的元素复制到新数组
  3. 将引用指向新数组

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) { ... }
}

不推荐的原因

  1. 性能低:即使在单线程环境,也有同步开销
  2. 粒度粗:锁的粒度太大,并发性能差
  3. 有替代品
    • 单线程 → 用 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[] arrString[] 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[] 存储?

答案

  1. 通用性:Object 是所有类的父类,可以存储任何类型的对象
  2. 多态性:利用多态特性,底层统一用 Object 存储,上层通过泛型控制类型
  3. 类型安全 :配合泛型 <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 的扩容机制是什么?

答案

  1. 初始容量 :默认为 10
  2. 扩容时机:当元素个数超过当前容量时触发
  3. 扩容大小 :扩容至原来的 1.5 倍
  4. 扩容步骤
    • 创建一个新数组,容量为 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]

📌 总结

🎯 核心知识点回顾

  1. ArrayList:基于数组,查询快O(1),增删慢O(n),非线程安全
  2. LinkedList:基于链表,查询慢O(n),头尾增删快O(1),非线程安全
  3. Vector:基于数组,线程安全,但性能差,不推荐使用

🚀 使用建议

场景 推荐选择
频繁查询、少量增删 ArrayList
频繁头尾增删 LinkedList
需要线程安全 Collections.synchronizedList(ArrayList)
读多写少的并发 CopyOnWriteArrayList

💡 面试高频问题

  1. ArrayList 和 LinkedList 的区别? → 数组 vs 链表
  2. ArrayList 的扩容机制? → 1.5倍扩容
  3. 为什么 ArrayList 用 Object[]? → 通用性 + 泛型配合
  4. 如何在遍历时安全删除元素? → 用迭代器
  5. ArrayList 线程安全吗? → 不安全,用 Collections.synchronizedList()

🎓 学习建议

  1. 动手实践:每个示例代码都运行一遍,修改参数观察结果
  2. 画图理解:ArrayList 的扩容、LinkedList 的指针变化,画图更清晰
  3. 对比记忆:制作对比表格,强化记忆
  4. 刷题巩固:LeetCode 上刷链表、数组相关题目

💬 结语:集合是 Java 基础中的重点,掌握 List 的三个实现类,对后续学习 Set、Map 有很大帮助。建议多动手写代码,理论结合实践,才能真正掌握!

📖 推荐阅读 :Java 集合框架源码解析、JDK 官方文档

🔗 相关文章:Java泛型详解、Java多线程与集合

相关推荐
智者知已应修善业2 小时前
【c# 想一句话把 List<List<string>>的元素合并成List<string>】2023-2-9
经验分享·笔记·算法·c#·list
laplace01232 小时前
JAVA-Redis上
java·redis·spring
不要喷香水2 小时前
26.java openCV4.x 入门-Imgproc之图像尺寸调整与区域提取
java·人工智能·opencv·计算机视觉
脸大是真的好~2 小时前
黑马JAVAWeb - SpringAOP
java
moxiaoran57532 小时前
RestTemplate使用示例
java
Gogo8162 小时前
从 Spring Boot 到 NestJS:模块化设计的哲学差异
java·后端·nestjs
D_alyoo3 小时前
06 Activiti 与 Spring Boot 整合
java·activiti·activiti7源码
小陈不好吃3 小时前
Spring Boot配置文件加载顺序详解(含Nacos配置中心机制)
java·开发语言·后端·spring
ゞ 正在缓冲99%…3 小时前
leetcode1770.执行乘法运算的最大分数
java·数据结构·算法·动态规划