全网最硬核的源码分析之——ArrayList源码分析

ArrayList 源码分析

一.ArrayList 数据结构

ArrayList 数据结构,就是一个数组结构,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-776x1TFU-1613895136729)(https://i.loli.net/2021/02/16/akJdPLIzBevjVp4.png)]

图中展示是长度为 10 的数组,从 1 开始计数,index 表示数组的下标,从 0 开始计数,elementData 表示数组本身

1.1 重要变量

/**
 * 表示数组的初始大小,默认是 10;
 */
private static final int DEFAULT_CAPACITY = 10;

/**
 * 统计当前数组被修改的版本次数,数组结构有变动,就会 +1。
 * 该变量在AbstractList中
*/
protected transient int modCount = 0;

/**
 * 表示当前数组的大小,类型 int,没有使用 volatile 修饰,非线程安全的
 */
private int size;

二.源码分析

2.1 ArrayList 类注释解析

  • 允许 put null 值,会自动扩容;
  • size、isEmpty、get、set、add 等方法时间复杂度都是 O (1);
  • 是非线程安全的,多线程情况下,推荐使用线程安全类:Collections#synchronizedList;
  • 增强 for 循环,或者使用迭代器迭代过程中,如果数组大小被改变,会快速失败,抛出异常。

2.2 初始化实现

源码解析:

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

/**
 * 无参数直接初始化,数组大小为空
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
  /**
     * 指定参数初始化
     */
    public ArrayList(Collection<? extends E> c) {
        //elementData 保存数组的容器,默认为null
        elementData = c.toArray();
        //如果给定的集合有值
        if ((size = elementData.length) != 0) {
            // 如果集合元素不是Object,会转换成Object
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 给定集合无值,则默认空数组
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
注意事项:
  • ArrayList 无参构造器初始化时,默认大小是空数组,并不是大家常说的 10,10 是在第一次 add 的时候扩容的数组值。

2.3 新增和扩容实现

源码解析:

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

  • 判断是否需要扩容,如果需要执行扩容操作;
  • 直接赋值。
新增:
public boolean add(E e) {
   //确保数组大小是否足够,不过则进行扩容,size为当前数组大小
    ensureCapacityInternal(size + 1); 
    // 直接复制线程不安全
    elementData[size++] = e;
    return true;
}
扩容:
private void ensureCapacityInternal(int minCapacity) {
  //如果初始化数组大小时,有给定初始值,以给定的大小为准,不走 if 逻辑
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //确保容积足够
    ensureExplicitCapacity(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 的意思
    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);
    }
扩容的本质:

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

/**
*@param src 被拷贝的数组
*@param srcPos 从数组那里开始
*@param dest 目标数组
*@param destPos 从目标数组那个索引位置开始拷贝
*@param length 拷贝的长度
*此方法是没有返回值的,通过 dest 的引用进行传值
 */
public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);
注意事项:
  • 扩容的规则并不是翻倍,是原来容量大小 + 容量大小的一半,扩容后的大小是原来容量的 1.5 倍;

  • ArrayList 中的数组的最大值是 Integer.MAX_VALUE,超过这个值,JVM 就不会给数组分配内存空间了。新增时,并没有对值进行严格的校验,所以 ArrayList 是允许 null 值的。

  • 源码在扩容的时候,有数组大小溢出,就是说扩容后数组的大小下界不能小于 0,上界不能大于 Integer 的最大值。

  • 扩容完成之后,赋值是非常简单的,直接往数组上添加元素即可:elementData [size++] = e。这种简单赋值,没有任何锁控制,所以这里的操作是线程不安全的:

2.4 删除实现

源码解析:

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) {
    // 记录数组的结构发生变动
    modCount++;
    // numMoved 表示删除 index 位置的元素后,需要从 index 后移动多少个元素到前面去
    // 减 1 的原因,是因为 size 从 1 开始算起,index 从 0开始算起
    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; 
}
注意事项:
  • 新增的时候是没有对 null 进行校验的,所以删除的时候也是允许删除 null 值的;
  • 找到值在数组中的索引位置,是通过 equals 来判断的,如果数组元素不是基本类型,需要我们关注 equals 的具体实现。
  • 某一个元素被删除后,为了维护数组结构,我们都会把数组后面的元素往前移动

三.时间复杂度

经过新增或删除方法的源码解析,对数组元素的操作,只需要根据数组索引,直接新增和删除,所以时间复杂度是 O (1)。

四.线程安全

4.1 出现线程安全原因

只有当 ArrayList 作为共享变量时,才会有线程安全问题,当 ArrayList 是方法内的局部变量时,是没有线程安全的问题的。

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

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

public boolean add(E e) {
    synchronized (mutex) {return c.add(e);}
}

我们也可以使用CopyOnWriteArrayList来保证线程安全 具体对比可以参考以下表格

4.2 保证线程安全方式的对比:

CopyOnWriteArrayList (JDK 1.5引入)SynchronizedList
创建List list = new CopyOnWriteArrayList();List list = new ArrayList(); List syncList = Collections.synchronizedList(list);
线程安全安全CopyOnWriteArrayList是ArrayList的线程安全变体。它设计用于从多个线程的并发访问。CopyOnWriteArrayList为ArrayList提供了线程安全的替代方法。安全
如何实现线程安全?通过使用每个可变操作(add, set等)对原始数组进行全新复制,可以实现线程安全。从名称中还可以看出,只要值更改,就可以在写入时复制。为原始列表上的所有操作锁定SynchronizedList,基本上为所有操作添加一个同步块
表现CopyOnWriteArrayList通过创建原始数组的新副本来实现所有可变操作(add, set等)。因此,在读操作期间没有额外的开销,但在写操作期间有大量的开销。由于整个列表被锁定,并且在给定时间只有一个线程可以访问它,因此性能非常差。
内存开销需要为诸如add,set等之类的可变操作创建原始数组的的新副本。
何时使用当读取次数多于写入次数时,应选择CopyOnWriteArrayList。当写的次数多于读的次数时,应选择Collections.synchronizedList()。

五.总结

ArrayList 底层是数组结构, API 都是对数组的操作进行封装,让使用者无需感知底层实现,只需关注如何使用即可。

推荐阅读

全网最硬核的源码分析之——String源码分析

Spring Boot 自动化配置原理带图全面讲解

MySQL 3大日志的作用

如果感觉对您有帮助,希望大家可关注一下,点个赞再走,感谢您的阅读。

评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值