【源码分析】——Java集合之ArrayList

准备写一个系列分析Java集合的源码,总体来说ArrayList源码除了个别方法其他都比较简单,本篇分析ArrayList的源码先练练手~

一、概述和继承关系

    ArrayList是基于动态数组实现的,也就是说ArrayList中的对象被存储在一个连续的数组中。ArrayList中的元素可以被任意访问,长度可以动态变化。ArrayList和Vector的用法类似,区别是:ArrayList是线程不安全的,当多个线程访问同一个ArrayList集合时,程序需要手动保证该集合的同步性,而Vector则是线程安全的。

    下面看继承关系

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

    ArrayList继承了AbstractList抽象类,同时实现了多个接口,下面一一分析:

    1)ArrayList没有直接实现List而是继承了AbstractList,由AbstractList来实现List接口。这是因为在List接口中所有的方法都是抽象方法,都需要实现类去一一实现,而在AbstractList中有一部分方法是抽象方法而另一部分公用的方法已经提供了实现,这样一来ArrayList在继承AbstractList的时候就可以省去很多代码量去实现公用方法。然后再去实现一些自己特有的方法。一般来说一个类继承自一个抽象类而抽象类又实现一个接口都是出于这样的考虑。

    而对于ArrayList在已经继承了AbstractList的情况下又实现了List接口的原因,则说法不一。有的人说是为了让阅读方便,但是据说ArrayList的作者自己说过这其实是一个mistake,他本以为预先这样写以后可能会用到,但后来没有用到也没有什么别的影响就沿用至今。

    2)下面实现的接口:

    RandomAccess接口。用来说明这个类可以被任意访问,实现了此接口的类的对象在遍历的时候使用for循环会比使用Iterator效率更高(例如LinkedList没有实现RandomAccess接口,使用Iterator实现遍历效率更高)。

    Cloneable接口。实现了这个接口就可以调用clone方法了。

    Serializable接口。表示类可以被序列化。

 

二、构造方法

    ArrayList有如下三个构造方法

《【源码分析】——Java集合之ArrayList》

    分别表示创建一个指定大小的ArrayList;创建一个空的ArrayList;创建一个ArrayList并且让其中的元素和参数中的Collection内的元素一一对应(相当于复制Collection到ArrayList中)。

    在分析构造方法之前先看一下ArrayList中的公共属性:

    //如字段名所示
    private static final long serialVersionUID = 8683452581122892189L;

    //默认capacity值,capacity用来管理数组大小
    private static final int DEFAULT_CAPACITY = 10;

    //空对象数组
    private static final Object[] EMPTY_ELEMENTDATA = {};

    //缺省空对象数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    //元素数组
    transient Object[] elementData; // non-private to simplify nested class access

    private int size;
    
    //size的最大值
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

ps:其中,为什么已经有一个EMPTY_ELEMENTDATA的情况下要再加一个DEFAULTCAPACITY_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.
*/

    意思就是说这个常量数组是用来和EMPTY_ELEMENTDATA区分,使得当在一个空数组中加入第一个元素时让我们知道应该给元素数组扩充多少的空间。

    另一处注释也需要额外注意一下

/**
*(…)
* Any empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/

    任何一个空的ArrayList数组并且其中的elementData是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这时如果给这个空数组加入一个元素,那么我们会将这个ArrayList的大小扩充到DEFAULT_CAPACITY也就是10。这样一解释,上面的问题也就更容易明白了。

/**
 * The maximum size of array to allocate.
 * Some VMs reserve some header words in an array.
 * Attempts to allocate larger arrays may result in
 * OutOfMemoryError: Requested array size exceeds VM limit
 */

    另外对于 MAX_ARRAY_SIZE 为什么等于 Integer.MAX_VALUE – 8 官方文档中这样解释道:有些虚拟机预留了一些位置用于存放数组的头信息(header words),如果size超过了这个数值,会出现OOM:请求的数组大小超出虚拟机限制。

    下面转入正题我们一一来看这三个构造方法:

1)无参数构造方法

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

    最简单的构造方法,直接将缺省的空元素数组赋值给elementData。

2)接收一个整形参数的构造方法

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

    首先判断initialCapacity的值,如果大于0那么new一个大小为initialCapacity的Object数组给elementData;如果等于0那么将EMPTY_ELEMENTDATA空元素数组赋给elementData。(这里又一次解释了两个空元素数组之间的区别)其实可以这样理解,如果我们使用无参构造方法创建了集合,一定程度上表明我们并不清楚最终会用到多大的空间,所以程序会将DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋给elementData,以便在首次插入元素的时候,直接将数组扩充到DEFAULT_CAPACITY的大小,也可以避免后续操作再反复进行扩充。

3)接收一个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;
        }
    }

    这个构造方法看参数应该能想到它的作用,就是将一个已经存在的Collection对象复制到一个新的ArrayList中去。首先让c通过toArray()方法转为数组后赋给elementData,然后进行判断如果elementData的大小为0则将上面的EMPTY_ELEMENTDATA赋给elementData;否则根据方法中的注释显示:

 // c.toArray might (incorrectly) not return Object[] (see 6260652)

    c.toArray()方法有可能不会返回一个Object[](其中的6260652应该是内部的bug编号,6开头说明是从jdk6中出现的)。因为可能会转化失败,所以就要做一次修正处理,也就有了下面的Arrays.copyOf()。

    copyOf()方法如下:

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

    可以看到方法在最后调用了System.arraycopy这个本地方法,用来复制数组。也就是说copyOf()方法会按照输入的参数要求返回一个数组,后面还会在遇到此方法的不同参数的重载方法,这里只要知道调用它是用来对数组进行修正,将转化后的数组修正为Object数组即可。

 

三、主要方法

1、插入add()方法

    add方法有以下四个,下面依次分析

public boolean add(E e)
public void add(int index, E element)
public boolean addAll(Collection<? extends E> c)
public boolean addAll(int index, Collection<? extends E> c) 

  1)boolean add(E e)

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

    此方法将一个元素e插入到ArrayList集合中,并且默认插入到末尾。可以看到方法先执行了ensureCapacityInternal(size+1),然后将e放入到了elementData中。下面我们来看ensureCapacityInternal方法都做了什么

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

    可以看到,程序会首先判断elementData,如果是DEFAULTCAPACITY_EMPTY_ELEMENTDATA(说明集合通过无参构造方法创建),直接比较minCapacity和DEFAULT_CAPACITY的大小,然后将较大的赋给minCapacity。

    然后方法内调用了ensureExplicitCapacity(),参数为minCapacity,我们看一下ensureExplicitCapacity()都做了什么

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    这个方法首先使modCount自增,这个变量是AbstractList中的变量,用来记录List被修改的次数(ModifiedCount)。然后比较minCapacity和elementData.length的大小,如果minCapacity大就执行grow方法

    grow方法是扩充数组的关键方法,如下:

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1); //新的newCapacity大小为旧值的1.5倍(oldCapacity+1/2的oldCapacity)
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity; //扩充1.5倍还不够的话,直接设为minCapacity
        if (newCapacity - MAX_ARRAY_SIZE > 0) //newCapacity大于最大值限制时
            newCapacity = hugeCapacity(minCapacity); //调用hugeCapacity赋最大值
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity); //返回一个newCapacity大小的并且copy了elementData原内容的数组,当作更新后的数组赋给elementData
    }

    最后简单看一下hugeCapacity()方法

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

    这个方法很简单就是当minCapacity超过了MAX_ARRAY_SIZE的时候那就把Integer.MAX_VALUE赋值给它,其实也并没有加大多少,如果再超过就会溢出。上面调用了这么多层,其实只做了一件事就是给ArrayList扩容,以便元素能够添加进去。而分析清楚了这个过程,对于后面其他方法的理解也会有帮助。

2)void add(int index, E element)

    public void add(int index, E element) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

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

    分析过上面的add(E e)方法在看这个就简单多啦~这个方法是将元素插入到指定位置。首先判断是否越界,然后老样子先将集合扩容,然后不同于上面方法的是,这里不是插入到末尾而是插入到index的位置。这时需要先将index位置之后的所有元素后移一位,然后再插入新元素。于是调用System.arraycopy方法,将elementData中从index位置起的长度为size – index的数组元素复制到elementData中的index + 1的位置。

    之后在elementData[index]处插入新元素即可。

3)boolean addAll(Collection<? extends E> c)

    此方法用于将Collection中的所有元素添加到ArrayList中

public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray(); //c转化为数组
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount //对集合扩容
        System.arraycopy(a, 0, elementData, size, numNew); //将a的从0开始的长度为numNew的元素copy到elementData中从size开始的位置
        size += numNew; //size记录增加
        return numNew != 0;
    }

4)boolean addAll(int index, Collection<? extends E> c)

    此方法用于将Collection中的所有元素添加到ArrayList中的指定位置

    public boolean addAll(int index, Collection<? extends E> c) {
        if (index > size || index < 0) //判断是否越界
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew); //扩容集合

        int numMoved = size - index; //要移动的元素数
        if (numMoved > 0) //numMoved>0即插入的位置不在末尾
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved); //先将numMoved个元素进行后移

        System.arraycopy(a, 0, elementData, index, numNew); //插入元素
        size += numNew;
        return numNew != 0;
    }

    可以看到,其实4个add方法的实现过程都很简单,主要进行了两步:1、将集合进行扩容;2、插入元素。而对集合扩容主要用到前面分析的一连串方法,最后使用grow来实现;插入元素则都围绕着System.arraycopy()这个方法来实现移动以及插入的操作。

    下面盗两张别人博客上看到的图,来解释扩容的过程。文章结尾会注明出处:

    调用无参构造方法创建集合,当加入第一个元素时,集合自动扩容至10。

  List<Integer> lists = new ArrayList<Integer>();
  lists.add(8);

 《【源码分析】——Java集合之ArrayList》             

    调用ArrayList(int)创建集合,当插入第一个元素时,程序发现集合容量足够不需要扩容,于是直接插入。

  List<Integer> lists = new ArrayList<Integer>(6);
  lists.add(8);

 《【源码分析】——Java集合之ArrayList》

 

2、删除remove()方法

1)E remove(int index)

    删除指定位置的元素,需要将删除位置之后的所有元素向前移动。

    public E remove(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        modCount++; //修改数记录
        E oldValue = (E) elementData[index]; //记录index位置原来的数据

        int numMoved = size - index - 1; //计算需要向前移动的元素数
        if (numMoved > 0) //numMoved>0表示删除的不是尾部的元素
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved); //
        elementData[--size] = null; //集合尾部的重复元素设为null,使GC回收多余对象

        return oldValue;
    }

2)boolean remove(Object o)

    删除ArrayList中和Object o相等的元素,这里的相等指的是用equals方法得到的结果为true。

    public boolean remove(Object o) {
        if (o == null) { //如果o为空
            for (int index = 0; index < size; index++) //遍历集合
                if (elementData[index] == null) { //找到elementData中所存的null元素
                    fastRemove(index);  //删除index位置的元素
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) { //找到elementData中与o相等的元素
                    fastRemove(index); //删除该位置的元素
                    return true;
                }
        }
        return false; //若没有匹配到则返回false
    }

    其中分为当o是否为null这两种情况,也说明了ArrayList中是可以存储null的。我们还看到,这里多次调用了fastRemove()方法,其实fastRemove方法和remove(int index)基本一样,只是免去了一些判断和计算。

    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; // 集合尾部的重复元素设为null,使GC回收多余对象
    }

3)void removeRange(int fromIndex, int toIndex)

    protected void removeRange(int fromIndex, int toIndex) {
        if (toIndex < fromIndex) {
            throw new IndexOutOfBoundsException("toIndex < fromIndex");
        }
        modCount++;
        int numMoved = size - toIndex; //移动元素的数量
        System.arraycopy(elementData, toIndex, elementData, fromIndex,
                         numMoved);  //移动元素
        // clear to let GC do its work
        int newSize = size - (toIndex-fromIndex); 
        for (int i = newSize; i < size; i++) { //批量删除多余的重复元素,置null等待GC回收多余对象
            elementData[i] = null;
        }
        size = newSize;
    }

4)boolean removeAll(Collection<?> c) 和 boolean retainAll(Collection<?> c)

    public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c); //若为空抛出空指针异常
        return batchRemove(c, false);
    }
    public boolean retainAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, true);
    }

    这两个方法都用到了batchRemove方法,下面分析一下这个方法

    private boolean batchRemove(Collection<?> c, boolean complement) { //complement参数用来指示操作,为true则,为false则批量删除
        final Object[] elementData = this.elementData; //用ArrayList与c做比较
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
                if (c.contains(elementData[r]) == complement) //若c中存在/不存在elementData[r]元素
                    elementData[w++] = elementData[r]; //记录两集合公有/ArrayList中独有的元素
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            // 这里要注意有可能在调用c.contains()的时候出现异常,则有可能r != size
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r); // 把r位置之后的所有元素接到w位置之后
                w += size - r; // w位置为当前的最后一个元素的位置+1,之前执行过w++所以多1(除去后面部分的重复元素)
            }
            if (w != size) { // 需要删除多余元素的情况(若两个集合完全相同/完全不相同则w=size)
                for (int i = w; i < size; i++)
                    elementData[i] = null; //批量将重复的元素置null,使GC回收多余对象
                modCount += size - w;
                size = w; // size设为w,因为w是最后一个元素位置+1所以可以作为size
                modified = true;
            }
        }
        return modified;
    }

    经过上面的分析我们可以发现,removeAll和retainAll两个方法的作用正好是相反的,重命名也可以看出来。retainAll是留下全部的,也就是说从ArrayList集合中留下和Collection共有的那部分,结果就是留下了两个集合的交集;removeAll是删除全部,也就是从ArrayList集合中删除Collection中共有的那部分,换句话说就是留下ArrayList所独有的那部分。

5)boolean removeIf(Predicate<? super E> filter)

    这个删除方法是删除满足一定条件的元素(也就是参数中的filter过滤出来的元素)。官方注释中提到任何由于Predicate而抛出的异常都会导致这个集合不发生改变。

    public boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        int removeCount = 0; //记录删除元素数
        final BitSet removeSet = new BitSet(size); //使用BitSet记录要删除的元素的位置
        final int expectedModCount = modCount; //期望改变数,用来进行同步控制
        final int size = this.size;
        for (int i=0; modCount == expectedModCount && i < size; i++) { //遍历集合,将符合删除条件的元素记录在removeSet中
            @SuppressWarnings("unchecked")
            final E element = (E) elementData[i]; 
            if (filter.test(element)) {
                removeSet.set(i); //记录位置,设为true
                removeCount++;
            }
        }
        if (modCount != expectedModCount) { //同步控制
            throw new ConcurrentModificationException();
        }

        final boolean anyToRemove = removeCount > 0; //设置返回值,是否删除元素
        if (anyToRemove) {
            final int newSize = size - removeCount; //最终的集合大小
            for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
                i = removeSet.nextClearBit(i);  //返回下一个false位的索引,false位代表此位置的元素没有被标记删除
                elementData[j] = elementData[i]; //更新集合
            }
            for (int k=newSize; k < size; k++) {
                elementData[k] = null;  //删除多余的元素,使GC回收多余对象
            }
            this.size = newSize;
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
            modCount++;
        }

        return anyToRemove;
    }

    这里出现两个概念Predicate接口和BitSet类。其中实现Predicate接口的对象可以完成一些条件过滤的操作,这里用来判断集合中的元素那些需要删除;BitSet用一个64位的long型数据记录每一位的信息,初始时64位上都存储false,当执行set(i)的时候,第i位上的值更改为true。

3、其他方法

1)void replaceAll(UnaryOperator<E> operator)

    替换集合中的所有元素,替换为参数operator。其中operator实现了UnaryOperator接口,这个接口是一个函数式接口,这里不做过多讨论,简单来说调用其中的operator方法可以得到一个类中定义好的对象。

    public void replaceAll(UnaryOperator<E> operator) {
        Objects.requireNonNull(operator);
        final int expectedModCount = modCount;
        final int size = this.size;
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            elementData[i] = operator.apply((E) elementData[i]); //遍历集合,每次赋值调用operator的apply方法,将返回的值赋到当前位置
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }

2)void sort(Comparator<? super E> c) 

    将集合元素进行排序,其中参数是一个实现Comparator接口的对象,这个接口可以自定义一些比较大小的方法、判断相等的方法等等,然后sort()方法会根据参数中定义的规则对集合进行排序。

    public void sort(Comparator<? super E> c) {
        final int expectedModCount = modCount;
        Arrays.sort((E[]) elementData, 0, size, c);
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }

3)另外还有一些get()、set()、indexOf()、clear()等等的方法源码较为简单,功能也可以顾名思义,在此不一一分析。

 

四、ArrayList总结

  1. ArrayList可以存放null元素,本质上是一个elementData数组。
  2. ArrayList区别于数组的地方在于能够自动扩展大小,在首次添加元素的时候会根据情况不同选择是否扩容至10;每次自动扩容都会扩容至原来的1.5倍。这些都是只有阅读了源码才会了解到的。
  3. 在向集合中插入元素或者删除元素时,发生移位的元素使用的是复制数组的方法,并且在移位后要及时将空余的位置设为null,便于GC去回收多余的对象。
  4. ArrayList的本质是数组,所以它在数据的查询方面会很快,而在插入删除这些方面,性能下降很多,有移动很多数据才能达到应有的效果。
  5. ArrayList实现了RandomAccess,所以在遍历它的时候推荐使用for循环。
  6. Java8中定义了很多函数式接口,这也在ArrayList中有许多体现,包括替换集合元素、遍历集合元素、排序等操作都用到了函数式接口,所以有空要补充一下Java函数式编程的知识。

 

本文部分内容参考以下文章,在此感谢

https://www.cnblogs.com/zhangyinhua/p/7687377.html

https://www.jianshu.com/p/7ed93f72fac1

https://www.jianshu.com/p/002ad4f7f95c

    原文作者:java集合源码分析
    原文地址: https://blog.csdn.net/u012198209/article/details/82383964
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞