ArrayList是最常使用的集合类之一了。在JDK文档中对ArrayList
的描述是:ArrayList
是对list
接口的一种基于可变数组的实现。ArrayList类的声明以下:java
1 |
public class ArrayList<E> extends AbstractList<E> |
ArrayList继承了AbstractList抽象类,并实现了List,RandomAccess,Cloneable以及Serializable接口。对 RandomAccess 接口的实现代表支持随机访问(由于基于数组嘛~),同Cloneable接口和Serializable接口同样,该接口只是一个标记,不须要实现任何方法。ArrayList 能够支持值为 null 的元素。数组
本文中的分析都是针对JDK8中的源码进行的。安全
从文档中的说明能够知道,ArrayList的底层是基于数组来实现的。那咱们就先来看一下ArrayList
的成员变量:app
1 |
private static final long serialVersionUID = 8683452581122892189L; |
使用了一个 Object 数组来存放数据,并维护一个计对数器来记录当前容器中元素的数量。注意到数组 elementData 是使用 transient 来修饰的,在后面会此进行进行解释。框架
除此之外,在 ArrayList 还有一个继承自父类 AbstractList
的成员变量 modCount 须要关注。使用 modCount 记录列表发生结构化修改的次数,从而提供 fail-fast 的迭代器。由于 ArrayList 的实现是非同步的,若是在迭代过程当中另外一个线程向同一个容器中添加元素或移除元素,就会致使ConcurrentModificationExceptions
。dom
1 |
//The number of times this list has been structurally modified. |
1 |
/** |
ArrayList 类提供了三个构造方法,如上所示。除了初始化一个空的ArrayList之外,还支持使用另一个容器中的元素来初始化ArrayList。注意到,在初始化一个空的ArrayList时,若是不指定容量的大小,默认容量是10。在初始化一个空的ArrayList时,若是指定容量为0,则数组引用指向的是一个静态成员变量EMPTY_ELEMENTDATA;若是使用默认容量,则数组引用指向的是一个静态成员变量DEFAULTCAPACITY_EMPTY_ELEMENTDATA;除此之外,按照实际指定的容量分配数组空间。ide
ArrayList既然是基于可变数组的,那么在底层数组的存储容量不足时确定会进行扩容操做,以改变容器的容量。扩容的操做是经过下面的代码进行实现的:源码分析
1 |
/** |
这一段代码的注释很清楚了,大体解释一下:ensureCapacity
方法可供外部调用,而ensureCapacityInternal
则仅供内部调用,都是要确保当前容器可以容纳给定数量的元素,它们都会调用ensureExplicitCapacity
方法;在每次调用ensureExplicitCapacity
方法时,会将modCount
的值加1,代表 ArrayList 发生告终构化的修改,而后根据当前数组能容纳的元素数量来决定是否须要调用grow
方法来调整数组的大小;grow
方法负责调整数组的大小,注意每次调整时将容量扩大为当前容量的1.5倍(oldCapacity + (oldCapacity >> 1)
),若是仍是不能知足容量要求,就按照所需的最小容量来分配,而后将原数组中的元素复制到新数组中。ArrayList 可以支持的最大容量为 int 值的上限,超过会报OutOfMemoryError
异常。优化
这里有一个奇怪的地方在于,modCount 的值会在 ensureExplicitCapacity
方法中加1。前面已经说过,modCount用来记录容器发生结构化修改的次数,按道理来讲实在加入或移除元素是才会修改的,为何会在这里调用呢。后面咱们会看到,每次新加入元素时,ensureExplicitCapacity
都会被调用,于是能够将modCount的修改放在此方法中,就没必要在 add
及 addAll
方法中进行修改了。ui
1 |
/** |
能够向ArrayList容器中添加单个元素,也能够添加一个容器;默认添加到数组的末尾,也能够添加到指定位置。首先会确认当前容量是否充裕,若是不足则会进行扩容操做。每次添加元素时都会修改modCount的值,前面已经详细地说明过了。在指定添加的位置时,会先检查指定的位置是否合理,不合理则会抛出IndexOutOfBoundsException
;若是插入位置合理,则会将相应位置后面的元素向后挪以腾出空间,而后将待添加的元素放入。
1 |
/** |
移除元素时其实就是使用System.arraycopy
将移除后仍保留的元素复制到正确的位置上,并调整当前的size大小。注意,在元素移动完成后,要显式地将数组中再也不使用的位置中存放的值赋为null,从而确保GC可以正常地回收资源。
下面再看看如何作到从ArrayList中移除指定容器内的元素以及保留指定容器中的元素。
1 |
/** |
咱们能够看到,核心的方法在于batchRemove(Collection<?> c, boolean complement)
,不管是移除给定容器中的元素removeAll(Collection<?> c)
仍是只保留指定容器中的元素retainAll(Collection<?> c)
都是经过该方法来实现的。该方法经过传入的一个布尔类型肯定ArrayList中每一个元素是否应该保留,详细的注释参见上面代码中的中文注释。
上面从ArrayList中移除元素的全部方法中都没有对移除元素后的数组大小进行调整,这种状况下可能会在移除大量元素后形成空间的浪费。这时候能够经过trimToSize
方法将数组大小调整为实际的大小。
1 |
/** |
1 |
|
基于数组的实现使得更新元素及查找元素变得比较简单。在set方法中不会修改modCount的值。
在AbstractList中其实已经提供了迭代器的一个实现,ArrayList类中又提供了一个优化后的实现。
1 |
/** |
迭代器中经过一个游标cursor来达到遍历全部元素的目的,同时还保留了上一个访问的位置以便于remove方法的实现。前面说过,ArrayList的实现并非线程安全,其fail-fast机制的实现是经过modCount变量来实现的。在这里咱们能够清楚地看到,在迭代器的next和remove等方法中,首先就会调用checkForComodification
方法来判断ArrayList容器是否在迭代器建立后发生过结构上的修改,其具体的实现是经过比较建立迭代器时的modCount(即expectedModCount)和当前modCount是否相同来完成的。若是不相同,代表在此过程当中其余线程修改了ArrayList(添加了或移除了元素),会抛出ConcurrentModificationException
异常。
List接口还支持另外一种迭代器,ListIterator<E>
,不只可使用next()方法向前迭代,还可使用previous()方法向后移动游标。ArrayList中也实现了listIterator()
和listIterator(int index)
方法,比较简单,这里就再也不详细说了。
所谓的子列表,就是列表中指定范围内的一些元素,经过调用subList(int fromIndex, int toIndex)
来获取。对子列表的操做会影响到父列表。经过子列表能够达到操做父列表中部分元素的目的,如只迭代部分范围内的元素,或者只对部分范围内的元素进行排序。
1 |
private class SubList extends AbstractList<E> implements RandomAccess { |
上面列出了ArrayList中使用的子列表的部分代码,SubList继承了AbstractList,并实现了RandomAccess接口。SubList中并无向ArrayList那样有一个数组来存放元素,而是持有了父列表的引用,并保存了元素相对于父列表的偏移及范围等信息。对子列表的全部操做都是经过父列表来完成的。值得说明的是,由于SubList也是AbstractList的子类,于是也有一个modCount字段。在建立子列表时,modCount和父列表一致;之后每当经过子列表修改父列表时也都会保持一致。在调用子列表的方法时,相似于迭代器,首先也会经过checkForComodification
方法确保父列表的结构没有发生改变,不然会抛出ConcurrentModificationException
异常。
前面提到过数组 elementData 是使用 transient 来修饰的,这个其实就和序列化及反序列化相关。transient 是一个关键字,用 transient 修饰的变量再也不是对象持久化的一部分,即默认序列化机制中该变量不用被序列化。
这一点可能让人很费解,若是不用被序列化,那么反序列化的时候不是就丢失了存储的数据了吗?实际上,在 ArrayList 中对序列化和反序列化过程进行了更细致的控制,即经过 writeObject()
和 readObject()
方法。
1 |
/** |
可见,在序列化时并非将整个数组所有写入输出流中,由于数组一般都不是处于彻底填充的状态,对于为 null 的元素就没必要保存,也能够达到节约空间的目的。后面咱们会看到不少集合类中都采起了这种方式进行序列化和反序列化。
本文经过源码分析了Java 8 集合框架中ArrayList的实现方式。ArrayList内部是经过数组进行实现的,具备高效的随机访问的特性;但插入和删除元素时每每须要复制数组,开销较大。在容器建立完成后须要进行大量访问,但插入和删除操做使用较少的状况下比较适合使用ArrayList。