Java集合-ArrayList 知识点详解

6 阅读12分钟

ArrayList 知识点详解

一、ArrayList 简介

1.1 什么是 ArrayList?

ArrayList 是 Java 集合框架中最常用的 List 实现类,它是一个动态数组,可以自动扩容。想象一下,普通的数组就像一个固定大小的盒子,一旦装满就不能再装了;而 ArrayList 就像一个会自动变大的魔法盒子,装满后会自动变大,永远不用担心装不下。

1.2 继承体系

                    Iterable(接口)
                        │
                   Collection(接口)
                        │
                    List(接口)
                        │
                  AbstractList(抽象类)
                        │
                    ArrayList(实现类)

ArrayList 实现的接口:

  • List 接口:提供列表的基本操作
  • RandomAccess 接口:标记接口,表示支持快速随机访问
  • Cloneable 接口:支持克隆
  • Serializable 接口:支持序列化

1.3 核心特点

特性说明
底层结构动态数组(Object[])
线程安全不安全
默认容量10
扩容比例1.5 倍
是否有序有序(按插入顺序)
是否可重复可重复
是否支持 null支持
随机访问O(1) 时间复杂度

二、核心数据结构与图解

2.1 底层数组结构

ArrayList 底层使用一个 Object 数组来存储元素:

┌─────────────────────────────────────────────────────────────┐
│                      ArrayList 内部结构                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   elementData (Object[] 数组)                                │
│   ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐  │
│   │  A  │  B  │  C  │  D  │  E  │ null│ null│ null│ null│  │
│   └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘  │
│     [0]   [1]   [2]   [3]   [4]   [5]   [6]   [7]   [8]     │
│                                                             │
│   size = 5(实际元素个数)                                     │
│   capacity = 9(数组总容量)                                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

说明:
- elementData:存储元素的数组
- size:实际存储的元素个数
- capacity:数组的总容量(elementData.length)

2.2 添加元素过程图解

初始状态:capacity = 5, size = 0

Step 1: 添加元素 "A"
┌─────┬─────┬─────┬─────┬─────┐
│  A  │ null│ null│ null│ null│    size = 1
└─────┴─────┴─────┴─────┴─────┘

Step 2: 添加元素 "B"
┌─────┬─────┬─────┬─────┬─────┐
│  A  │  B  │ null│ null│ null│    size = 2
└─────┴─────┴─────┴─────┴─────┘

Step 3: 继续添加...直到 capacity = 5, size = 5
┌─────┬─────┬─────┬─────┬─────┐
│  A  │  B  │  C  │  D  │  E  │    size = 5(满了!)
└─────┴─────┴─────┴─────┴─────┘

Step 4: 添加元素 "F",触发扩容
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│  A  │  B  │  C  │  D  │  E  │  F  │ null│ null│    新容量 = 5 * 1.5 = 7
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
  [0]   [1]   [2]   [3]   [4]   [5]   [6]   [7]

2.3 插入元素过程图解

在索引 2 的位置插入元素 "X"

操作前:
┌─────┬─────┬─────┬─────┬─────┐
│  ABCDE  │
└─────┴─────┴─────┴─────┴─────┘
  [0]   [1]   [2]   [3]   [4]

Step 1: 将索引 2 及之后的元素向后移动一位
┌─────┬─────┬─────┬─────┬─────┐
│  AB  │     │  CDE  │
└─────┴─────┴─────┴─────┴─────┴─────┘
  [0]   [1]   [2]   [3]   [4]   [5]
              ↑
           空出位置

Step 2: 在索引 2 的位置放入 "X"
┌─────┬─────┬─────┬─────┬─────┬─────┐
│  ABXCDE  │
└─────┴─────┴─────┴─────┴─────┴─────┘
  [0]   [1]   [2]   [3]   [4]   [5]

时间复杂度:O(n),因为需要移动元素

2.4 删除元素过程图解

删除索引 2 的元素

操作前:
┌─────┬─────┬─────┬─────┬─────┐
│  ABXCDE  │
└─────┴─────┴─────┴─────┴─────┴─────┘
  [0]   [1]   [2]   [3]   [4]   [5]

Step 1: 将索引 3 及之后的元素向前移动一位
┌─────┬─────┬─────┬─────┬─────┐
│  ABCDEnull │
└─────┴─────┴─────┴─────┴─────┴─────┘
  [0]   [1]   [2]   [3]   [4]   [5]

Step 2: size1,最后一个位置置为 null(帮助 GC)
┌─────┬─────┬─────┬─────┬─────┐
│  ABCDE  │
└─────┴─────┴─────┴─────┴─────┘
  [0]   [1]   [2]   [3]   [4]

时间复杂度:O(n),因为需要移动元素

三、源码分析

3.1 核心成员变量

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    // 默认初始容量
    private static final int DEFAULT_CAPACITY = 10;

    // 空数组实例(用于空构造函数)
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 默认空数组实例(用于默认构造函数)
    // 与 EMPTY_ELEMENTDATA 区分开,以区分第一次添加元素时扩容多少
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // 真正存储元素的数组
    // transient 表示不参与序列化,ArrayList 自定义了序列化方法
    transient Object[] elementData;

    // 实际元素个数
    private int size;

    // 数组最大容量
    // -8 是因为某些 JVM 会在数组头部存储一些信息
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}

3.2 构造方法

/**
 * 无参构造函数
 * 使用默认容量 10
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/**
 * 指定初始容量的构造函数
 * @param initialCapacity 初始容量
 */
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // 创建指定容量的数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        // 容量为 0,使用空数组
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        // 负数容量,抛出异常
        throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
    }
}

/**
 * 使用其他集合初始化
 * @param c 集合
 */
public ArrayList(Collection<? extends E> c) {
    // 将集合转换为数组
    Object[] a = c.toArray();
    if ((size = a.length) != 0) {
        if (c.getClass() == ArrayList.class) {
            elementData = a;
        } else {
            // 复制数组
            elementData = Arrays.copyOf(a, size, Object[].class);
        }
    } else {
        // 空集合,使用空数组
        elementData = EMPTY_ELEMENTDATA;
    }
}

3.3 添加元素方法(核心)

/**
 * 在末尾添加元素
 * 时间复杂度:平均 O(1)
 */
public boolean add(E e) {
    // 确保容量足够
    ensureCapacityInternal(size + 1);
    // 将元素放入数组末尾
    elementData[size++] = e;
    return true;
}

/**
 * 在指定位置插入元素
 * 时间复杂度:O(n)
 */
public void add(int index, E element) {
    // 检查索引是否越界
    rangeCheckForAdd(index);

    // 确保容量足够
    ensureCapacityInternal(size + 1);

    // 将 index 及之后的元素向后移动一位
    // System.arraycopy 是 native 方法,效率较高
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);

    // 在 index 位置放入新元素
    elementData[index] = element;
    size++;
}

/**
 * 确保内部容量
 */
private void ensureCapacityInternal(int minCapacity) {
    // 如果是默认空数组,取 DEFAULT_CAPACITY 和 minCapacity 的较大值
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

/**
 * 确保明确容量
 */
private void ensureExplicitCapacity(int minCapacity) {
    // 修改次数加 1(用于快速失败机制)
    modCount++;

    // 如果需要的容量大于当前数组长度,进行扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

3.4 扩容方法(核心)

/**
 * 扩容方法
 * 这是 ArrayList 的核心方法之一
 */
private void grow(int minCapacity) {
    // 旧容量
    int oldCapacity = elementData.length;
    // 新容量 = 旧容量 + 旧容量/2 = 旧容量 * 1.5
    // 右移一位相当于除以 2
    int newCapacity = oldCapacity + (oldCapacity >> 1);

    // 如果新容量仍然不够,直接使用需要的最小容量
    if (newCapacity - minCapacity <= 0) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        if (minCapacity < 0) // 溢出
            throw new OutOfMemoryError();
        newCapacity = minCapacity;
    }

    // 检查是否超过最大容量
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);

    // 将旧数组复制到新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

/**
 * 处理大容量
 */
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // 溢出
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

3.5 获取元素方法

/**
 * 获取指定位置的元素
 * 时间复杂度:O(1)
 */
public E get(int index) {
    // 检查索引是否越界
    rangeCheck(index);

    // 直接通过数组下标访问
    return elementData(index);
}

/**
 * 通过下标访问元素
 */
@SuppressWarnings("unchecked")
E elementData(int index) {
    return (E) elementData[index];
}

/**
 * 检查索引范围
 */
private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

3.6 删除元素方法

/**
 * 删除指定位置的元素
 * 时间复杂度:O(n)
 */
public E remove(int index) {
    // 检查索引是否越界
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    // 需要移动的元素个数
    int numMoved = size - index - 1;
    if (numMoved > 0)
        // 将 index 之后的元素向前移动一位
        System.arraycopy(elementData, index + 1, elementData, index,
                         numMoved);

    // 将最后一个位置置为 null,帮助 GC
    elementData[--size] = null;

    return oldValue;
}

/**
 * 删除指定元素(第一个匹配的)
 * 时间复杂度:O(n)
 */
public boolean remove(Object o) {
    if (o == null) {
        // 删除 null 元素
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        // 删除非 null 元素
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

/**
 * 快速删除(不检查索引)
 */
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index,
                         numMoved);
    elementData[--size] = null;
}

3.7 修改元素方法

/**
 * 替换指定位置的元素
 * 时间复杂度:O(1)
 */
public E set(int index, E element) {
    // 检查索引是否越界
    rangeCheck(index);

    // 获取旧值
    E oldValue = elementData(index);

    // 设置新值
    elementData[index] = element;

    return oldValue;
}

3.8 其他常用方法

/**
 * 返回元素个数
 */
public int size() {
    return size;
}

/**
 * 判断是否为空
 */
public boolean isEmpty() {
    return size == 0;
}

/**
 * 判断是否包含指定元素
 */
public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

/**
 * 查找元素第一次出现的位置
 */
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i] == null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

/**
 * 清空列表
 */
public void clear() {
    modCount++;

    // 将所有元素置为 null,帮助 GC
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

/**
 * 将容量缩小为实际大小
 */
public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}

四、扩容机制详解

4.1 扩容时机

当添加元素时,如果当前数组已满(size == elementData.length),就会触发扩容。

4.2 扩容公式

newCapacity = oldCapacity + (oldCapacity >> 1)

即:新容量 = 旧容量 + 旧容量 / 2 = 旧容量 * 1.5
位运算解析
newCapacity = oldCapacity + (oldCapacity >> 1)
表达式含义
oldCapacity >> 1右移1位 = 除以2
oldCapacity + oldCapacity/2原值 + 原值的一半
最终结果oldCapacity × 1.5
举例说明

假设原容量 oldCapacity = 10

oldCapacity >> 1  = 10 >> 1 = 5    (10 ÷ 2 = 5)
newCapacity = 10 + 5 = 15          (10 × 1.5 = 15)
为什么用位运算?
  1. 性能更好 — 位运算比乘除法更快
  2. 避免浮点数 — 整数运算,无精度问题
  3. 向下取整 — 奇数时会自动向下取整(如 11 >> 1 = 5

4.3 扩容示例

初始容量:10
第 1 次扩容:10 * 1.5 = 15
第 2 次扩容:15 * 1.5 = 22(取整)
第 3 次扩容:22 * 1.5 = 33
第 4 次扩容:33 * 1.5 = 49
...以此类推

4.4 扩容流程图

                    ┌─────────────────┐
                    │   添加元素请求   │
                    └────────┬────────┘
                             │
                             ▼
                    ┌─────────────────┐
                    │  size + 1 是否  │
                    │  超过容量?      │
                    └────────┬────────┘
                             │
              ┌──────────────┴──────────────┐
              │ 否                          │ 是
              ▼                             ▼
    ┌─────────────────┐            ┌─────────────────┐
    │   直接添加元素   │            │   触发扩容      │
    └─────────────────┘            └────────┬────────┘
                                            │
                                            ▼
                                   ┌─────────────────┐
                                   │ 计算新容量      │
                                   │ = old * 1.5     │
                                   └────────┬────────┘
                                            │
                                            ▼
                                   ┌─────────────────┐
                                   │ 创建新数组      │
                                   │ 复制旧数据      │
                                   └────────┬────────┘
                                            │
                                            ▼
                                   ┌─────────────────┐
                                   │   添加元素      │
                                   └─────────────────┘

4.5 扩容的性能影响

// 假设要添加 100 万个元素

// 方式 1:不指定初始容量(多次扩容)
ArrayList<Integer> list1 = new ArrayList<>();
// 会经历多次扩容:10 -> 15 -> 22 -> 33 -> 49 -> 73 -> ...

// 方式 2:指定初始容量(一次扩容都不需要)
ArrayList<Integer> list2 = new ArrayList<>(1000000);
// 直接分配 100 万容量,不需要扩容

// 性能对比:
// 方式 1 需要进行多次数组复制,性能较差
// 方式 2 直接分配足够容量,性能最佳

五、时间复杂度分析

5.1 各种操作的时间复杂度

操作方法时间复杂度说明
随机访问get(index)O(1)直接通过数组下标访问
末尾添加add(e)O(1) 平均可能触发扩容,但平均还是 O(1)
指定位置添加add(index, e)O(n)需要移动元素
删除末尾remove(size-1)O(1)直接删除
删除指定位置remove(index)O(n)需要移动元素
查找元素indexOf(o)O(n)需要遍历
是否包含contains(o)O(n)基于 indexOf
修改元素set(index, e)O(1)直接通过下标修改

5.2 与 LinkedList 对比

                    ArrayList          LinkedList
随机访问              O(1) ✅             O(n) ❌
末尾添加           O(1) 平均 ✅          O(1) ✅
头部添加              O(n) ❌           O(1) ✅
中间插入              O(n) ❌           O(n) ❌
内存占用             较小 ✅            较大 ❌
缓存友好              好 ✅              差 ❌

六、线程安全问题

6.1 为什么 ArrayList 不是线程安全的?

// 假设有两个线程同时对 ArrayList 进行操作

// 线程 1:添加元素
list.add("A");

// 线程 2:同时添加元素
list.add("B");

// 可能出现的问题:
// 1. 数据覆盖:两个线程同时写入同一位置
// 2. 数组越界:扩容过程中的并发问题
// 3. size 值不正确:多个线程同时修改 size
// 4. 扩容时数据丢失

6.2 线程安全的替代方案

// 方案 1:使用 Collections.synchronizedList
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());

// 方案 2:使用 CopyOnWriteArrayList(读多写少场景推荐)
CopyOnWriteArrayList<String> copyOnWriteList = new CopyOnWriteArrayList<>();

// 方案 3:使用 Vector(不推荐,已过时)
Vector<String> vector = new Vector<>();

// 方案 4:手动加锁
List<String> list = new ArrayList<>();
synchronized (list) {
    list.add("element");
}

七、最佳实践

7.1 初始化时指定容量

// ❌ 不推荐:使用默认容量,可能多次扩容
ArrayList<String> list1 = new ArrayList<>();

// ✅ 推荐:预估容量,避免扩容
ArrayList<String> list2 = new ArrayList<>(1000);

7.2 遍历方式选择

ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

// 方式 1:普通 for 循环(推荐,适合需要索引的场景)
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

// 方式 2:增强 for 循环(推荐,代码简洁)
for (String s : list) {
    System.out.println(s);
}

// 方式 3:迭代器(推荐,适合需要删除元素的场景)
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String s = iterator.next();
    if ("B".equals(s)) {
        iterator.remove(); // 安全删除
    }
}

// 方式 4:forEach + Lambda(Java 8+,代码简洁)
list.forEach(System.out::println);

// ❌ 不推荐:遍历时直接删除元素
for (String s : list) {
    if ("B".equals(s)) {
        list.remove(s); // 会抛出 ConcurrentModificationException
    }
}

7.3 大量数据删除优化

// ❌ 不推荐:逐个删除,效率低
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
    list.add(i);
}
for (int i = 0; i < list.size(); i++) {
    if (list.get(i) % 2 == 0) {
        list.remove(i);
        i--; // 需要回退索引
    }
}

// ✅ 推荐:使用 removeIf(Java 8+)
list.removeIf(n -> n % 2 == 0);

// ✅ 推荐:使用迭代器删除
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
    if (it.next() % 2 == 0) {
        it.remove();
    }
}

7.4 转换为数组

ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

// 方式 1:返回 Object[] 数组
Object[] array1 = list.toArray();

// 方式 2:传入指定类型的数组(推荐)
String[] array2 = list.toArray(new String[0]);

// 方式 3:传入指定大小的数组
String[] array3 = list.toArray(new String[list.size()]);

八、常见面试题

8.1 ArrayList 和 Array 的区别?

特性ArrayArrayList
大小固定动态
类型可以是基本类型只能是引用类型
功能基本操作丰富的 API
性能稍快稍慢(封装开销)
泛型不支持支持

8.2 ArrayList 和 LinkedList 的区别?

特性ArrayListLinkedList
底层结构动态数组双向链表
随机访问O(1)O(n)
插入删除O(n)O(1)(已知位置)
内存占用较小较大
缓存友好
实现接口List, RandomAccessList, Deque

8.3 为什么 ArrayList 的扩容是 1.5 倍?

  1. 权衡时间和空间:扩容太少需要频繁扩容,扩容太多浪费空间
  2. 经验值:1.5 倍是一个经验值,在实际使用中表现良好
  3. 数学计算:如果希望均摊成本最低,最佳扩容因子约为 1.618(黄金分割比),1.5 是一个接近的值

8.4 ArrayList 如何实现序列化?

// ArrayList 使用自定义的序列化方法
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
    // 写入非静态和非瞬态字段
    s.defaultWriteObject();

    // 写入 size
    s.writeInt(size);

    // 只写入实际存储的元素,不写入整个数组
    for (int i = 0; i < size; i++) {
        s.writeObject(elementData[i]);
    }
}

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {

    // 读取非静态和非瞬态字段
    s.defaultReadObject();

    // 读取 size
    int size = s.readInt();

    // 创建数组并读取元素
    elementData = new Object[size];
    for (int i = 0; i < size; i++) {
        elementData[i] = s.readObject();
    }
}

为什么这样设计?

  • elementData 用 transient 修饰,不参与默认序列化
  • 只序列化实际存储的元素,不序列化整个数组
  • 节省空间,避免序列化 null 元素

九、总结

9.1 适用场景

适合使用 ArrayList 的场景:

  • 需要频繁随机访问元素
  • 主要在末尾添加/删除元素
  • 数据量已知或可预估
  • 单线程环境

不适合使用 ArrayList 的场景:

  • 需要频繁在中间插入/删除元素
  • 需要线程安全
  • 频繁在头部插入/删除元素(考虑 LinkedList)

9.2 核心要点

  1. 底层数组:ArrayList 底层是 Object[] 数组
  2. 动态扩容:自动扩容,扩容因子 1.5
  3. 随机访问:支持 O(1) 随机访问
  4. 线程不安全:多线程环境需要同步处理
  5. 序列化:自定义序列化,只存储实际元素

9.3 记忆口诀

ArrayList 要记牢,
动态数组效率高。
随机访问 O(1),
末尾添加也是妙。
中间插入要移动,
扩容 1.5 别忘掉。
线程不安全要注意,
初始化容量要预料。