ArrayList 和 Vector 的比较是面试中一个常见的问题,要想回答得好,还需要下点工夫,本文来总结下它们之间的相似以及区别。
相似点
先看下相似点。首先我们都是由数组来实现的,并且它们都实现了 RandomAccess 接口,因此支持快速随机访问,也就是通过索引快速定位元素。因此从本质上来说,它们之间是可以能通用的。
区别
Vector 现在很少用到,这是因为它与 ArrayList 之间的区别造成的。
线程安全
Vector 是线程安全的类,因为它的大部分方法都是通过 synchronized 关键字进行同步的,例如 add()
方法
public synchronized boolean add(E e) {
// ...
return true;
}
然而 ArrayList 并不是线程安全的类,它的方法没有 synchronized 关键字,也没有通过锁来保证线程安全。
大部分时候,我们都不需要考虑线程安全,因此我们经常使用 ArrayList 而不是 Vector ,毕竟同步是需要付出一定的性能代价的。然而如果一旦考虑线程安全问题,我们还是可以使用 Vector 的。
虽然 ArrayList 不是线程安全的,但是我们还是有办法使之成为线程安全,例如我们可以在某个对象上进行同步,然后再操作 ArrayList 对象,例如下面的添加元素的操作
synchronized (object) {
arraylist.add("element");
}
但是这似乎显得比较笨拙,我们还可以在创建 ArrayList 对象时,使用 Collections.synchronizedList()
来创建一个 ArrayList 的包装类对象
List list = Collections.synchronizedList(new ArrayList());
这样是不是就方便很多。
ArrayList 还有一个线程安全的版本 CopyOnWriteArrayList,它的修改方法,例如 add()
, set()
都是在底层数组的拷贝上进行的,但是 CopyOnWriteArrayList 通常用于遍历操作的数量远远大于修改的数量的情况下。
扩容方式
首先看下 ArrayList 的扩容方式
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 扩容0.5倍大小
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);
}
ArrayList 的基本扩容策略是先扩容0.5倍,然后再根据实际需要进行调整。
再来看下 Vector 扩容方式
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 使用自定义扩容方式或者扩容1倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
capacityIncrement
表示扩容时的增长量,这个是在构造函数时赋值的,默认是0。
因此,Vector 的扩容策略是,如果指定的扩容的大小,那么就使用自定义的策略,否则扩容一倍。再根据需要进行调整。
通常,我们并不知道设置扩容量为多大,才最合适,因此都不会在构造函数中设置这个值,那么这个值默认就是0。因此 Vector 的基本扩容策略是扩容一倍。
我们并不能一概而论地说扩容策略谁优谁劣,要看实际情况而定的。
序列化
首先看下 ArrayList 的序列化
// tansient 修饰的数组,默认不会执行序列化操作
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();
}
}
ArrayList 背后的数组被 transient
关键字修饰,那么它就不会执行默认序列化操作,因此在 writeObject()
方法中要手动进行保存操作,为什么这样操作呢?
因为数组中可以存在一些空元素,如果使用默认序列化机制来保存数组的元素,那么这些空元素是会被保存的。那么在反序列化时,会额外为这些空元素分配不必须的空间。如此看来,ArrayList 的序列化操作还是比较高效的嘛。
我们再来看下 Vector 的序列化操作
protected Object[] elementData;
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
final java.io.ObjectOutputStream.PutField fields = s.putFields();
final Object[] data;
synchronized (this) {
fields.put("capacityIncrement", capacityIncrement);
fields.put("elementCount", elementCount);
data = elementData.clone();
}
fields.put("elementData", data);
s.writeFields();
}
首先我们注意到 Vector 背后的数组没有使用 transient
关键字修饰,但是它还是定义了 writeObject()
方法来自己处理序列化操作。可以看到,Vector 序列化,保存了数组的完整的克隆版本,其中就包括一些空元素。那么反序列化时,可以会浪费额外的空间。
因此在序列化问题上,ArrayList 可能比 Vector 更高效点。
结束
在平时的工作中,Vector 很少用到,但是并不是无用武之地,只是大部分时候我们不需要线程安全,因此 ArrayList 占据主流。并且在需要线程安全时,ArrayList 也提供了线程安全的方式,因此 Vector 渐渐退出历史舞台。