【Java基础】Java 的ArrayList集合

1. 核心概念与底层实现

1.1 ArrayList 的本质

ArrayList 是基于 动态数组List 实现类,其底层数据结构是 Object[] elementData。它通过动态扩容机制(自动扩展数组长度)实现灵活的存储需求,但牺牲了线程安全性。

  • 定义
    ArrayList是Java集合框架中的一个动态数组实现类,继承自AbstractList,实现了List接口。它允许存储重复元素null值,并支持通过索引快速访问元素
  • 核心特性
    • 动态数组:容量可自动扩展,无需手动管理。
    • 线程不安全:多线程环境下需手动同步或使用Vector/CopyOnWriteArrayList
    • 随机访问高效:通过索引访问元素的时间复杂度为O(1)
    • 增删操作较慢:中间位置的插入/删除需要移动元素,时间复杂度为O(n)

1.1.1 底层数据结构

ArrayList的底层基于对象数组Object[] elementData)实现:

  • 初始容量:默认为10(通过DEFAULT_CAPACITY定义)。
  • 动态扩容:当数组空间不足时,自动扩容为原容量的1.5倍oldCapacity + (oldCapacity >> 1))。
  • 关键成员变量
    // 底层数组,存储所有元素
    transient Object[] elementData;
    // 当前元素个数
    private int size;
    
JDK 1.7 vs JDK 1.8的初始化差异
特性JDK 1.7JDK 1.8+
无参构造的elementData.length100(DEFAULTCAPACITY_EMPTY_ELEMENTDATA
首次添加元素时的扩容不触发(已预分配)触发扩容到10
内存占用立即分配10个元素的内存空间延迟分配,节省初始内存

1.1.2 动态扩容机制
扩容触发条件

当调用add()方法时,若当前数组容量(elementData.length)小于size + 1,则触发扩容。

扩容规则
  • 新容量计算newCapacity = oldCapacity + (oldCapacity >> 1)(即原容量的1.5倍)。
  • 特殊情况处理
    • 若计算后的容量仍小于所需最小容量(minCapacity),则直接使用minCapacity
    • 若扩容过程中出现整数溢出(极端大容量),抛出OutOfMemoryError

1.2 关键成员变量

// 核心成员变量
transient Object[] elementData; // 底层数组,存储元素
private int size; // 当前元素个数

// 静态常量
private static final Object[] EMPTY_ELEMENTDATA = new Object[0]; // 空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0]; // 无参构造时使用
private static final int DEFAULT_CAPACITY = 10; // 默认初始容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // 最大数组长度

2. 初始化机制:从0到10的蜕变

2.1 无参构造的陷阱

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 初始化为空数组(长度0)
}
  • 关键点
    • elementData.length 初始为0,但 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是一个静态空数组(长度0)。
    • 首次添加元素时,会触发 强制扩容到10,而非直接使用 elementData 的原始长度。

2.2 首次扩容的触发过程

当调用 add() 方法时:

public boolean add(E e) {
    ensureCapacityInternal(size + 1); // 此处触发扩容
    elementData[size++] = e;
    return true;
}
2.2.1 ensureCapacityInternal 的核心逻辑
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // 强制设置为10
    }
    ensureExplicitCapacity(minCapacity);
}
  • 关键步骤
    1. 检查 elementData 是否为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA(即无参构造的空数组)。
    2. 若是,则将 minCapacity 设为 max(10, minCapacity),确保首次扩容至少到10。
    3. 调用 ensureExplicitCapacity 继续检查。
2.2.2 ensureExplicitCapacity 的逻辑
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 如果当前容量不足
    if (minCapacity - elementData.length > 0) {
        grow(minCapacity);
    }
}
  • 触发扩容条件minCapacity > elementData.length
2.2.3 grow 方法的扩容计算
private void grow(int minCapacity) {
    int oldCapacity = elementData.length; // 当前容量
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量 = 原容量的1.5倍
    if (newCapacity - minCapacity < 0) {
        newCapacity = minCapacity; // 若新容量仍不足,则直接使用minCapacity
    }
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        newCapacity = hugeCapacity(minCapacity); // 处理溢出
    }
    elementData = Arrays.copyOf(elementData, newCapacity); // 复制数据到新数组
}
  • 首次扩容时
    • oldCapacity = 0newCapacity = 0 + 0 = 0,但经过 ensureCapacityInternal 的修正后,minCapacity = 10
    • 最终 newCapacity = 10,触发 Arrays.copyOf 创建新数组。

3. 容量演变的详细过程


初始容量的定义
  • 默认初始容量ArrayList的默认初始容量是10,但这仅在首次添加元素时生效
  • 底层数组的初始状态
    • JDK 1.7:无参构造时直接创建长度为10的数组(预分配策略)。
    • JDK 1.8+:无参构造时底层数组初始化为空数组(长度为0),采用懒汉式初始化,直到首次调用add()方法时才会分配容量为10的数组。
关键成员变量
// 源码片段(JDK 1.8+)
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // 默认初始容量为10
    private static final int DEFAULT_CAPACITY = 10;

    // 空数组,用于区分不同状态的空ArrayList
    private static final Object[] EMPTY_ELEMENTDATA = {};
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 无参构造时使用

    // 底层数组,存储元素
    transient Object[] elementData;
    // 当前元素个数
    private int size;
}
无参构造方法的初始化
// 无参构造方法(JDK 1.8+)
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 初始化为空数组
}
  • elementData的初始状态
    • elementData被初始化为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,其长度为0。
    • 此时,ArrayList的容量(elementData.length)为0,但默认容量(DEFAULT_CAPACITY)是10。
首次添加元素时的初始化

当调用add()方法添加第一个元素时,会触发以下流程:

// add(E e)方法(JDK 1.8+)
public boolean add(E e) {
    // 确保容量足够
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    // 如果当前elementData是DEFAULTCAPACITY_EMPTY_ELEMENTDATA(即无参构造的情况)
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 将minCapacity设为max(DEFAULT_CAPACITY, minCapacity)
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 如果当前容量不足
    if (minCapacity - elementData.length > 0) 
        grow(minCapacity);
}
  • 关键步骤
    1. elementDataDEFAULTCAPACITY_EMPTY_ELEMENTDATA时(即无参构造的空列表),minCapacity被强制设为Math.max(DEFAULT_CAPACITY, minCapacity)
    2. 此时,minCapacity为10(假设首次添加一个元素,minCapacity = 1,但会被替换为10)。
    3. 调用grow(minCapacity)扩容到10。
初始容量的动态变化
  • JDK 1.8+的初始容量流程
    • 无参构造时:elementData.length = 0size = 0
    • 首次添加元素时:扩容到10,elementData.length = 10size = 1
    • 后续添加元素时:当size达到10时,触发下一次扩容(10 → 15)。

3.1 不同场景的容量变化

3.1.1 无参构造的初始状态
ArrayList<String> list = new ArrayList<>();
System.out.println(list.size()); // 0
System.out.println(list.elementData.length); // 0(JDK 1.8+)
3.1.2 首次添加元素
list.add("Hello");
// 此时:
System.out.println(list.size()); // 1
System.out.println(list.elementData.length); // 10(扩容到10)
3.1.3 添加第11个元素
for (int i = 1; i < 10; i++) {
    list.add("World");
}
list.add("World"); // 第11个元素
// 此时:
System.out.println(list.size()); // 11
System.out.println(list.elementData.length); // 15(10 → 15)
3.1.4 添加第16个元素
for (int i = 0; i < 5; i++) {
    list.add("Java");
}
// 此时:
System.out.println(list.elementData.length); // 22(15 → 22)

4. 扩容的数学模型与性能分析

4.1 扩容的数学公式

  • 扩容公式
    newCapacity = oldCapacity + (oldCapacity >> 1)
    等价于 newCapacity = oldCapacity * 1.5(向下取整)。

4.2 扩容的渐进特性

当前容量新容量计算新容量实际值
00 + 010(强制修正)
1010 + 515
1515 + 722
2222 + 1133

4.3 扩容的性能代价

  • 时间复杂度
    每次扩容需复制所有元素,时间复杂度为 O(n),但通过 指数增长策略总扩容时间复杂度为 O(n)(摊还分析)。
  • 空间复杂度
    最终容量可能超过实际需求,但通过 trimToSize()(JDK 11+)可回收冗余空间。

5. 核心方法的源码解析

5.1 尾部添加 add(E e)

public boolean add(E e) {
    ensureCapacityInternal(size + 1); // 确保容量足够
    elementData[size++] = e; // 直接写入数组末尾
    return true;
}
  • 时间复杂度O(1)(不考虑扩容开销)。

5.2 中间插入 add(int index, E element)

public void add(int index, E element) {
    rangeCheckForAdd(index); // 索引检查
    ensureCapacityInternal(size + 1);
    // 将[index, size) 的元素后移一位
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    elementData[index] = element;
    size++;
}
  • 时间复杂度O(n)(需移动元素)。

5.3 删除元素 remove(int index)

public E remove(int index) {
    rangeCheck(index); // 索引检查
    modCount++;
    E oldValue = (E) elementData[index];
    int numMoved = size - index - 1;
    if (numMoved > 0) {
        System.arraycopy(elementData, index + 1, elementData, index, numMoved);
    }
    elementData[--size] = null; // 释放引用
    return oldValue;
}
  • 时间复杂度O(n)(需移动元素)。

6. 特殊场景与常见问题

6.1 初始容量为0的误解

  • 常见误区:认为 ArrayList 的初始容量是0,但实际:
    • JDK 1.8+:无参构造时 elementData.length = 0,但首次添加元素时强制扩容到10。
    • size 的初始值始终为0,直到元素被添加。

6.2 扩容溢出的处理

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) { // 无法处理负数
        throw new OutOfMemoryError();
    }
    return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
  • 作用:当扩容请求超过 MAX_ARRAY_SIZE(2^31-9)时,使用 Integer.MAX_VALUE

6.3 并发修改异常(ConcurrentModificationException)

  • 触发条件:在迭代过程中修改集合(非通过迭代器)。
  • 解决方案
    List<String> list = new ArrayList<>();
    // 错误示例:
    for (String s : list) {
        if (s.equals("remove")) {
            list.remove(s); // 抛出异常
        }
    }
    // 正确示例:
    Iterator<String> it = list.iterator();
    while (it.hasNext()) {
        String s = it.next();
        if (s.equals("remove")) {
            it.remove(); // 安全删除
        }
    }
    

7. 性能优化与最佳实践

7.1 预分配容量

// 优化示例:已知数据量时预分配容量
ArrayList<String> list = new ArrayList<>(1000000); // 初始容量100万
for (int i = 0; i < 1000000; i++) {
    list.add("Data" + i);
}

7.2 避免频繁扩容

  • 场景:添加大量元素时,预分配容量可减少扩容次数。
  • 数学证明
    • 假设初始容量为 C,每次扩容增长1.5倍,添加 N 个元素时,扩容次数为:
      log_1.5(N / C)
      
    • 预分配 C = N 可完全避免扩容。

7.3 使用 trimToSize()

// JDK 11+:回收冗余容量
list.trimToSize(); // 将数组长度调整为当前size

8. 与 LinkedList 的深度对比

8.1 底层结构对比

特性ArrayListLinkedList
存储结构连续内存数组双向链表(每个节点存储前后指针)
访问速度快(O(1))慢(O(n)需遍历)
插入/删除速度慢(中间操作需移动元素,O(n))快(修改指针,O(1))
内存占用低(连续内存)高(每个节点存储额外指针)

8.2 适用场景

  • ArrayList
    • 频繁查询、少量增删。
    • 数据量较大但访问模式固定。
  • LinkedList
    • 频繁增删、少量查询。
    • 需要双向遍历或链表特性(如队列、栈)。

9. 源码级优化技巧

9.1 避免 toArray() 的性能陷阱

// 错误示例:频繁调用toArray()导致额外开销
for (Object obj : list.toArray()) {
    // ...
}

// 优化示例:直接使用elementData(需谨慎)
Object[] arr = list.toArray();
for (Object obj : arr) {
    // ...
}

9.2 使用 subList() 的注意事项

// 避免直接修改子列表的引用
List<String> sublist = list.subList(0, 10);
sublist.clear(); // 会修改原列表

10. 总结:ArrayList 的设计哲学

  • 核心思想:通过 动态数组 实现高效随机访问,以 1.5倍扩容 平衡内存与性能。
  • 适用场景:优先选择 ArrayList,除非需要频繁的中间增删操作。
  • 最佳实践:预分配容量、避免并发修改、合理使用 trimToSize()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值