源码阅读(2):Java中主要的List结构——Vector集合》)
ArrayList拥有与Vector类似的接口和操作逻辑(JDK1.2+提供),不过它不支持线程安全的操作(上文已经讲到Vector的操作是线程安全的,但是基于线程安全的多线程操作性能不高)。ArrayList同样也支持随机访问操作,也就是说其单线程下对指定索引位置的数据读取操作的时间复杂度为O(1)。ArrayList的主要继承体系如下图所示:

ArrayList和Vector的差异首先就体现在他们内部不同的重要变量/常量定义上,代码片段如下所示:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;
}
从上文对ArrayList容器中的重要变量介绍来看,我们至少可以“猜测”出ArrayList容器在初始化过程、和扩容过程(特别是第一次扩容)上与Vector容器可能存在较大差异,这些差异我们会在后文进行详细说明。首先本文需要说明在elementData变量定义中出现的transient关键字,官方对于该关键字的解释是:
Variables may be marked transient to indicate that they are not part of the persistent state of an object.
由于进行对象持久化操作时必须首先进行对象的序列化操作,所以读者可以将transient关键字简单理解为:该属性可以在进行对象序列化操作时被忽略。我们看以下的实际示例:
public static void main(String[] args) throws IOException , ClassNotFoundException {
MyObject myObject = new MyObject();
myObject.setParamOne("param1");
myObject.setParamTwo("param2");
myObject.setParamThree("param3");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(myObject);
byte[] objectBytes = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(objectBytes);
ObjectInputStream oin = new ObjectInputStream(bis);
MyObject otherObject = (MyObject)oin.readObject();
System.out.println(String.format("otherObject.getParamThree() = %s ", otherObject.getParamThree()));
}
class MyObject implements Serializable {
private String paramOne;
private String paramTwo;
private transient String paramThree;
}
和设想的结果相同,通过序列化/反序列化动作得到的新对象“otherObject ”中,没有paramThree属性的值,以下是输出结果:
otherObject.getParamThree() = null
但是读者很快会找到一个矛盾点,那就是我们在实际工作中经常需要进行ArrayList容器的序列化/反序列化操作,但为什么没有出现新的ArrayList容器短缺元素的情况呢(前提是这些元素对象本身是可序列化的)。这是因为ArrayList容器出于节约内存空间的考虑,采用了一种折中的序列化/反序列方式。这个详细内容将在后文(4.2小节)进行介绍。
4.1、ArrayList的初始化和扩容操作
本小节我们详细讲解ArrayList容器的初始化操作和扩容操作,并结合前文中已经介绍的Vector容器进行类比。首先我们来讲述一下ArrayList容器的初始化方式,先看代码片段:
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);
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
从以上ArrayList容器实例化的代码可以看出,如果初始化时不指定ArrayList容器的大小,那么ArrayList容器都将会初始化成一个容量为0的容器。
请注意上文中那个没有带任何参数的ArrayList容器初始化方法,官方给出的初始化意义是“Constructs an empty list with an initial capacity of ten.”,但是我们所知的那个DEFAULTCAPACITY_EMPTY_ELEMENTDATA常量,是一个长度为0的Object类型的数组。这个矛盾的情况是怎么回事呢?这是因为当ArrayList容器处于这种状态时,后续无论是通过add(E e)方法还是add(int index, E element)方法向ArrayList容器添加一个新的对象(进行“写”性质的操作),ArrayList容器都会将elementData数组变量“扩容”成一个容量为10的数组——通过“private void grow(int minCapacity)”方法。
grow(int minCapacity)方法是ArrayList容器用来进行实际“扩容”的方法,由于我们已知的知识:一个给定的数组在完成初始化后其大小不能改变。所以ArrayList容器实际的“扩容”机制和Vector容器类似——通过某种算法创建一个容量更大的数组,并按照一定的逻辑将原数组中的元素一次“复制”到新的数组中。只不过ArrayList容器中计算新数组大小的方式更“动态”一些。我们来看grow(int 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);
elementData = Arrays.copyOf(elementData, newCapacity);
}
hugeCapacity(int minCapacity) 方法在上文介绍Vector容器时,已经进行了详细介绍,所以这里也不赘述了。两者的hugeCapacity(int minCapacity) 方法中的内容一模一样——甚至方法中的注释、换行符都一样。 Arrays.copyOf(Object[] original, int newLength) 方法在上文中也进行了详细介绍,这里也不赘述了。
虽然我们已经详细描述了ArrayList容器中关于“扩容”的核心方法,但实际上当ArrayList容器进行第一次扩容和进行非第一次扩容的情况还是有区别的,现在我们进行详细的分析:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
这种情况实际上就是ArrayList容器使用默认的构造函数方式刚完成初始化动作所处的状态——但凡有一次“写”性质的操作发生,ArrayList容器都不会是这样的状态。那么处于这种状况下,通过ensureCapacityInternal(int minCapacity)方法进行的扩容操作,就会满足条件“elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA”,也就是说系统会使用DEFAULT_CAPACITY(大小为10)常量作为要求的扩容后容量。
这种情况下,ensureCapacityInternal(int minCapacity)方法中的判断式“elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA”就不成立。方法将会按照传入的minCapacity参数值作为要求的扩容后容量大小。那么下图可以很好表达ArrayList容器的常规扩容过程:

4.2、ArrayList的序列化和反序列化
本文不准备讨论什么叫做序列化,什么叫做反序列化,也不准备讨论为什么要进行序列化/反序列化,如果读者不清楚相关知识可自行查询资料。ArrayList容器和Vector容器都支持序列化——前提是容器中存储的对象也必须支持序列化/反序列化,否则在进行相关操作时,会抛出“java.io.NotSerializableException”异常。
但是ArrayList容器相比Vector容器,其序列化/反序列化过程进行了相关优化,相关代码片段如下所示:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
protected transient int modCount = 0;
transient Object[] elementData;
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
int expectedModCount = modCount;
s.defaultWriteObject();
s.writeInt(size);
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
s.defaultReadObject();
s.readInt();
if (size > 0) {
ensureCapacityInternal(size);
Object[] a = elementData;
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
}
以上代码在Vector容器的定义中可没有,那么我们考虑一种情况,当Vector容器中的elementData数组容量足够大(例如超过9万的数组容量),但是elementData数组中实际已使用的容量(elementCount变量描述,类似于ArrayList容器中的size变量)还没有那么大时(如下图所示):

很明显,elementData数组中在elementCount变量所表示的索引以外的11000个位置中的元数是没有序列化的必要的,Vector容器理论上最多会浪费掉50%的计算资源进行无用的序列化过程。所以之后的ArrayList容器对序列化过程做了改进——按照以上所述的序列化过程,只对elementCount数组在size变量所表示的索引以内的元素进行序列化,而不是将整个elementCount数组全部序列化。
另外一个有意思的点是writeObject(java.io.ObjectOutputStream s)方法中的以下关联代码片段:
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
int expectedModCount = modCount;
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
由于ArrayLst容器不是线程安全的,也就是说在多个线程同时对一个ArrayLst容器实例进行操作写操作时,其最终操作结果可能会出现混乱,例如在第X个位置设定的新元素,可能并不是第X个位置最终元素(也就是我们所说的“脏数据”问题)。这个问题在一般情况下开发人员都可以适应——因为在ArrayList的文档和源代码中都对其线程不安全性进行了明确说明:不要在多线程情况下使用同一个ArrayList的实例,可以使用CopyOnWriteArrayList类进行替换。
但是在ArrayList容器的序列化操作时,需要保证新生成的ArrayList容器不会出现“脏数据”问题,就要保证正在进行序列化操作的ArrayList容器在这个过程中不会被其它线程进行“写”性质的操作。
那么如何解决这个问题呢?ArrayList容器的序列化操作借鉴了“乐观锁”思路(关于乐观锁的详细介绍会在本专题后续文章中机型),简单来说就是:在操作前对操作情况进行操作结果预测,然后再进行操作,最后比较实际操作结果和预计结果是否相同,如果相同则认为操作成功;如果失败则认为操作失败(回溯操作/抛出异常/重新操作都可)。那么操作过程如下:
-
在进行序列化操作前首先记录当前ArrayList容器的modCount值,这个值在之前已经提到过,其类似于一个计数器,记录了当前ArrayList容器被进行“写”性质操作的次数,在诸如add()、set()、remove()等“写”性质的操作方法中,该计数器都会进行增加。
-
记录了modCount值后,再对ArrayList容器进行实际的序列化操作。在这个过程中如果有其它线程对这个ArrayList容器进行了“写”性质的操作,那么modCount值就会增加, 且本线程进行的“序列化”操作也就没有意义。
-
最后,在完成序列化操作后,会对操作前的modCount值和当前ArrayList容器的modCount值进行比较,如果两个值不一样则抛出ConcurrentModificationException异常。
========
(接后文《源码阅读(4):Java中主要的List结构——ArrayList集合(下)》)