小读ArrayList源码

74 阅读6分钟

“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情 ArrayList实现了RandomAccess,一个空的接口,支持随机访问,用索引查询,删除,插入的数据结构,才会用到随机访问, 而且实现了这个接口,for循环的效率要比迭代器高(LinkedList就没有实现这个接口)

实现了Cloneable接口,标识着可以它可以被复制.注意,ArrayList里面的clone()复制其实是浅复制。

实现了Serializable,支持序列化传输

默认容量 DEFAULT_CAPACITY = 10,当超出后,会自动扩容为原来的1.5倍。数组的扩容是新建一个原数组大小+扩充容量的大数组,将原数组拷贝到新数组(扩容的代价很大,应该尽量减少)。如果是删除数组指定位置的元素,那么可能会挪动大量的数组元素;如果是删除末尾元素的话,那么代价是最小的.

ArrayList采用了Fail-Fast机制,面对并发的修改时,迭代器很快就会完全失败,报异常 concurrentModificationException(并发修改一次),而不是冒着在将来某个不确定时间发生任意不确定行为的风 险。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 其他两个构造函数,在不满足if条件的情况下,使用的是EMPTY_ELEMENTDATA 
     * 非常单纯的提供空的数组
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 只有默认构造函数使用了DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * 用处是在空list首次调用add方法的时候判断如何初始化数组的容量。
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
	 *既然实现了序列化接口,为什么还要修饰不让其进行序列化?
	 *为了节省空间,自己重写了序列化方法
	 *由于ArrayList的数组是动态扩增的,并不是所有被分配的空间都存储了数据
	 *如果使用外部的序列化,会序列化整个数组,为了避免那些没有存储数据的空间被序列化,自己提供了writeObject和readObject来自己序列化和反序列化
	 *因此使用 transient 修饰数组,是防止对象数组被其他外部方法序列化
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * size 表示容器当中数据的个数 注意和容器的长度区分开来
     */
    private int size;

    /**
     * 构造方法其一,当传入参数时,参数的不同会选择是开辟具体大小的数组还是空数组
     * 当我们传入0参数的时候,走向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);
        }
    }

    //数组最大长度减8,因为一些vm可能会在数组中保留一些header信息,分配更大的长度可能会导致OutOfMemoryError异常。
    //这个8就是就是存了数组_length字段。
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    
    
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
}

ps: 如果我们确定了一个固定的值,传入一个参数后就不要再进行add或者remove操作,让初始时就进行精确的扩容

扩容操作

add主要的执行逻辑如下:

  1. 确保数组已使用长度(size)加1之后足够存下 下一个数据
  2. 修改次数modCount 标识自增1,如果当前数组已使用长度(size)加1后的 大于当前的数组长度,则调用grow方法,增长数组,grow方法会将当前数组的 长度变为原来容量的1.5倍。
  3. 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。
  4. 返回添加成功布尔值。
public boolean add(E e) {
        //确保容量有size + 1,防止越界
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

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

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

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }


    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        //传入参数等于0就不会进这个if,进的话会被赋值一个10大小的数组,相当于进行一次扩容(至少一次)
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }


	//扩容方法
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
		//近似1.5倍的扩容
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果新数组的长度,小于需要的最小的容量,则更新数组的长度为 minCapacity
        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进行拷贝,开辟1.5倍的内存空间,浅拷贝
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

注意: 扩容是一件非常耗时且损耗空间的事情,如果能在使用ArrayList之前能够确定List的容量大小,那就不会调用grow()进行扩容。

再看看插入指定位置的方法,执行逻辑如下:

  • 确保数插入的位置小于等于当前数组长度,并且不小于0,否则抛出异常
  • 确保数组已使用长度(size)加1之后足够存下一个数据
  • 修改次数(modCount)标识自增1,如果当前数组已使用长度(size)加1后的大于当前的数组长度,则调用grow方法,增长数组(grow方法会将当前数组的长度变为原来容量的1.5倍)
  • 确保有足够的容量之后,使用System.arraycopy将需要插入的位置(index)后面的元素统统往后移动一位。
  • 将新的数据内容存放到数组的指定位置(index)上
public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

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

获取元素

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

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

删除元素

    //remove方法会让下标到数组末尾的元素向前移动一个单位,并把最后一位的值置空,方便GC
    public E remove(int index) {
        //下标检查
        rangeCheck(index);
        //移除元素的时候也会改变modCount,并且是++操作
        modCount++;
        E oldValue = elementData(index);
	   // 这里需要计算需要移动的数据的个数
        int numMoved = size - index - 1;
        //如果移除的不是最后一个元素,进行arrayCopy进行元素向前移动
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

    private void rangeCheck(int index) {
        if (index < 0 || index >= this.size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    /**
     * 没有重置list的size,即还是原来的数组,添加元素不会再次扩容
	 * 要对一个list重复使用的话就要使用这个方法,如果再次赋值new Arraylist(),再次添加元素后会再次扩容
     * Removes all of the elements from this list.  The list will
     * be empty after this call returns.
     */
    public void clear() {
        modCount++;

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

        size = 0;
    }

PS:

ArrayList源码

B站:河北王校长