手撕ArrayList,ArrayList底层原理是什么,它是怎么扩容的?
ArrayList是什么?
ArrayList其实就是List接口下的一个重要实现,也就是一个动态数组,当更多的元素加入到ArrayList中时,其大小将会动态地增长,内部的元素可以直接通过get与set方法进行访问。
ArrayList的扩容机制是怎样的?
流程图如下:
下面我们来自己手撕一个ArrayList,来看看它的底层是怎么样的。
import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;public class ArrayListCapacityDemo {// 自定义ArrayList类用于演示扩容过程static class DebuggableArrayList<E> {// 实际存储元素的数组private Object[] elementData;// 当前元素数量private int size;// 默认初始容量private static final int DEFAULT_CAPACITY = 10;// 共享的空数组实例private static final Object[] EMPTY_ELEMENTDATA = {};public DebuggableArrayList() {this.elementData = EMPTY_ELEMENTDATA;this.size = 0;}// 添加元素方法public boolean add(E e) {// 确保容量足够ensureCapacityInternal(size + 1);// 添加元素elementData[size++] = e;return true;}// 内部容量检查private void ensureCapacityInternal(int minCapacity) {// 计算所需容量int capacity = calculateCapacity(elementData, minCapacity);// 明确是否需要扩容ensureExplicitCapacity(capacity);}// 容量计算private static int calculateCapacity(Object[] elementData, int minCapacity) {// 如果是空数组,返回默认容量和所需容量的较大值if (elementData == EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;}// 明确是否需要扩容private void ensureExplicitCapacity(int minCapacity) {// 修改计数器(用于迭代器快速失败机制)// modCount++;// 如果所需容量大于当前数组长度,则扩容if (minCapacity - elementData.length > 0) {grow(minCapacity);}}// 最大数组大小(一些VM在数组中保留头信息)private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;// 扩容核心方法private void grow(int minCapacity) {int oldCapacity = elementData.length;// 新容量 = 旧容量 + 旧容量/2 (即1.5倍增长)int newCapacity = oldCapacity + (oldCapacity >> 1);// 如果1.5倍仍不够,则使用minCapacityif (newCapacity - minCapacity < 0) {newCapacity = minCapacity;}// 如果超过最大限制,则调整if (newCapacity - MAX_ARRAY_SIZE > 0) {newCapacity = hugeCapacity(minCapacity);}// 打印扩容信息(调试用)System.out.printf("扩容: %d -> %d (添加第%d个元素时触发)%n", oldCapacity, newCapacity, size + 1);// 实际扩容操作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;}// 获取当前容量(仅用于演示)public int capacity() {return elementData.length;}}public static void main(String[] args) {System.out.println("ArrayList扩容演示:");System.out.println("----------------");DebuggableArrayList<Integer> list = new DebuggableArrayList<>();// 添加30个元素,观察扩容过程for (int i = 0; i < 30; i++) {list.add(i);System.out.printf("添加元素: %2d, 当前大小: %2d, 当前容量: %2d%n",i, i + 1, list.capacity());}}
}
整体流程概览:
add(E e)
方法调用: 用户调用add
方法尝试添加一个新元素。- 容量检查 (
ensureCapacityInternal
):add
方法首先会调用ensureCapacityInternal
来确保内部数组有足够的容量。 - 计算所需容量 (
calculateCapacity
): 如果是第一次添加元素(即内部数组是EMPTY_ELEMENTDATA
),则计算出初始容量(通常是DEFAULT_CAPACITY
即 10 或用户指定的minCapacity
中的较大值)。 - 明确容量需求 (
ensureExplicitCapacity
): 这一步会判断当前数组的实际长度是否小于所需的最小容量。 - 触发扩容 (
grow
): 如果minCapacity
大于elementData.length
,则会调用grow
方法执行实际的扩容操作。 - 计算新容量:
grow
方法会根据当前容量计算一个新的容量值(通常是旧容量的 1.5 倍)。 - 容量调整: 如果计算出的新容量仍然小于
minCapacity
,或者新容量超过了MAX_ARRAY_SIZE
,则会进行相应的调整。 - 数组复制: 使用
Arrays.copyOf
将旧数组的元素复制到一个新的、更大容量的数组中。 - 添加元素: 扩容完成后,新元素被添加到
elementData
数组中,size
增加。
那为什么不是在原有数组上做扩展?
因为数组是在创建的时候就指定好大小的,不可变。所以说他需要new一个新的数组然后拷贝过去,然后旧的数组会被gc标记为一个没引用的就会被回收掉
size
和 capacity
的区别
size
:ArrayList
中实际存在的元素数量。这是list.size()
返回的值。capacity
:内部数组在不进行扩容的情况下可以容纳的最大元素数量。这是elementData.length
。- 关系:
size
总是小于或等于capacity
。当size
等于capacity
时,添加新元素就会触发扩容。
性能特点(时间复杂度)
add(E e)
(摊销常数时间 O(1)):在列表末尾添加元素。大多数情况下是 O(1)。只有当发生扩容时,由于所有元素需要被复制,它会变为 O(N)。然而,由于扩容不常发生且以固定倍数(1.5 倍)增长,平均(摊销)成本仍然是 O(1)。get(int index)
(常数时间 O(1)):通过索引访问元素非常快,因为它是一个直接的数组查找。set(int index, E element)
(常数时间 O(1)):替换特定索引处的元素也是 O(1)。add(int index, E element)
(线性时间 O(N)):在任意位置(非末尾)插入元素。从该索引开始到末尾的所有元素都必须向右移动一个位置。在最坏情况下(插入到索引 0),所有 N 个元素都需要移动。remove(int index)
(线性时间 O(N)):删除任意位置的元素。从index + 1
到末尾的所有元素都必须向左移动一个位置。在最坏情况下(删除索引 0),所有 N-1 个元素都需要移动。remove(Object o)
(线性时间 O(N)):查找并删除一个对象。这首先需要遍历列表找到该对象,然后移动元素。contains(Object o)
(线性时间 O(N)):查找元素。- 迭代 (O(N)):遍历列表中的所有元素与元素数量成比例。
如果我的内容对你有帮助,请辛苦动动您的手指为我点赞,评论,收藏。感谢大家!!