ArrayList源码分析

ArrayList的底层是动态数组,其容量可以动态增长。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  • RandomAccess 是一个标志接口,表明实现这个这个接口的List集合是支持快速随机访问的。在ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。
  • ArrayList 实现了Cloneable 接口 ,即覆盖了函数clone(),能被克隆。
  • ArrayList 实现了 java.io.Serializable 接口,这意味着ArrayList支持序列化,能通过序列化去传输。

ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置i插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。

属性

private static final long serialVersionUID = 8683452581122892189L;
// 集合的默认容量
private static final int DEFAULT_CAPACITY = 10;

// 用于空对象的共享空数组实例
// 当用户指定该ArrayList容量为0时,返回该空数组
private static final Object[] EMPTY_ELEMENTDATA = {};

// 用于默认容量大小空对象的共享空数组实例。当用户没有指定ArrayList的容量时(即调用无参构造函数),返回的是该数组。把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 保存ArrayList数据的数组
transient Object[] elementData;

// 当前集合包含的元素数量,也就是集合的实际长度
private int size;

还有一个从父类AbstractList继承过来的属性modCount,用于记录ArrayList集合的修改次数。结构修改是指更改列表的大小的结构修改,或以其他方式干扰列表进度的迭代可能产生不正确结果的方式。

protected transient int modCount = 0;

构造函数

​ 创建集合时,如果不传入参数,则使用默认无参构建方法创建ArrayList对象,当进行第一次add的时候,elementData将会变成默认的长度:10,即容量扩为10。

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

也可以在创建集合时,指定ArrayList的初始数组长度,即传入一个int型的参数,传入参数如果是大于0,则使用用户的参数初始化;如果传入的参数等于0,则创建的的是一个空对象的共享空数组实例;如果用户传入的参数小于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);
    }
}

也可以在创建ArrayList的时候,传入一个指定的Collection对象,按照它们由集合的迭代器返回的顺序存入数组列表。

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

它的具体执行逻辑是:

​ 1)将collection对象转换成数组,然后将数组的地址的赋给elementData。

​ 2)更新size的值,同时判断size的大小,如果是size等于0,直接将空对象EMPTY_ELEMENTDATA的地址赋给elementData

​ 3)如果size的值大于0,则执行Arrays.copy方法,把collection对象的内容(可以理解为深拷贝)copy到elementData中。

​ 注意:this.elementData = arg0.toArray(); 这里执行的简单赋值时浅拷贝,所以要执行Arrays,copy做深拷贝

注意:JDK7 new无参构造的ArrayList对象时,直接创建了长度是10的Object[]数组elementData 。jdk7中的ArrayList的对象的创建类似于单例的饿汉式(先创建好,等待被使用),而jdk8中的ArrayList的对象的创建类似于单例的懒汉式(使用时才创建)。JDK8的内存优化也值得我们在平时开发中学习。

扩容机制

add方法

// 将指定的元素追加到此列表的末尾
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

​ 添加元素之前,先调用ensureCapacityInternal方法,查看一下该方法的底层逻辑:

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 获取默认的容量和传入参数的较大值
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

​ 传入到ensureCapacityInternal方法中的参数值是size加1,即minCapacity = size + 1,这是当前加入一个新元素后所需的容量,该方法也就是要确保当前数组是否有足够容量存下这个minCapacity数量的元素。

​ ensureCapacityInternal方法调用ensureExplicitCapacity之前,会先将当前最新的元素数量传入calculateCapacity方法,用于计算当前对象数组所需的容量,然后作为参数传给ensureExplicitCapacity方法。

​ 在calculateCapacity方法内部,如果当我们使用无参构造函数创建数组列表时,没有指定容量大小,其内部对象数组的实例是DEFAULTCAPACITY_EMPTY_ELEMENTDATA当我们调用add方法第一次添加元素时,传入的minCapacity为1,在经过Math.max方法比较之后,对象数组所需的容量大小也就变为了10。

​ 然后将对象数组的所需的容量大小传给ensureExplicitCapacity方法,用于判断是否需要扩容,如果当前所需的容量大于对象数组的长度,则调用grow方法进行扩容。

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        //调用grow方法进行扩容,调用此方法代表已经开始扩容了
        grow(minCapacity);
}

​ 用于标识arraylist修改记录的modcount会自增1,表示集合的结构修改了一次。

所以add方法的逻辑可以总结为以下几步:

(1)首先判断数组列表已经使用的长度加上1之后是否足够存下当前这个新元素

1)如果add添加的是数组列表的第一个元素,此时elementData.length为0 (因为还是一个空的list),此时执行了 ensureCapacityInternal()方法 , minCapacity会被设置为默认容量10。;
2)如果add添加的不是数组列表的第一个元素,而此时传入ensureCapacityInternal方法的参数值是已有元素数量加1(size+1),那么在calculateCapacity方法中就直接返回的是这个值;

(2)ensureExplicitCapacity方法接收calculateCapacity计算的minCapacity值,与当前对象数组的容量进行比较,如果minCapacity - elementData.length > 0则会进行扩容,否则不扩容。

(3)将元素e添加到对象数组中。

​ 将指定的元素插入此列表中的指定位置。同时将当前位于该位置的元素(如果有的话)和任何它后面位置的元素右移(将它们的索引加一)。

public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //arraycopy()方法此时就是将index位置及其之后的元素全部向后挪动1个位置
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

该方法的执行逻辑是:

​ 该方法先调用rangeCheckForAdd对index进行界限检查;

​ 然后调用ensureCapacityInternal方法保证capacity足够大,同时增加该集合的修改记录数;

​ 再将从index开始之后的所有成员后移一个位置;

​ 将element插入index位置;最后size加1。

此处的界限检查函数要保证指定的元素插入位置既要大于0,也要位于小于等于当前数组列表含有的元素数量,其源码如下:

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

此处的size是ArrayList的大小(它包含的元素数)。

grow方法

// 要分配的最大数组大小
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 给数组列表扩容的核心方法,通过该方法会增加数组列表的容量,以确保至少能够容纳minCapacity参数指定的元素数。
private void grow(int minCapacity) {
    // overflow-conscious code
    // 旧容量
    int oldCapacity = elementData.length;
    // 新的容量,使用位运算比算术运算更加的快
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    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;
}

​ 从newCapacity = oldCapacity + (oldCapacity >> 1)这段代码可以看出,每次扩容时,首先采取的方法是将容量变为原先容量的1.5倍左右。进行1.5倍左右的扩容操作之后,会与minCapacity参数进行比较,如果仍小于minCapacity,那么就将minCapacity的值直接赋值给新容量变量。

​ 如果新容量大于MAX_ARRAY_SIZE,则进入(执行) hugeCapacity()方法来比较minCapacity和 MAX_ARRAY_SIZE,如果minCapacity大于最大容量MAX_ARRAY_SIZE,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为MAX_ARRAY_SIZE即为Integer.MAX_VALUE - 8。

System.arraycopy()和Arrays.copyOf()

System.arraycopy

从指定的源数组复制数据,从源数组中指定的位置开始复制,将复制以后的数据复制到目标数组的指定位置。

public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);

其参数含义如下:

src:源数组
srcPos:要复制源数组中的数据的起始位置
dest:目标数组
destPos:将复制的数据放置到目标数组中的起始位置。
length:要复制的数组元素的数量

Arrays.copyOf

​ 复制指定的数组,以空值截断或填充(如有必要),以便副本具有指定的长度。

public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
    return copy;
 }

copyOf有两个参数:

T[] original 源数组
int newLength 指定新数组的长度

copyOf底层调用的依旧是System.arraycopy方法,该方法不用调用者指定目标数组,而是自己在底层创建了一个用于存储源数组中被复制数据的数组copy。它进行数组复制时,是将源数组从索引位置为0的地方开始复制,复制到copy中,存储位置也是从0开始。要复制的元素数量是newLength和源数组元素数量二者之间的最小值。最终该方法会返回copy。

二者区别

arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置

copyOf() 是系统自动在内部新建一个数组,并返回该数组。

案例

public static void main(String[] args) {


    int[] a = {1,2,3,4,5};
    int[] b = new int[10];

    // System.arraycopy(a,0,b,0,10);
    // java.lang.ArrayIndexOutOfBoundsException

    //System.arraycopy(a,0,b,0,5);
    // [1, 2, 3, 4, 5, 0, 0, 0, 0, 0]

    System.arraycopy(a,2,b,0,3);
    // [3, 4, 5, 0, 0, 0, 0, 0, 0, 0]

    System.out.println(Arrays.toString(b));


    int[] ints = Arrays.copyOf(a, 6);

    System.out.println(ints.length);
    System.out.println(Arrays.toString(ints));
    /*
    6
    [1, 2, 3, 4, 5, 0]
     */   
}

​ 使用System.arraycopy方法时,一定要注意源数组中被复制的起始位置与要复制的元素数量之和,即srcPos+length要小于等于源数组的长度,否则就会发生数组越界异常。

​ 对于Arrays.copyOf方法,如果要复制的元素数量大于源数组中长度,则会使用数组类型对应的默认值填充。数组的类型是在运行时确定的。

其他方法

get

​ 获取指定位置上的元素。

public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}
E elementData(int index) {
    return (E) elementData[index];
}

set

参数index的值要大于等于0,小于当前数组列表中的元素数量。获取指定位置(index)元素,然后放到oldValue存放,将需要设置的元素放到指定的位置(index)上,然后将原来位置上的元素oldValue返回给用户。

public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

contains

​ 调用indexOf方法,遍历数组中的每一个元素作对比,如果找到对于的元素,则返回true,没有找到则返回false。

public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

remove(int index)

​ 删除此列表中指定位置index处的元素, 将index之后的所有元素向左移动(它们的索引都减去1)。

public E remove(int index) {
    // 判断索引index是否越界,合法范围:0 =< index < size
    rangeCheck(index);
	// 自增结构修改次数
    modCount++;
    // 将原来在index位置上的值取出来给oldValue
    E oldValue = elementData(index);
	// 计算出要左移的元素数量
    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

    return oldValue;
}

注意:调用这个方法不会缩减对象数组的容量,只是将最后一个数组元素置空而已。比如看下面的案例,创建一个ArrayList,添加完三个元素之后,此时ArrayList底层的对象数组elementData的容量应该是默认值10,当采用remove方法移除索引为1的元素之后,elementData数组的长度依旧是10,但是其中的元素数量已经变为2。

public static void main(String[] args) {

    ArrayList<Object> objects = new ArrayList<>();

    objects.add(1);
    objects.add(1);
    objects.add(1);

    objects.set(0,2);

    objects.remove(1);

    System.out.println(objects.size());
}

remove(Object o)

​ 循环遍历所有对象,得到对象所在索引位置,然后调用fastRemove方法,执行remove操作。如果该列表中有多个与指定对象相等的元素,那么该方法只会删除列表中出现的第一个与之相等的元素。如果列表不包含该元素,则它不会更改,此时返回false。如果此列表包含指定的元素,则在删除后,会返回true。

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

private void fastRemove(int index) {
    // 自增结构修改次数
    modCount++;
    // 计算要被删除的元素的索引位置
    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
}

clear

​ 添加操作次数(modCount),将数组内的元素都置空,等待垃圾收集器收集,不减小数组容量。

public void clear() {
    modCount++;

    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

trimToSize方法

​ 将elementData中空余的空间(包括null值)去除,例如:数组长度为10,其中只有前三个元素有值,其他为空,那么调用该方法之后,数组的长度变为3.

public void trimToSize() {
    // 修改结构记录数加1
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}

toArray()

​ 以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。返回的数组将是“安全的”,因为该列表不保留对它的引用。(换句话说,这个方法必须分配一个新的数组)。因此,调用者可以自由地修改返回的数组。此方法充当基于阵列和基于集合的API之间的桥梁。

public Object[] toArray() {
    return Arrays.copyOf(elementData, size);
}

需要遍历数组的方法

​ clear、remove(Object o)、contains

参考:https://snailclimb.gitee.io/javaguide/#/docs/java/collection/ArrayList源码+扩容机制分析?id=_34-ensurecapacity方法

posted @ 2021-03-15 20:34  有心有梦  阅读(69)  评论(0编辑  收藏  举报