ArrayList源码解析笔记

一、ArrayList概述

(一)简介(引用源码顶部注释)

List接口的大小可变数组的实现。实现了所有可选列表操作,并允许包括null在内的所有元素。除了实现List接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。(此类大致上等同于Vector类,除了此类是不同步的。)

size、isEmpty、get、set、iterator和listIterator操作都以固定时间运行。add操作以分摊的固定时间运行,也就是说,添加n个元素需要O(n)时间。其他所有操作都以线性时间运行(大体上讲)。与用于LinkedList实现的常数因子相比,此实现的常数因子较低。

每个ArrayList实例都有一个容量。该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素,其容量也自动增长。并未指定增长策略的细节,因为这不只是添加元素会带来分摊固定时间开销那样简单。

在添加大量元素前,应用程序可以使用ensureCapacity操作来增加ArrayList实例的容量。这可以减少递增式再分配的数量。

注意,此实现不是同步的。如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。(结构上的修改是指任何添加或删除一个或多个元素的操作,或者显式调整底层数组的大小;仅仅设置元素的值不是结构上的修改。)这一般通过对自然封装该列表的对象进行同步操作来完成。如果不存在这样的对象,则应该使用Collections.synchronizedList方法将该列表“包装”起来。这最好在创建时完成,以防止意外对列表进行不同步的访问:
List list = Collections.synchronizedList(new ArrayList(…));

此类的iterator和listIterator方法返回的迭代器是快速失败的:在创建迭代器之后,除非通过迭代器自身的remove或add方法从结构上对列表进行修改,否则在任何时间以任何方式对列表进行修改,迭代器都会抛出ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。

注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的快速失败行为应该仅用于检测bug。

此类是Java Collections Framework的成员。

(二)特点

①ArrayList的底层结构是大小可变数组(动态数组);

②ArrayList可以存储任意类型的数据,并且允许null元素存在;

③ArrayList不是同步的,是线程不安全的,非常适合用于对元素进行查找,效率非常高;

④ArrayList容量可以自动增长;

⑤ArrayList的iterator()和listIterator(int)方法返回的迭代器是fail-fast快速失败机制。

(三)数据结构

ArrayList源码解析笔记_第1张图片

 

二、源码解析

//ArrayList继承结构:
public class ArrayList extends AbstractList
        implements List, RandomAccess, Cloneable, java.io.Serializable


//RandomAccess接口是一个标志接口,List集合实现这个接口能支持快速随机访问,使用for循环效率更高(更详细的解释找度娘)

(一)重要的属性

    /**
     * 调用无参构造函数,返回该空数组
     *它与EMPTY_ELEMENTDATA的区别就是:该数组是默认返回的,而后者是在用户指定容量为0时返回
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 调用有参构造函数,指定该ArrayList容量为0时,返回该空数组
     */
    private static final Object[] EMPTY_ELEMENTDATA= {};

    /**
     * 存储ArrayList元素的数组缓冲区。
     * ArrayList的容量是这个数组缓冲区的长度.(包含null)
     * 任何空ArrayList在使用elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,将在添加第一个元素时扩展为DEFAULT_CAPACITY=10
     */
    transient Object[] elementData;

    /** 
     * 默认数组初始容量
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * ArrayList的元素数量大小(它包含的元素的数量)。 也是进行add添加的时候添加在动态数组的索引位置
     */
    private int size; //默认size=0

    /**
     *从结构上修改此列表的次数
     */
     protected transient int modCount = 0;  //注意这个变量是从父类AbstractList类引用来的

     /**
      *给数组分配容量的最大大小
      */
     private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

(二)构造方法

    /**
     * Constructs an empty list with the specified initial capacity.
     * 构造一个指定初始容量的空list。
     * @param  initialCapacity  list的初始容量
     * @throws IllegalArgumentException 指定的初始容量为负数就报IllegalArgumentException
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) { // 初始化数组,容量为指定容量
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) { // 指定数组容量为0,初始化一个空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else { // 指定容量小于0,报异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    /**
     * 构造一个初始容量为10的空list。
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 构造一个list包含指定参数集合的所有元素
     *
     * @param c 一个集合,将c集合中的元素转入ArrayList
     * @throws NullPointerException if the specified collection is null
     */
    public ArrayList(Collection c) {
        elementData = c.toArray(); // 将指定参数集合e转为数组赋给全局数组elementData 
        if ((size = elementData.length) != 0) { // 将参数集合e内的元素转移到elementData
            // c.toArray可能(不正确地)不返回Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 集合长度为0,用空数组替换
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

(三)主要方法

ArrayList源码解析笔记_第2张图片

1)add(E e)

    /**
     * 将指定的元素e添加到list的末尾
     *
     * @param e 添加的元素
     * @return true
     */
    public boolean add(E e) {
        //list的容量加1,每次添加容量只加1,保证资源不会被浪费
        ensureCapacityInternal(size + 1);  //  检查数组空间,不够就扩容
        elementData[size++] = e; //将元素e插入elementData数组的索引size位置,添加完成后元素个数size自加
        return true; 
    }

     // 数组缓冲区为空就扩容为容量10大小的数组
     private void ensureCapacityInternal(int minCapacity) { // 注意minCapacity=size+1,这是个面试点
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

    //elementData不为空且最小数组容量大于等于数组缓冲区当前容量,就扩容
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++; // 对数组修改次数加1

        // 保证扩容最小容量大于数组缓冲区当前容量
        if (minCapacity - elementData.length > 0)
            grow(minCapacity); // 扩容方法
    }

  扩容机制:

    /**
     * 要分配的数组的最大大小。
     * 为什么是Integer.MAX_VALUE - 8 ?
     * 有些虚拟机在一个数组中保留了一些头
     * 尝试分配最大的容量Integer.MAX_VALUE,会超过虚拟机的限制,就会出现OutOfMemoryError
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    
    /**
     * 增加容量,以确保它至少可以容纳minCapacity个元素
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // 取到当前数组缓冲区的容量
        int oldCapacity = elementData.length;
        // 扩容:新容量=旧容量+旧容量/2
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0) // 新容量小于minCapacity ,就扩容为minCapacity 
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)//新容量大于MAX_ARRAY_SIZE ,就扩容为MAX_ARRAY_SIZE 
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);//elementData容量调整为新容量大小
    }

   /**
    *大容量扩容    
    */
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow  minCapacity不能小于0
            throw new OutOfMemoryError();
        // 需要的minCapacity大于MAX_ARRAY_SIZE,返回Integer.MAX_VALUE,否则返回MAX_ARRAY_SIZE 
        return (minCapacity > MAX_ARRAY_SIZE) ? 
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE ;
    }

add方法流程简述:

1)检查elementData容量是否足够,不够进行扩容

      扩容流程简述:

          1)确定最小需求容量:elementData为空,最小需求容量为10;否则最小需求容量为size+1

          2)检查是否需要扩容,最小需求容量minCapacity大于当前数组缓冲区的容量,则需要扩容,调用grow(int minCapacity)扩容

          3)第一次扩容,新容量=旧容量+旧容量/2

          4)第二次扩容,如果第一次扩容后,新容量小于最小需求容量minCapacity,就扩容新容量为minCapacity

          5)第三次扩容,如果第二次扩容后,新容量大于允许最大容量MAX_ARRAY_SIZE,就再判断最小需求容量minCapacity是否大于MAX_ARRAY_SIZE,大于则新容量=Integer.MAX_VALUE,否则新容量=MAX_ARRAY_SIZE

          6)扩容完成,elementData容量扩为新容量大小

2)将元素e插入到elementData的索引size位置

 2)add(int index, E element)

    /**
     * 将指定元素插入其中的指定位置
     * 当前位置的元素和后面的元素,位置后移一位
     * any subsequent elements to the right (adds one to their indices).
     *
     * @param index 要插入指定元素位置的索引
     * @param element 要插入的指定元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
        rangeCheckForAdd(index); // 检查索引是否数组越界
        //扩容,检查容量是否足够,不够只加1,保证资源不被浪费
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //复制数组,将当前index位置的元素和后面的元素后移
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element; // 将元素插入到index位置
        size++; //元素个数+1
    } 

    /**
     * 数组越界检查
     */
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    
    /**
     *数组复制  java.lang.System.class
     * @param      src      源数组.
     * @param      srcPos   源数组复制的起始索引
     * @param      dest     目标数组
     * @param      destPos  目标数组粘贴的起始索引
     * @param      length   复制数组的长度
     */
     public static native void arraycopy(Object src,int srcPos,Object dest, int destPos,int length);

 流程简述:

               1)index索引数组越界检查

               2)检查elementData容量是否足够,不够扩容(扩容流程上面已简述)

               3)数组复制,将index位置的元素和index位置后面的元素后移一位

               4)将指定元素插入到数组index位置

3)get(int index)

    /**
     * 返回此列表中指定位置的元素
     *
     * @param  index 要返回元素的索引
     * @return 数组中指定位置的元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        rangeCheck(index); // index索引越界检查
        //返回在elementData中索引为index的元素
        return elementData(index); 
    }

    
    // Positional Access Operations   位置访问操作
    //返回指定索引的元素
    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }

4)remove(int index)

    /**
     * 删除列表中指定位置的元素。
     * 删除后,将index位置后面的元素前移一位
     *
     * @param index 需要删除的指定index索引
     * @return 返回被删除index索引位置的元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
        rangeCheck(index);  //index索引数组越界检查

        modCount++;  // 对数组操作修改次数+1
        E oldValue = elementData(index); // 取到index索引位置的元素oldValue

        int numMoved = size - index - 1; // index索引位置后一个元素的索引
        if (numMoved > 0)
            // 数组复制,将numMoved位置及后面的元素位置前移一位,index位置的元素会被覆盖
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //注意数组复制的时候,elementData[--size]还是有元素的,在这里给它赋null,让GC起作用
        elementData[--size] = null; // 清除,让GC做它的工作

        return oldValue;
    }

流程简述:

              1)检查index索引是否数组越界

              2)取到数组中index位置元素的值,作为方法的返回值

              3)找到index位置后一个元素的索引numMoved,然后将numMoved及后面的元素前移一位,index位置的元素会被覆盖

              4)在数组中size-1位置的元素赋为null,为了让GC起作用

5)set(int index, E element)

    /**
     * 将此列表中指定位置的元素替换为指定元素
     *
     * @param index 指定索引位置
     * @param element 指定需要替换的元素
     * @return 返回被替换的元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E set(int index, E element) {
        rangeCheck(index); // 索引数组越界检查

        E oldValue = elementData(index); // 取到index索引的元素
        elementData[index] = element; //用指定元素替换掉index位置的元素
        return oldValue;
    }

流程简述:

              1)检查index索引是否数组越界

              2)取index位置的元素,作为方法的返回值

              3)用指定元素替换index位置的元素

三、考虑两个问题

1)elementData被关键字transient修饰,那么它的序列化和反序列化是如何实现的?

答:在ArrayList内提供了两个方法对它进行序列化和反序列化,分别是writeObject(java.io.ObjectOutputStream s)实现序列化和readObject(java.io.ObjectInputStream s)实现反序列化。

2)为什么不直接要用elementData来序列化,而要使用transient来修饰。

答:因为elementData是一个缓存数组,它预留了容量,等容量不足时再扩容,这些预留的容量的空间都为null,如果直接序列化也会将这些为null的元素也被序列化,这样会浪费空间和时间。

四、ArrayList与LinkedList的区别

1)ArrayList底层结构是基于动态数组,LinkedList底层数据结构是基于链表

2)ArrayList在查询、修改上快,但是增删慢,因为要移动数据;LinkedList在增删上快,但是查询、修改慢,因为要移动指针 (ArrayList如果增删频繁出现在尾部,其他地方没有出现,可能会增删不比LinkedList慢,甚至快,数据集越大越明显)

3)ArrayList遍历使用for循环快,使用Iterator迭代器慢;LinkedList遍历使用Iterator迭代器快,使用for循环慢(ArrayList实现了RandomAccess接口,实现了这个“快速随机访问”接口的类,使用for循环遍历效率更高,可以使用instance来鉴别是不是实现了这个接口)

4)ArrayList多用于查询多,增删少的情景下,一般在随机访问和增删并存的场景下会使用ArrayList;LinkedList多用于增删多,查询少的情景下,一般在需要队列(先进先出)结构的时候会使用LinkedList(LinkedList实现了队列接口)

你可能感兴趣的:(java)