ArrayList 源码解析和设计思路

ArrayList是一个基于数组的动态集合,其内部结构简单,使用数组存储元素。默认初始容量为10,非线程安全。添加、读取元素的时间复杂度为O(1),删除元素为O(n)。扩容时按1.5倍当前容量增长。删除元素会移动后续元素。线程不安全问题可通过Collections.synchronizedList解决。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ArrayList 源码解析和设计思路

整体架构

ArrayList 整体架构比较简单,就是一个数组结构,比较简单,如下图:

image-20210619110603712

图中展示是长度为 10 的数组,从 1 开始计数,index 表示数组的下标,从 0 开始计数,elementData 表示数组本身,源码中除了这两个概念,还有以下三个基本概念:

  • DEFAULT_CAPACITY 表示数组的初始大小,默认是10,这个数字要记住;
  • size 表示当前数组的大小,类型 int ,没有使用 volatile 修饰,非线程安全的;
  • modCount 统计当前数组被修改的版本次数,数组结构有变动,就会 +1.

类注释

看源码,首先要看类注释,我们看看类注释上面都说了什么:

  • 允许 pull null 值,会自动扩容

  • size、isEmpty、get、set、add 等方法时间复杂度都是 O(1)

  • 非线程安全的,多线程情况下,推荐使用线程安全类:Collection#synchronizedList

  • 增强 for 循环,或者使用迭代器迭代的过程中,如果数组的大小被改变,会快速失败,抛出异常

除了上述注释中提到的 4 点,初始化、扩容的本质、迭代器等问题也经常被问,接下来我们从源码出发,一一解析。

源码解析

初始化

有三种初始化方法:无参数直接初始化、指定大小初始化、指定初始数据初始化,源码如下:

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 无参数直接初始化,数组大小为空
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

ArrayList 无参构造器初始化时,默认大小是空数组,并不是大家常说的 10,10 是在第一次 add 的时候扩容的数组值

transient Object[] elementData;
// 指定数组长度初始化
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
// 指定初始数据初始化
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;
    }
}

新增和扩容实现

新增就是往数组中添加元素,主要分成两步:

  • 判断是否需要扩容,如果需要执行扩容操作;
  • 直接赋值。

两步源码体现如下:

public boolean add(E e) {
    // 确保数组大小是否足够,不够执行扩容,size 为当前数组大小
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 直接赋值,线程不安全的
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// 计算所需的容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

// 确保容量足够用
private void ensureExplicitCapacity(int minCapacity) {
    // 记录数组修改次数
    modCount++;

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

// 扩容,并把现有数据拷贝到新的数组里面去
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // oldCapacity >> 1 是把 oldCapacity 除以 2 的意思
    // 是原来容量大小 + 容量大小的一半,即1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果扩容后的值 < 我们的期望值,扩容后的值就等于我们的期望值
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 如果扩容后的值 > jvm 所能分配的数组的最大值,那么就用 Integer 的最大值
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 拷贝数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}
  • 扩容的规则并不是翻倍,是原来容量 + 容量大小的一半,即扩容后的大小是原来容量的 1.5 倍

  • ArrayList 中的数组的最大值是 Integer.MAX_VALUE,超过这个值,JVM 就不会给数组分配内存空间了

  • 新增时,并没有对值进行严格的校验,所以 ArrayList 是允许 null 值的

  • 扩容完成后,赋值是非常简单的,没有任何锁控制,所以这里的操作是线程不安全的

扩容的本质

扩容是通过这行代码来实现的: Arrays.copyOf(elementData, newCapacity) ,这行代码描述的本质是数组之间的拷贝,扩容是会先新建一个符合我们预期容量的新数组,然后把老数组的数据拷贝过去,底层是通过 System.arraycopy 方法进行拷贝,此方法是 native 方法

删除

ArrayList 删除元素有很多种方式,比如根据数组索引删除、根据值删除或批量删除等等,原理和思路都差不多,我们选取根据值删除方式来分析下源码:

public boolean remove(Object o) {
    // 如果要删除的值是 null,找到数组中第一个值为null的删除
    if (o == 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++)
            // 这里是根据 equals 来判断值相等的,相等后再根据索引位置进行删除
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

private void fastRemove(int index) {
    // 数组修改次数+1
    modCount++;
    // 删除后需要把后面的元素往前挪动,计算移动的数量
    int numMoved = size - index - 1;
    if (numMoved > 0)
        // 移动后面的元素
        // 从 index +1 位置开始被拷贝,拷贝的起始位置是 index,长度是 numMoved
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // 数组最后一个位置赋值 null,帮助 GC
    elementData[--size] = null; // clear to let GC do its work
}
  • 新增的时候没有对 null 进行校验,所以删除的时候也是允许删除 null 值的
  • 找到值在数组中的索引位置,是通过 equals 来判断的,如果数组元素不是基本类型,需要关注 equals 的具体实现
  • 某一个元素被删除后,为了维护数组结构,会把数组后面的元素往前移动

时间复杂度

操作时间复杂度
读取 get(index)根据下标直接查数组,O(1)
添加 add(E)直接尾部添加,O(1)
指定位置添加 add(index, E)查询后添加,涉及元素挪动,O(n)
删除 remove(E)删除指定元素,需要遍历查找元素下标,删除后需要挪动后面元素,O(n)

线程安全

ArrayList 作为共享变量使用时,会出现线程安全问题。

ArrayList 有线程安全问题的本质,是因为 ArrayList 自身的 elementData、size、modCount 在进行操作时,都没有加锁,而且这些变量的类型并非是可见的(volatile),座椅如果多个线程对这些变量进行操作时,可能会有值被覆盖的情况。

类注释中推荐我们使用 Collections#synchronizedList 来保证线程安全,SynchronizedList 是通过在每个方法上面加上锁来实现,虽然实现了线程安全,但是性能大大降低,具体实现源码:

public E get(int index) {
    synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
    synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
    synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
    synchronized (mutex) {return list.remove(index);}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值