一、ArrayList 概述
摘自JDKAPI-1.6版本部分解释:ArrayList 实现了 List 接口的所有可选实现,是一个“大小可变数组”的容器,允许 null 元素和重复元素,此容器的操作是不同步的(线程不安全的)。在添加大量元素之前,我们可以在程序中调用 ensureCapacity 方法来增加 ArrayList 容器底层数组的大小容量,这样可以减少 ArrayList 扩容的次数,从而提高效率。ArrayList 除了实现了 List 接口之外还实现了 RandomAccess 接口,实现此接口的容器表明其支持快速随机访问,如果列表很大时应该使用 for(int i = 0,n = list.size; i < n; i++)的方式进行遍历容器。RandomAccess 是一个标记接口,所谓的标记接口标识着你可以采用 if (list instanceof ArrayList) 来判断使用循环的方式。同时 ArrayList 也实现了可克隆、可序列化。
二、总结 ArrayList 的关键点
1、ArrayList 是
大小可变的数组容器,按照放入元素的先后顺序排序,允许 null 元素,允许重复元素,多线程中不同步。
2、ArrayList 使用 for ( int i = 0, n = list.size(); i < n, i++) 的方式遍历比使用迭代器的方式效率要高。
3、实现了浅表克隆,可序列化。
三、结论分析
如果你看过 ArrayList 的 JDKAPI 解释和源码的实现,一定会产生一些疑问,下面对 ArrayList 的一些疑问点进行详细分析。
1、为什么 ArrayList 是大小可变的数组容器?
ArrayList 有3个构造器,无参构造器初始化一个底层数组容量默认为 10 的 ArrayList,使用者也可以使用指定容量的构造器,还可以构造一个包含指定 collection 的元素的容器。查看 JDK 源码可以看到,ArrayList 有一个私有属性是Object[] elementData,指定容量便是指定这个数组的大小。
private transient Object[] elementData;
构造完 ArrayList 之后,使用 add 方法新增元素时首先调用了 ensureCapacity 方法,它的参数为 size + 1。其中 size 表示的是 ArrayList 这个容器的元素数量,并不是 elementData 这个数组的长度,我们看一下 ensureCapacity 做了一些什么事情。
public void ensureCapacity(int minCapacity) {
// 修改次数 + 1
modCount++;
// 获取 ArrayList 的元素数组的长度
int oldCapacity = elementData.length;
// 如果 size + 1 大于元素数组的长度,就进行扩容,否则就忽略
if (minCapacity > oldCapacity) {
// 用 oldData[] 指向 elementData 的引用,是为了防止数组在复制的过程中被 GC回收掉
Object oldData[] = elementData;
// 将原元素数组的长度扩大到 1.5倍 + 1
int newCapacity = (oldCapacity * 3) / 2 + 1;
if (newCapacity < minCapacity)
newCapacity = minCapacity;
// 进行扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
从代码上可以看出,每次新增的时候会判断 ArrayList 的 size + 1 的数量是否大于了 elementData 的长度,如果没有那就不进行扩容,否则在原数组长度的基础上扩大 1.5 倍 + 1,具体扩容的代码就是 Arrays.copyOf ( elementData, newCapacity),那面再看一下 copyOf 这个方法。
public static T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
public static T[] copyOf(U[] original, int newLength, Class extends T[]> newType) {
// 如果源数组的类型是 Object 类型就按照 newLength 创建一个新的 Object 数组,否则创建一个和源数组类型一致的新数组
T[] copy = ((Object) newType == (Object) Object[].class) ? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
/**
* original 源数组
* 0 源数组中开始复制的起始位置
* copy 目标数组
* 0 目标数组中开始被复制的起始位置
* Math.min(original.length,newLength)要复制的数组元素的数量
*/
System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
return copy;
}
其中最后一句 System.arraycopy 的调用是本地方法,看不到源码,从其他代码中,我们可以得出一个结论:
ArrayList 的扩容实际上是增加它内部的元素数组的长度,增加的方式就是将旧的数组复制到新的数组中去,这个新的数组的长度是旧数组的 1.5倍+1 。
2、为什么 ArrayList 查询快,增删慢?
ArrayList 是可变数组,可变的原因是创建了一个新的大于原旧数组的数组,将旧数组的数据复制到新数组中,这种操作是消耗性能的。根据 ArrayList 提供的构造器,我们在具体的业务编程中,如果大概知道 ArrayList 中将存储多少元素,那么可以通过构造器指定 elementData 数组的大小,避免扩容以提高效率。ArrayList 查询快,增删慢也是相对其他储存容器而言的,可以根据具体业务自己实现 List 容器。至于查询快,因为底层是数组的原因,就不多解释了。
3、为什么 ArrayList 是可序列化的容器,但是 elementData 要用 transient 修饰。
transient 用来表示一个域不是该对象序列化的一部分,当一个对象被序列化时,transient 修饰的变量的值是不包括在序列化的行为中的。elementData 是 ArrayList 具体存放元素的成员,按道理来讲也是应该被序列化的,否则岂不是将会丢失元素吗?这是因为 ArrayList 实现了两个方法:
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 array length
s.writeInt(elementData.length);
// 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();
}
}
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in array length and allocate array
int arrayLength = s.readInt();
Object[] a = elementData = new Object[arrayLength];
// Read in all elements in the proper order.
for (int i = 0; i < size; i++)
a[i] = s.readObject();
}
ArrayList 在序列化时会调用 writeObject ,直接将 size 和 element 写入 ObjectOutputStream,反序列化时调用 readObject ,从 ObjectInputStream 中获取 size 和 element ,再恢复到 elementData。这样做的好处在于因为 ArrayList 扩容的机制,elementData 的实际大小一般是大于 size 的,它通常会预留一些容量,等容量不足了再扩充容量。那么有些空间可能就没有实际存储元素,采用上诉方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。
4、为什么 ArrayList 使用 for (int...) 循环比使用迭代器迭代的效率要高?
ArrayList 是 RandomAccess 接口的子类,它用来表明其支持快速随机访问,在程序中可以使用 instanceof 判断 List 的类型来决定使用哪种迭代方式。ArrayList 本身是一个实现随机访问的容器而 LinkedList 是连续访问的数据存储。 ArrayList 底层是数组结构,使用索引获取元素有天然的优势。
总结
作为一个开发人员,只是学会某个工具的使用方法,这并不完善。例如上面的分析,如果没有学习过源码,没有这些疑问,那么在编码时就不会考虑到效率问题,写出来的东西就是会比知道这些细节的同行差。如果你能知道扩容的原理,在知道存储量时你就能指定容量;如果你能知道 ArrayList 使用 for 循环比较快,那你就不会使用迭代器去迭代;如果你能知道 ArrayList 随机访问快,那么你就不会用其他容器进行随机访问。综上所述,要走的路还有很长,和别人不一样,就差这么一点点。