介绍
ArrayList采用一个Object[]数组来存储元素,并且具有默认的空间大小,所以我们采用new ArrayList()声明一个对象时,它是具有默认长度10的。
// java.util下的ArrayList.java
// 默认元素大小
private static final int DEFAULT_CAPACITY = 10;
我们知道,ArrayList并不是单纯的链表或者是数组,而是一种动态数组,在调用add方法增加元素时,通常不需要考虑容量的问题。那么ArrayList是如何实现扩容的呢?
首先是ArrayList的创建,ArrayList内部采用一个Object[]数组来存储对象:
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;//区别无参构造的DEFAULTCAPACITY_EMPTY_ELEMENTDATA
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
如果是调用空的构造方法,实际上做的事情和initialCapacity == 0分支很像,但是不是完全一样的,二者等号右边的变量并不相同:
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;//区别EMPTY_ELEMENTDATA
}
此处的EMPTY_ELEMENTDATA被定义成了一个静态的数组空间,并且设置为了final
// 用于存储数据的空间
private static final Object[] EMPTY_ELEMENTDATA = {};
当我们调用add方法时,依次调用下面三个方法:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
//如果当前的ArrayList采用默认构造函数生成,那么会赋值默认的容量,为10.
//如果使用带参构造函数initialCapacity = 0,添加了第一个元素后,由于elementData是EMPTY_ELEMENTDATA,而不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,他的minCapacity为0+1,传入的值为1
//如果是设置了初始非0容量的ArrayList,那么则会直接传入size + 1,这个size会和elementData.length取比较,如果空间不足,则会触发扩容,在初始容量为5的ArrayList下,第一次add不会触发扩容,size = 1,this.elementData.length = 5,当第5次添加时,则会由于size+1 = 6,而触发扩容,容量被扩充为7。
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
// 判断容量,是否进行扩容;
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
此处的grow(minCapacity)就是我们的扩容函数:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); //每次扩容n + n/2的空间,即1.5倍。
// 再判断一下新数组的容量够不够,够了就直接使用这个长度创建新数组,
// 不够就将数组长度设置为需要的长度
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//若预设值大于默认的最大值检查是否溢出
if (newCapacity - MAX_ARRAY_SIZE > 0)
//# MAX_ARRAY_SIZE : 2147483639
newCapacity = hugeCapacity(minCapacity);
// 调用Arrays.copyOf方法将elementData数组指向新的内存空间时newCapacity的连续空间
// 并将elementData的数据复制到新的内存空间
elementData = Arrays.copyOf(elementData, newCapacity);
}
显然,ArrayList并没有对add(e)、grow(args)等函数进行加锁,ArrayList是线程不安全的。 ArrayList的初始容量为10,而采用带有initialCapacity参数的构造方法构造的对象会在初始时就分配elementData空间,直到不够时才进行扩容。
采用默认构造函数时,则会在第一次add时为其分配空间、进行默认为10的扩容操作,因此如果我们需要声明一个容量为100的ArrayList,如果采用直接分配法,那么只需要创建一次,不需要扩容;而我们采用默认的构造方法,则内部用来存储元素的空间elementData容量会产生如下变化:(0[初始化]->10->15->22->33->49->73->109),扩容时元素的复制占据了大量的时间,所以如果知道ArrayList大概的长度,可以在初始化时为其优先分配空间。同样地,如果设置initialCapacity = 0 ,会从(0->1->2->3->4->6->9->······)进行扩容,在大容量的数据操作下,这三种方法的差距尤为明显,例如如下三个测试方法:
//不带有初始容量的ArrayList
public void _run() {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
list.add(i);
}
}
//带有初始容量的ArrayList耗时
public void _run() {
List<Integer> list = new ArrayList<>(1000000);
for (int i = 0; i < 1000000; i++) {
list.add(i);
}
}
public void _run() {
List<Integer> list = new ArrayList<>(0);
for (int i = 0; i < 1000000; i++) {
list.add(i);
}
}
运行结果如下:
不带有初始容量的ArrayList耗时:总耗时127 带有初始容量的ArrayList耗时:总耗时62 带有初始容量的ArrayList,初始容量为0,耗时:总耗时:97
总结
ArrayList基于数组实现,可以在O(1)时间复杂度内进行元素访问,
-
采用无参构造函数可以实现elementData数组的懒加载(即第一次add时才进行扩容elementData为默认长度10);
-
而当initialCapacity=0时,则是实现了以EMPTY_ELEMENTDATA为模板的初始空间,长度为0,在第一次add后,初始空间为1,同样可以实现懒加载。
-
而采用initialCapacity = X(X不为0),则在构造函数中执行了如下语句:
this.elementData = new Object[initialCapacity];
直接在构造函数阶段就为elementData赋值,且按照X的大小为其确定容量。