Java集合—ArrayList的源码深度解析以及应用介绍

   本文对Java中的ArrayList集合的源码进行了深度解析,包括各种方法、扩容机制、迭代器机制、快速失败/安全失败机制的底层实现,并且给出了ArrayList的基本应用。

1 ArrayList的概述

public class ArrayList
   extends AbstractList
   implements List, RandomAccess, Cloneable, Serializable

   ArrayList 继承自 AbstractList,实现了 List 接口 ,底层基于数组实现容量大小动态变化,在物理内存上采用顺序存储结构,即数组。因此可根据索引快速的查找元素,还具有基于索引操作元素的一套方法,允许 null 元素的存在。
   同时还实现了 RandomAccess标志性接口,这意味着这个集合支持 快速随机访问 策略,那么使用传统for循环的方式遍历数据会优于用迭代器遍历数据,即使用get(index)方法获取数据相比于迭代器遍历更加快速!
   ArrayList还实现了Cloneable、Serializable两个标志性接口,所以ArrayList支持克隆、序列化。

2 ArrayList的源码解析

  ArrayList的底层数据结构就是一个数组,数组元素的类型为Object类型,对ArrayList的所有操作底层都是基于数组的。

初始容量:
  JDK7以前: 调用空构造器则是立即初始化为10个容量的数组。
  JDK7开始: 调用空构造器初始化容量为0的空数组,在第一次add()之时默认扩容至少为10个容量,也可指定初始化数组的容量。

扩容:
  JDK7之前:大约1.5倍
  int newCapacity = (oldCapacity * 3)/2 + 1;
  JDK7开始:大约1.5倍
  int newCapacity = oldCapacity + (oldCapacity >> 1)

注意: 当计算出的扩容后的容量仍然小于最小容量时,此时设置扩容后的容量改为最小容量。另外,实际上扩容机制没有上面那么简单,后面原码处会讲到。

2.1. 主要类属性

/**
 * 如果不指定容量(空构造器),则在添加数据时的空构造器默认初始容量最小为10
 */
private static final int DEFAULT_CAPACITY = 10;
/**
 * 出现在需要用到空数组的地方,其中一处是使用自定义初始容量构造方法时候如果你指定初始容量为0的时候,那么elementData指向该数组。另一处是使用包含指定collection集合元素的列表的构造方法时,如果被包含的列表中没有数据,那么elementData指向该数组。
 */
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
 *如果使用默认构造方法,那么elementData指向该数组。在添加元素时会判断是否是使用默认构造器第一次添加,如果是数组就会扩容至10个容量。
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
 * 默认未初始化的储存ArrayList集合元素的底层数组,其长度就是ArrayList的容量。
 */
transient Object[] elementData;
/**
 * 私有的elementData数组中具体的元素对象的数量,可通过size方法获得。默认初始值为0,在add、remove等方法时size会改变
 */
private int size;

2.2 构造器与初始化容量

2.2.1 ArrayList()

  实际上当我们创建一个空ArrayList集合时,其数组为空数组,即初始化容量为0。其源码为:

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

  每次我们调用初始化一个空白的集合ArrayList,它的底层数组其实是空的。那人们说的初始化容量是10到底是从哪来的呢?
使用空构造器时,其实当我们第一次为ArrayList添加元素的时候,底层数组扩容到了至少10。这在扩容机制中会讲到!

2.2.2 ArrayList(Collection<? extends E> c)

  构造一个包含指定collection集合元素的列表,这些元素是按照该collection的迭代器返回它们的顺序排列的。

public ArrayList(Collection<? extends E> c) {
    //获取指定collection的内部元素数组,简单的直接复制给新集合内部的数组引用,toArray方法会去除了后面的空余的容量只返回有效数据
    elementData = c.toArray();
    //判断是否是空数组,即添加进来的集合是否有数据
    if ((size = elementData.length) != 0) {
        //如果有数据,转换为Object[]类型的数组
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        //如果没有数据,则直接初始化为一个容量为0的空集合
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

2.2.3 ArrayList(int initialCapacity)

  构造一个具有指定初始容量的空列表。

public ArrayList(int initialCapacity) {
    //判断指定初始容量的值
    if (initialCapacity > 0) {
        //如果指定初始容量大于0,则构建指定长度的空数组并赋值给elementData
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        //如果指定初始容量等于0,则将已有的空数组赋值给elementData
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        //如果指定初始容量小于0,则将抛出IllegalArgumentException异常
        throw new IllegalArgumentException("Illegal Capacity: "+
                initialCapacity);
    }
}

2.3 add方法与扩容机制

2.3.1 源码解析

  添加元素的方法如下,看起来很简单,但是却可以分为几步:

public boolean add(E e) {
    //判断并进行数组的扩容or长度超限的方法
    ensureCapacityInternal(size + 1);  //modCount+1
    //为size的所在索引赋值,并且size自增1
    elementData[size++] = e;
    return true;
}

  方法中调用的ensureCapacityInternal方法主要用来扩容or判断长度超限。

/**
 * @param minCapacity 最小容量,此时minCapacity=size + 1
 */
private void ensureCapacityInternal(int minCapacity) {
    //如果内部数组=DEFAULTCAPACITY_EMPTY_ELEMENTDATA,即是采用空构造器初始化集合并且第一次添加元素
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        //比较DEFAULT_CAPACITY(10)和minCapacity的大小,即最小容量不小于10
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //走下一个方法,判断是否需要扩容or判断数组长度是否溢出
    ensureExplicitCapacity(minCapacity);
}

/**
 * 判断是否需要走扩容or判断数组长度是否可能溢出
 * @param minCapacity 最小容量
 */
private void ensureExplicitCapacity(int minCapacity) {
    //该字段定义在ArrayList的父类AbstractList,用于存储结构修改次数,这确保了快速失败机制。
    modCount++;
    //如果minCapacity减去此时数组的长度的值大于0,此时开始扩容或者进行数组长度溢出判断。这里说明加载因子为1,即size+1:capacity=1时进行扩容
    if (minCapacity - elementData.length > 0)
        //扩容or长度溢出判断方法
        grow(minCapacity);
}

/**
 * 要分配的数组的最大大小。尝试分配较大的数组可能会导致内存错误OutOfMemoryError:请求的数组大小超过 VM 限制,该值没有特别实际的意义。
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * 扩容or长度溢出判断方法
 * @param minCapacity 最小容量
 */
private void grow(int minCapacity) {
    //原容量
    int oldCapacity = elementData.length;
    //新容量,即扩容后的容量,这里就是如何扩容的.新容量扩大到原容量的1.5倍左右,右移一位相当于原数值除以2的商。
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //如果新容量减去最小容量的值小于0
    if (newCapacity - minCapacity < 0)
        //新容量等于最小容量
        newCapacity = minCapacity;
    //如果新容量减去建议最大容量的值大于0
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        //调整新容量上限或者抛出OutOfMemoryError
        newCapacity = hugeCapacity(minCapacity);
    /*
     * 最终进行新数组的构建和重新赋值,此后原数组被摒弃
     * elementData:原数组
     * newCapacity:新数组容量
     * */
    elementData = Arrays.copyOf(elementData, newCapacity);
}

/**
 * Arrays类的copyof方法,其内部调用内一个多参数方法
 * @param original
 * @param newLength
 * @param <T>
 * @return
 */
public static <T> T[] copyOf(T[] original, int newLength) {
    //original.getClass():获取原数组的类型,作为新数组的类型
    return (T[]) copyOf(original, newLength, original.getClass());
}

/**
 * Arrays类的copyof方法,最终还是调用的System.arraycopy方法进行数组元素的转移。
 * @param original  原数组
 * @param newLength 返回的新数组的长度
 * @param newType   新数组的类型
 *                  将返回新的数组
 */
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);
    /*
     * 调用arraycopy方法,将数据克隆到新数组
     * 参数一:原数组
     * 参数二:copy的起始索引
     * 参数三:新数组
     * 参数四:copy到新数组的起始索引
     * 参数五:要复制的数组元素的数量。
     * */
    System.arraycopy(original, 0, copy, 0,
            Math.min(original.length, newLength));
    return copy;
}

2.3.2 执行流程

  根据上面的源码可以总结出arraylist的add方法的流程,共有六步:

  1. 计算出最小容量;
  2. 判断是否需要扩容or数组长度溢出;
  3. 计算新的容量;
  4. 考虑数组长度溢出;
  5. 数组扩容;
  6. 添加元素。

2.3.2.1 计算出最小容量

  调用ensureCapacityInternal(minCapacity)方法,方法内部首先判断elementData 是否等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这在类属性和构造器部分已经见过了,如果相等,那么说明:该集合是使用的默认空构造器初始化的,并且是第一次添加数据。
  然后minCapacity 设置为DEFAULT_CAPACITY和minCapacity的最大值,即至少是10,可能超过10是因为。addAll方法可能一次性添加超过10个的数据。
  注意,使用指定容量或者指定包含集合的构造方法创建的对象,使用add方法时,上面的判断都是false,即此时minCapacity不会设置为10,而是size+1。

2.3.2.2 判断是否需要扩容or数组长度溢出

  接下来走ensureExplicitCapacity(minCapacity)方法,该方法首先modCount自增一,该字段与ConcurrentModificationException异常有关,后面会讲到。
  然后判断如果最小容量减去底层数组长度的值大于0时,即需要扩容或者可能是数组长度溢出(后面步骤会讲),进入步骤3,可以看到扩容因子是1;如果最小容量减去底层数组长度小于等于0,那么该方法结束,说明不需要扩容or数组长度并没有溢出,进入步骤6。

2.3.2.3 计算新的容量

  grow(minCapacity)方法就是扩容or数组长度溢出判断的方法,首先会计算新的容量:

int newCapacity = oldCapacity + (oldCapacity >> 1);

  明显新容量是原长度加上原长度右移1位,这个>>是右移运算符,相当于oldCapacity/2,即新容量(newCapacity)为原长度的1.5倍左右。
  然后判断如果新容量减去最小容量的值小于0,那么设置新容量等于最小容量。 这是有可能发生的情况,比如:使用指定容量的构造器创建集合,指定初始容量为0,然后在添加第一个数时,计算新容量=0+(0>>1)明显还是为0,此时小于最小容量(0+1),因此新容量需要等于1。
  然后进入步骤4,也就是考虑数组长度溢出或者重新分配新容量的情况。

2.3.2.4 考虑数组长度溢出

  接下来是步骤4,考虑数组长度溢出或者重新分配新容量的情况。
  首先判断新容量减去最大数组容量是否大于0,如果大于0,那么进入hugeCapacity方法;否则该步骤结束。
  hugeCapacity用于重新分配新容量或者抛出数组大小溢出异常。
  首先它会判断minCapacity是否小于0,如果是,那么就是数组长度大小溢出了,直接抛出OutOfMemoryError异常,很多人会疑问minCapacity还会小于0吗?这种情况是有可能发生的,并且这里牵扯到了计算机底层二进制数据存储的问题。 关于数值的计算机存储,可以看这篇文章:计算机进制转换详解以及Java的二进制的运算方法,讲的很详细,这是弄懂后面的源码的关键,主要是看“二进制计算的坑”那一部分!
  在步骤1中,当数组长度size等于Integer.MAX_VALUE,即等于int类型的最大值2147483647时,此时再加1,根据计算机的运算机制,此时得出的minCapacity值为-2147483648,很明显小于0,然后而进行到步骤2时,计算minCapacity-size的值,即-2147483648-2147483647,根据计算机的运算机制,此时得出的值为1,就会进入步骤3。
  然后计算新容量,此时newCapacity=2147483647+2147483647>>1,算出来的值为-1073741826。此时minCapacity=-2147483648,newCapacity=-1073741826,明显newCapacity- minCapacity是大于0的。
  接下来才是进入到步骤4,此时在if条件部分:-1073741826-MAX_ARRAY_SIZE,我们找到MAX_ARRAY_SIZE字段,它的值为(Integer.MAX_VALUE – 8),实际上这只是ArrayrList建议的数组的最大长度,某些VM的实现可能需额外的长度存储一些头信息,最大长度超过MAX_ARRAY_SIZE在某些VM上可能引发OutOfMemoryError。但是这个最大长度是一定的吗?那肯定不是,这只是一个建议值,实际上很多虚拟机对数组元素添加个数可以超过MAX_ARRAY_SIZE的长度。这个具体还是要看JVM的实现,以及堆内存的大小,本人HotSpot JDK8测试结果如下:

/**
 * 测试数组最大分配长度,这和VM实现,以及堆内存大小有关
 */
@Test
public void test3() {
    // 尝试分配Integer.MAX_VALUE-1长的byte数组,将会抛出异常:
    // java.lang.OutOfMemoryError: Requested array size exceeds VM limit
    //表示请求分配的数组长度超过了VM的限制
    byte[] arr1=new byte[Integer.MAX_VALUE-1];
    // 但是分配Integer.MAX_VALUE-2长的byte数组,则不会抛出异常
    //说明本人HotSpot JDK8虚拟机允许分配的数组最大长度为Integer.MAX_VALUE-2
    byte[] arr2=new byte[Integer.MAX_VALUE-2];
    //尝试分配Integer.MAX_VALUE-2长的int数组,则直接抛出异常:
    //java.lang.OutOfMemoryError: Java heap space
    //这说明,你具体能够分配多大长度的数组,还要看数组的类型,说白了就是你的JVM的堆空间内存的大小
    int[] arr3=new int[Integer.MAX_VALUE-2];
}

  根据本人做出的实验,所以说MAX_ARRAY_SIZE这个东西,看看就行了,实际上没啥太大用作,不必过于深究,对于网上所说的用于存放数组长度之类的,切记不必过分相信。实际上Java因不支持超过231 -1(约21亿)个元素的数组而广泛受到批评。
  扯远了,我们回到刚才的地方,-1073741826-MAX_ARRAY_SIZE的值明显还是大于0的,值为1073741831,因此进入hugeCapacity方法,此时首先判断minCapacity是否小于0,明显经过上面的一系列步骤,minCapacity=- 2147483648<0,那么说明数组长度的分配超过了来最大限制(Java数组长度不能超过int的最大值,即Integer.MAX_VALUE=2147483647),此时即可抛出长度分配超限异常,程序结束,但是为什么这里要抛出OutOfMemoryError这个异常呢?目前还没想通……
  如果此时minCapacity没有小于0,如果minCapacity大于MAX_ARRAY_SIZE,那么直接将newCapacity设置为Integer.MAX_VALUE,即最大容量;否则,newCapacity设置为MAX_ARRAY_SIZE (通过返回值设置),从这里我们也能看出来MAX_ARRAY_SIZE实际上没什么太大用处,也并不是数组最大分配长度。
  如果步骤4顺利结束,而不是抛出异常,那么进入步骤5。

2.3.2.5 数组扩容

  接下来步骤5就是一段代码:

elementData = Arrays.copyOf(elementData, newCapacity);

  很明显就是,重新构建一个数组,然后将原来数组的元素拷贝到新数组中,新数组的长度为newCapacity。并修改原数组的引用指向这个新建数组,原数组自动抛弃(Java垃圾回收机制会自动回收)。
  从这一步也能看出来,ArrayList所谓的可变长度,实际上在底层也只是新建一个更长的数组,然后拷贝原数组的元素到新数组,并将引用指向新数组而已。 因此,ArrayList的改变集合结构的方法,比如增、删等方法,性能都比较一般,因为增(触发扩容)、删(触发后续元素左移)一个元素就可能涉及到大量其他元素的移动,并且随着ArrayList集合元素越来越多,其增、删的性能越来越低,但是由于是数组,因此根据索引查询元素的效率很高。
  还有一种优化就是,在创建 ArrayList 对象时就指定大概的最大容量大小,这样就能减少扩容操作的次数。
  步骤5结束,该方法结束,进入最后一步,即步骤6——添加元素。

2.3.2.6 添加元素

  进入步骤6时,ensureCapacityInternal方法已经彻底结束了,此时只剩下最后一步,即添加元素,哈哈,添加元素方法的真正添加元素的步骤却是在最后一步,神奇吧!
  添加元素也很简单,默认添加在size的索引处,即添加在末尾,然后size自增一,此时程序正常结束,返回true。

2.3.3 补充说明

  在add方法的源码中,我们能看到很多判断的是通过计算得到的值来判断的:x-y>0。比如minCapacity - elementData.length>0。很多人看到这里就会直接简化理解成比较两个数大小,事实真的这么简单的吗?如果真的仅仅是比较两个数的大小,那为什么不直接使用比较符号>或者<呢?
  通过上面的分析以及介绍的计算机二进制计算原理,其实我们能够知道,通过计算得到的值来判断 而不是直接的比较两个数大小的原因是为了能够兼容数组长度溢出的情况。
  比如最开始size=Integer.MaxValue-8,即2147483639,然后你调用addAll方法添加9个元素,minCapacity=size+9,minCapacity不是变成了2147483648,而是变成了-2147483648,如果此时直接使用比较符号,那么minCapacity是肯定小于elementData.length的值的,但是如果使用-2147483648-elementData.length,那么得到的值肯定是大于0的正数,后续的方法调用才能够处理这种数组长度溢出的情况。如果仅仅使用比较符号,就不能判断数组容量是否溢出的情况,从而造成异常!
  JDK的源码有很多看起来不能理解或者很傻的地方,但是实际上这些操作都是很有深意的,不要轻信别人的介绍(包括我),多多了自己思考,源码这么写到底是为了什么?你就能从读源码的过程中学到更多!

2.4 addAll方法

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

  按照指定 collection 的迭代器所返回的元素顺序,将该 collection 中的所有元素添加到此列表的尾部。说白了就是将一个集合的全部元素添加到另一个集合中.

/**
 * @param c 需要被添加的集合
 * @return 如果此列表由于调用而发生更改,则返回 true
 */
public boolean addAll(Collection<? extends E> c) {
    //获取被添加集合的元素数组
    Object[] a = c.toArray();
    //获取元素数组的长度
    int numNew = a.length;
    //确保容量能容纳这些数据,该方法上面已经讲解了,注意这里modeCount只自增1,并且由于addAll存在。
    ensureCapacityInternal(size + numNew);
    //数组元素的拷贝
    System.arraycopy(a, 0, elementData, size, numNew);
    //size增加numNew
    size += numNew;
    //如果获取元素数组的长度numNew不等于0,则返回 true 
    return numNew != 0;
}

2.5 remove方法

public E remove(int index)

  移除此列表中指定索引位置上的元素。向左移动所有后续元素(将其索引减1)。
  从源码中可以看到,需要调用System.arraycopy() 将删除元素 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素和扩容一样,代价是非常高的。
  remove的源码,还是比较简单的:

public E remove(int index) {
    rangeCheck(index);
    modCount++;
    //获取将要被移除的数据
    E oldValue = elementData(index);

    //要移动的数据长度size-(index + 1)  最小值0最大值size-1
    int numMoved = size - index - 1;
    if (numMoved > 0)
        //将index+1后面的列表对象前移一位,该操作将会覆盖index以及之后的元素,相当于删除了一位元素
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    // 数组前移一位,size自减-,空出来的位置(原数组的有效数据的最后一位)置null,原来的具体的对象的销毁由Junk收集器负责
    elementData[--size] = null;
    //返回原数据
    return oldValue;
}

2.6 get方法

public E get(int index)

  返回此列表中指定索引位置上的元素。

public E get(int index) {
    //检查索引长度是否符合要求
    rangeCheck(index);
    return elementData(index);
}
private void rangeCheck(int index) {
    //如果索引长度大于等于size,则抛出IndexOutOfBoundsException异常
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

2.7 set方法

public E set(int index,E element)

  用指定的元素替代此列表中指定索引位置上的元素。

public E set(int index, E element) {
    //检查索引长度是否符合要求
    rangeCheck(index);
    //获取旧的值
    E oldValue = elementData(index);
    //替换值
    elementData[index] = element;
    //返回旧值
    return oldValue;
}

2.8 clone方法

  返回的是一个全新的ArrayList实例对象,但是其elementData,也就是存储数据的数组,存储的对象还是指向了旧的ArrayList存储的那些对象。也就是ArrayList这个类实现了深拷贝,但是对于存储的对象还是浅拷贝。

public Object clone() {
    try {
        //浅克隆
        ArrayList<?> v = (ArrayList<?>) super.clone();
        //elementData的深克隆,旧的数组和新的数组的element不是同一个了,但是elementData数组中的储存对象还是浅克隆(储存的是直接量则会深克隆)
        //两个集合内部数组的对象指向相同的地址
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

2.9 序列化

  ArrayList 基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。
  保存元素的数组 elementData 使用 transient 修饰,该关键字声明存储元素的数组默认不会被序列化。
transient Object[] elementData;
  很多人就有疑问了,那么ArrayList为什么序列化之后再反序列化回来时还能保存原来的数据呢?实际上ArrayList额外自己实现了writeObject() 和 readObject() 来控制序列化数组中有元素填充那部分内容,该部分内容才是真正需要被序列化的。
  Java在序列化时需要默认调用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而writeObject()方法会判断——在传入的对象存在 writeObject()的时候,即对象自己实现了writeObject()方法的时候,会去反射调用该对象实现的writeObject()方法来实现序列化。
  反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理类似。
writeObject:

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

    // 这里可以看到,被序列化的那部分数据是真正存在元素的空间,后续没被使用空间并没有被序列化,因以节省内存空间。
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

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

readObject:

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
        ensureCapacityInternal(size);

        Object[] a = elementData;
        //反序列化时,按正确顺序读取所有元素.
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}

2.10. 其他方法

public int size()

  返回此列表中的元素数。

public int size() {
    return size;
}
public boolean isEmpty()
如果此列表中没有元素,则返回 true
public boolean isEmpty() {
    return size == 0;
}

public Object[] toArray()

  按适当顺序(从第一个到最后一个元素)返回包含此列表中所有元素的数组。

public Object[] toArray() {
    //返回一个新数组,新数组只包含elementData的有效元素,size:有效元素个数
    return Arrays.copyOf(elementData, size);
}

public List subList(int fromIndex,int toIndex)

  返回列表中指定的 fromIndex(包括 )和 toIndex(不包括)之间的部分视图。因此无论改变哪一个集合的数据,另一个的对应数据都会随之改变。并且返回的集合属于SubList类型,不能强转换为Arraylist。

public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}

public void clear()

  清空列表。

public void clear() {
    // modCount自增1
modCount++;
    // 将底层数组的每个位置的值都置空
    for (int i = 0; i < size; i++)
        elementData[i] = null;
// size置为0
    size = 0;
}

3 迭代器

  list集合均具有获取迭代器的方法iterator()和listIterator()。

3.1 Iterator迭代器

Iterator iterator()

  返回在此 collection 的元素上进行迭代的Iterator迭代器,可以用来遍历、操作collection集合。
  iterator迭代器和枚举的区别:

  1. 迭代器允许调用者利用定义良好的语义在迭代期间从迭代器所指向的 collection
    移除元素。这里的“枚举”是指Vector类的elements()方法。
  2. 方法名称得到了改进。

3.1.1 Iterator设计思想

  Iterator为什么不定义成为一个类而是一个接口?
  Java提供了很多的集合类,这些集合类的数据结构是不同的和存储方式不同,所以它们的遍历方式也应该不是一样的。
  假如迭代器定义为一个类,首先若是具体类那么就会提供一个公共的实现不同集合遍历的方法,我们知道这是不可能的。
  若是迭代器一个抽象类又因为java中类是单继承的,继承了迭代器便无法继承其他类,所以不行,而且集合的根接口无法继承抽象类。
  而无论哪种集合,都应该具备获取元素的操作,而且,最好在辅助于判断功能,这样,在获取前,先判断,更不容易出错。也就是说,判断功能和获取功能应该是一个集合遍历所具备的,而每种集合的方式又不太一样,所以我们把这两个功能给提取出来,并不提供具体实现,这种方式就是接口。
  那么迭代器真正的具体的实现类在哪里呢?真正的实现在具体的子类中,以内部类的方式体现的。
  下面是Iterator迭代器和ArrayList集合的关系:

//Iterator接口的定义和方法
public interface Iterator {
    boolean hasNext();
    Object next();
    void remove();
}

// Iterable接口具有获取Iterator接口的方法
public interface Iterable {
    Iterator iterator();
}

// Collection接口继承Iterable接口,具有获取迭代器的功能
public interface Collection extends Iterable {
    Iterator iterator();
}

//List接口继承Collection接口,同样继承了获取迭代器的功能
public interface List extends Collection {
    Iterator iterator();
}

// ArrayList实现List接口,具体实现了获取迭代器的功能
public class ArrayList implements List {
    //实现iterator方法
    public Iterator iterator() {
        //返回的匿名内部类对象,已经对Iterator做出了具体的实现
        return new Itr();
    }

    //Iterator接口的具体实现,是在具体实现类的内部类中
    private class Itr implements Iterator<E> {
        public boolean hasNext() {
            //实现hasNext的方法体
        }
        public Object next() {
            //实现next的方法体
        }
        public void remove() {
            //实现remove的方法体
        }
    }
}

Collection c = new ArrayList();
//编译看左边,调用看右边,实际上调用的是ArrayList的iterator方法
Iterator it = c.iterator();     //返回new Itr();
while(it.hasNext()){
    System.out.println(it.next());
}

4.3.1.1. Iterator(接口)的方法

boolean hasNext()

  如果仍有元素可以迭代,则返回true。

E next()

  返回迭代的下一个元素。

void remove()

  从迭代器指向的collection中移除迭代器返回的最后一个元素。

案例:

Collection<String> cl = new ArrayList<>();
cl.add("aa");
cl.add("bb");
cl.add("cc");
cl.add("dd");
//使用while循环,结构更加明了
Iterator<String> iterator1 = cl.iterator();
while (iterator1.hasNext()) {
    String next =  iterator1.next();
    System.out.println(next);
}
//使用for循环,利于回收内存
for (Iterator<String> iterator2 = cl.iterator();iterator2.hasNext();)
{
    String next = iterator2.next();
    System.out.println(next);
}

3.1.2 Iterator源码解析

Iterator iterator()

  该方法属于集合的方法,被描述为:返回按适当顺序在列表的元素上进行迭代的迭代器。

public Iterator<E> iterator() {
    return new Itr();
}

  可以看到,实际上就是返回一个对象,我们能够猜到,这就是实现类自己提供的迭代器的实现对象。因此,我们的重点就是看Itr对象是如何实现的!

/**
 * ArrayList内部的迭代器的具体实现
 */
private class Itr implements Iterator<E> {
    //要返回的下一个元素的索引
    int cursor;
    //返回的最后一个元素的索引;如果没有元素,则是-1
    int lastRet = -1;
    //在创建迭代器对象的时候设置预期的被修改次数 等于该集合最新的被修改次数
    //被用于实现快速失败机制。
    int expectedModCount = modCount;

    /**
     * 是否有下一个元素
     *
     * @return
     */
    public boolean hasNext() {
        //如果下一个元素的索引不等于集合的size那么说明还有下一个元素
        return cursor != size;
    }

    /**
     * 获取下一个元素
     *
     * @return 下一个元素
     */
    public E next() {
        /*首先检查 该集合在迭代过程是结构是否被改变,如果是将会抛出ConcurrentModificationException异常*/
        checkForComodification();
        //i记录当前cursor的值,初始化Itr时cursor默认值为0
        int i = cursor;
        //如果i大于等于size,即下一个元素的索引大于等于size,抛出NoSuchElementException异常
        //出现这种情况可能是出现了"并发修改集合元素"
        if (i >= size)
            //抛出NoSuchElementException异常
            throw new NoSuchElementException();
        //获取外部类的elementData
        Object[] elementData = ArrayList.this.elementData;
        //如果如果i大于等于elementData.length,将会抛出ConcurrentModificationException异常
        //出现这种情况可能是出现了"并发修改集合元素"
        if (i >= elementData.length)
            //抛出ConcurrentModificationException异常
            throw new ConcurrentModificationException();
        //设置要返回的下一个元素的下一个元素的索引为原值+1
        cursor = i + 1;
        //返回当前cursor索引的值,并将lastRet设置为当前将要被返回的元素的索引
        return (E) elementData[lastRet = i];
    }

    /**
     * 移除下一个元素。
     */
    public void remove() {
        //如果lastRet小于0,则抛出异常,默认是小于0的
        //因此要想移除下一个元素,那么必须先要获取下一个元素
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();
        /*移除过程中,如果发生IndexOutOfBoundsException异常,那么可能是出现了"并发修改集合元素"*/
        try {
            //调用外部类的方法 尝试移除下一个元素
            ArrayList.this.remove(lastRet);
            //设置cursor等于被移除的下一个元素的索引
            cursor = lastRet;
            //移除元素之后 重新设置lastRet等于-1
            lastRet = -1;
            //设置expectedModCount重新等于modCount
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    /**
     * 检测是否发生了"并发修改",如果是则抛出ConcurrentModificationException异常
     * 这里的"并发修改异常"是字面翻译,实际上在单线程情况下也可能触发
     */
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

3.1.3 Iterator实例解析

  上面已经提供了详细的源码注释,下面根据案例来理解其工作流程!

@Test
public void test5() {
    ArrayList<Integer> integers = new ArrayList<Integer>(Arrays.asList(1, 2, 3));
    //获取迭代器
    Iterator<Integer> iterator = integers.iterator();
    //是否存在下一个元素
    while (iterator.hasNext()) {
        //获取下一个元素
        Object next = iterator.next();
        //移除下一个元素
        iterator.remove();
    }
}

  首先建立一个集合,使用的是传入一个数组转换成的集合的构造器,之后integers集合便拥有了那3个元素(1,2,3)!
  然后是获取迭代器的方法iterator(),即获取Itr对象,此时对象中的cursor字段默认为0,lastRet字段初始化为-1,expectedModCount位0,因此当前该集合并没有结构的改变,即modCount=0。
  然后是第一次循环,循环条件是hasNext()方法返回true,明显cursor(0)不等于size(3),表示存在下一个元素,因此进入循环体。
  next()即获取下一个元素,首先判断是否出现并发修改,其实就是判断modCount和expectedModCount是否还是相等,这里明显还是相等的,进入下一步设置i=cursor=0,接下来是一系列,都满足,然后设置cursor=i+1=1,然后返回elementData[lastRet = i],即返回elementData[0],并且设置lastRet=0。
  可以想象,如果下一个方法还是next方法,那么cursor=1,lastRet=0,然后cursor=2,lastRet=1,返回elementData[1]。如此循环,当cursor==size时,上一次循环返回的数据是elementData[size-1],此时数组迭代遍历完毕即可退出循环。
  但是本例后续还有一个remove方法,我们来继续分析remove的源码,首先是一个判断,明显lastRet=0,因此不会抛出异常,但是如果是多线程条件下,或者多次调用remove都是可能抛出异常的。
  接下来是移除lastRet索引的元素,即移除数组头部0索引的元素,然后设置cursor=lastRet=0,lastRet=-1。我们知道在外部类的remove方法中,有一个modCount++的代码,此时modCount和expectedModCount不一致了,因此为了不抛出异常重新设置expectedModCount = modCount。
  到此删除方法结束,该次循环结束。此时cursor=0,lastRet=-1,modCount和expectedModCount还是相等的。进入下一次循环我们发现还是和第一次循环一样的参数值,但是集合中的第一个元素已经被我们取出来并且移除了,如此循环,当移除全部元素时,hasNext方法立即判断cursor=size=0返回true,此时循环结束,遍历并移除集合元素的操作结束,程序结束。
  以上就是Iterator迭代器的工作原理,还是比较简单的!

3.2 ListIterator列表迭代器

  List集合专用的迭代器,继承了Iterator迭代器,JDK2!

public interface ListIterator extends Iterator

相比于Iterator的特点(优点):

  1. 在迭代期间可以对元素进行增删改操作
  2. 可以反向迭代容器当中的元素。

3.2.1 获取ListIterator的方法

ListIterator listIterator();

  想要用此方法反向迭代,必须先正向迭代,将迭代器移动至列表结尾。再反向迭代。

public ListIterator listIterator(int index)

  想要用此方法反向迭代,需要index=list.size()。将迭代器定义至列表结尾。

3.2.2 ListIterator的特性

相比于Iterator增加的API方法(功能):

void add(E e)

  将指定的元素插入列表。新元素被插入到隐式光标前:不影响对 next 的后续调用,并且对 previous 的后续调用会返回此新元素。即对正向迭代无影响,反向迭代,将会返回新元素。

void set(E e)

  用指定元素替换 next 或 previous 返回的最后一个元素

boolean hasPrevious();

  如果以逆向遍历列表,列表迭代器有多个元素,则返回 true。

E previous()

  返回列表中的前一个元素。

int nextIndex()

  返回对 next 的后续调用所返回元素的索引,如果列表迭代器在列表的结尾,则返回列表大小。

int previousIndex()

  返回对 previous 的后续调用所返回元素的索引,如果列表迭代器在列表的开始,则返回 -1。

3.2.3 ListIterator的实现

  列表迭代器ListIterator,实际上是Iterator的加强版,采用的是继承Itr来实现的,因此他们的实现思想和基本原理一致,这里不再赘述!
在这里插入图片描述

4 快速失败(fail-fast)与安全失败(fail-safe)

4.1 快速失败(fail-fast)

  如果我们去查看ArrayList的API,能够看到这一段描述:

  此类的 iterator 和 listIterator方法返回的迭代器是快速失败的:在创建迭代器之后,除非通过迭代器自身的 remove 或 add方法从结构上对列表进行修改,否则在任何时间以任何方式对列表进行修改,迭代器都会抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
  注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的快速失败行为应该仅用于检测bug。

  上面说的快速失败机制是什么呢?
  实际上在源码中我们已经见过该机制的工作过程了。就是checkForComodification方法中检测的内容,如果expectedmodCount和当前modCount不相等,那么就算“并发修改”,此时触发快速失败机制,即马上抛出ConcurrentModificationException异常。
具体工作过程是如何的呢?
  首先是modCount,modCount变量实际上被定义在ArrayList的父类AbstractList中,被用来记录集合结构修改的次数,在add、remove、addAll、clear等方法调用过程中,modCount会自增,表示集合元素结构被修改。
  在初始化迭代器时,会在迭代器内部设置expectedModCount变量等于外部的modCount变量,此时是肯定不会抛出ConcurrentModificationException异常的。
  集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
  接下来就是,如果在遍历期间,集合元素结构被修改了之时快速失败机制的处理。我们知道修改集合元素结构的方式有两种一种是使用集合的方法,另一种是使用迭代器提供的方法,我们刚才看到了,使用迭代器提供的remove方法时,实际上还是调用的集合的remove方法删除元素,但是在删除元素之后会将expectedModCount重新置为modCount的值,即让这两个值变得想等了,因此单线程下使用迭代器的remove方法删除元素是不会除法快速失败机制的;但是如果仅仅使用集合的remove方法删除元素,此时expectedModCount不等于modCount,因此下一次调用next方法时,checkForComodification()方法马上触发快速失败机制抛出异常!
  快速失败演示:

/**
 * 快速失败演示
 */
@Test
public void test6() {
    ArrayList<Integer> integers = new ArrayList<Integer>(Arrays.asList(1, 2, 3));
    //获取迭代器
    Iterator<Integer> iterator = integers.iterator();
    //是否存在下一个元素
    while (iterator.hasNext()) {
        //获取下一个元素
        Object next = iterator.next();
        //使用集合的方法 移除一个元素,此时会在next()方法中抛出异常
        integers.remove(0);
    }
}

  以上就是快速失败机制的原理,还是比较简单的!
  场景:实际上,java.util包下的集合类都是快速失败的,不能在迭代过程中使用集合的方法修改集合元素结构。

4.2 安全失败(fail-safe)

  有了快速失败,我们自然会想是否存在“安全失败”呢?实际上还真的存在。
  安全失败,是相对于快速失败而言的,快速失败时立即抛出异常,安全失败则是不会抛出异常,失败的“很安全”,但是,也是属于一个“失败”的操作。
  采用安全失败机制的集合容器,实际上在遍历时不是直接在原来的集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。这种机制也被称为“写时复制”(Copy-On-Write,简称COW),很多实现都使用了该机制,比如Redis的快照功能,Redis写快照的时候,就用到了Linux底层的Copy-On-Write技术。
  原理:
  由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException。
  优缺点:
  基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到对于原始集合修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。同时这样造成的代价就是产生一个拷贝的对象,占用内存,同时数组的copy也是相当损耗性能的。
  因此,例如CopyOnWriteArrayList等集合从根本上直接杜绝了使用迭代器的remove、add方法,调用CopyOnWriteArrayList的迭代器的remove、add直接抛出UnsupportedOperationException
  场景:
  实际上,java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改,不会抛出异常,但是同样会造成数据异常。

/**
 * 安全失败机制演示,不会抛出异常,但是造成了数据不一致
 */
@Test
public void test8() {
    CopyOnWriteArrayList<Integer> integers = new CopyOnWriteArrayList<Integer>(Arrays.asList(1, 2, 3));
    //获取迭代器
    Iterator<Integer> iterator = integers.iterator();
    //是否存在下一个元素
    while (iterator.hasNext()) {
        //使用集合的方法 移除第一个元素,此时不会在next()方法中抛出异常
        Integer remove = integers.remove(0);
        System.out.println("被移除的: " + remove);
        //获取下一个元素,被移除的元素还是能获取到,正是由于Copy-On-Write技术造成的
        Object next = iterator.next();
        System.out.println("获取到的: " + next);
    }
}

  上面的演示中,首先删除集合第一个元素,但是下面还是能够从迭代器中获取,这也是数据不一致的一种表现。

5 ArrayList的应用

5.1 List去重

  去重思路一: 借助辅助集合

ArrayList<String> al = new ArrayList<String>();
al.add("aa");
al.add("bb");
al.add("aa");
al.add("dd");
al.add("dd");
al.add("dd");
al.add(null);
al.add("ee");
al.add("ee");
//去重思路一  借助辅助集合
ArrayList<String> al2 = new ArrayList<String>();
for (String s : al) {
    if (!al2.contains(s))
    {
        al2.add(s);
    }
}
al.clear();
al.addAll(al2);
System.out.println(al);   //[aa, bb, dd, null, ee]

  去重思路二:
  直接利用列表迭代器,无需借助辅助集合(打乱顺序)

ListIterator<String> sli = al.listIterator();
while (sli.hasNext()) {
    String next =  sli.next();   //获得下一个元素
    sli.remove();   //移除获得的元素
    if (!al.contains(next))  //判断源集合是否包含被移除的元素
    {
        sli.add(next);  //没包含就再添加进来
    }
}
System.out.println(al);

  注意: contains(obj); remove(Object obj);
  以上两个方法底层是依据equals方法:根据equals方法返回的值,判断是否移除/或者是判断是否存在。因此对对象去重时,需要重写equals方法,使得equals比较的是具体值而不是地址。

  去重思路三:
  使用Java8的lambda表达式轻松实现集合去重

al = al.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(HashSet::new), ArrayList::new));
System.out.println(al);

5.2 List排序

  Clooections的sort方法:

public static <T extends Comparable<? super T>> void sort(List list) --自然排序
public static void sort(List list,Comparator<? super T> c) --自定义排序

5.3 获取ArrayList的容量

  明显我们无法直接通过可用方法获取ArrayList的容量,因此只有使用反射获取:

// 获取list容量
public static Integer getCapacity(ArrayList list) {
    Integer length = null;
    Class clazz = list.getClass();
    Field field;
    try {
        field = clazz.getDeclaredField("elementData");
        field.setAccessible(true);
        Object[] object = (Object[]) field.get(list);
        length = object.length;
        return length;
    } catch (Exception e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    return length;
}

5.4 Array和ArrayList的区别以及使用条件

  Array 和 ArrayList都是存放数据的容器.array是代表的是数组,arraylist是一个集合,arraylist底层使用的封装了一个object数组。它的可变就是数组扩容
区别:

  1. Array可以包含基本类型和对象类型,ArrayList只能包含对象类型,jdk1.5之后传入基本类型会自动装箱。
  2. Array数组在存放的时候一定是同种类型的元素。ArrayList就不一定了,因为ArrayList可以存储Object。
  3. Array大小是固定的,ArrayList的大小是动态变化的。
  4. ArrayList作为Array的增强版,当然是在方法上比Array更多样化,比如添加全部addAll()、删除全部removeAll()、返回迭代器iterator()等。

使用条件:

  1. 当存放的内容数量不固定,不确定,有限时采用arraylist。
  2. 如果想要保存一些在整个程序运行期间都会存在而且不变的数据,可以放在数组里。
  3. 如果我们需要对元素进行频繁的移动或删除,或者是处理的是超大量的数据,那么,使用ArrayList的效率很低,使用数组进行这样的容量调整动作很麻烦,我们可以选择LinkedList。

5.5 迭代器和for循环速度测试

public class ArrayListTest {
    static List<Integer> list = new ArrayList<Integer>();

    static {
        for (int i = 1; i <= 100000000; i++) {
            list.add(i);
        }
    }

    public static long arrayFor() {
        //开始时间
        long startTime = System.currentTimeMillis();
        for (int j = 0; j < list.size(); j++) {
            Object num = list.get(j);
        }
        //结束时间
        long endTime = System.currentTimeMillis();
        //返回所用时间
        return endTime - startTime;
    }

    public static long arrayIterator() {
        long startTime = System.currentTimeMillis();
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            Object next = iterator.next();
        }
        long endTime = System.currentTimeMillis();
        return endTime - startTime;
    }

    public static void main(String[] args) {
        long time1 = arrayFor();
        long time2 = arrayIterator();

        System.out.println("ArrayList for循环所用时间==" + time1);
        System.out.println("ArrayList 迭代器所用时间==" + time2);
    }
}

6 总结

  ArrayLIst的底层实现就是使用的数组,因此支持重复元素,支持null元素,元素按照插入顺序存放、取出。它的可变长度,实际上就是在底层做的数组的来回拷贝,所以实际上如果增、删操作比较多的话,性能还是比较低下的。但是由于底层是数组,支持随机访问,因此如果是遍历操作比较多的话,那么性能还是比较高的。
  ArrayLIst稍微复杂点的地方可能是判断数组容量是否超限的原理(涉及计算机二进制数据的计算原理),以及迭代器的原理,但是都不算很难,总体来说ArrayLIst源码非常简单了。
  我们后续将会介绍的更多集合,它们的难度总体也会越来越高,比如LinkedList、TreeMap、HashMap,LinkedHashMap等基本集合以及JUC包中的高级并发集合。如果想学习集合源码的关注我的更新!

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

猜你喜欢

转载自blog.csdn.net/weixin_43767015/article/details/106490024