ArrayList
概述
ArrayList底层通过数组的方式来实现List接口,size、isEmpty、get、set等操作都是O(1)时间复杂度,而add是均摊常数时间复杂度(amortized constant time,可以理解为扩容的频率不太高,主要关注统计情况下的复杂度,因此均摊到所有操作上就是常数时间)。
size表示元素的数目,capacity表示当前数组的容量,ArrayList在添加元素时会进行判断,若数目超过了容量会进行自动扩容。
该容器不是线程安全的,可通过Collections.synchronizedList()方法来转化为线程安全容器,这应该在创建的时候完成。
iterator返回的迭代器是fail-fast的,就是说如果返回迭代器后,用除了ListIterator的remove和add的其他方法进行修改时,会抛出ConcurrentModificationException,这主要是为了避免一些随机的风险。
源码分析
- 构造器
主要的两个构造器如下:
// 创建一个默认容量的空列表
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 创建一个给定容量的空列表
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);
}
}
逻辑很简单,没有参数时是构造一个空数组,传入一个capacity时也只是判断一下数值得范围做检查而已。
EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个static final的空数组,其实就是用作一个共享的状态,既然都是空数组,为什么有两个?区别在于add第一个元素时的扩容机制不同,后面分析add时会看到。
- add
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
// 保证内部数组的容量能够容纳元素,必要时进行扩容
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// DEFAULT_CAPACITY为10,所以如果构造时没传容量,是默认的空列表,第一次添加元素时minCapacity为max(10,1)=10;如果是容量为0的空列表,这句不会执行。
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
// 增加修改次数
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
// 如果所需的容量比当前容量大,则进行扩容
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);
}
主要的逻辑都注释了,grow方法就是扩容的代码,主要就是扩充为1.5倍,如果没有溢出就用Arrays.copyOf复制到新的数组,可见这种方式复制数组是比较快的。
3. remove
// 删除索引上的元素。删除元素后,index后面的元素都会向前移动
public E remove(int index) {
// 检查范围的合法性
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
// 需要移动的元素数
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 注意到这一行,手动设为null是为了防止内存泄漏
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
// 删除数组中第一个对应的元素
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
// fastRemove的逻辑其实与上一个remove中的逻辑基本一样,移动后面元素,只是少了边界检查。
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
整体逻辑也比较简单,检查了范围后用System.arraycopy来移动元素,并将最后一个元素设为null防止内存泄漏。
另外也可以看到,删除元素时是不会缩小数组的容量的,如果要节省空间,可以使用trimToSize来收缩数组.
4. get和set
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
这两个方法就很简单了,检查一下范围就直接进行数组的对应操作。
总结
总的来说ArrayList比较简单,主要要知道底层是由数组实现的列表,非线程安全,了解其扩容机制,使用时就心中有数了。
ArrayList随机查找是常数时间,而随机增删元素时是线性时间,适合查找较多、修改较少的顺序存储的场景。