什么是ArrayList
ArrayList的底层是基于一个数组实现的,实际上ArrayList就是一个动态数组。
我们先直接上定义
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
ArrayList的一些特性
- ArrayList并不是线性安全的,在多线程环境下不能使用。
- 每个ArrayList实例都有一个容量属性,该容量属性是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素,其容量也自动增长。
- ArrayList实现了 RandomAccess、Cloneable、Serializable 接口,所以ArrayList
是支持快速访问、复制、序列化的。
ArrayList的属性
private static final int DEFAULT_CAPACITY = 10;
表示集合的默认大小
//空的数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
//这也是一个空的数组实例,和EMPTY_ELEMENTDATA空数组相比是用于了解添加元素时数组膨胀多少
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
两个空数组实例
//对象数组
transient Object[] elementData;
//表示集合的长度
private int size;
在这里有必要讲解一下transient,其作用为,当对象的属性加上transient关键字后,该对象被序列化时,此属性不会被保存。
我们需要注意:
- 当 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时将会清空 ArrayList
- 当添加第一个元素时,elementData 长度会扩展为 DEFAULT_CAPACITY=10
ArrayList的构造函数
无参构造函数
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
此无参构造函数将创建一个 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 声明的数组,其初始容量为0,而不是10
注意,执行以下语句时,集合的长度为0
ArrayList list = new ArrayList();
有参构造函数
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);
}
}
当传入参数大于0,则创建指定长度的数组,等于0则将elementData 指向空数组,小于0直接抛出异常。
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;
}
}
其实相当于复制集合c到ArrayList中
ArrayList的一些方法
增加操作
public boolean add(E e) {
ensureCapacityInternal(size + 1); //添加元素之前,首先要确定集合的大小
elementData[size++] = e;
return true;
}
如上所示,给数组增加元素之前,我们需要调用ensureCapacityInternal方法来确定集合的大小,若集合已满,我们需要进行扩容操作。
//这里的minCapacity 是集合当前大小+1
private void ensureCapacityInternal(int minCapacity) {
//elementData 是实际用来存储元素的数组,注意数组的大小和集合的大小不是相等的,前面的size是指集合大小
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
我们看到这个方法内部又调用了两个方法
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
//扩容
grow(minCapacity);
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//如果数组为空,则从size+1的值和默认值10中取最大的
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;//不为空,则返回size+1
}
ensureExplicitCapacity调用的grow方法用于扩容,源代码如下
private void grow(int minCapacity) {
int oldCapacity = elementData.length;//得到原始数组的长度
int newCapacity = oldCapacity + (oldCapacity >> 1);//新数组的长度等于原数组长度的1.5倍
if (newCapacity - minCapacity < 0)//当新数组长度仍然比minCapacity小,则为保证最小长度,新数组等于minCapacity
newCapacity = minCapacity;
//MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 = 2147483639
if (newCapacity - MAX_ARRAY_SIZE > 0)//当得到的新数组长度比 MAX_ARRAY_SIZE 大时,调用 hugeCapacity 处理大数组
newCapacity = hugeCapacity(minCapacity);
//调用 Arrays.copyOf 将原数组拷贝到一个大小为newCapacity的新数组(注意是拷贝引用)
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) //
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ? //minCapacity > MAX_ARRAY_SIZE,则新数组大小为Integer.MAX_VALUE
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
从中我们易得
- 当通过 ArrayList() 构造一个空集合,初始长度是为0的,第 1
次添加元素,会创建一个长度为10的数组,并将该元素赋值到数组的第一个位置。 - 第 2 次添加元素,集合不为空,而且由于集合的长度size+1是小于数组的长度10,所以直接添加元素到数组的第二个位置,不用扩容。
- 第 11 次添加元素,此时 size+1 = 11,而数组长度是10,这时候创建一个长度为10+10*0.5 = 15的数组(扩容1.5倍),然后将原数组元素引用拷贝到新数组。并将第 11 次添加的元素赋值到新数组下标为10的位置。
- 第 Integer.MAX_VALUE - 8 = 2147483639,然后
2147483639%1.5=1431655759(这个数是要进行扩容) 次添加元素,为了防止溢出,此时会直接创建一个
1431655759+1 大小的数组,这样一直,每次添加一个元素,都只扩大一个范围。 - 第 Integer.MAX_VALUE - 7 次添加元素时,创建一个大小为 Integer.MAX_VALUE
的数组,在进行元素添加。 - 第 Integer.MAX_VALUE + 1 次添加元素时,抛出 OutOfMemoryError 异常。
我们看到上面方法调用时用到了一个属性modCount
protected transient int modCount = 0;
该变量是定义在 AbstractList 中的。记录对 List 操作的次数。主要使用是在 Iterator时,防止在迭代的过程中集合被修改。
删除元素
删除指定索引的元素
public E remove(int index) {
rangeCheck(index);//判断给定索引的范围,超过集合大小则抛出异常
modCount++;
E oldValue = elementData(index);//得到索引处的删除元素
int numMoved = size - index - 1;
if (numMoved > 0)//size-index-1 > 0 表示 0<= index < (size-1),即索引不是最后一个元素
//通过 System.arraycopy()将数组elementData 的下标index+1之后长度为 numMoved的元素拷贝到从index开始的位置
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; //将数组最后一个元素置为 null,便于垃圾回收
return oldValue;
}
删除首次出现的元素
public boolean remove(Object o) {
if (o == null) {//如果删除的元素为null
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {//不为null,通过equals方法判断对象是否相等
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;
}
修改元素
把索引index处的元素替代为element
public E set(int index, E element) {
rangeCheck(index);//判断索引合法性
E oldValue = elementData(index);//获得原数组指定索引的元素
elementData[index] = element;//将指定所引处的元素替换为 element
return oldValue;//返回原数组索引元素
}
检查索引合法性的函数
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
查找元素
根据索引查找元素
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
根据元素查找索引
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
回收多余的内存,令集合的数组大小刚好调整为集合元素的大小。
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = Arrays.copyOf(elementData, size);
}
}
在ArrayList的实现中大量使用了Arrays.copyof()和System.arraycopy()方法,下面我们对其进行一定的讲解。
Arrays.copyof()
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
明显调用了另外一个方法,其中最后一个参数指明要转换的数据的类型。下面我们分析其源码
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
该方法实际上是在其内部又创建了一个长度为newlength的数组,调用System.arraycopy()方法,将原来数组中的元素复制到了新的数组中。
System.arraycopy()
该方法被标记了native,调用了系统的C/C++代码,在JDK中是看不到的,但在openJDK中可以看到其源码。该函数实际上最终调用了C语言的memmove()函数,因此它可以保证同一个数组内元素的正确复制和移动,比一般的复制方法的实现效率要高很多,很适合用来批量处理数组。Java强烈推荐在复制大量数组元素时用该方法,以取得更高的效率。
ArrayList和Vector的区别之处
- ArrayList在内存不够时默认是扩展50% + 1,Vector是默认扩展1倍
- Vector属于线程安全级别的,但是大多数情况下不使用Vector,因为线程安全需要更大的系统开销
- Vector提供indexOf(obj, start)接口,ArrayList没有
对ArrayList的总结
- 在确定ArrayList的元素数量之后使用ArrayList较好,否则建议使用LinkedList,因为若不确定元素数量可能会不断发生数组扩容,此操作非常耗时。
- ArrayList基于数组实现,可以通过下标索引直接查找元素,但进行增加或删除操作时需要大量移动元素,故查找效率高,增加或删除效率低。
- ArrayList中允许元素为null,故在查找给定元素索引值等的方法中,源码都将该元素的值分为null和不为null两种情况处理。