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


目录


一、Java集合框架概述

1.1 什么是集合?

在Java中,集合(Collection) 是用来存储和管理多个对象的容器。就像一个可以动态调整大小的"篮子",你可以往里面放东西,也可以拿出来。

1.2 为什么需要集合?

传统数组的局限性:

// 数组的缺点示例
String[] names = new String[3]; // ❌ 长度固定,不能动态扩展
names[0] = "张三";
names[1] = "李四";
names[2] = "王五";
// names[3] = "赵六"; // ❌ 报错!数组越界

集合的优势:

// 集合的优点示例
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 类型存储?
// ArrayList 源码简化版
public class ArrayList<E> {
  // 底层使用 Object 数组存储元素
  transient Object[] elementData;
  // 实际存储的元素个数
  private int size;
  }

为什么使用 Object[]?

  1. 通用性:Object 是所有类的父类,可以存储任何类型的对象
  2. 多态性:通过泛型 <E> 实现类型安全,编译时检查类型
  3. 灵活性:底层统一用 Object 存储,上层通过泛型控制类型
// 示例:泛型的作用
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 的扩容机制
// 扩容原理示例
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 基础操作方法
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 删除操作方法
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 的三种方式
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 源码简化版
// 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 添加元素原理
// 在尾部添加元素的过程演示
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 添加操作
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 获取操作
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 修改操作
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 删除操作
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 查询操作
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 清空操作
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 作为队列和栈使用

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 为什么不推荐使用?

// 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 基本用法(了解即可)

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 多线程场景示例

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 的核心区别

对比项ArrayListLinkedListVector
底层结构数组双向链表数组
查询速度快 O(1)慢 O(n)快 O(1)
增删速度慢 O(n)快 O(1)慢 O(n)
线程安全
扩容机制1.5倍不需要扩容2倍
初始容量1010
内存占用较小较大(存指针)较小
使用场景频繁查询频繁增删多线程(不推荐)

5.2 使用场景选择

// ========== 场景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 性能对比测试

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<>()
// ========== 数组长度固定 ==========
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[] arrArrayList<Integer>ArrayList<String>
// ========== 数组可以存基本类型 ==========
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 元素内容区别

对比项数组集合
同一性只能存同一数据类型理论上可以存不同类型(但不推荐)
类型安全编译时检查使用泛型检查
// ========== 数组只能存同一类型 ==========
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>,在编译时检查类型,运行时通过类型擦除保证兼容性
// 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 的区别?如何选择?

答案

维度ArrayListLinkedList
底层结构动态数组双向链表
随机访问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() 将旧数组元素复制到新数组
    • 将引用指向新数组
// 源码简化
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)
// 删除中间元素的过程
ArrayList: [A, B, C, D, E]
删除索引2的元素C1. 找到索引2O(1)
2. 删除C
3.DE向前移动 → O(n)
结果: [A, B, D, E]

面试题5:为什么 LinkedList 增删快,查询慢?

答案

增删快(仅限头尾操作):

  • 基于双向链表,只需修改指针
  • 头部/尾部增删:直接操作 first/last 指针 → O(1)
  • 中间增删:需要先找到位置 → O(n)

查询慢

  • 不支持随机访问,必须从头/尾遍历
  • 时间复杂度:O(n)
// 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()

List<String> syncList = Collections.synchronizedList(new ArrayList<>());
  syncList.add("元素");  // 线程安全

方案2:使用 CopyOnWriteArrayList(读多写少场景)

CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
  cowList.add("元素");  // 线程安全,写时复制

方案3:使用 Vector(不推荐,性能差)

Vector<String> vector = new Vector<>();
  vector.add("元素");  // 线程安全,但性能低

面试题7:ArrayList 和 Vector 的区别?

对比项ArrayListVector
线程安全是(synchronized)
扩容大小1.5倍2倍
性能慢(同步开销)
推荐程度✅ 推荐❌ 不推荐(已过时)

结论:优先使用 ArrayList,需要线程安全时用 Collections.synchronizedList() 包装。


面试题8:如何在遍历时删除元素?

❌ 错误方式1:增强 for 循环删除

ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
  for (String item : list) {
  if ("B".equals(item)) {
  list.remove(item);  // ❌ 抛出 ConcurrentModificationException
  }
  }

❌ 错误方式2:普通 for 循环删除(会漏删)

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:迭代器删除

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:倒序遍历删除

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) 有什么区别?

答案

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(会改变顺序)

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(保持顺序)

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+)

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多线程与集合

posted @ 2025-12-07 10:50  yangykaifa  阅读(30)  评论(0)    收藏  举报