ArrayList底层原理及源码分析

ArrayList是java容器中很重要很基础的一部分,在面试中,容器相关的底层问题简直不要太多,那么对于其底层的东西,还是需要结合源码(此处版本:jdk8)进行分析。

一 :数据结构

ArrayList底层数据结构核心其实是一个Object数组,对于ArrayList的操作都是基于这个Object进行操作而实现的。

二:内存模型/内存分配

这里我贴上一张我自己画的图,因为我在网上看到一些关于ArrayList的内存模型的说法其实是有问题的,比如说有人认为无参初始化是构造一个默认容量为10的数组(错的);还有就是认为ArrayList的地址其实就是底层的Object数组的地址(错的,这个是我看到的网上一张很常见的图的说法,里面讲到你new出来的ArrayList的地址就是Object[0]的地址),这里可能你看不懂,后面我会解释清楚。
ArrayList的内存原型

三 : 继承关系

从源码上看,它继承了AbstractList抽象父类,实现了List(规定了List的操作规范)、RandomAccess(可随机访问)、Cloneable(可拷贝)、Serializable(可序列化)几个接口;这可以从源码中看的到:

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

这里主要分析两个地方:RandomAccess和Serializable

RandomAccess:(可随机访问)

查看RandomAccess源码,会发现其并没有任何内容,是一个标记接口;它只是用来标记说实现这个接口的List集合就具备快速随机访问的能力,也就是能快速地访问List集合中的随机任何一个元素。

同时,如果你认真查看RandomAccess源码,你会看到注释中还说到一个东西,就是:如果某个List实现了这个接口,那么使用for循环方式来获取数据的速度会快于使用迭代器的方式。

总结一下就是:
当一个List实现了RandomAccess接口,就意味着拥有快速访问功能,其遍历方法采用for循环最快速。而没有快速访问功能的List,遍历的时候采用Iterator迭代器最快速。

这里多说一个,LinkedList是基于链表实现的,其不具备快速随机访问能力,其遍历需要遍历实现,时间复杂度为O(n);

附上一个小测试类吧:

 @Test
    public void RandomAccessTest(){
        List<Integer> arrayList = new ArrayList<Integer>();
        for (int i = 1; i <= 10000000; i++) {
            arrayList.add(i);
        }

        long startTime = System.currentTimeMillis();

        // for循环
        System.out.println("此处使用for循环");
        for (int i = 0;i< arrayList.size();i++) {
            Object num = arrayList.get(i);
                }
        long endTime = System.currentTimeMillis();
        System.out.println(endTime-startTime);

        // 迭代器
        System.out.println("采用迭代器遍历");
        startTime = System.currentTimeMillis();

        Iterator it = arrayList.iterator();
        while(it.hasNext()){
            Object num = (it.next());
                }
        endTime = System.currentTimeMillis();
        System.out.println(endTime-startTime);
        }
        
    输出:
    此处使用for循环
    2
    采用迭代器遍历
    4

Serializable:

这一部分需要放到最后再讲,请先跳过

四:ArrayList的核心成员变量

直接附上源码:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    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;
    
    /**
     * 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
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

DEFAULT_CAPACITY:这个是一个默认初始容量,为10

EMPTY_ELEMENTDATA:一个共享空数组,具体作用后面讲,注意它是static final

DEFAULTCAPACITY_EMPTY_ELEMENTDATA:也是一个共享空数组,也是static final,但是它和EMPTY_ELEMENTDATA的区别在于DEFAULTCAPACITY_EMPTY_ELEMENTDATA在第一次添加元素(add操作)时,知道要扩容多少(扩容,意思是扩大容量,具体后面讲)

elementData:ArrayList的数组缓冲区,也就是数据结构那里讲到的ArrayList底层的那个Object数组;ArrayList的容量就是elementData的长度;这里的注释还讲到,当添加第一个元素时,任何带有elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空ArrayList都将扩展为DEFAULT_CAPACITY,也就是容量扩容为10。

MAX_ARRAY_SIZE:elementData的最大值。这个的话很多人会问ArrayList的最大容量是多少,怎么试,这个就是答案,但是实际上Integer的最大容量为 0x7fffffff,接近2个g,你本地的话是到这个程度早崩了,而服务器的话虽然可以,但是没必要做这样的骚操作吧?

可能会有人对于EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA这两个数组的存在有疑问,为什么要这样规范呢?别急,后面有答案;

size:这个是ArrayList真正包含的元素的个数,它和容量是两个概念;

五:构造函数

无参构造:

源码如下:

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

由上面我们已经知道了,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空数组,虽然这里的注释写的是构造一个初始容量为10的空列表,但是,实际上并不是在进行无参构造的时候就去创建一个容量为10的空数组了,实际上这个时候它还是空数组,只是之后进行第一次添加元素的时候,会和上面的关于DEFAULTCAPACITY_EMPTY_ELEMENTDATA的注释的内容一样,到那个时候才变成一个容量为10 的列表,这也是我为什么上面在内存模型那里说到的,认为无参构造时就构造了默认容量为10的空数组这样的说法是错的的原因。这里附上我写的验证验证ArrayList无参构造时数组的容量的链接,这里会详细讲:验证ArrayList无参构造时数组的容量

这里可以加以思考的另一个东西,就是我在内存模型中讲到的另一个东西:new出来的ArrayList的地址就是elementData的地址,思考一下,如果是这样的话,由于上面验证了无参构造新new出来的arrayList里的elementData都是指向同一个位置,如果真如他们所讲的那样,那么无参构造新new出来的ArrayList应该都是指向同一个位置,这样就可以很清楚知道这种思路是错的了吧。实际上,ArrayList的内存模型就像一个二维数组,并不复杂,可以参考上面图片进行理解。

给定初始容量构造:

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

源码具体逻辑如下:
当 传入的初始容量initialCapacity > 0为真时,创建一个大小为initialCapacity的数组,并将引用赋给elementData;

当 传入的初始容量initialCapacity = 0为真时,将空数组EMPTY_ELEMENTDATA赋给elementData;

当 传入的初始容量initialCapacity < 0为真时,直接抛出IllegalArgumentException异常。

集合类中构造:

 /**
     * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
     */
    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;
        }
    }

这里源码逻辑如下:

将传入集合转化为数组,并赋值给elementData;判断参数集合是否为空,如果为空,则将空数组EMPTY_ELEMENTDATA赋给elementData;

如果传入参数集合不为空,则判断传入参数转换后的数组是否为Object数组,如果不是,则利用浅拷贝将其转化为Object类型的数组。

这里可能会有人有疑问,为什么一定要转化为Object数组,还有那句 // c.toArray might (incorrectly) not return Object[] (see

6260652)注释是什么意思,这里不加以说明,具体原因看这篇:关于 c.toArray might (incorrectly) not return Object[]以及为什么一定要返回Object数组的原因

这里总结几个很重要的点:

Collection接口的toArray方法依赖于实现类中的具体实现,也就是说集合转ArrayList之后元素的顺序是由具体集合的toArray方法所决定的;

toArray可能会不正确地返回数组对象Object[].class

看完构造方法,可能对于之前在第四点哪里提到的两个空数组的区别你已经有了答案了,这里也附上我写的关于我对两个空数组的一点理解吧(个人觉得对比jdk7会有更好的理解)。点击:EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA两个空数组的区别

六:add操作以及ArrayList的扩容机

直接贴上一张我做的图片:

在这里插入图片描述

从上面可以看出,扩容操作需要调用Arrays.copyOf()把原始副本整个复制到新副本中,,然后丢弃旧数组,这个操作代价很高,因此最好在创建ArrayList对象时就指定大概的容量大小,减少扩容操作的次数,这样会减少数组创建和Copy的操作,还会减少内存使用。

七:indexOf(Object o)方法

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

从源码上看,这个方法的作用是从数组头开始寻找和传入的元素相等的元素,如果找到了,则返回其在数组中的位置(下标);若没找到,则返回-1;这里要注意的一点是,需要把情况分为是null和不是null;因为如果是null情况下还使用equals()方法会出现空指针异常。

八:get(int index)方法


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

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

get方法也是我们常用到的一个方法,作用是返回指定下标位置的元素的值;从源码上看,它会先调用rangeCheck方法进行范围检查,只要你指定的下标不大于size的值,则返回elementData(index);在这里,也会发生向下转型

九:set(int index,E element)方法

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

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

set方法是我们修改指定下标位置的元素的值的方法,从源码上看也会先进行范围的检查;然后把我们给定的值放到指定的位置,最后把旧的值返回;

十:remove(int index)方法

第一种:

 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(int index)方法的作用是删除指定下标的元素,从效果上来看,是将指定下标后面一位到数组末尾的全部元素向前移动一个单位,所以,很多人会把这个现象当成是是过程,说删除指定位置的元素就是把指定位置的元素后面的全部元素向前移动一个单位,实际上不是的;看源码我们就知道;其实是调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,然后把数组最后一个元素设置为null;该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的。

第二种:

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
    }

仔细一看,是不是很熟悉;其实它的实现就是遍历数组,如果发现存在传入对象;那么就调用fastRemove(int index)方法;而fastRemove基本和remove(int index)方法没差别,只是少了一个范围判断而已,而为什么可以不用范围判断,是因为既然你在数组内找到了这个对象,那么肯定没超过范围,所以直接fastRemove方法来做数组删除;真正的删除元素的地方,和第一种其实是一样的实现过程。

其实,还有一个方法:

 protected void removeRange(int fromIndex, int toIndex) {
        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++) {
            elementData[i] = null;
        }
        size = newSize;
    }

执行过程是将elementData从toIndex位置开始的元素向前移动到fromIndex,然后将toIndex位置之后的元素全部置空顺便修改size。

十一:补充

序列化:

在最上面的地方,我就写到了ArrayList实现了序列化接口,并且说后面再解释,没错,到这里才解释;其实也没什么,只有一两个地方要注意下理解下的而已;比如在elementData定义的地方,可以看到使用了关键字 transient 修饰该关键字声明数组默认不会被序列化;为什么这样做呢?原因在于ArrayList 是基于数组实现,并且具有动态扩容特性,保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。

其实;ArrayList 内部还实现了 writeObject() 和 readObject() 方法,来控制只序列化数组中有元素填充那部分内容。

 private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();

        // Write out size as capacity for behavioural compatibility with clone()
        s.writeInt(size);

        // Write out all elements in the proper order.
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }

        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
 private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;

        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in capacity
        s.readInt(); // ignored

        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            int capacity = calculateCapacity(elementData, size);
            SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
            ensureCapacityInternal(size);

            Object[] a = elementData;
            // Read in all elements in the proper order.
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }

这里有个东西:

writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。反序列化也是类似的道理。

这里有一个点:在writeObject() 最后面有这样的代码:

if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }

这个modCount 看到这里应该比较眼熟了,上面很多方法内部都存在着对modCount进行操作;实际上,modCount是用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。

而在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。

关于浅克隆

可以看到的一点是,ArrayList的实现中大量地调用了Arrays.copyof()和System.arraycopy()方法。查源码:

   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;
    }
   public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

可以看到的是,其实真正实现的地方是 System.arraycopy()这个方法,而查看源码可以知道,这个方法被标记为native方法;意味着这是在底层调用了C/C++库的一个方法;实际上这个方法最终调用了C语言库中的的memmove()函数,因此它可以保证同一个数组内元素的正确复制和移动,比一般的复制方法的实现效率要高很多,很适合用来批量处理数组。Java强烈推荐在复制大量数组元素时用该方法,以取得更高的效率。

十二:总结

ArrayList组存/取效率高:

ArrayList底层以数组实现,它实现了RandomAccess接口,且由于数组是连续存放元素的,找到第一个元素的首地址,再加上每个元素的占据的字节大小就能定位到对应的元素。因此查找也就是get的时候非常快,时间复杂度是O(1),同样的对于取特定位置的set操作也是一样的。

ArrayList的添加元素操作:

当进行add操作的时候,最理想的情况就是不指定位置直接添加元素时(add(E element)),元素会默认会添加在最后,理想状态下不会触发底层数组的复制,也不用考虑底层数组的自动扩容,这种情况下时间复杂度为O(1) ;但是假如是在指定位置添加元素(add(int index, E element)),需要复制底层数组,根据最坏打算,时间复杂度是O(n)。

ArrayList的插入删除元素操作:
ArrayList的插入和删除元素效率不高,每次插入或删除元素,就要大量地移动元素,这两种操作时间复杂度为O(n)。当ArrayList里有大量数据时,这时候去频繁插入/删除元素会触发底层数组频繁拷贝,效率低还会造成内存空间的浪费

结束语:

关于容器类的学习其实很让我兴奋,但是关于它的总结却让我非常头疼,因为我感觉要写的能写的太多了,同时还要整理整体的连接关系;但是,不管过程和结构都是很有意思的,特别是在画上面两张图的时候;学习道路道阻且长,唯有不断坚持,才有资格说无愧于心,内心坦荡。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章