1. 概述
ArrayList是平常工作中非常常用的集合类之一,说起这个类,我就想起了我大学时期找实习的时候,经常被问起ArrayList与其他集合类的区别,我总是会像背书一样的说,ArrayList是基于数组,可添加重复元素,有序的,随机查找快但删除慢,那今天我们就从源码的角度分析一下,ArrayList为什么会具有这些特点.
2.源码分析(JDK1.8)
注:下面出现在代码全都出自JDK1.8的ArrayList源码,为了方便理解,所以拆分开来分析
首先,ArrayList中有两个非常重要的成员变量:
// 实际存放数据的结构,从这里我们可以看出,ArrayList确实是基于数组的
transient Object[] elementData;
// ArrayList的大小,大家经常调用的size()方法就直接返回该值
private int size;
public int size() {
return size;
}
看到这里我想大家一定有一个疑惑,平时我们实例化一个数组时,必须在实例化时指定数组的长度,并且长度不能改变,那么ArrayList基于数组,为什么可以做到一直添加呢?难道ArrayList中实例化了一个超大的数组吗?当然是不可能的了,看完add方法的代码后,我们就会理解,在此之前先让我们来看看它的构造方法:
// 无参构造方法,直接将一个静态的空数组赋值给elementData
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 传入一个数字为初始化数组的大小,在可以估算出该List大小的情况下,推荐使用这种方法,理由下面会讲
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);
}
}
private static final Object[] EMPTY_ELEMENTDATA = {};
// 该构造方法主要作用是将其他的集合类型(如List家族、Set家族等)转换成ArrayList
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
this.elementData = EMPTY_ELEMENTDATA;
}
}
看完了构造方法,我们现在来看看add方法来解除我们刚开始的疑惑:
public boolean add(E e) {
// 该方法判断数组长度是否足够,参数含义为需要的最小数组长度
ensureCapacityInternal(size + 1); // Increments modCount!!
// 将元素添加进数组,并将size加1
elementData[size++] = e;
return true;
}
private static final int DEFAULT_CAPACITY = 10;
private void ensureCapacityInternal(int minCapacity) {
// 若该List是无参构造方法实例化,并且第一次添加元素,则取需要的最小数组长度和默认长度10中较大的值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
// 操作次数加1,作用于fast-fail机制,这里暂时可不关注,不是本文的重点
modCount++;
// 终于到了重点的地方,如果需要的最小数组长度比当前数组长度大,则需要对当前数组进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 将数组长度扩容为之前的1.5倍,若扩容后还是达不到需要的最小数组长度,则直接扩容到最小数组长度
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 若扩容后的长度大于MAX_ARRAY_SIZE,则需要判断是否溢出了
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 真正的扩容操作,其实就是实例化一个更大的数组,然后将原数组中的所有值复制到新数组中(还记得之前说过如果能估算出ArrayList的大小,推荐使用给定长度的构造方法吗?原因就在这里,如果不指定大小,而一直添加元素的话,会不断的扩容,不断的复制数组到新数组,影响性能)
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
看到这里,我想大家都明白了概述中一开始提到的一些ArrayList的特点:
1.基于数组 : elementData成员变量
2.可添加重复元素 : 我们在添加时,并没有看到检测重复元素的代码,所以可以添加重复元素
3.有序的 : 既然是基于数组,那肯定是有序的,数组天然就是有顺序的
还有一点随机查找快但删除慢,好像还不能从以上代码中提现出来,我们继续看get()方法:
public E get(int index) {
// 检测index是否越界
rangeCheck(index);
// 返回该索引数组元素
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
E elementData(int index) {
return (E) elementData[index];
}
从get()方法的代码中我们可以看出,由于是基于数组,所以查找值直接是返回索引处的元素,时间复杂度为O(1),效率非常高.
至于删除慢我们来看一下remove()方法的代码,remove方法有两种,一种是根据索引删除,一种是根据元素删除,我们先来看第一种,根据索引删除:
public E remove(int index) {
// 检测索引越界
rangeCheck(index);
modCount++;
// 被移除的元素,需要作为方法的返回值
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
// 将index+1以及之后的元素复制到index以及之后,效果相当于将index之后的元素都向前移动了一位,这里就可以解释移除慢的特点了,因为当ArrayList中元素非常多的时候,这样的复制是很慢的
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 帮助GC回收垃圾
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
再来看另一种,根据元素删除:
// 该方法若移除成功返回true,否则返回false
public boolean remove(Object o) {
if (o == null) {
// 从初始位置遍历每个元素,找到和传入元素相同的,调用fastRemove方法进行移除
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
// 该方法和根据索引移除元素的代码一致
private void fastRemove(int index) {
modCount++;
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
}
从以上代码可以看出,根据元素移除元素,需要先找到该元素索引,再根据索引去复制覆盖进行移除.
3.总结
经过分析,我们从原理上理解了ArrayList为什么会有基于数组,可添加重复元素,有序的,随机查找快但删除慢这些特性,其实ArrayList中还有很多其他的方法,例如:
1. public int indexOf(Object o) : 返回元素o在数组中的索引,从0开始遍历
2. public int lastIndexOf(Object o) : 返回元素o在数组中的索引,从size处开始遍历
3. public void trimToSize() : 将elementData长度缩短到size大小.当我们将一个ArrayList扩容到很大,但是之后又将其中的元素remove了,这时,实际的数组长度很大,而存储的元素很少,就很浪费空间,这时可以调用一下这个方法,将其实际数组长度缩小.
4. public void add(int index, E element) : 在指定的位置添加一个元素,这个方法其实效率也并不是很高,因为从之前的源码分析中我们可以看出,在数组中间添加一个元素,肯定要将其之后的元素向后移动一位,也就需要进行复制操作.
还有一些方法这里就不一一列举了,感兴趣的同学可以自己去看看源码.
(完)