ArrayList 源码

ArrayList 源码

ArrayList概览

基本概念

ArrayList 的结构较为简单,就是一个数组。结构如下图所示。
image.png
ArrayList中有一些重要概念、属性:

  1. index:当前下标
  2. elementData:数组,该数组的大小,经常与 ArrayList 的size 混淆,需要注意。
  3. DEFAULT_CAPACITY:数组的初始大小,默认是 10
  4. size 表示当前ArrayList实际有多少个数据,没有使用 volatile 修饰;
  5. modCount :当前数组的版本号,数组结构有变动,就会 +1。

类介绍(注释)

类注释大致内容如下:

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

源码解析

构造方法

ArrayList提供了三种构造方法。

  1. 无参构造方法
  2. 指定大小
  3. 指定初始数据
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

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

// 指定大小,主要是判断指定大小的合理性(>=0)
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) {
    elementData = c.toArray();
    // c 有内容
    if ((size = elementData.length) != 0) {
        // 如果集合元素类型不是 Object 类型,我们会转成 Object
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // c 没有内容,则为空数组
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

构造方法补充

  1. ArrayList 无参构造器初始化时,默认大小是空数组,并不是大家常说的 10,10 是在第一次 add 的时候扩容的数组值。
  2. 指定初始数据初始化时,我们发现一个这样子的注释 c.toArray might (incorrectly) not return Object[] see 6260652,这是 Java8 的一个 bug,意思是当给定集合内的元素不是 Object 类型时,我们会转化成 Object 的类型。一般情况下都不会触发此 bug,只有在下列场景下才会触发:ArrayList 初始化之后(ArrayList 元素非 Object 类型),再次调用 toArray 方法,得到 Object 数组,并且往 Object 数组赋值时,才会触发此 bug。官方查看文档地址:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6260652 ,问题在 Java 9 中被解决。

添加元素、扩容

ArrayList 提供了两种方式添加元素。我们选取较简单的一种,即在末尾添加。
添加元素分为两步:

  1. 确认容量是否足够,不足则扩容
  2. 在末尾赋值
public boolean add(E e) {
    // 判断容量是否足够(当前大小+1)
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    
    // 直接赋值,非线程安全操作
    elementData[size++] = e;
    return true;
}

接下来追溯到ensureCapacityInternal方法的源码,为了便于理解,以下是简单整理所得,并非原版。

private void ensureCapacityInternal(int minCapacity) {
    // 如果初始化数组大小时,没有给定初始值,才会走if分支
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity();
}


private void ensureExplicitCapacity(int minCapacity) {
    // 版本号+1
    modCount++;

    // 如果我们期望的容量,(即add操作时,传入的size+1) 超过 目前数组的长度,那么就扩容
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

最后是扩容-grow方法源码

// 允许JVM分配的最大数组空间
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // oldCapacity >> 1 相当于 oldCapacity / 2
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    // 如果扩容后的容量 < 我们的期望值
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    
    // 如果扩容后的容量 > 允许JVM分配的最大数组空间,则使用 Integer.MAX_VALUE
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    
    // minCapacity is usually close to size, so this is a win:
    // 通过Arrays.copyOf方法进行扩容,同时复制了数据
    elementData = Arrays.copyOf(elementData, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
    	MAX_ARRAY_SIZE;
}

在添加元素、扩容的源码中,我们应该注意:

  1. 扩容是原来容量大小 + 容量大小的一半,直白来说,扩容后的大小是原来容量的 1.5 倍;
  2. ArrayList 中的数组的最大值是 Integer.MAX_VALUE,超过这个值,在hugeCapacity方法中,也只会给Integer.MAX_VALUE。
  3. 新增时,并没有对值进行严格的校验,所以 ArrayList 是允许 null 值的。
  4. 源码在扩容的时候,有数组大小溢出意识,就是说扩容后数组的大小下界不能小于 0,上界不能大于 Integer 的最大值

扩容的核心实现

从以上源码,我们发现grow方法的核心实现,在于以下一句。

elementData = Arrays.copyOf(elementData, newCapacity);

该方法最终调用的,是System.arraycopy。
image.png

删除元素

ArrayList提供了多个重载的remove方法,其中的实现大同小异,下面以remove``(``**Object **``o``),即删除指定对象的方式为例。

public boolean remove(Object o) {
    // 需要删除的对象为 null
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                // 删除第一个找到的null值后,直接结束方法
                fastRemove(index);
                return true;
            }
    } else {
        // 需要删除的对象不为 null
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                // 通过equals方法,删除第一个找到的指定元素后,直接结束方法
                fastRemove(index);
                return true;
            }
    }
    return false;
}

以上源码中,可以发现,最终都是调用了fastRemove(index);通过索引来快速删除元素,除此之外需要额外注意:

  • 新增的时候是没有对 null 进行校验的,所以删除的时候也是允许删除 null 值的;
  • 需要删除的对象不为 null时,找到值在数组中的索引位置,是通过 equals 来判断的,如果数组元素不是基本类型,我们需要关注 equals 的具体实现。

下面来看看fastRemove的实现:
那为什么叫快速删除呢?通过注释可以看出,此方法跳过了下标范围检测、未返回任何值。

	/*
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
private void fastRemove(int index) {
    // 更新版本号
    modCount++;
    
    // 计算需要移动的元素个数(从中间删除后,保证数组连续,会将后方元素前移)
    // 减 1 的原因,是因为 size 从 1 开始算起,index 从 0开始算起
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,numMoved);
    
    elementData[--size] = null; // clear to let GC do its work
}

迭代(Iterator)

ArrayList通过实现 java.util.Iterator接口,达到迭代器的效果。以下是几个重要参数:

  1. int cursor;// 迭代过程中,下一个元素的位置,默认从 0 开始。
  2. int lastRet = -1; // 新增场景:表示上一次迭代过程中,索引的位置;删除场景:为 -1。
  3. int expectedModCount = modCount;// expectedModCount 表示迭代过程中,期望的版本号;

迭代器的实现,通常有hasNext、next、remove 三个方法。以下为ArrayList中方法的实现:

public boolean hasNext() {
  // cursor 表示下一个元素的位置,size 表示实际元素个数
  // 如果两者相等,说明已经没有元素可以迭代了;如果不相等,说明还可以迭代
  return cursor != size;
}

public E next() {
  // 迭代过程中,判断版本号是否被修改。如被修改,抛出 ConcurrentModificationException
  checkForComodification();
  // 本次迭代过程中,元素的索引位置
  int i = cursor;
  if (i >= size)
    throw new NoSuchElementException();
    
  Object[] elementData = ArrayList.this.elementData;
  if (i >= elementData.length)
    throw new ConcurrentModificationException();
    
  // 下一次迭代时,元素的位置
  cursor = i + 1;
  // 返回元素值
  return (E) elementData[lastRet = i];
}

// 判断版本号是否被修改。如被修改,抛出 ConcurrentModificationException
final void checkForComodification() {
  if (modCount != expectedModCount)
    throw new ConcurrentModificationException();
}

从源码中可以看到,next 方法就干了两件事情,第一是检验能不能继续迭代,第二是找到迭代的值,并为下一次迭代做准备( cursor = i + 1)。再看remove方法,我们需要注意其中两点:

  1. lastRet = -1 的操作目的,是防止重复删除操作
  2. 删除元素成功,数组当前 modCount 就会发生变化,这里会把 expectedModCount 重新赋值,下次迭代时两者的值就会一致了
public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    // 判断版本号是否被修改
    checkForComodification();

    try {
        // 这里调用的是 ArrayList的remove方法
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        
        // -1 表示元素已经被删除,这里也防止重复删除
        lastRet = -1;
        
        // 删除元素时 modCount 的值已经发生变化,在此赋值给 expectedModCount
    	// 这样下次迭代时,两者的值是一致的了
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

缩容

ArrayList并未提供自动缩容的机制,但可以通过手动调用trimToSize方法,来使其空间利用率达到100%。
如以下场景:系统启动,初始化一些数据到ArrayList中缓存起来,这些数据比较多(几千个元素)但是根据业务场景是不会变的。源码较为简单:

public void trimToSize() {
    // 增加版本号
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
            ? EMPTY_ELEMENTDATA
            : Arrays.copyOf(elementData, size);
    }
}

ArrayList的线程安全

只有当 ArrayList 作为共享变量时,才会有线程安全问题,当 ArrayList 是方法内的局部变量时,是没有线程安全的问题的。
ArrayList 有线程安全问题的本质,是因为 ArrayList 自身的 elementData、size、modConut 在进行各种操作时,都没有加锁,而且这些变量的类型并非是可见(volatile)的,所以如果多个线程对这些变量进行操作时,可能会有值被覆盖的情况。
类的注释中推荐我们使用 Collections#synchronizedList 来保证线程安全,SynchronizedList 是通过在每个方法上面加上锁来实现,虽然实现了线程安全,但是性能大大降低,如add操作:

public void add(int index, E element) {
    // synchronized 是一种轻量锁,mutex 表示一个当前 SynchronizedList
    synchronized (mutex) {list.add(index, element);}
}

时间复杂度

从我们上面新增或删除方法的源码解析,根据数组索引对数组元素的操作,复杂度是 O (1)。而遇到扩容时,复杂度为O(n)。

猜你喜欢

转载自blog.csdn.net/lik_lik/article/details/106987521