基本特性
对于集合具体实现类来说,首先需要掌握的基本特性是:
- 元素是否允许为null
- 元素是否允许重复
- 是否有序,指读取数据的顺序是否与存储数据的顺序一致
- 是否线程安全
对于ArrayList,如下表:
基本特性 | 结论 |
---|---|
元素是否允许为null | 是 |
元素是否允许重复 | 是 |
是否有序 | 是 |
是否线程安全 | 否 |
源码分析
本文使用的是JDK 1.8.0_201的源码。
成员变量
ArrayList是一个底层以数组实现的集合,它最主要的成员变量是:
成员变量 | 作用 |
---|---|
transient Object[] elementData; | elementData作为底层数组 |
private int size; | 集合中元素的个数,不同与elementData数组的长度 |
添加元素操作
ArrayList是用数组实现的,在Java中数组的长度是不可变的,数组在初始化的时候就要指定大小,我们知道ArrayList初始化时可以不指定大小,那么ArrayList是如何实现动态数组扩容的呢?
先看 add(E) 方法:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
ArrayList每次添加元素时,都首先调用ensureCapacityInternal(size + 1)方法,确保数组的容量。我们跟到ensureCapacityInternal(size + 1)方法中去:
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
// 这个值用于遍历时的快速失败,避免并发时导致的不可预料的错误
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
上面calculateCapacity(elementData, minCapacity)方法判断ArrayList初始化时是否指定了大小,如果没有指定大小返回默认大小10。接着,modCount++操作与扩容关系不大,它用于遍历时的快速失败,避免并发时导致的不可预料的错误。最后的条件判断才是真正进行数组扩容的地方,继续跟到grow(minCapacity)方法中去:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 将数组的大小扩大的原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
int newCapacity = oldCapacity + (oldCapacity >> 1),这行代码将数组的大小扩大的原来的1.5倍。而进行数组扩容的代码Arrays.copyOf(elementData, newCapacity),底层调用的是System.arraycopy()方法,这个方法是本地方法,效率比较高。我们自己在实现数组扩容时,也可以直接调用这个本地方法,提高程序性能。
删除元素操作
ArrayList支持两种删除方式:
- 按照下标进行删除
- 按照元素进行删除,ArrayList允许元素重复,这个操作只会删除第一个匹配的元素
对于ArrayList来说,由于其底层是数组实现,那么数组在删除元素后,需要将该元素之后的每个元素都向前移动一个位置,因此效率是比较低的。
两种删除方式的逻辑略有不同,但是底层的删除操作都是下面这段代码:
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
插入元素操作
ArrayList插入元素的操作也是使用的add()方法,只不过参数不同:
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
同样地,由于ArrayList是数组实现,因此插入元素将导致插入位置后的所有元素向后移动一位,因此效率不高。
ArrayList的优缺点
优点:
- 由于底层用数组实现,因此通过下标进行的随机访问,比如get(int index)、set(int index, E e)等操作会比较快。
- 由于底层用数组实现,每个元素存储占用的内存空间相对较小。
缺点:
- 由于底层用数组实现,在删除和插入元素时,需要移动元素,在元素较多情况下,效率会比较低。
综上所述,ArrayList只适合在对数据进行存储和访问的情况下使用,不适用频繁修改数据的场景。
ArrayList和Vector的区别
两者之间最大的区别就在于,ArrayList非线程安全,而Vector是线程安全的。尽管Vector是线程安全的,不代表在多线程的情况下就应该使用Vector。事实上我们应该避免使用Vector,因为Vector是在每个独立的方法上进行同步,而不是对整个集合数据进行同步,在进行迭代的时候可能会抛出ConcurrentModificationException。
除此之外,Vector即是“数组动态扩容”的实现又是同步操作的实现,违反了面向对象的“单一职责”设计原则。我们更应该使用装饰器模式的Collections.synchronizedList()。
无论是Vector还是Collections.synchronizedList(),采用的都是同步的方式来实现线程的安全性。这种方式将会降低并发性,当线程竞争激烈时,会严重影响程序的性能。Java 5.0提供了多种并发容器类,其中CopyOnWriteArrayList,用于在遍历操作为主要操作的情况下代替同步List。
为什么ArrayList的elementData是用transient修饰的?
ArrayList中的数组,是这么定义的:
private transient Object[] elementData;
ArrayList是可以序列化的,而elementData被transient关键字修饰后,将不会被序列化,那么为什么要这么做呢?
因为ArrayList序列化时,elementData数组不一定恰好就是满的,比如elementData数组大小为10,而真正只存储了3个元素,那么为了提高序列化速度和减少序列化文件大小,程序只需要序列化有数据的3个元素,而不是整个elementData数组。为此,ArrayList重写了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);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
参考
Java并发编程实战 P66-70
Stack Overflow :why-is-java-vector-and-stack-class-considered-obsolete-or-deprecated
五月的仓颉 :图解集合1:ArrayList