美文网首页
ArrayList源码逐条解析

ArrayList源码逐条解析

作者: KevenPotter | 来源:发表于2020-02-15 20:46 被阅读0次

    一家之言 姑妄言之 絮絮叨叨 不足为训

    ArrayList类注释翻译:

       ArrayList是一个实现了List接口的可改变大小的数组结构。它实现了关于List集合所有的操作,并且允许存有所有的类型元素,包括null类型。
       ArrayList除了实现了List接口之外,还提供了一系列用来操作对存储元素数组大小的方法(ArrayList大致相当于Vector,只不过它是不同步的)。这个类里面的sizeisEmptygetsetiterator,以及listIterator操作可以在常数时间内完成,并且add操作会在分段常数时间内完成。也就是说,添加n个元素需要O(n)的时间。那么粗略地说,所有的其他操作都可以在线性时间内运行。
       相较于LinkedList的实现,ArrayList的常数因子还是较低的。
       在这里,每一个ArrayList实例都有其自身的容量,这里所谓的“容量”则是用于存储于列表中的元素数组的大小。而这个“容量”始终大于等于列表的长度,即list.size()
       当一个元素添加到ArrayList中时,其容量还会自动扩容。那么事实上这个增长策略的细节是没有指定的,但是当添加一个元素的时候其开销是均摊常数时间。在使用ensureCapacity()操作添加大量元素之前,我们的应用程序可以增加ArrayList实例的容量。这样的操作可能会减少增量再分配的次数。
       当然,这个实现不是同步的。如果有多个线程同时访问一个ArrayList实例,并且至少有一个线程从结构上修改了列表,那么这组操作必须在外部做好同步(任何对ArrayList的添加或删除操作,添加一个或多个元素或删除一个或多个元素,或是显式的修改ArrayList中备份数组的大小等这类操作都是一种结构性修改。当然,仅仅设置元素的值并不属于结构修改)。这通常是通过对一些自然封装列表的对象进行同步来实现的。如果不存在这一类对象,那么列表就可以使用Collections.synchronizedList()方法来对List同步。当然这种操作最好是在创建的时候完成,以防止突发性的非同步访问List。例如:

    List list = Collections.synchronizedList(new ArrayList(...));
    

       ArrayList这个类的迭代器和listIterator方法返回的迭代器具有fail-fast的机制:即,当我们在创建迭代器之后,一旦对List进行了结构上的修改(当然除了通过iterator自己的删除或添加方法),iterator将会抛出ConcurrentModificationException异常信息。
       因此,在面对并发修改时,iterator会快捷的地失效,而不是在未来某个不确定的时间冒着突然地、不确定行为的风险进行迟缓的失效。不过我们需要注意的是,fail-fast机制是不能被完全保证的,因为一般来说,我们不能在存在非同步并发修改的情况下做出任何严格的机制保证。
       注意,我们不能保证迭代器的快速故障行为,因为通常来说,在存在非同步并发修改的情况下,谁都不可能做出任何严格的绝对保证。但是fail-fast将会在最大努力的基础上抛出ConcurrentModificationException异常。因此,如果我们想依赖于这个fail-fast机制来编写程序将会是一个错误的选择。所以,我们的iterator的fail-fast机制应该只用于检测bug,而不是依此来进行复杂业务处理。
       最后,ArrayList是Java集合框架的成员。

    ArrayList类信息:

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

       我们可以清楚的看到,ArrayList一个是继承自AbstractList并实现了List接口、RandomAccess接口、Cloneable接口、java.io.Serializable接口的类。

    ArrayList静态变量信息:

    /* 序列化版本编号 */
    private static final long serialVersionUID = 8683452581122892189L;
    /* 默认初始化容器大小  */
    private static final int DEFAULT_CAPACITY = 10;
    /* 用于一系列空数组实例的共享数组实例  */
    private static final Object[] EMPTY_ELEMENTDATA = {};
    /* 
     * 用于默认大小的空实例的共享空数组实例。我们将其与EMPTY_ELEMENTDATA区分开来,
     * 以明确当第一个元素被添加时什么时候可以进行数组的扩容。
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    /**
     * 需要分配的数组的最大容量。
     * 一些虚拟机会在数组中保留一些标题词。
     * 如果我们试图分配较大的数组容量,可能会导致OutOfMemoryError错误:
     * 请求的数组大小超过虚拟机限制。
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    

       我们来看ArrayList的静态变量信息。
       首先,因为ArrayList实现了java.io.Serializable接口,所以会生成一个序列化版本编号,这个属性个人认为无需赘言。自动忽略即可。
       其次,DEFAULT_CAPACITY也明确了我们的ArrayList的默认初始化容器大小为10。也就是说,如果我们使用无参构造器创建ArrayList时,当添加第一个元素之后,我们的ArrayList将会将其数组容量扩展为默认的大小10
       再次,EMPTY_ELEMENTDATA声明了一个空的、类型为Object的数组。
       从次,DEFAULTCAPACITY_EMPTY_ELEMENTDATA也声明了一个空的、类型为Object的数组。不过我们可以从该属性的源码注释中可以看到,其区别于EMPTY_ELEMENTDATA空数组。从其他资料上来看,这样的做法在JDK8中,EMPTY_ELEMENTDATA在一定程度上减少了空数组的存在,降低了内存的消耗,在一定程度上是对性能的优化。
       最后,MAX_ARRAY_SIZE声明了我们的ArrayList所允许的最大容量,也就是ArrayList所能包含的最大元素个数。其最大容量为Integer.MAX_VALUE - 8,也就是2147483647-8 = 2147483639。这里面的减8操作从其注释里可以明确的是,在一些虚拟机里面,因为虚拟机的类型不同,如果声明了最大的Integer数值,可能会造成OutOfMemoryError错误。所以这个属性可以理解为对ArrayList容量的一个合理范围。
       不过需要注意的是,在ArrayList中的hugeCapacity(int minCapacity)方法中也可以清晰的看到,如果最小容量(minCapacity)大于了我们的MAX_ARRAY_SIZE,我们还是会给ArrayList的容量分配最大的Integer数值(Integer.MAX_VALUE-2147483647)。不过我个人相信,一般不会发生这种情况,这种极端的情况完全是可以忽略不计的。

    ArrayList成员变量信息:

    /**
     * 存储ArrayList元素的数组缓冲区。ArrayList的容量就是这个数组缓冲区的长度。
     * 任何elementData为DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空数组,都会在第
     * 一个元素添加后将其容量扩展成为DEFAULT_CAPACITY,也就是10。
     */
    transient Object[] elementData; // 非私有以简化嵌套类访问
    /* ArrayList的大小(也就是ArrayList包含的元素个数)。 */
    private int size;
    

       我们来看ArrayList的成员变量信息。
       首先,elementData依旧是一个Object类型的空数组。只不过这个数组用transient,也就说明了该属性是不参与ArrayList的序列化操作的,这样做的目的可能是为了减少磁盘的占用率,也可能是为了安全。从其他一些资料来看,减少磁盘占用率关键词出现的次数要往往大于安全这个关键词的字数。
       其次,size这个属性就不需要多说了,其代表的就是我们ArrayList内部存储的元素个数。

    ArrayList构造函数信息:

    /** 构造一个初始容量为10的空列表 */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
    /**
     * 构造具有指定初始容量的空列表。
     * @param initialCapacity 构造ArrayList时,初始化的容量
     * @throws IllegalArgumentException 如果指定的初始容量为负数,则抛出IllegalArgumentException异常。
     */
    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);
        }
    }
    
    /**
     * 构造一个包含指定集合的元素的列表,按集合的iterator返回元素的顺序排列。
     * @param c 要将其元素放入此列表的集合
     * @throws NullPointerException 如果指定的集合为空,则抛出NullPointerException异常
     */
    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;
        }
    }
    

       首先,是我们的无参构造器public ArrayList(),这个无参构造器用其注释里面的话来说,它会构造一个初始容量为10的空列表。但是我们从源码方面观察到,当我们调用这个无参构造器构造ArrayList时,也仅仅是将DEFAULTCAPACITY_EMPTY_ELEMENTDATA空数组赋给了elementData属性,并没有注释所说的初始化容量为10的现象。
       其实elementData的源码注释已经明确说明了此种情况:

    Any empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA will be expanded to DEFAULT_CAPACITY when the first element is added.

       也就是说任何elementDataDEFAULTCAPACITY_EMPTY_ELEMENTDATA的空数组,都会在第一个元素添加后将其容量扩展成为DEFAULT_CAPACITY,也就是当我们在调用了add(E e)方法之后,我们的ArrayList的数组长度才会变为10。
       其次,就是我们的有参构造器public ArrayList(int initialCapacity),这个构造方法指定了创建ArrayList时,ArrayList所能拥有的初始化容量。也就是说我们可以创建不大于MAX_ARRAY_SIZE的任意大小的ArrayList。这个方法里面会对initialCapacity也就是初始容量进行判断:
       如果大于零,那么就会创建出一个类型为Object,容量为initialCapacity的数组并赋予elementData。如果等于零则会将空数组EMPTY_ELEMENTDATA赋予elementData(注意:这里的空数组不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA)。除了以上两种情况,也就是说initialCapacity并不大于等于零,而是负数,这个时候我们的程序就会抛出IllegalArgumentException异常。
       最后,我们的最后一个有参构造器public ArrayList(Collection<? extends E> c),这个构造方法我们可以明显的看到它在往构造函数的参数里面传递集合类型的数据。而这个集合的类型必须符合我们当前ArrayList规定泛型的子类或其本类
       那么,这个构造方法是先将传入的集合转为数组并赋予elementData。然后将这个elementData的长度赋予size属性,并判断其是否不等于零。这个判断其实就是在判断我们传进来的这个集合是否为空。那么如果为空,就会创建一个空数组,也就是我们看到else语句中的EMPTY_ELEMENTDATA。重点就在于如果不为空,还需要判断一下该数组的类型是否为Object类型,如果不是的话,则通过Arrays.copyOf(elementData, size, Object[].class)函数对已有的elementData数组进行一次复制重组,约定其为Object类型的数组。需要注意的是:这里进行的一次elementData类型判断是为了避免JDK本身的一个bug,这个bug已经在JDK1.9里进行了修复,如果想要了解具体的情况,可以根据其提供的注释来查看具体的bug信息:

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

    ArrayList的功能性方法解析:


    /**
     * 将ArrayList实例的容量调整为列表的当前大小。
     * 应用程序可以使用此操作最小化ArrayList实例的存储。
     */
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size);
        }
    }
    

       这个方法的作用从注释上面就可以看出它其实是用来缩减数组大小开支的。我们仔细来看这里面的操作。首先该方法除了会对修改次数modCount进行+1操作外,接下来就是对当前ArrayList的元素个数和当前ArrayList的elementData数组长度进行对比。如果个数小于了数组长度,那么就开始进行数组长度的缩减(这个不难理解,因为正常情况下,我们的元素个数是不可能超过,也就是大于数组整体长度的,否则就下标越界,出问题了!)。
       那么接下来的操作就是对数组元素的个数进行与零的比较,如果元素个数为空,也就是说根本就没有元素,显而易见我们的elementData就是一个空数组EMPTY_ELEMENTDATA。否则,我们就会调用Arrays.copyOf(elementData, size)方法进行数组的复制。重点就是这个方法的第二个参数表示了新数组的长度,而我们传入的就是我们当前ArrayList内部元素的个数。显式的告诉了我们ArrayList内部元素的个数就是底层数组的长度。


    /**
     * 如果必要,增加这个ArrayList实例的容量,
     * 以确保它至少可以容纳由minCapacity参数指定的元素数量。
     * @param minCapacity 所需的最小容量
     */
    public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;
        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }
    

       这个方法的作用其实从字面意思来看就是单纯的保障我们ArrayList的最小容量。我们从代码入手,首先它是需要得到一个minExpand值,也可以叫做最小扩展值,它是拿我们当前的数组elementData与ArrayList的默认空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA进行了比对。如果两者不相同,那么最小扩展值就会取0,否则就是ArrayList的默认容量DEFAULT_CAPACITY,即10。
       接下来我们才会根据我们传入的最小容量minCapacity与上述的minExpand进行比对,如果我们当前指定数组的最小容量大于了我们得到的最小扩展值,我们就必须对其进行扩容。
       这个我们不难理解,当我们需要添加一个元素时,为避免我们的元素在数组内部超出范围(下标越界),必须适当地对其进行合理的容量判断。当我们所需要的最小空间都已经比实际空间大了,那么我们这个时候就需要进行必要的扩容了。
       不过该方法在这里仅做简单的阐述,并不具体详细讲解,因为根据源码来看,该方法的调用者是由com.sun.corba.se.impl.orbutil.DenseIntMapImpl这个类里面的ArrayList调用,超出了本篇文章讲解范畴,说白了就是逻辑无法对接上,我想不过来,所以在这里就点到即止了。


    /**
     * 确保内部容量
     * @param minCapacity 最小容量
     */
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    /**
     * 确保明确的内部容量
     * @param minCapacity 最小容量
     */
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        if (minCapacity - elementData.length > 0) grow(minCapacity);
    }
    
    /**
     * 计算数组容量
     * @param elementData 需要计算的数组
     * @param minCapacity 最小容量
     */
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    

       在这里我们将会一同讲解三个方法,因为上述的三个方法息息相关。
       我们首先说的就是第一代码段ensureCapacityInternal这个方法。通过观察,该方法被add(E e)add(int index, E element)addAll(Collection<? extends E> c)等一系列添加行为的方法调用。而这个方法的本质也是在保证数组内部容量是否合理。其实不难理解,当我们向ArrayList,也就是数组添加元素时,我们确实是需要知道我们的数组容器是否还能容纳这些元素,抑或是我们的这些元素,是否能按照需求放入指定的容器位置里面。这就需要我们建立一个函数来观测我们当前数组的健康状态,一旦发现无法容纳当前诸多元素,那么就会理解采取扩容的机制来对容器长度就行扩大。这样就有效的避免了IndexOutOfBoundsException异常的出现。
       从上述三段代码段的代码中可以看到,ensureCapacityInternal会调用ensureExplicitCapacity方法,而我们的这个ensureExplicitCapacity方法还会调用calculateCapacity方法。
       所以,现在我们要讲解的则是第三代码段calculateCapacity。这个方法接收两个参数,一个是需要计算的数组,一个是最小容量。这里的Object[] elementData其实就是我们ArrayList内部的elementData数组。我们看,它会将这个elementDataDEFAULTCAPACITY_EMPTY_ELEMENTDATA进行比对,询问是否是我们的空数组。如果不是的话,直接返回我们传入的minCapacity,因为这个minCapacity存在的前提是我们已经计算好且明确了的最小容量。相反,如果比对正确的话,就返回DEFAULT_CAPACITYminCapacity中的最大值,简单来说要么就是默认的10,要么就是给定的比10还要大的最小容量。因为我们考虑两种情况,一种是向空数组内存入小于10这个容量的多个元素,这个时候我们所需保证的容量只要不超过10即可;另一种则是存入另一组集合,而这个集合可能是11,12,20等超出默认容量10的多个元素,所以这个时候我们必须保证当前数组的容量必须是我们传进的最小容量
       至此,我们开始我们的第二代码段ensureExplicitCapacity
    这个方法承前启下,它在自增修改次数后,会对传入的minCapacity与当前的数组elementData的长度进行比对。这是判断是否扩容的关键一步,它在判断我们需要的最小容量是否超出了当前数组本身的容量,如果所需的容量超出了当前数组本身的容量,那毋庸置疑,我们的数组是需要扩容的,即,调用grow方法进行扩容。


    /**
     * 增加容量,以确保它至少可以容纳由最小容量参数指定的元素数量。
     * @param minCapacity 所需的最小容量
     */
    private void grow(int minCapacity) {
        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通常接近于size,所以这是一个胜利
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    
    /**
     * 返回最大容量
     * @param minCapacity 所需的最小容量
     */
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
    }
    

       我们接下来讲解的是另一个核心方法——grow方法。这个方法是ArrayList里面一个极为重要的方法。因为它涉及了如何对ArrayList进行扩容。这里需要说明的是,所谓的ArrayList扩容无非就是数组的扩容,其本质就是数组的复制
       我们仔细来看,当我们的grow拿到了最小容量后(这个minCapacity是由ensureExplicitCapacity方法传入进来的,也就是计算好之后当前数组所需的最小容量),并不会直接参与进去。而是还要获取当前数组的长度,并将此长度赋予oldCapacity,然后通过这个oldCapacity来计算新的数组容量newCapacity
       这个newCapacity的计算方法是利用oldCapacity加上oldCapacity右移一位后的得数之和。说白了,就是将现有的oldCapacity除以2后再加上其本身,也就是所谓的扩展原先数组的1.5倍。
       那么接下来我们继续往下走。当两个新旧容量计算出来的时候,还要进行一次新容量newCapacity与最小容量minCapacity的对比,如果计算出来的新容量newCapacity要小于最小容量minCapacity,那么我们就把这个minCapacity赋予newCapacity,也就是最终我们的数组长度是根据这个newCapacity来指定的。
       上述的这种做法我们不难理解,譬如我们在对ArrayList进行添加操作时,如果添加的是一个大型的集合呢?可能本身我们的数组长度为10,计算后扩容的结果为15,但是传进来集合长度就已经为30了,也就是所谓的minCapacity为30,这时候难道我们还需要用计算后的长度吗?显而是不可能的,我们必须把这个minCapacity作为我们新扩容数组的长度。
       好的,我们接着说下一句。当上述的判断完成之后,其实还需要再判断一次,也就是代码中写的newCapacity要和MAX_ARRAY_SIZE进行一次比对。如果我们的newCapacity比ArrayList所规定的最大容量MAX_ARRAY_SIZE还要大,那我们只能调用hugeCapacity方法进行最大容量的分配了。
       注意:这里是将minCapacity传入了hugeCapacity方法,而不是将newCapacity传入。这个行为下面会做阐述。
       好,到这里我们就已经计算出了当前数组所需要的最合适的容量,也就是我们的最小容量minCapacity,接下来只需要利用Arrays.copyOf这一步数组复制就可以了。
       至此,ArrayList的动态扩容我们就完成了!
       当然,这里我们简单的描述一下hugeCapacity这个方法。这个方法其实就是为了分配数组所能承担的最大容量。这个方法的调用就是我们上面所说的当最小容量minCapacity大于了ArrayList所规定的最大容量MAX_ARRAY_SIZE,我们就需要对其进行最大容量的分配。首先还是对这个传入的容量进行判断,如果小于0,那么就直接抛出OutOfMemoryError异常,否则我们就对其分配所谓的最大容量。然而这里其实还需要进行一次判断,即,传入的最小容量minCapacity是否小于我们的最大容量MAX_ARRAY_SIZE。这个判断不难理解,因为之前计算好的newCapacity仅仅是经过1.5倍计算过后的容量值,它可能超出了Integer.MAX_VALUE2147483647),也可能实际需要的还未到达Integer.MAX_VALUE2147483647)。所以前者解释了我们在利用hugeCapacity进行扩容的时候接受的参数是最小容量minCapacity而不是newCapacity,因为如果用到了超出最大容量的数值,就一定会造成错误。那么后者,也就是解释了为什么还需要一层与MAX_ARRAY_SIZE对比的原因,即,如果我们数组所需的最合理的最小容量minCapacity并不超过ArrayList里面定义的MAX_ARRAY_SIZE(2147483639),那么我们为什么还需要分配Integer.MAX_VALUE呢?仅仅分配当前的MAX_ARRAY_SIZE就好了。除非,就是发生了最坏的结果,我们需要的容量超出了MAX_ARRAY_SIZE,因此只能分配数组所需要的最大空间Integer.MAX_VALUE了。
       那么到这里,ArrayList的动态扩容就讲解完毕了。
       不过grow方法中的代码还需要提示两点:

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

       这段代码,确保了ArrayList动态扩容的下限

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

       而这一段代码,确保了ArrayList动态扩容的上限


    /**
     * 返回列表中元素的个数
     * @return 返回列表中元素的个数
     */
    public int size() { return size; }
    
    /**
     * 如果此列表不包含任何元素,则返回true
     * @return 如果此列表不包含任何元素,则返回true
     */
    public boolean isEmpty() { return size == 0; }
    

       接下来这两段代码其实就很简单了,一个是获取ArrayList中元素的个数,也就是数组内元素个数。另一个则是判断ArrayList是否为空,也就是数组内元素的个数是否为空。然后......就没了(*^ω^*)。


    /**
     * 如果此列表包含指定的元素,则返回true。
     * 正式地说,当且仅当此列表包含至少一个元素e(o==null?e==null:o.equals (e))。
     * @param o 被检查在此列表中存在的元素
     * @return 如果此列表包含指定的元素,则返回true
         */
        public boolean contains(Object o) {
            return indexOf(o) >= 0;
        }
    

       接下来的这个contains方法也是很简单的,就是在单纯的判断我们的ArrayList,也就是数组是否包含我们传进来的参数o。换人话就是,其目的就是检查是否包含指定的元素。不过下面我们将要说的是其内部的真实逻辑,也就是代码中的indexOf(o) >= 0,这里是进行元素检查的核心部分。


    /**
     * 返回该列表中指定元素第一次出现的索引,
     * 如果该列表不包含该元素,则返回-1。
     * 正式地说,返回最低索引 i,使得(o==null?get(i)==null:o.equals(get(i))
     * 如果没有这样的索引,则为-1。
     */
    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。
     * 正式地说,返回最高索引 i,使得(o==null?get(i)==null:o.equals(get(i))
     * 如果没有这样的索引,则为-1。
     */
    public int lastIndexOf(Object o) {
        if (o == null) {
            for (int i = size-1; i >= 0; i--) if (elementData[i]==null) return i;
        } else {
            for (int i = size-1; i >= 0; i--) if (o.equals(elementData[i])) return i;
        }
        return -1;
    }
    

       OK,现在我们到了解析contains方法核心的部分了,不过这里我准备把indexOflastIndexOf放在一起说。因为一个找最头,一个找最尾,一起凑合凑合过得了。
       那我们先开始indexOf方法,毕竟它是contains方法起作用的真正方法。
       我们可以明确地从方法注释中获取到该方法返回的是指定元素出现的索引,也就是通常说的数组下标。而这个下标还是该元素第一次出现的地方。
       那么我们从代码入手来看看其实现。
       首先,它会对我们指定的元素进行判空
       1). 如果为空(也就是说这里需要找到一个空参数),那么接下来就是一个for循环。而这个for循环其实就是为了遍历数组。我们可以看到这里的数组是由数组下标0开始遍历,一直到ArrayList的元素个数size为止。
       接下来就是取出遍历到的当前元素,让其与null值做比对,如果比对成功,则直接返回该下标,否则就继续查找。如果上述都不满足,很遗憾就会返回-1。
       2).如果不为空,那么接下来依旧是一个for循环。只不过这个for循环里面的判断条件是对传入元素和具体的数组项进行了equals对比。如果符合了判断条件,也就是找到了指定的元素,就直接返回该下标,否则就继续查找。如果还是不满足,则返回-1。
       那么在这里就可以先解释一下,为什么indexOf方法是返回列表中指定元素第一次出现的索引呢,代码已经告诉我们了,因为我们是从数组的第一个元素开始遍历的,只要碰见就返回,碰不见,就没办法了......
       到了这里,indexOf方法就完毕了,那么我们开始说一下lastIndexOf方法。lastIndexOf这个方法无论是从方法名称命名来看还是从方法注释来看,这个方法的作用都是清清楚楚的。它也是返回指定元素出现的索引,也就是通常说的数组下标。而这个下标却是该元素最后一次出现的地方。
       简单的看一下代码部分,lastIndexOfindexOf的唯一区别就是遍历数组的起始位置,其他的,类似于条件,默认返回值(-1)都是一样的。
       我们的lastIndexOf的数组遍历是从ArrayList的元素个数size-1开始的,一直到0这个起始位置,也就是所谓的从后遍历。所以理所应当的就是返回此列表中指定元素最后一次出现的索引。


    /**
     * 返回此ArrayList实例的浅拷贝(元素本身不会被复制)。
     * @return 这个ArrayList实例的拷贝
     */
    public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // 只要我们实现了Cloneable接口,这里是不会发生这种错误
            throw new InternalError(e);
        }
    }
    

       这里我们介绍一下ArrayList里面的这个clone方法。它不难理解,其实就是对原数组的一个拷贝,一个原elementData的副本而已。
       我们从代码入手的话不难发现最初它是利用了自己祖类clone方法进行了一次拷贝。但是其祖类java.lang.Objectclone是一种浅拷贝,所以此时声明的类型为ArrayList<?>velementData持有的依旧是原ArrayList的elementData的引用,说白了,如果我们此时对这个v进行操作,那么原ArrayList的elementData内部的元素就会受到影响!
       所以,这里它又用Arrays.copyOf方法进行了一次数组的复制,并将其赋予现在的velementData。那么此时,这个velementData就是原ArrayList的elementData的一个副本了。其不再是内存中同一个数组。所以说,这个时候我们再操作v就不会影响原ArrayList的elementData内部的元素(这句话说成“不会影响原ArrayList”也行,只不过不严谨而已 →_→)。


    /**
     * 返回一个数组,该数组按适当的顺序(从第一个元素到最后一个元素)包含列表中的所有元素。
     * 返回的数组将是“安全的”,因为这个列表不维护对它的引用(换句话说,这个方法必须分配一
     * 个新数组)。因此,调用者可以自由地修改返回的数组。
     * 
     * 此方法充当着基于数组和基于集合的API之间的桥梁。
     * @return 一个数组,包含列表中所有元素的正确顺序
     */
    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }
    

       我们接下来说的这个方法叫toArray,秉承“絮絮叨叨”的原则还是简单的说一下。
       这个方法其实注释已经说的很清楚了,这个注释是我按照原先的英文语义翻译过来的,原语义很清楚,翻译的也很清楚。它就是利用Arrays.copyOf方法复制了一个数组并返回给调用方。而且人家说了,这个数组是安全的,因为这个列表不维护对它的引用。到这里其实也就没什么了。
       但是有一点特别的是,你会发现,这个方法是不是和上面的clone方法和trimToSize方法相似呢?所以这里会有一个小的对比
       相同点
       1. 三者均为对数组的拷贝,且都是浅拷贝
       2. 三者均是通过Arrays.copyOf(elementData, size)进行数组的复制;
       不同点
       1. 对于操作数modCount来说,trimToSize方法会对操作数modCount+1的操作,clone方法会将新建的ArrayList中的操作数modCount0,而toArray方法则对操作数modCount没有任何行为的修改;
       2. 对于数组形式来说,trimToSize方法是将复制好的数组又赋回给了ArrayList本身的数组elementDataclone方法是返回了一个ArrayList的对象,类型为Object,而toArray方法则是直接将复制好的数组进行返回,类型为Object[]
       3.从操作程度来说,trimToSize方法返回后进行操作是影响原有数组的,而clone方法和toArray方法返回后进行操作是不影响原有数组的。
       4. 从出发点来说,trimToSize方法是为了缩减数组大小开支,clone方法是复制了一个ArrayList对象,同时也缩减了数组大小开支,但其目的是供调用者进行单独处理使用的。也就是说,经过clone返回的数组其实是原数组的一个副本,可以随意修改而不影响原有的elementData举个例子,我想要减肥,trimToSize是真正的当前的“我”进行了减肥,而clone则是得到另一个“我”进行减肥,虽说都是减肥,然而从“观感”和“行为”上来看是不一样的)。那么接下来的toArray方法则是在缩减数组大小开支后返回一个“安全的”新数组,我们可以更加灵活的去操控它。可以将toArray方法理解为clone方法的一个缩减,也就是直接将复制过的数组提前返回了。
       那么到此,这个toArray方法我们也就说完了。


    /**
     * 返回一个数组,该数组按适当的顺序(从第一个元素到最后一个元素)包含列表中的所有元素。
     * 返回数组的运行时类型是指定数组的运行时类型。如果列表符合指定的数组,则返回该列表。
     * 否则,将使用指定数组的运行时类型和此列表的大小分配新数组。
     * 
     * 如果列表符合指定的数组的剩余空间(即,数组的元素比列表的元素多),那么数组中紧跟
     * 在集合末尾的元素被设置为null(只有在调用方知道列表不包含任何空元素时,这才有助于
     * 确定列表的长度)。
     * @param 如果列表的元素足够大,则将其存储到其中的数组;否则,将为此分配相同运行时
     * 类型的新数组
     * @return 返回包含列表元素的数组
     * @throws ArrayStoreException 如果指定数组的运行时类型不是此列表中每个元素的运
     * 行时类型的超类型则抛出此异常
     * @throws NullPointerException 如果指定的数组为空则抛出此异常
     */
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            // 创建一个新的数组的运行时类型,但我写的是:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size) a[size] = null;
        return a;
    }
    

       歇会儿回来哈。那么现在我们要说的一个方法是另一个toArray方法,只不过这个是携带参数了的——toArray(T[] a)
       废话不看段落:这个方法其实最好还是与上面的public Object[] toArray()方法进行对比讲解。但是没有放在一起的原因主要是因为,之前的三个方法(trimToSizeclonetoArray)的可对比性强度要高,而此时的这两个toArray的可对比性显得要低些,所以,目前这个带有参数的toArray方法我们是单独出来说明的。
       还是老样子,上面的方法注释其实已经说明的.....我反正是一知半解,进行了原有的语义翻译反而越翻越乱,想加入自己的意思又怕搞乱原文。所以现在,直接干代码(*•̀ᴗ•́*)و ̑̑:
       我们先看返回类型的格式,返回类型的格式是一个数组,这个与无参toArray方法一致,只不过这个有参的toArray方法指定了返回数组的类型。那么我们再看参数,也是一个数组,而且类型与返回类型相同。那么我们再瞅一眼泛型级别,你会发现泛型级别是和返回类型和参数类型是一致的。那么这个时候我们就明白了这个方法的含义:利用当前指定泛型后的ArrayList的有参toArray方法,传入一个符合原ArrayList泛型的数组,然后构造一个符合原ArrayList泛型的新数组且保留原有元素信息,之后,将其返回。再直白一些:将当前ArrayList转换为指定类型的数组
       方法意义解释完后我们看一下方法体部分。首先它会进行一次判断,将传入的数组a的长度与当前ArrayList内的元素个数size做一次对比。如果我们传入的数组长度比当前ArrayList内的元素个数要小,那么我们直接复制一个长度为ArrayList内的元素个数大小的数组就可以了。然后将这个数组返回,里面的内容还是原来的配方,还是原来的味道,只不过是利用Arrays.copyOf方法进行了数组的缩容。这里必须举个例子了:

    /**
     * 请放心运行
     * 但是这里写法不正确啊,仅仅是做示例用的!!!
     */
    public static void main(String[] args) {
        ArrayList<String> arrayList = new ArrayList<>();
        arrayList.add("a");
        arrayList.add("b");
        arrayList.add("c");
        arrayList.add("d");
        arrayList.add("e");
        String[] a = new String[]{"1", "2", "3"};
        System.out.println("原ArrayList集合: " + arrayList + " 元素个数为: " + arrayList.size());
        System.out.println("新建的参数a数组: " + Arrays.toString(a) + " 数组长度为: " + a.length);
        String[] strings = arrayList.toArray(a);
        System.out.println("执行arrayList.toArray(strings)中.................................");
        System.out.println("返回的数组strings1: " + Arrays.toString(strings) + " 数组长度为: " + strings.length);
    }
    /*
     * 输出结果:
     * 原ArrayList集合: [a, b, c, d, e] 元素个数为: 5
     * 新建的参数a数组: [1, 2, 3] 数组长度为: 3
     * 执行arrayList.toArray(strings)中.................................
     * 返回的数组strings1: [a, b, c, d, e] 数组长度为: 5
     */
    

       OK,那么如果数组a的长度大于了当前ArrayList内的元素个数size呢?那么就利用System.arraycopy(elementData, 0, a, 0, size)方法将原ArrayList的数组elementData复制到我们传进来的这个a数组里面(源数组为elementData,目标数组为a,都是从最开始的下标0开始替换,长度为当前ArrayList内的元素个数size)。即,把原elementData挪到a里面去。
       其实与上面这一步相关的,还有接下来的这个判断。就是显式的判断数组a的长度是否大于当前ArrayList内的元素个数size。没有的话,直接将这个a数组返回就行了。然而如果有的话,那么就把当前即将返回的数组a的索引为size位的值置为null。也就是我们原ArrayList的数组elementData的最后一个元素的后一位(因为最后一位元素我们是用elementData[size-1]表示的,所以这里应该是可以理解的)置为null。再来个例子啊:

    /**
     * 请放心运行
     * 但是这里写法不正确啊,仅仅是做示例用的!!!
     */
    public static void main(String[] args) {
        ArrayList<String> arrayList = new ArrayList<>();
        arrayList.add("a");
        arrayList.add("b");
        arrayList.add("c");
        arrayList.add("d");
        arrayList.add("e");
        String[] a = new String[]{"1", "2", "3","4","5","6","7","8"};
        System.out.println("原ArrayList集合: " + arrayList + " 元素个数为: " + arrayList.size());
        System.out.println("新建的参数a数组: " + Arrays.toString(a) + " 数组长度为: " + a.length);
        String[] strings = arrayList.toArray(a);
        System.out.println("执行arrayList.toArray(strings)中.................................");
        System.out.println("返回的数组strings1: " + Arrays.toString(strings) + " 数组长度为: " + strings.length);
    }
    /*
     * 输出结果:
     * 原ArrayList集合: [a, b, c, d, e] 元素个数为: 5
     * 新建的参数a数组: [1, 2, 3, 4, 5, 6, 7, 8] 数组长度为: 8
     * 执行arrayList.toArray(strings)中.................................
     * 返回的数组strings1: [a, b, c, d, e, null, 7, 8] 数组长度为: 8
     */
    

       我们来看这个例子,不用细看,只看输出结果。我们发现在执行完arrayList.toArray(strings)这步操作后返回的数组是[a, b, c, d, e, null, 7, 8]
       抛开null值不说,这里面的元素除了原ArrayList的数组elementData的元素以外,还囊括了我们传入的a数组的元素。这恰恰也证明了之前我们的分析——即,把原elementData挪到a里面去。那么这个参数a到底干了啥呢?
       其实,这个参数a是起到了一个标尺的作用,你也可以理解为它在丈量数组。整个方法的目的是为了将原ArrayList由List类型转换为数组类型,但这种转换其实就是一种数组复制,而数组复制就需要一个合理的容量范围。我们不能单纯的利用Arrays.copyOf(elementData, size)这个方法进行数组的复制,因为这样操作的话扩展性难免会收到约束,况且我们已经有了无参的toArray方法。
       我们在这一行行的逻辑中无不发现都是在利用a的长度进行“丈量”测算。然后根据测算结果返回拥有合理容量的数组。不过这里面也会发生“扯egg”的现象,就是我们传入的数组远远超出了原ArrayList内的元素数量,导致返回的数组可能会非常的臃肿。上面的错误示例就恰恰说明了这一点。
       我们来上一个正确的示例:

    /**
     * 这个更可以放心运行啊
     * 这回是正确的!!!
     */
    public static void main(String[] args) {
        ArrayList<String> arrayList = new ArrayList<>();
        arrayList.add("a");
        arrayList.add("b");
        arrayList.add("c");
        arrayList.add("d");
        arrayList.add("e");
        System.out.println("原ArrayList集合: " + arrayList + " 元素个数为: " + arrayList.size());
        String[] strings = arrayList.toArray(new String[0]);
        System.out.println("执行arrayList.toArray(strings)中.................................");
        System.out.println("返回的数组strings1: " + Arrays.toString(strings) + " 数组长度为: " + strings.length);
    }
    /*
     * 输出结果:
     * 原ArrayList集合: [a, b, c, d, e] 元素个数为: 5
     * 执行arrayList.toArray(strings)中.................................
     * 返回的数组strings1: [a, b, c, d, e] 数组长度为: 5
     */
    

       这里面唯一的改动就是将我们之前需提前定义的a数组替换为了直接声明:arrayList.toArray(new String[0]);。即,隐式地触发有参toArray方法的Arrays.copyOf(elementData, size, a.getClass());,避免出现之前带有null值的非预期结果。
       至此,这个toArray(T[] a)方法我们就说完了。


    ArrayList的增删改查系重要方法的解析:

       到现在,我们开始进入ArrayList的增删改查系重要方法的解析,当然这里可能会涉及到另一些底层代码,所以以下出现的可能不仅仅是通常意义上的addremovesetget等特定方法。也会出现诸如elementDatarangeCheckForAddlistIterator等一系列功能方法。目的就是不单单的隔离这些相互作用的功能,他们可能是息息相关的。


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

    这里先说一个简单的方法,
       好,现在我们先来一个简单的elementData方法。不过这个方法确实整个ArrayList可以进行删、改、查的重要方法。
       这个方法非常的简约,即使不阅读注释(那注释也是我自己加的(>ω<)),也能从代码中明白这个elementData方法的含义。其含义就是获取数组中指定索引的元素
       唯一可以能拿出来说的也就是它的访问修饰符,也就是default默认类型,该访问修饰符指出了我们的这个elementData方法仅仅可以由类内部本包java.util下的类进行访问。解析完毕~


    /**
     * 检查给定的索引是否在范围之内。如果不是的话,则抛出适当的运行时异常。
     * 这个方法不检查索引是否为负数:因为它总是在数组访问之前使用,如果索引
     * 是负的,则会抛出ArrayIndexOutOfBoundsException异常。
     */
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    
    /**
     * 由add和addAll使用的rangeCheck的一个版本。
     */
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    
    /**
     * 构造IndexOutOfBoundsException详细信息。
     * 在错误处理代码的许多可能的重构中,这种“概括”在服务器和客户端虚拟机上执行得最好。
     */
    private String outOfBoundsMsg(int index) {
        return "Index: "+index+", Size: "+size;
    }
    

       这里,我们一下三联三个方法rangeCheckrangeCheckForAddoutOfBoundsMsg。为什么要先介绍这三个方法呢?因为这三个方法涉及了ArrayList的增删改查系列方法中最重要的一环,即,检查索引范围。
       这里我们不一一讲解,因为代码过于简练,通俗易懂,所以就不说的那么繁琐了。我们仅仅区分一下就行。
       首先,rangeCheck方法和rangeCheckForAdd方法两个都是对索引范围进行检查的方法。唯独rangeCheck方法是不检查索引是否为负数的。通过其注释说明,如果索引为负数,那么就会抛出ArrayIndexOutOfBoundsException异常,所以就不进行检查了。
       其次,rangeCheck方法是由ArrayList中的get(int index)set(int index, E element)remove(int index)这三个方法直接调用,涵盖了ArrayList的查、改、删三大功能。然后就是我们的rangeCheckForAdd方法,该方法是由ArrayList中的add(int index, E element)addAll(int index, Collection<? extends E> c)两个方法直接调用,涵盖了ArrayList的的功能。不过它与rangeCheck方法不同的是,它对索引是否为负进行了判断。
       最后,就是我们的outOfBoundsMsg方法。这个方法是前两个方法在抛出IndexOutOfBoundsException异常时进行信息提示的方法。其实它就是一个简单的字符串拼接。把超出范围的索引打印出来,把当前的元素个数打印出来就没了。
       这里有一个细节的问题,那就是rangeCheck方法判断范围的时候,是把size值包含进去的。而rangeCheckForAdd是不包含size值的。这其实就说明了为什么这两个方法看似一样,实则不一样的原因。
       那就是调用rangeCheck方法的查、改、删这三个功能是不能取size这个值的,因为size这个值表示的是元素的个数,而最坏情况它还能表示数组的长度,也就是整个数组都被填满。而我们能查、改、删的位置最大也就是size-1。所以,当索引位变为size的时候,是超出数组范围的,这也就是为什么会抛出IndexOutOfBoundsException异常了。
       而调用rangeCheckForAdd方法的则是这个功能。具体这个功能其实就是add(int index, E element)addAll(int index, Collection<? extends E> c)这两个方法。他们都是指定索引位置进行添加。那么我们在向数组内添加元素的时候是允许在数组为满的情况下(这种情况非常少非常少)再次添加元素的。也就是最坏的情况,是将元素添加至数组的最后一个元素的后一个位置。而这个最后一个元素的位置的下标为size-1,其后一个位置就是size
       提示:别忘了我们后续还会扩容的。


    ArrayList的增删改查方法的解析:

       从现在开始,我们进入到了增删改查等方法的讲解。经过上面一系列的讲解,可能发现,其实我们经常认为的重点方法,只不过是前面一些方法的整合。
       第一,从访问修饰符来看,这个ArrayList提供的这些增删改查方法全都是public可以由我们的对象直接访问的方法。
       第二,从整体代码细节来看,这些方法其实也是之前那些功能性方法的一个整合,所以这里面就涉及了一个问题,也就是出发点的问题。那就是我们看似重要的方法(增删改查)可能仅仅是最普通的,而那些边边角角的方法(上面提到过的)才是最重要的。
       废话不看段落:这里面的区别就在于那个所谓的“出发点”问题。平常我们看些博客,总是先从一系列的增删改查方法进行一步一步调试,一步一步进入,然后一步一步解析。然而这些所谓的入口我却称之它为“表层代码”,这些“表层代码”就是一种“拿来主义”,拿来就用。当然,这个拿来就用我说的是增删改查这些方法本身的内部逻辑,并不是指他们的调用者。因为这些方法本身就是为了开发者拿来即用的。
       闲言碎语不要讲,开始开始~~~


    查:
    /**
     * 返回列表中指定索引的元素
     * @param index 要返回元素的索引
     * @return 位于列表中指定位置的元素
     * @throws 如果索引超出范围(index< 0||index>= size())
     * 则抛出IndexOutOfBoundsException异常
     */
    public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }
    

       这个方法就是我们ArrayList的查询方法,它会返回列表中指定索引的元素。这个说法好纠结,换个说法就是,返回数组中指定位置的元素。当我们拿到指定的索引值后,会先对这个索引进行范围检查,如果该索引大于或等于当前元素的个数,那么就会抛出IndexOutOfBoundsException异常,否则直接返回该数组下标所匹配的元素即可。


    改:
    /**
     * 将列表中指定位置的元素替换为指定元素。
     * @param index 要替换的元素的索引
     * @param element 存储在指定位置的元素
     * @return 返回替换之前位于指定位置的元素
     * @throws 
     * 1.如果索引超出范围(index< 0||index>= size())则抛出IndexOutOfBoundsException异常
     * 2.如果此列表不支持set操作则抛出UnsupportedOperationException异常
     * 3.如果指定元素的类阻止将其添加到此列表中则抛出ClassCastException异常
     * 4.如果指定的元素为空,并且此列表不允许空元素则抛出NullPointerException异常
     * 5.如果指定元素的某些属性阻止将其添加到此列表中则抛出IllegalArgumentException异常
     */
    public E set(int index, E element) {
        rangeCheck(index);
        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }
    

       这个方法就是我们ArrayList的修改方法,它会将列表中指定位置的元素替换为指定元素。也就是说替换当前数组中指定位置的元素,并将该位置之前的元素返回。当我们拿到指定的索引值后,会先对这个索引进行范围检查。如果通过了检查则先声明一个值,我们称之为“旧值”oldValue,这个oldValue就是我们将要返回的那个之前的元素
       我们声明完毕后还需要从当前数组中检索出这个值并将其赋予oldValue,到这里,我们完成了上述代码的第二行操作。
       接着,就是修改(替换)的步骤。很明显,这里直接对数组中指定位置的地方进行了赋值操作,将上层传过来的新值element替换了这里的旧值。那么这里,就是ArrayList修改元素的本质。
       最后,再将获取到的“旧值”oldValue返回就可以了。


    增:
    /**
     * 将指定的元素追加到此列表的末尾
     * @param e 需要添加到此列表中的元素
     * @return 返回是否添加成功的标志
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 操作数+1
        elementData[size++] = e;
        return true;
    }
    

       这个方法就是我们ArrayList的添加方法,它会将指定的元素追加到此列表的末尾。也就是说把指定的元素放到当前数组最后一个元素的后面
       我们来看第一行。当我们传入指定的元素时,并没有着急的将其放置到数组中。而是先进行确保数组容量,而这个ensureCapacityInternal方法我们之前是说过的,代码中出现的size+1就是这个数组所需的最小容量minCapacity。那么为什么最小容量minCapacitysize+1呢?因为这个size就是当前数组的元素个数,而我们添加元素时,首先需要确认的是我们这个数组能否还可以容纳这一个元素。所以这里就是原元素个数+待添加元素个数。这样,把这个值传入到ensureCapacityInternal方法中进行运算即可。该不动的不动,该扩容的扩容。
       当我们把容量的事儿算清楚之后我们就可以添加元素了,怎么添加呢?其实就是在该数组最后一个元素的后面添加就可以了。那么这最后一个元素在哪里呢?其实就是当前元素的个数,也就是size,因为我们的数组是从数组下标为0的地方开始的,所以最后一个元素的索引其实是size-1,那么后一个就是size-1+1,也就是size这个值。
       但是我们看到代码中数组的索引位是size++,这个还是好理解的。注意,这个自增操作是后++,也就是当前的size如果是5的话,此时这个size++不会是6,它依旧还是5。只不过这个size++值已经在内存中进行了计算,也就是变为了6。而这个6表示的则是元素的个数。如果还不好理解的话,可以参考下列代码:

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 操作数+1
        elementData[size] = e;
        size++;
        return true;
    }
    

       也就是换了方式而已,不过可能看起来更直观而已(*^ω^*)。


    /**
     * 将指定元素插入到列表中的指定位置。将当前位于该位置的元素(如果有)和
     * 任何后续的元素向右移动(将一个元素添加到它们的索引中)。
     * @param index 要插入指定元素的索引
     * @param element 要插入的元素
     * @throws 如果索引超出范围(index< 0||index>size())则抛出IndexOutOfBoundsException异常
     */
    public void add(int index, E element) {
        rangeCheckForAdd(index);
        ensureCapacityInternal(size + 1);  // 操作数+1
        System.arraycopy(elementData, index, elementData, index + 1, size - index);
        elementData[index] = element;
        size++;
    }
    

       这个方法也是我们ArrayList的添加方法,只不过它会将指定元素插入到列表中的指定位置。也就是说把指定的元素放到当前数组中指定的位置上
       这个方法的特点注释也说的很明白了,就是在插入的时候,会将插入点位置的元素连同后面的元素一齐往后()移动。其实就是给新来的元素“挪坑”。
       不过我们还是一步一步的往下看吧。
       我们来看第一行。这里拿到指定的索引之后,先对其范围进行了判断,判断这个索引是否安全。这个判断是很有必要的,因为我们的插入操作是不可能允许你随意的在数组中的不合理的地方进行插入操作的。
       紧接着,第二行开始进行数组容量的检测。依旧是把minCapacity指定为size+1,这一步我们上面的方法已经解释过了,这里不做赘述了。
       第三步,这才是我们插入操作的重点。我们发现,这里做了一步利用System.arraycopy方法进行数组复制的操作。其中源数组和目标数组都是同一个elementData,也就表示我们还是在原来的基础上进行了复制。那么具体复制了哪些内容呢?
       其中System.arraycopy的第二个参数表示了源数组要复制的起始位置,而第四个参数表示了目的数组放置的起始位置,最后一个参数表示了需要复制的长度。这三个地方分别对应的值就是代码中的indexindex+1size-index
       搞清楚了这个System.arraycopy的作用,我们就可以解答这步操作的作用了。
       首先,我有两个数组,一个是elementData,另一个,还是elementData
       然后,需要复制源数组的起始位置是我们指定的位置index,需要放置到目标数组的位置是我们指定的位置的后一位,也可以理解为index所对应的元素在数组中的右一格,也就是index+1。那么复制多少呢?代码告诉我们要复制size-index个,也就是需要复制从当前指定索引的元素算起且包含该元素并一直到当前数组的最后一个元素的长度。
       这样,我们就明白了这一层的含义,它就是要把我们指定位置的元素以及其后的所有元素向后()挪一格,为了就是把指定的位置空出来
       空出来干嘛呢?这就到了我们的第四行,那就是把我们要插入的元素赋予这个空出来的位置。也就是插入进去。这样,我们的元素就插入到指定的位置里面了。
       那么到了第五步尾声,把我们当前数组的元素个数size自增1,告诉我们数组里的的元素又多了一个。当然我把话述改为当前ArrayList里面的元素多了一个也是可以的。


    /**
     * 将指定集合中的所有元素追加到此列表的末尾,它们按照指定集合的迭代器的顺序进行添加。
     * 如果在操作过程中修改了指定的集合,则此操作的行为未定义(这意味着,如果指定的集合
     * 是这个列表,并且这个列表不是空的,则此调用的行为是未定义的。)
     * @param c 被添加到该列表的集合
     * @return 如果此列表的调用结果更改了,则返回添加成功
     * @throws 如果指定的集合为空,则抛出NullPointerException异常
     */
    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // 操作数+1
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }
    

       接下来我们介绍的是另一类行为的添加方法,即,向该ArrayList内添加另一组集合。
       废话不看段落:经过上一个方法的解析,这里我们的流程就显得通顺多了。也就是说这里就不再一步一步的进行分析,因为不需要重复add(int index, E element)方法的解析过程。特意说一下的原因不是因为懒得写,而是到了这一步,相信代码阅读应该已经很顺畅了。如果还有难度,那么就参照add(int index, E element)方法的解析过程。那现在我们开始:
       好,这里需要注意的是,传入的参数不再是某个指定的元素了,而是一个集合。那么这个集合有什么特点呢?答,通过参数泛型可观察到该集合是当前ArrayList集合的子类集合,或其本类集合(<? extends E>)。那么如何把这个集合添加进我们现在的ArrayList内呢?我们来看代码。
       第一步,它将这个传入的集合c转换成了一个Object类型的数组a
       第二步,计算集合a的长度,也就是之后新增元素个数。
       第三步,保证当前数组的内部容量,根据容量计算情况进行扩容操作。其中minCapacity指定为当前元素个数加上新增元素个数size + numNew注意:这里不再是size+1size+1代表的是添加一个元素。
       第四步,数组的复制。其中源数组为a,目标数组是当前数组elementData。源数组复制的起始位置是从索引0开始,复制numNew个,也就是全部复制。之后,复制到目标数组elementData中,而复制到的位置则是从size位上开始一直往后。这个size位就是我们当前数组elementData最后一个元素的后一位(因为数组索引是从0开始,所以最后一位元素的索引是index => size-1,那么它的后一位就是index => (size-1)+1 => size)。
       第五步,更新当前数组的元素个数size += numNew
       最后,判断新增元素个数是否不为0,并将判断结果返回。其实这一步就是证明是否添加了元素,如果正常添加了元素,其结果肯定不为0,否则就是没有添加。


    /**
     * 从指定位置开始,将指定集合中的所有元素插入此列表。将当前位于该位置的元素(如果有)
     * 和任何后续元素向右移动(增加它们的索引)。新元素将按照指定集合的迭代器顺序的出现在
     * 此列表中。
     * @param index 从指定集合中插入第一个元素的索引
     * @param c 被添加到该列表的集合
     * @return 如果此列表的调用结果更改了,则返回添加成功
     * @throws 如果索引超出范围(索引<0||索引>size())则抛出IndexOutOfBoundsException异常
     * @throws 如果指定的集合为空,则抛出NullPointerException异常
     */
    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }
    

       接下来的这一个方法也是向我们的ArrayList内添加另一组集合。只不过这里指定了插入位置index
       那么这里参数就不用细说了,还是当前ArrayList集合的子类集合,或其本类集合(<? extends E>)。我们直接来看代码干了什么。
       第一步,检查索引范围。这一步毋庸置疑是必须的,因为在add(int index, E element)这个方法中也同样运用了rangeCheckForAdd方法。毕竟我们还是需要检查一下传进的索引是否是合法的;
       第二步,依旧是将这个传入的集合c转换成了一个Object类型的数组a
       第三步,计算集合a的长度,也就是之后新增元素个数。
       第四步,保证当前数组的内部容量,根据容量计算情况进行扩容操作。其中minCapacity指定为当前元素个数加上新增元素个数size + numNew
       第五步,从这里开始,就要计算移动元素的个数了。这里移动元素的个数是通过size-index计算出来的。这个很简单,其实就是从当前索引且包含当前索引上的元素并一直到最后的元素个数,由此来得出需要移动元素的个数numMoved
       第六步,对移动元素的个数numMoved做判断,问是否大于了0。其实就是在问需不需要挪动元素;
       第七步,我们来看不需要的时候怎么办。不需要的话其实就是在尾部添加集合,因为只有在尾部添加才不需要挪动元素。这就和上一个方法addAll(Collection<? extends E> c)一样,直接将数组a复制到当前数组elementData的尾部就可以了。不过需要注意的是,这里目标数组的起始位置不再是size,而是我们传入的索引index
       第八步,也就是需要挪动元素呢?如果需要挪动元素的话我们看到,此时源数组和目标数组都是当前的elementData,源数组的起始位置是传入的index,也就是指定插入的位置。需要挪动的元素个数也是在第五步计算出来的numMoved。只不过我们把这些东西复制到哪里去呢?答案就是第四个参数index + numNew
       这个index + numNew就是当前索引加上新增元素的个数。说白了,就是让我们当前的elementData数组空出numNew个位置,给新增的元素腾出位置。到这一步,这才仅仅是腾出了位置,将原有位置的元素向后挪动了。
       挪动完毕后,才开始正式的插入。这一步就是我们现在的第七步,将数组a复制到当前数组elementData指定的位置index中。到这时候,才完成了元素的添加。
       第九步,依旧是更新当前数组的元素个数size += numNew
       最后,判断新增元素个数是否不为0,并将判断结果返回。也就是判断有没有正确的添加元素。

    删:
    /**
     * 删除列表中指定位置的元素。
     * 将任何后续元素向左移动(从它们的索引中减去1)。
     * @param index 要删除的元素的索引
     * @return 返回从列表中删除的元素
     * @throws
     * 1.如果索引超出范围(index< 0||index>= size())则抛出IndexOutOfBoundsException异常
     * 2.如果此列表不支持删除操作则抛出UnsupportedOperationException异常
     */
    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;
    }
    

       这个方法就是我们ArrayList的删除方法,它将会删除列表中指定位置的元素并将之前存在于此的元素返回。
       这个方法的特点注释也说的很明白了,就是在删除的时候,会将插入点位置的元素连同后面的元素一齐往前()移动。其实就是“补坑”。补得就是被删元素的那个“坑”。
       我们来看第一行。这里拿到指定的索引之后,先对其范围进行了判断,判断这个索引是否安全。紧接着第二行则是对操作数modCount做自增操作。
       第三行它做了一个和set方法类似的操作,就是声明“旧值”oldValue,从当前数组中取出指定索引位置的元素并将其赋予“旧值”oldValue。这个oldValue就是我们将要返回的那个被删的元素。
       第四行中,它声明了一个叫做numMoved的变量,那么这个变量是什么呢?我们看它是如何计算的。
       代码显示,numMoved的计算公式是:size-index-1。什么意思呢?其实就是后续需要挪动元素的个数
       我们仔细看,这个方法的作用是删除,前提也说了,删除后需要将删除元素的后面的元素整体往前挪,那么这里就涉及了一个挪多少的问题。显而易见是全部挪动。那么,我们就需要计算一下了。首先size代表的元素的个数,其次,index代表了数组下标。但是,我们的数组下标是以0位开始的,所以,要想让index代表第几个元素,那就得用index+1才能表示。所以,我们得到了最后的计算过程,即,size-(index+1)。去掉括号,就是size-index-1,即我们需要向前挪动size-index-1个元素。
       当前,这里的运算过程的入手点是从“元素个数”出发进行验算的,如果从“下标”出发就变成了size-1-index表示法了。不过,殊途同归,他们都是用来表示挪动元素个数numMoved的。
       接下来,到了第五行。它又对numMoved进行了一次判断,它在问我们挪动的元素个数是否大于0。其实他在问的是,我们是否删除的是最后一个元素
       所以,我们第六行和第七行一起说。如果我们删除的是最后一个元素的话,那么就直接按照第七行表示的将最后一个元素格置为null即可,然后整个元素个数自减。否则,就是第六行所说的,进行数组赋值(不过这里需要强调的是,第七行(最后一位置为null)可是永远执行的,跳过的步骤仅仅是第六行的数组复制而已。)。
       OK,秉承事无巨细的元素,我再絮叨一下第六行数组的复制。因为这才是整个删除过程的核心。我们运用System.arraycopy方法之前确定了源数组elementData与目标数组elementData,其实就是它自己。那么从源数组的什么地方开始复制呢?答,就从我们删除了的元素的后一位开始复制,即,index+1。那么复制到目标数组的哪里呢?答,复制到被删除了的那个元素的位置,以那里为起点,即,index。那么复制多少呢?答,复制被删元素之后的所有元素,即,size-index-1=numMoved个。这样,整个ArrayList的删除操作就完成了。
       当然,不要忘了第七行的置null操作和元素个数size的自减操作就可以了。
       第八行,将“旧值”oldValue返回,这一步就不说了。
       废话不看段落:说个这里面有意思的,remove(int index)方法针对elementData的操作用了--sizeadd(E e)方法针对elementData的操作用了size++add(int index, E element)方法针对elementData的操作用了size;size++;一来二去,各有姻缘,三三两两,难解难分(ಥ_ಥ)。


    /**
     * 私有的移除方法,该方法跳过边界检查且不返回被移除的值。
     */
    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
    }
    

       接下来我们先介绍一个简单的方法,它的名称是“快速移除”。怎么个快速移除呢?其实它的注释已经说的很清楚了,就是对索引不做范围检查了,同时,也不会将删除的元素进行返回。
       其整个代码的核心流程和我们之前介绍的remove(int index)方法其实是一模一样的。依旧经历了操作数自增->计算需移动元素个数->复制数组->数组最后一位置null
       这个方法可说的其实并不是这些,真正可以讲解的:
       1. 其调用方法remove(Object o)fastRemove(int index)的唯一调用者就是remove(Object o),除此无他;
       2.为什么不需要索引的范围检查和返回被删除元素(这个放到下面讲。稍微提一下,能调到这个方法的索引都是合法索引)。


    /**
     * 从该列表中删除指定元素的第一个匹配项(如果存在)。如果列表不包含该元素,它将保持不变。
     * 正式地说,删除索引i最低的元素,使(o==null?==null:o.equals(get(i))(如果存在这样一
     * 个元素)。
     * 如果该列表包含指定的元素,则返回true(如果该列表由于调用而更改,则返回true)。
     * @param o 将从此列表中删除的元素(如果存在)
     * @return 如果此列表包含指定的元素,则为真
     */
    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;
    }
    

       到这里我们开始讲解ArrayList的第二种删除用法,具体的信息也能从方法注释中摘取到。它会从该列表中删除指定元素的第一个匹配项。也就是说该方法会根据元素进行删除,只不过删除的是第一次出现的那个。
       这个方法的讲解就不能以行为讲解单位了,而应该以块。这样就分为了判断、循环、返回
       1. 第一块我们先来看返回。我们不说别的地方的return语句,就看最后一行。你会发现这个remove(Object o)方法是默认返回false。意味着只要前面的条件不成立我这儿无论如何也要返回false,告诉程序删除失败。这是这个方法的基调,即,默认删除失败
       2. 第二块我们看判断。这里对传入的值进行了区分,一种为null,另一种为其他形式的对象。因为这里涉及到了不同值会有不同的判断方式,所以要加以区分。
       3. 最后第三块,其实是最明显的那一块——循环。我们发现这两个循环都是在遍历当前数组。当然,这种遍历是从低位开始遍历,也就是从索引为0的地方开始遍历。而唯一不同的是循环体内的判断条件。
       如果我们需要删除null,也就是我们传进来的参数是nul,那么就会不断的遍历数组中每一位的参数是否为nul,即,elementData[index] == null。一旦发现就调用fastRemove(int index)方法执行删除逻辑(数组复制),然后返回true即可。
       那么如果我们删除的是其他类型的数据呢?其实它也是遍历数组的每一位进行对比。不过不是之前的==对比,而是对象本身的
    equals方法进行对比,即,o.equals(elementData[index])。一旦发现也开始调用fastRemove(int index)方法执行删除逻辑(数组复制),然后返回true即可。
       现在我们这个方法就解析完毕了。接下来我们说一个狠的!


    /**
     * 从列表中删除所有元素。该调用返回后,列表将为空。
     */
    public void clear() {
        modCount++;
        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;
        size = 0;
    }
    

       这个狠的方法就是上面的clear方法。从注释上来看,它会删除数组中所有元素。我们从代码看的话也是很简单的。
       首先操作数modCount做自增操作。然后就开始遍历整个数组。将里面的每一个元素都置为null。最后将当前数组元素个数size修改为0即可。
       一定要记住,这个方法,只不过是将ArrayList内部元素全部清空,其本身还是可以使用的。举个例子:

    /**
     * 放心运行
     */
    public static void main(String[] args) {
        ArrayList<String> arrayList = new ArrayList<>();
        arrayList.add("a");
        arrayList.clear();
        arrayList.add("b");
        System.out.println(arrayList);
    }
    /*
     * 输出结果:
     * [b]
     */
    

       你看它这里是可以运行的。为什么要举这个例子呢,就是因为我们要区别ArrayList清空操作。一种清空操作就是我们的这个clear方法,再一种是将整个ArrayList置为null。这么置null操作可就把当前的ArrayList进行了GC操作,再次调用的话就会出现空指针异常NullPointerException了。来个对比例子:

    /**
     * 不放心运行
     */
    public static void main(String[] args) {
        ArrayList<String> arrayList = new ArrayList<>();
        arrayList.add("a");
        arrayList = null;
        arrayList.add("b");
        System.out.println(arrayList);
    }
    /*
     * 输出结果:
     * Exception in thread "main" java.lang.NullPointerException
     */
    

       所以这块儿要特意区分ArrayList的清空操作。


    /**
     * 从该列表中删除其索引包含在fromIndex和toIndex之间的所有元素。将任何后续元素
     * 向左移动(减少它们的索引)。这个调用通过(toIndex - fromIndex)元素缩短列表。
     * (如果toIndex==fromIndex,则此操作无效)。
     * @throws 如果fromIndex或toIndex超出范围(fromIndex<0 || 
     *                                       fromIndex>=size() || 
     *                                       toIndex>size() || 
     *                                       toIndex<fromIndex)
     * 则抛出IndexOutOfBoundsException异常
     */
    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;
    }
    

       接下来我们介绍的这个删除方法比较特殊,它会删除指定范围内的元素。formIndex开始一直到toIndex结束。它是一个左闭右开区间,也就是说这个toIndex位置的元素是不会删除的。
       那么,它所特殊的点不仅仅这一个,我们先把代码分析完,然后再来说说它特殊的另一个点。好我们开始分析代码:
       第一步,操作数modCount做自增操作;
       第二步,计算需要挪动的元素。这里还是说一下,不要忘了,删除元素始终是需要将后续的元素填补回来的,也就是“占坑”。这里需要挪动的元素numMoved是通过size - toIndex计算出来的。其中size代表了当前数组中元素的个数,而toIndex则是删除范围内的右边界(当然并不包括它)。这句其实可以解释成toIndex及其自身以后的所有元素个数。这样是不是理解就方便些了?
       第三步,这里是所谓删除的核心。所谓的“删除”无非就是覆盖了当前范围内的元素。你看,这一步数组复制已经说的很明确了。源数组和目标数组都是其本身elementData,从源数组的toIndex位置开始复制,一直到最后一个元素。然后复制到哪里呢?复制到目标数组elementDatafromIndex位置上,因为你要从这里删除嘛,所以就覆盖掉这里的每一个元素,也就是将toIndex及其自身以后的所有元素进行前移操作。
       这里有一个问题,就是你挪动了以后就完成整个操作了吗?不一定吧?因为这里仅仅是复制,也就是说数组中原位置上的元素换了另外一个元素而已,其他尾部的元素还是保留了下来。那么接下来我们开始清空这些遗留下的数据。
       第四步,开始计算执行了“删除”操作后该数组的最新元素个数newSize其实就是剩下多少个元素)。我们也看到这个newSize是通过size - (toIndex-fromIndex)就算出来的。这个toIndex-fromIndex其实就是删除了多少个元素,那么我们剩下多少个元素呢?是不是得用原先元素的个数减去我们删了的?所以就是size - (toIndex-fromIndex)从而得到了newSize注意:这里这个newSize不光光是代表了当前数组的最新元素个数,它也代表了我们当前数组elementData的最后一位索引的后一位(最后一位索引位是newSize-1)。**
       第五步,我们开始删除从newSize索引位开始,一直到原先数组最后一位元素索引之间的数据。即,利用这个for循环开始将新数组所对应的索引下的值置为null
       最后,将新元素的个数newSize覆盖之前的size即可。
       到此,代码分析就完毕了。剩下的就是我们所谓的这个removeRange(int fromIndex, int toIndex)方法的另一个特点,为什么,它会由protected访问修饰符修饰。
       我们不难发现,这个方法是由protected访问修饰符修饰修饰的。这个修饰符限定了其只能由类内部、本包、子类来进行访问,外部包是无法访问到的。这是为什么呢?
       我们仔细跟踪里一下这个方法的引用,发现其仅仅由5个类在对其使用。

    removeRange方法的引用.png
       到这里,我们给出我们的结论,其实,就是为了减少冗余,所以这个方法并不对外进行开放。
    /**
     * 从此列表中移除指定集合中包含的所有元素。
     * @param c 包含要从此列表中删除的元素的集合
     * @return 如果此列表因调用而更改,则为真
     * @throws 如果此列表中的元素的类与指定的集合不兼容则抛出ClassCastException异常
     * @throws 如果此列表包含空元素,而指定的集合不允许空元素,或者指定的集合为空则抛
     * 出NullPointerException异常
     */
    public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    }
    
    /**
     * 仅保留此列表中包含在指定集合中的元素。换句话说,从这个列表中删除指定集合中不包
     * 含的所有元素。
     * @param c 要保留在此列表中的元素的集合
     * @return 如果此列表因调用而更改,则为真
     * @throws 如果此列表中的元素的类与指定的集合不兼容则抛出ClassCastException异常
     * @throws如果此列表包含空元素,而指定的集合不允许空元素,或者指定的集合为空则抛
     * 出NullPointerException异常
     * @throws如果此列表不支持retainAll操作则抛出UnsupportedOperationException异常
     */
    public boolean retainAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, true);
    }
    
    /**
     * 批量删除或保留指定集合内的元素
     * @param c 指定删除或保留的集合
     * @param complement 是否删除标记
     */
    private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData;
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            if (r != size) {
                System.arraycopy(elementData, r, elementData, w, size - r);
                w += size - r;
            }
            if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }
    

       到这里,恭喜,我们即将完成所有ArrayList的删除操作解析。这里我们将一连介绍上面3个方法。因为这三个方法是息息相关的,可以理解为一种方法的衍生。而这个“被衍生”的方法就是上面代码段中最后的这一个batchRemove(Collection<?> c, boolean complement)方法。
       这个方法将会被removeAll(Collection<?> c)retainAll(Collection<?> c)两个方法调用。而且我们从这两个代码中可以看到,唯一的区别就是在调用batchRemove(Collection<?> c, boolean complement)方法时所传的第二个参数不一致而已,一个false,另一个true
       所以我们这里简单说一下removeAll(Collection<?> c)retainAll(Collection<?> c)两个方法,然后着重分析batchRemove(Collection<?> c, boolean complement)
       首先,功能方面:removeAll(Collection<?> c)方法表示移除当前数组elementData内指定集合c中的元素;retainAll(Collection<?> c)方法则表示保留当前数组elementData内指定集合c中的元素,其他的元素则删除。这两个方法的功能是相反的。
       其次,步骤方面:都会对传入的集合c进行非空判断,之后开始调用batchRemove(Collection<?> c, boolean complement)方法。其中,removeAll(Collection<?> c)方法传入参数falseretainAll(Collection<?> c)方法传入参数true
       到此,removeAll(Collection<?> c)retainAll(Collection<?> c)我们就解释完毕了,最主要的batchRemove(Collection<?> c, boolean complement)方法我们来正式讲解。
       首先,我们来看这个方法的参数。第一个参数c代表了我们传入的集合,第二个参数complement代表了我们这个传入的集合是删除还被被保留。可能到这里还是不太清楚,我们继续.......不过一定要记住,删除方法所传参数complementfalse,而保留方法所传参数complementtrue
       其次,我们看到代码第一行,就把当前的数组elementData赋予了一个被final修饰的另一个elementData。往后的一切操作都是对这个新的elementData的操作。我们可以理解其为是对原有数组的一个复制。
       再次,到了下一步,这里声明了两个int类型的变量rw,并初始化为0这两个参数的含义下面说)。然后又声明了一个boolean类型的变量modified,并初始化为false。这个modified的含义其实就是判断我们的程序是否正确执行,也就是该方法的调用方的注释所写的——如果此列表因调用而更改,则为真。所以一旦程序删除或保留了元素,则将其更改为true,否则,就是默认的false
       接下来我们来看这里的循环操作:

    for (; r < size; r++)
        if (c.contains(elementData[r]) == complement)
            elementData[w++] = elementData[r];
    

       这个循环操作被包围在了try块当中,我们先不分析这里的操作是什么,我们就看rw的位置。显而易见,这两个变量都是在数组elementData的中括号内~明白了,这俩变量就是数组索引
       这个时候我们开始解释这个循环在干什么:
       我们看到,for循环的第二参数为r < sizesize是当前数组的元素个数。再结合if语句中elementData[r]我们就能得知,它是在遍历我们当前的数组elementData
       然后,这个if语句在判断什么呢?第一,它在判断我们传入的集合c里面是否包含遍历数组elementData所对应索引的元素。第二,将第一判断的结果与complement参数进行比对,之后比对成功才能执行下一步操作。所以,这个complement参数是什么就至关重要了。
       我们之前说过,removeAll(Collection<?> c)方法所传参数complementfalse,而retainAll(Collection<?> c)方法所传参数complementtrue。那么我们这里分别进行对比解说:
       1. complementfalse,即删除操作:
       如果我们当前的集合c含有所遍历出的元素,即为true,但是complementfalse。那么这个时候,if判断不成立,下方的代码是不执行的。
       如果我们当前的集合c不含有所遍历出的元素,即为false,同时complementfalse。这个时候,if判断成立,则执行下面的代码elementData[w++] = elementData[r];
       elementData[w++] = elementData[r];表示将我们遍历出的元素从数组elementData第一位开始进行存储(因为w初始化为0),也可以叫覆盖,也可以叫替换。之后w进行自增,也就是索引+1,向数组的后一位挪动,准备下一次操作。
       这样循环往复,你看,我们是不是把非c集合内的元素都重新存入了elementData,而c集合内的元素就全部跳过不管了。这也就是为什么当complementfalse时产生的效果为删除。简单来说就是,你含有,我不要,你不含有,我就要
       2. complementtrue,即保留操作:
       如果我们当前的集合c含有所遍历出的元素,即为true,同时complementtrue。这个时候,if判断成立,则执行下面的代码elementData[w++] = elementData[r];
       elementData[w++] = elementData[r];表示将我们遍历出的元素从数组elementData第一位开始进行存储(因为w初始化为0),也可以叫覆盖,也可以叫替换。之后w进行自增,也就是索引+1,向数组的后一位挪动,准备下一次操作。
       如果我们当前的集合c不含有所遍历出的元素,即为false,但是complementtrue。那么这个时候,if判断不成立,下方的代码是不执行的。
       又这样循环往复,你看,我们是不是把c集合内的元素都重新存入了elementData,而非c集合内的元素就全部跳过不管了。这也就是为什么当complementtrue时产生的效果为保留。简单来说就是,你含有,我就要,你不含有,我不要
       那么到这里是不是所有操作都完成了呢?并不是。要知道,这仅仅就是复制了部分元素到数组elementData中指定的位置上,其原有位置的元素可能被覆盖也可能依旧是原先的元素。到这里你可能就明白了,我们还有一步,就是清空这些多余出来的元素。
       好,我们来看看如何去清空这些多余的元素。

    finally {
      // Preserve behavioral compatibility with AbstractCollection,
      // even if c.contains() throws.
      if (r != size) {
          System.arraycopy(elementData, r, elementData, w, size - r);
          w += size - r;
      }
      if (w != size) {
          // clear to let GC do its work
          for (int i = w; i < size; i++)
              elementData[i] = null;
          modCount += size - w;
          size = w;
          modified = true;
      }
    }
    

       这里我们看,相应的操作完成后,也就是相应的元素处理完成之后,它下面写了一个finally代码块,说明这里的代码是必须执行的。那我们就来看看这个必须执行的代码是怎么回事儿(这里面肯定就是处理多余元素的代码,因为这里再不处理,代码可就执行完毕了)。
       注意啊,重难点开始了!我们开始解析这里的代码。
       不难发现,这个finally代码块里面有两个判断,分别在判断r是否不等于sizew是否不等于size。这里面的rw之前都说过,是数组的索引,那么他们具体是什么的索引呢?其实他们都是目前操作数组elementData的索引。只不过,r代表操作之前的数组,而w则代表操作之后的数组。r主抓遍历,w主抓存储。所以这里面的rw也可以理解为(就是readwrite两个字母的简写。
       1. 那么我们先看第一个判断if (r != size)。这个判断诡异的很,因为按照我们正常的流程来看,上面for循环的代码操作完,这个r绝对是会等于size的,因为你遍历了整个数组。那么什么时候这个r不等于size呢?只有两种情况:
       1.1. r < size:没有遍历完整个数组,也就是出现异常了;
       1.2. r > size:怎么会有这种情况呢?因为并发操作。要记住,我们ArrayList可不是线程安全的。当然,这种情况不需要考虑,为什么呢?因为System.arraycopy(elementData, r, elementData, w, size - r);这个方法会对我们的索引进行检查,如果超出了数组范围就会抛出IndexOutOfBoundsException异常。
       那么出现了上述第一种的问题以后怎么办呢?这就是if判断成立后的代码了。我们先把结论说出来,这里需要保留“犯罪现场”,之前处理过的无所谓了,但是未处理的一定要保留下来。仔细看,这里是一个数组复制。
       针对哪个数组呢?针对的就是我们现在操作的数组elementData,源数组和目标数组都是它。
       从哪里复制呢?从“案发现场”复制,也就是此时的r开始,因为你是在r这里出现的问题。
       复制多少个呢?当然是从r开始,一直到该数组elementData最后一个元素结束,也就是size - r个。
       为什么是size - r个呢?因为你就是从r这里出现的问题,r索引之前的数据无论删除操作还是保留操作,你都遍历到这里了,这说明r以前的数据已然是做过处理了(要么进行了存储,要么进行了跳过),我们无需担心了,需要担心的则是r索引之后的数据——size - r个元素。
       复制到哪里呢?复制到我们操作的数组elementData理论上应该执行下一刻存储的地方——w处。因为操作后的数组elementData我们中最后需要的“结果数组”,而这个“结果数组”是由w索引控制的。这个w的存在就是我们的for循环下一次循环判断后需要存储符合判断条件的元素位置。
       这个时候你就会发现,这里其实是做了一个数组拼接。即,将未遍历的元素与现有操作后的元素拼到了一起。操作完这一步后,我们还需更改w的值,也就是更改现在w的索引位置。
       那么w索引现在的位置在哪里呢?在w + (size - r)这个地方。其中的(size - r)是未遍历的元素个数,w则是当前操作后的数组elementData的元素个数(不要忘了w虽说是索引,但是到这一步它已经完成了w++的操作,这时的w已经是之前元素索引的下一个位置了,也就是可以表示当前操作后的数组elementData的元素个数了)。那么这两者一相加,就得到了最后这个复制完数组的元素个数,即已完成的+未完成的。最后,将这个得数还赋予w,这也就是代码中所写到的w += size - r
       好,到这里,我们就解析完了第一组判断的代码。不过这里有一个提问,只要好好看了我这篇博客讲的东西,这个提问其实都不必在这里写。那就是数组是复制完毕了,那么就一定保证我们的数组个数完整不变吗?情况好的话,能拼接出之前大小的数组,这相当于这个数组elementData就没变;情况不好的话就会出现数组的缩减,缩减就缩减,那么那些多余出来的元素呢?理应删除吧?
       下面我们来讲这个所谓“理应删除”的操作。
       2. 接下来看第二个判断if (w != size)。这个判断是在问w是否不等于size,那么我们说,什么时候这个w等于了size呢?那就是,无论我们是删除操作还是保留操作,这个数组就根本没动,所以,这个时候w是等于size的。你想,你要删除或保留一部分东西,这个操作后的数组elementData一定会是缩小的。相反数组没有缩小要么就是删除操作一个都没删除,要么就是保留操作全部被保留。这不就是数组没动吗?所以,一旦w等于size了,我们对理论缩容后的数组中多余元素的处理就没必要了。这个时候if判断不成立,直接返回结果false就是合理的了。
       那么我们来看那种正常的情况,也就是我们逾期的情况,即w不等于size的情况。那么什么时候这个w不等于size呢?还是只有两种情况:
       2.1. w < size:预期上正常删除或预期上正常保留后的缩容数组;
       2.2. w > size并发操作,当然这里我们就不解释了,之前的r > size已经做了充足的说明。
       那么出现了上述第一种的问题以后怎么办呢?来,开始“前后呼应”,我们,理应删除那些多余出来的元素。
       到了这一步,无论是否出现异常,是中途中断进行了“犯罪现场”保留,还是正常程序进行了正常缩容,到了这里我们都需要将后续多余出来的数组元素进行清空。我们单看这段代码内for循环的elementData[i] = null就已经证实了我们的想法。它在将一部分元素进行置null操作,也就是我们正在删除这个操作后的数组elementData的多余元素。
       那么从什么地方开始呢?for循环已经告诉我们了,从w索引位置开始。因为还是那句话,无论是中途中断进行了“犯罪现场”保留,还是正常程序进行了正常缩容,到了这里,这个w索引就是我们操作过后的数组elementData的最后一个元素的后一位(因为这一步的w是经过w++得到的)。所以从这里的w往后,一切都不要。
       那么这之后,我们还需修改我们的操作数modCount,我们操作了多少次呢?其实是在问我们到底清空了多少个元素,也就是说我们需要拿之前的操作数modCount与被删除了的size - w个元素相加,即,modCount + (size - w)。最后,将这个得数还赋予w,这也就是代码中所写到的modCount += size - w
       然后,更新当前数组elementData的元素个数,即size = w,因为你也就剩w个了。
       再然后,将modified置为true,表示我们的程序正确执行,也就是说,此列表因调用而更改,则为真。
       最后,将我们的这个modified返回即可。
       到此,我们整个batchRemove(Collection<?> c, boolean complement)方法就解析完毕了。
       同时,我们ArrayList增删改查方法到此全部解析完毕(ಥ_ಥ)。
       又同时,我们即将开启ArrayList的遍历功能解析(>ω<)。


    ArrayList的遍历功能解析:

    相关文章

      网友评论

          本文标题:ArrayList源码逐条解析

          本文链接:https://www.haomeiwen.com/subject/jnayfhtx.html