Java集合之ArrayList(源码层面上的分析)

简介

大家都知道ArrayList是由数组实现的,有自动扩容的功能。在超出限制时,会自动扩容50%或扩容至当前所需最小容量(二者谁大扩容至哪里) ,最终通过Arrays.copyOf()(其实内部实现就是调用System.arraycopy方法)扩充容量。

无参数创建ArrayList情况中,第一次插入元素时,会默认创建大小为10的数组

按数组下标访问元素—get(i)/set(i,e) 的性能很高,这是数组的基本优势。

直接在数组末尾加入元素—add(e)的性能也高,但如果按下标插入(add(index,e))、删除元素—add(i,e), remove(i), remove(e),则要用System.arraycopy()来移动部分受影响的元素,性能就变差了,这是基本劣势。

本文将通过源码层面讲解ArrayList的各个方法,以及自动扩容的流程。

源码

ArrayList的属性(好好理解理解size)

  1. serialVersionUID,代表ArrayList可以被序列化。
  2. DEFAULT_CAPACITY,默认初始长度,也就是上文提到的10
  3. EMPTY_ELEMENTDATA,一个空对象,当创建一个大小为0的ArrayList时会使用下面这个数组。
    使用的含义就是:
this.elementData = EMPTY_ELEMENTDATA;
  1. DEFAULTCAPACITY_EMPTY_ELEMENTDATA:一个空对象,如果使用默认构造函数创建时会使用该数组。
  2. elementData当前实际数组存放的地址。我们可以通过注释看到,当elementdata = DEFAULTCAPACITY_EMPTY_ELEMENTDATA时第一次(add)添加数据时,会扩展至DEFAULT_CAPACITY长度。当前对象不参与序列化。
  3. size当前数组已使用的长度。当调用add函数时,新加入的元素就会放入size位置,所以这个size可以理解为当前数组已使用的长度。(因为放置位置都是从0开始放的)
  4. MAX_ARRAY_SIZE:当前数组长度所允许的最大值。
 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

源码如下:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
	//序列化ID
    private static final long serialVersionUID = 8683452581122892189L;
    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

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

    /**
     * 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 = {};

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

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

构造函数

1.无参数的构造函数

可以看到这里直接就将DEFAULTCAPACITY_EMPTY_ELEMENTDATA数组赋给了elementData。

/**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
2.带int类型的构造函数

如果传入参数,则代表指定ArrayList的初始数组长度,传入参数如果是大于等于0,则使用用户的参数初始化,如果用户传入的参数小于0,则抛出异常。(等于0时,会赋值EMPTY_ELEMENTDATA)

 /**
     * 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.带Collection对象的构造函数

1)将collection对象转换成数组,然后将数组的地址的赋给elementData。
2)更新size的值,同时判断size的大小,如果是size等于0,直接将空对象EMPTY_ELEMENTDATA的地址赋给elementData
3)如果size的值大于0,则执行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函数

add有两个函数一个是直接添加,一个是在固定位置添加。首先说直接添加的函数。

add(E e)(重要)

调用 ensureCapacityInternal方法,并传入size+1。之后在赋值时,将e赋值在size位置。

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

我们继续看ensureCapacityInternal函数,这里也出现了为什么是,无参数创建时,第一次插入元素时,默认创建大小为10的数组

因为无参数创建ArrayList时,elementdata会指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA,进入该函数后,也就会进入if判断,minCapacity就会取10。否则,如果是有参数创建时,就不会进入这个if语句。

private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

继续看ensureExplicitCapacity函数,首先将修改次数(modCount)自增,之后判断是否需要扩容,判断条件:当前最小容量(minCapacity)是否大于当前数组的长度(elementData.length)

还是以无参数创建ArrayList,第一次插入元素为例,此时的minCapacity是10,而数组长度为0,所以肯定会扩容。

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

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

继续看核心的扩容函数grow函数。这里新的风暴出现了,oldCapacity是当前数组的长度,newCapacity可以认为是想要去扩充的大小,这里默认扩充到原容量的1.5倍。

 private void grow(int minCapacity) {
        // overflow-conscious code
        //当前数组的容量
        int oldCapacity = elementData.length;
        //右移1位代表除2,newCapacity扩充至原容量的1.5倍
        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);
    }

但是注意之后的if语句,如果新扩充的大小(newCapacity)小于传入的当前所需最小容量(minCapacity),就要将newCapacity赋值为minCapacity,这点很好理解,因为至少要扩充至所需最小容量。

if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;

什么时候会出现这个情况呢,还是以上面的例子为例,此时传入的minCapacity=10,oldCapacity=0,newCapacity=0,此时就会进入上述的if语句。

下述if语句的作用是如果newCapacity大于MAX_ARRAY_SIZE,就默认扩充至Integer.MAX_VALUE。

 if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);

最后会通过Arrays.copyOf进行扩充,将elementData扩充至newCapacity长度。

Arrays.copyOf(重要)

参数的含义

(原数组,拷贝的个数)

示例:

int[] a1 = {1, 2, 3, 4, 5};
        int[] a2 = Arrays.copyOf(a1, 3);
        int[] a3 = Arrays.copyOf(a1,10);
        System.out.println(Arrays.toString(a1)); // [1, 2, 3, 4, 5]
        System.out.println(Arrays.toString(a2)); // [1, 2, 3]
        System.out.println(Arrays.toString(a3)); // [1, 2, 3, 4, 5, 0, 0, 0, 0, 0]

其实Arrays.copyOf底层也是调用System.arraycopy实现简单来说创建一个newlength长度的数组copy,再将original(旧数组)的部分元素(如果copy长度比original小)或全部元素(copy长度大于original)拷贝到copy中。

//基本数据类型(其他类似byte,short···)
public static int[] copyOf(int[] original, int newLength) {
        int[] copy = new int[newLength];
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }
总结

综上所述,add(E e)方法可以总结如下:

1.确保数组已使用长度(size)加1之后足够存下当前要存的数据

(具体是通过 ensureCapacityInternal(size + 1)-> ensureExplicitCapacity(minCapacity)

2.修改次数modCount 标识自增1,如果当前数组所需最小容量(minCapacity)大于当前的数组长度,则调用grow方法,增长数组,grow方法会将当前数组的长度变为原来容量的1.5倍,前提是如果增长1.5倍后的长度大于数组所需最小容量(minCapacity),如果小于,就增长至minCapacity

 1.if (minCapacity - elementData.length > 0)
            grow(minCapacity);
 2.private void grow(int minCapacity) {
        // overflow-conscious code
        //当前数组的容量
        int oldCapacity = elementData.length;
        //右移1位代表除2,newCapacity扩充至原容量的1.5倍
        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);
    }

3.确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。

elementData[size++] = e;

4.add(E e)如果在数组可以添加时,时间复杂度就只有O(1),而如果数组不可以添加进去的话,时间复杂度就是O(n),因为要扩充数组,而扩充数组分两步:11.创建一个长度加1的数组,2.将原数组中的数据赋值到新数组。

add(int index, E element)(重要)

这里体现了ArrayList的缺陷,因为在增加的时候,需要向后移动数组,造成时间复杂度的损失

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++;
    }
总结与流程

首先通过rangeCheckForAdd进行范围判断,判断包括:index是否大于size,是否小于0,如果是的话就要抛出异常。

之后使用ensureCapacityInternal来判断是否需要扩容(这里之前已经说过了)。

之后通过System.arraycopy将index之后的元素都向后移动一位,并将element放入index位置,最终将size自增,代表当前已使用的长度多了一位。

System.arraycopy(重要)

System.arraycopy方法是一个本地的方法,源码里定义如下:

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

其含义为从原数组的开始位置开始拷贝,将(拷贝个数)个元素拷贝至目标数组的开始位置,并依次顺延。

(原数组, 原数组的开始位置, 目标数组, 目标数组的开始位置, 拷贝个数)

示例

int[] a1 = {1, 2, 3, 4, 5};
int[] a2 = new int[10];
System.arraycopy(a1, 1, a2, 3, 3);
System.out.println(Arrays.toString(a1)); // [1, 2, 3, 4, 5]
System.out.println(Arrays.toString(a2)); // [0, 0, 0, 2, 3, 4, 0, 0, 0, 0]

当使用这个方法的时候,需要复制到一个已经分配内存单元的数组。

一个实例

在这里插入图片描述
就以上图为例,当我们调用add(2,5)方法之时,size=4,我们假设ArrayList是通过无参数创建的,因此,当ArrayList在第一次调用add时,elementdata数组就会扩容至10。

因此当我们调用add(2,5)时,首先调用ensureCapacityInternal(5),在该函数中不会调用grow函数,因为mincapacity<elementdata.length(5<10),直接调用System.arraycopy即可,也就是将elementData中位置2之后的,size-index(2)个元素放到index+1(3)位以及之后。其实就是将index之后的元素都向后移动一位,之后把5放置到2这个位置,并将size自增。

自动扩充流程(最重要)

注意:mincapacity代表的是当前所需最小容量,比如使用add函数,那么mincapacity就是size+1,而不是1。所以比较是否要扩充时,是用mincapacity与数组的长度,而不是size(数组已经使用的长度)。

1.首先会调用ensureCapacityInternal方法来判断是否需要扩充,这个方法会传入mincapacity,代表当前所需最小容量。其实调用该方法的原因就是为了判断当前是否是无参数构造方法的第一次add操作包括add有无插入位置,以及addAll),就会把minCapacity置为10与minCapacity中的较大值(当调用addAll时,可能传入的mincapacity会比10还大)。

private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

2.执行 ensureExplicitCapacity方法,在该方法中会判断是否需要扩充,如果需要的话就要调用grow函数进行扩充。

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

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

3.执行grow函数,在grow函数中,会将当前数组(elementData)长度扩充至1.5倍的值与最小所需容量进行比较(minCapacity),并将二者较大值赋给最终扩充的长度;如果最终扩充的长度大于所允许最大值(MAX_ARRAY_SIZE),就让最终扩充的长度为Integer.MAX_VALUE;最终使用Arrays.copyOf扩充数组。

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);
    }
流程图

在这里插入图片描述

remove函数

remove(int index)(重要)

1.检查index是否越界
2.修改次数自增
3.获取要删去的元素(只是为了返回用
4.计算要前移的元素个数
5.通过System.arraycopy让index后的元素向前移动1位
6.将当前数组所使用的最后一个元素赋值为null,以便于用垃圾回收器回收。

注意这里先size自减,而不是先调用size再自减,比如当前size=4,说明数组已使用了0,1,2,3,那么应该将elmentdata[3]赋值为null,因此size先自减。

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

注:调用这个方法不会缩减数组的长度,只是将已使用的最后一个数组元素置空而已

remove(Object o)

其实这个方法与上面的方法没有太大差别,最大差别就是一个是通过位置删除,另一个是通过元素删除,同时一个返回删除的元素,另一个返回的是是否删除完成。第二个差别是这个remove在进行删除操作之前需要遍历数组,判断是否存在该元素

循环遍历所有对象,得到对象所在索引位置,然后调用fastRemove方法,执行remove操作

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

定位到需要remove的元素索引,先将index后面的元素往前面移动一位(调用System.arraycooy实现),然后将最后一个元素置空。

 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
    }

总结(重要)

1.System.arraycopy方法
先创建一个新数组,再将旧数组中的元素拷贝到新数组

2.两个add的区别
add(int index, E element),需要将index之后的元素都向后移一位,而add(E e)不需要移动元素,直接把元素放入最后。但两个add方法都需要先判断当前数组容量是否可以添加进这个元素(ensureCapacityInternal(int mincapacity))。如果需要扩容,就会调用System.arraycopy函数。使时间复杂度增加,而原本add(E e)为O(1),需要扩容就变成了O(n)。原本add(int index, E element)为O(n),现在为O(2n)。

3.两个remove的区别
remove(Object o)需要遍历数组,因此效率较低。而remove(int index)不需要遍历数组,效率较高,但仍然需要移动数组元素。这里要注意一下,删除元素并没有更改内部数组长度,也没有删除元素,而是将当前数组所使用的最后一个元素设为null,等垃圾回收器回收这个内存空间。

将使用的最后一个元素设为null(size先自减在使用)
 elementData[--size] = null; // clear to let GC do its work

4.get(int index):获取指定位置上的元素时,可以通过索引直接获取(O(1))

5.set(int index, E element):与get同理,都是通过索引直接定位到index元素,直接更改,也是O(1)。

6.modCount的作用
modCount是用来记录数组修改的次数,是想通过该次数来消除一些并发冲突,但ArrayList仍是线程不安全的。

7.ArrayList自己实现了序列化和反序列化的方法,因为它自己实现了 private void writeObject(java.io.ObjectOutputStream s)和 private void readObject(java.io.ObjectInputStream s) 方法

8.对ArrayList进行排序,其内部是直接对存储数据的数组进行排序,Arrays.sort((E[]) elementData, 0, size, c)通过Arrays.sort()方法对数组进行排序,c为指定的比较器Comparator<? super E> c

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值