以下针对JDK 1.8版本中的ArrayList进行分析。
概述
ArrayList
基于List
接口实现的大小可变的数组。其实现了所有可选的List
操作,并且元素允许为任意类型,包括null
元素。除了实现List
接口,此类还提供了操作内部用于存储列表数组大小的方法(这个类除了没有实现同步外,功能基本与Vector
一致)。
每个ArrayList
实例都有一个容量。容量是用于存储列表中元素的数组的大小。它始终至少与列表大小一样大。随着元素添加到ArrayList
,其容量会自动增加。除了添加元素具有恒定的摊销时间成本这一事实之外,增长策略并没有详细指出。
我们在添加大容量数据的时候可以使用ensureCapacity
方法来主动扩容,这可以减少自动扩容的次数。
值得注意的是,这些实现都不是同步的。因此,当多个线程并发访问一个ArrayList
实例并且至少有一个线程对这个实例进行结构性调整的时候,必须在外部额外实现同步(对于结构性调整,主要指增加或删除一个或多个元素,更精确的说,就是对这个列表的大小进行了调整,对于更改元素的数值并非是结构性调整)。这通常通过在自然封装列表的某个对象上进行同步来完成。如果不存在此类对象,则应使用Collections.synchronizedList
方法“包装”该列表。这最好在创建时完成,以防止意外地不同步访问列表:List list = Collections.synchronizedList(new ArrayList(...));
注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出ConcurrentModificationException
。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的快速失败行为应该仅用于检测 bug。
源码分析
主要字段
|
|
注意此处的elementData
字段是用的transient
修饰的以及对于空实例有DEFAULTCAPACITY_EMPTY_ELEMENTDATA
和EMPTY_ELEMENTDATA
两个共享空数组实例,下面会对提到的这些注意点进行分析。
构造函数
|
|
注意到对于无参构造器使用的是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,而对于带参构造器,当 initialCapacity 为0时,使用的是EMPTY_ELEMENTDATA
;另外,在无参构造器中的注释——“初始化容量为10的空列表”,我们不禁有以下疑惑:
EMPTY_ELEMENTDATA
和DEFAULTCAPACITY_EMPTY_ELEMENTDATA
都是空的对象数组,为什么在构造器中要对其进行区分- 无参构造器中,只是把空的对象数组赋值给了
elementData
,为什么注释称声明了长度为10的空数组
对于以上问题,将在存储和扩容部分进行讲解。
存储和扩容
|
|
当ArrayList实例是个空列表并且 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
时,minExpand 设置为DEFAULT_CAPACITY
(10),此时如果 minCapacity 小于 minExpand,那么不马上进行扩容操作,在进行add
操作时候,会初始化一个容量为 10 的空列表,这样不仅符合无参构造器中的注释,并且保证了 ArrayList 实例能够存储 minCapacity 个元素。
|
|
在add
操作内部,会调用这个私有方法来确保有足够的容量来放置元素。注意,这个函数一开始就对 elementData 进行判断是否为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,如果是的话,证明是无参构造器初始化的实例,在下一步会初始化一个容量为 10 的空列表,符合无参构造器中的注释,其实就是一个延迟初始化的技巧。
|
|
因此对于无参构造器的注释的疑问,到达这里就可以解答了,它确确实实初始化了一个大小为 10 的空列表,只是不是一开始就初始化,而是使用了延迟初始化的方式,在add
的时候才进行初始化。
对于另一个问题,无参构造器使用DEFAULTCAPACITY_EMPTY_ELEMENTDAT
,对于new ArrayList(0);
使用的是EMPTY_ELEMENTDATA
,前者是不知道需要的容量大小,后者预估元素较少。因此ArrayList
对此做了区别,通过引用判断来区别用户行为,使用不同的扩容算法(扩容速度:无参:10->15->22…,有参且参数为0 :0->1->2->3->4->6->9…)。另外,在 JDK 1.7 中,没有通过两个空数组来对用户行为进行区分,因此容量为 0 的话,会创建很多空数组new Object[0]
,因此上述方式也对这种情况进行了优化。
删除
|
|
ArrayList的序列化
在 主要字段 部分我们可以看到,elementData 是通过transient
修饰的(transient具体用法可参看Java对象序列化一文),通过transient
声明,因此其无法通过序列化技术保存下来,但仔细阅读源码发现其内部实现了序列化和反序列化函数:
通过一个例子验证其序列化和反序列化过程:
可以得出结论:ArrayList支持进行序列化操作,此时不禁会思考既然要将 ArrayList 的字段序列化(即将 elementData 序列化),那为什么又要用 transient 修饰 elementData 呢?实际上,ArrayList 通过动态数组的技术,当数组放满后,自动扩容,但是扩容后的容量往往都是大于或者等于 ArrayList 所存元素的个数。如果直接序列化 elementData 数组,那么就会序列化一大部分没有元素的数组,导致浪费空间,为了保证在序列化的时候不会将这么大部分没有元素的数组进行序列化,因此设置为 transient。
从源码中,可以观察到循环时是使用i < size
而不是i<elementData.length
,说明序列化时,只需实际存储的那些元素,而不是整个数组。
问题
List<Integer>list = new ArrayList<>(10); out.println(list.get(1));
是否会抛出异常- ArrayList 支持序列化,为什么 elementData 要设置为
transient
- 对于空实例数组,为什么要区分
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
和EMPTY_ELEMENTDATA