ArrayList 原理剖析

ArrayList 是 Java 中用于动态扩容的数组,解决了数组不能自动扩容的问题。文章详细介绍了 ArrayList 的增删查改操作,包括扩容机制、删除方法(如 System.arraycopy 的使用)、查找和修改元素的过程。此外,还讨论了 ArrayList 的优缺点,如非线程安全,以及如何通过 Collections.synchronizedList 或 CopyOnWriteArrayList 实现线程安全。

前言

在了解这个类之前,先了解它解决了什么问题。ArrayList 的出现是为了解决数组无法动态扩容的缺陷,使用 java 的数组需要在创建数组时便确定数组的容量,因为数组本身不会自动扩容。对于元素数量不确定的场景如果使用数组则需要预估所需的数组容量,如果估少了还需要重新申请空间并对原数组进行拷贝。这些代码逻辑在每一个用到数组的场景都需要重复进行处理,所以 ArrayList 对这部分重复编码的逻辑进行了封装,实现了动态扩容的效果。

既然 ArrayList 解决了数组扩缩容问题,那么其就相当于是一种创新型的数据结构,对应的它也需要解决好增删查改的问题,后面我们就从增删查改四个维度看一下它的实现方案。

增删查改

Add 数据的第一步是创建 ArrayList,其有三个构造函数,分别是用指定容量创建,无参构造创建和使用别的 Collection 来创建新的 ArrayList。这其中如果采用无参构造创建那么默认的 elementData 会被设置成 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这是一个空数组,你会发现它和 EMPTY_ELEMENTDATA 这个空数组被区分开了,其目的是为了判断出如果以无参构造创建 ArrayList,那么第一次添加元素时数组最小容量会被设置为 DEFAULT_CAPACITY,也就是下面这段代码控制。

private int newCapacity(int minCapacity) {
    // ...
    // 这里会判断是不是默认容量空数组,是的话最小容量会用 DEFAULT_CAPACITY
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    // ...
}

了解完创建的逻辑,接下来就是新增,新增的逻辑中有几个关键变量,分别是 elementData, size, modCount, ArrayList 中使用 elementData 存放数据,size 指针表明当前数组中插入了多少个元素,同时也用于帮助新的数据插入数组尾部,modCount 用于控制数据版本,ArrayList 中只要对 elementData 进行了修改那么便会同时修改 modCount,确保迭代内不出现写入操作,防止迭代时数据出错。

如果在添加元素的时候出现元素长度不够的情况则会进行扩容,扩容时每次扩容 1.5 倍,由下面的代码计算扩容后的容量。

private int newCapacity(int minCapacity) {
    // ...
    // 这里的 oldCapacity >> 1 可以直接理解为老容量除 2
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // ...
}

在扩容的代码里有一个有意思的方法是 hugeCapacity,仔细看代码会发现这里有一个 MAX_ARRAY_SIZE 常量定义了数组的最大长度为 Integer.MAX_VALUE - 8,之所以定义这个长度为数组的最大长度是因为某些虚拟机会在数组头寸写东西,所以如果超过这个长度就有可能出现 OutOfMemoryError,不过若是超过了这个长度,代码也会以 Integer.MAX_VALUE 去申请试试。

删除方法有三个,分别是按数组下标删除、按指定对象删除和按给定集合删除,第一种方式比较简单,直接通过下标删除元素,不过 ArrayList 内部的实现不是通过循环来实现,而是通过调用 System.arraycopy 方法来删除,效率更高。按对象删除则和第一种方式类似,其会先用循环找到元素下标,然后采用和第一种方式相同的方法删除元素。

最后一种按给定集合删除比较有意思,其内部采用了双指针的方式来删除数据,逻辑是先循环找到要第一个要删除的元素下标,保存该指针,然后第二个指针从该位置继续向后遍历,下一个元素要删除则继续找下一个,如果下一个不用删除则将其拷贝到第一个指针的位置,然后将第一个指针加一,以下是关键源代码。

boolean batchRemove(Collection<?> c, boolean complement,
    final int from, final int end) {
    final Object[] es = elementData;
    int r;
    // ...
    // 保存第一个指针为 w,r 为第二个指针
    int w = r++;
    try {
        // 这里便是双指针遍历拷贝数据
        for (Object e; r < end; r++) 
            if (c.contains(e = es[r]) == complement) 
                es[w++] = e;
    }
    // ...
    finally {
        modCount += end - w;
        // 最终将尾巴抹成 null
        shiftTailOverGap(es, w, end);
    }
    return true;
}

查找元素的方法有三个,分别是 get, indexOf, lastIndexOf, get 是直接按照数组下标找元素,indexOf 和 lastIndexOf 则是通过循环找元素然后返回数组下标。

对 ArrayList 的遍历有两种方式,一种是使用数组下标循环遍历,另一种则是使用 ArrayList 提供的迭代器进行遍历,ArrayList 提供了两种迭代器,分别是 Itr 和 ListItr,其中 ListItr 是继承 Itr 并在其中增加了一些方法,使其能够一边遍历一边 Add 元素。需要注意的是使用 Itr 遍历元素不能一边遍历一边修改数组内元素,否则 expectedModCount 与 modCount 不一致会导致报错,而使用 ListItr 则可以一边修改,这是因为 ListItr 修改元素后会更新 expectedModCount。

修改元素直接使用 set 方法,内部会直接根据下标修改数组元素。

优缺点

这样实现带来了动态扩容的优点,但其没有解决添加元素与元素扩容时需要拷贝元素的问题,如果想解决这类问题一般会使用 JDK 中的 LinkedList,不过好在我们实际的编程场景下绝大部分场景 ArrayList 的性能都优于 LinkedList,所以这些性能上的问题在使用时可以先忽略,如果在业务场景中出现了性能问题再针对对应的场景做优化。除了前面说的动态扩容,ArrayList 也继承了数组随机查找的优点,在通过数组下标查找元素的场景可以以 O(1) 的时间复杂度获取到数据。

常见问题

ArrayList 是不是线程安全的

ArrayList 不是线程安全的,判断一个类是不是线程安全是看类本身有没有状态,如果有状态那么还要看在修改这些状态的时候有没有上锁之类的操作保证修改这些状态确保不会出错。

另外如果想让 ArrayList 变成线程安全,可以使用 Collections.synchronizedList 进行包装,拿到的新 List 在 List 方法上均加了锁,需要注意的是新的 List 迭代器依然使用的是原集合的迭代器,所以其迭代器依然是线程不安全的,另一种方式是使用 CopyOnWriteArrayList 这个线程安全的 List。

总结

文章引言讲述了 ArrayList 诞生的原因以及其所解决的问题,之后分析了 ArrayList 内部的实现原理和优缺点,最后提了一些 ArrayList 中常见的问题,读完全文应该能对 ArrayList 的基本原理会有一定的认知,希望能对一些朋友产生帮助。

<think>我们被要求回答关于Java ArrayList的底层实现原理。根据引用[3],我们知道ArrayList的核心是基于动态数组实现的,具有动态扩容能力,支持高效随机访问(O(1)),但插入和删除可能需要移动元素(O(n))。同时,引用[3]提供了源码的关键字段和核心机制。 回答结构: 1. 介绍ArrayList的基本概念和特点。 2. 底层数据结构:数组(elementData)和关键字段(size, 默认容量等)。 3. 核心机制:初始化、动态扩容、添加元素、删除元素等。 4. 线程安全性问题(引用[2]提到是非线程安全的,以及如何实现线程安全)。 5. 迭代器注意事项(引用[4]提到modCount机制,用于快速失败fail-fast)。 6. 性能分析(随机访问、插入、删除的时间复杂度)。 同时,根据系统指令,我们需要: - 行内数学表达式用$...$,例如:时间复杂度$O(1)$。 - 独立公式单独成段用$$...$$(但这里可能用不到复杂公式)。 - 使用中文。 - 生成相关问题。 另外,在回答中引用的段落末尾添加引用标识,如[^1]。 注意:引用[1]和[5]似乎与ArrayList关系不大,引用[1]是介绍ArrayList的简单性,引用[5]是关于HashMap的get方法,所以我们主要参考引用[2][3][4]。 开始回答:</think>### Java ArrayList 底层实现原理详解 #### 1️⃣ **核心数据结构** - **动态数组**:底层使用 `Object[] elementData` 存储元素(JDK 11 源码)[^3]。 - **关键字段**: - `transient Object[] elementData`:实际存储数据的数组(`transient` 避免序列化空位)。 - `private int size`:记录当前元素数量。 - `DEFAULT_CAPACITY = 10`:默认初始容量(首次添加元素时生效)。 #### 2️⃣ **四大核心机制** 1. **初始化** - **无参构造**:延迟初始化,创建空数组 `elementData = {}`。 - **首次添加元素**:若数组为空,扩容至默认容量 10[^3]。 ```java public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 空数组 } ``` 2. **动态扩容(核心)** - **触发条件**:添加元素时 `size == elementData.length`。 - **扩容规则**: - 新容量 = 旧容量 × 1.5(位运算优化:`newCapacity = oldCapacity + (oldCapacity >> 1)`)。 - 首次扩容从 0 → 10,后续按 1.5 倍增长(如 10→15→22)[^3]。 - **数据迁移**:`System.arraycopy()` 复制旧数组到新数组。 3. **添加元素** - **尾部插入**:直接赋值 `elementData[size++] = e`,时间复杂度 $O(1)$。 - **中间插入**:需移动后续元素(`System.arraycopy`),时间复杂度 $O(n)$。 4. **删除元素** - **尾部删除**:直接 `size--`,时间复杂度 $O(1)$。 - **中间删除**:需移动后续元素填补空位,时间复杂度 $O(n)$。 ```java public E remove(int index) { E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); // 移动元素 elementData[--size] = null; // 释放引用,帮助GC return oldValue; } ``` #### 3️⃣ **关键特性** - **随机访问高效**:通过索引直接访问数组元素,时间复杂度 $O(1)$。 - **插入/删除成本高**:需移动元素,最坏 $O(n)$。 - **线程不安全**:多线程操作需同步(如 `Collections.synchronizedList`)[^2]。 - **快速失败机制(Fail-Fast)**:迭代器通过 `modCount` 检测并发修改,抛出 `ConcurrentModificationException`[^4]。 #### 4️⃣ **性能对比** | 操作 | 时间复杂度 | 说明 | |--------------|------------|--------------------------| | `get(index)` | $O(1)$ | 直接数组索引访问 | | `add(e)` | 均摊 $O(1)$| 尾部插入,可能触发扩容 | | `add(i,e)` | $O(n)$ | 需移动元素 | | `remove(i)` | $O(n)$ | 需移动元素 | #### 5️⃣ **设计思想** - **空间换时间**:预分配容量减少频繁扩容。 - **均摊复杂度**:扩容成本分摊到多次插入操作。 - **懒加载**:无参构造时延迟分配内存,减少资源占用。 --- ### 相关问题 1. **为什么 ArrayList 的扩容因子是 1.5 而不是 2?** > 1.5 倍平衡了内存浪费与扩容频率,避免 2 倍扩容可能导致的过量空间浪费[^3]。 2. **如何优化 ArrayList 的插入性能?** > 预分配足够容量(`new ArrayList<>(initialCapacity)`),避免中间插入。 3. **ArrayList 和 LinkedList 如何选择?** > 频繁随机访问用 ArrayList;频繁插入/删除用 LinkedList。 4. **为什么迭代过程中修改 ArrayList 会抛出异常?** > 迭代器通过 `modCount` 检测结构性修改,实现快速失败机制[^4]。 5. **ArrayList 的序列化为何自定义 `writeObject/readObject`?** > 跳过空元素(`transient elementData`),仅序列化有效数据[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值