学习笔记系列(2)——ArrayList底层原理

774 阅读5分钟

前言

2020.12.02,又是一个平平无奇的加班的周六。今天记录下自己看ArrayList源码的一些学习心得。
现在难免文笔稚嫩粗糙,而且笔记也都是以自己能够理解的语言文字方式表达出来用于自己总结梳理。如果有幸被大佬萌看到,非常希望各位大佬可以指出我的不足缺漏,并且可以指导我该往什么方向更深入的学习思考。 掘金——帮助开发者成长的社区,也希望自己能够从小菜鸡一点点进步成长。

ArrayList数据结构

ArrayList底层的数据结构是数组,数组元素类型是Object,即内部是用Object[]实现的,可以存放所有数据类型。

属性定义

序列化id

private static final long serialVersionUID = 8683452581122892189L;

默认的初始化容量

    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

ArrayList容量为0时,返回空数组。

   /**
    * Shared empty array instance used for empty instances. 
    */
   private static final Object[] EMPTY_ELEMENTDATA = {};

当调用无参构造方法,返回的是该数组。刚创建一个ArrayList 时,其内数据量为0。它与EMPTY_ELEMENTDATA的区别就是:该数组是默认返回的,而EMPTY_ELEMENTDATA是在用户指定容量为0时返回。

    /**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

ArrayList的实际大小。

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;

分派给arrays的最大容量。
为什么要减去8呢? 因为某些VM会在数组中保留一些头字,尝试分配这个最大存储容量,可能会导致array容量大于VM的limit,最终导致OutOfMemoryError。

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

构造函数

ArrayList提供了三种构造函数。

1. 空参构造

当我们new一个空参ArrayList的时候,系统调用了EmptyArray.OBJECT属性,系统内部使用了一个 new Object[0]数组。

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

2. 带参构造(1)

该构造函数传入一个 int 值,该值作为数组的长度值。
如果初始容量>0,该值为数组长度值
初始容量=0,空数据赋值给elementData
初始容量<0,抛出异常

/**
     * Constructs an empty list with the specified initial capacity.
     *
     * @param  initialCapacity  the initial capacity of the list
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
    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);
        }
    }

3. 带参构造(2)

调用该构造函数时传入了一个非空集合(源码中加入了@NotNull)Collection的子类。如果集合为空则抛出空指针异常。
将collection对象转换成数组,然后将数组的地址的赋给elementData。
如果容量为0,则空数据赋值给elementData。
容量>0,先判断elementData.getClass()是否等于Object[].class,是则执行Arrays.copy方法,把collection对象的内容(可以理解为深拷贝)copy到elementData中。

 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;
        }
    }

主要方法

接下来,我主要贴出add、remove、clear这三个主要方法的源码并记录下自己的理解。

add 方法

public boolean add(E e) {
        //确认容量,如果不够就+1
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

ensureCapacityInternal方法:
检查数组的容量,容量不够时则需要扩容

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

calculateCapacity方法:
若elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则取minCapacity为DEFAULT_CAPACITY和参数minCapacity之间的最大值。其中DEFAULT_CAPACITY在此之前已经定义为默认的初始化容量是10。

private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

ensureExplicitCapacity方法:
当minCapacity>初始化容量10时,此时数组需要扩容
注意:其中modCotun++,该变量是父类中声明的,用于记录集合修改的次数,记录集合修改的次数是为了防止在用迭代器迭代集合时避免并发修改异常,或者说用于判断是否出现并发修改异常的。

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

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

grow方法:
1、定义oldCapacity为数组初始化长度
2、扩容。
新的容量=当前容量+当前容量/2.即将当前容量增加一半(当前容量增加1.5倍)。这里面用到了三元运算符和位运算,s >> 1,意思就是将s 往右移 1 位,相当于 s=s/2,只不过位运算是效率最高的运算。
3、如果新容量<想要的最小容量,将扩容后的容量再次扩容为想要的最小容量。
4、如果新容量>数组最大容量,则进行大容量分配。
5、新的容量大小已经确定好就copy数组,改变容量大小。

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);
    }

hugeCapacity方法:
1、如果想要的最小容量<0,抛出异常
2、如果想要的容量大于MAX_ARRAY_SIZE,则分配Integer.MAX_VALUE:2147483647,否则分配MAX_ARRAY_SIZE:2147483639

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

remove方法

1、检查索引是否越界。如果参数指定索引index>=size,抛出一个越界异常
2、记录修改次数
3、定义oldValue:索引处的元素。即通过索引找到要删除的元素
4、定义numMoved:删除指定元素后,需要左移的元素个数。即计算要移动的位数
5、如果个数>0,即有需要左移的元素,就移动元素(移动后,该删除的元素就已经被覆盖了)
6、size-1,将索引为size-1处的元素置为null。为了让GC回收起作用,必须显式的为最后一个位置赋null值
7、返回被删除的元素

public E remove(int index) {
        rangeCheck(index);

        modCount++;
        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;
    }

clear方法

如果集合长度不等于 0,则将所有数组的值都设置为 null,然后将成员变量size设置为0即可。

public void clear() {
        modCount++;

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

        size = 0;
    }