ArrayList源码分析
ArrayList的底层是动态数组,其容量可以动态增长。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
RandomAccess
是一个标志接口,表明实现这个这个接口的List集合是支持快速随机访问的。在ArrayList
中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。ArrayList
实现了Cloneable
接口 ,即覆盖了函数clone()
,能被克隆。ArrayList
实现了java.io.Serializable
接口,这意味着ArrayList
支持序列化,能通过序列化去传输。
ArrayList
采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)
方法的时候, ArrayList
会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置i插入和删除元素的话(add(int index, E element)
)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
属性
private static final long serialVersionUID = 8683452581122892189L;
// 集合的默认容量
private static final int DEFAULT_CAPACITY = 10;
// 用于空对象的共享空数组实例
// 当用户指定该ArrayList容量为0时,返回该空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 用于默认容量大小空对象的共享空数组实例。当用户没有指定ArrayList的容量时(即调用无参构造函数),返回的是该数组。把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 保存ArrayList数据的数组
transient Object[] elementData;
// 当前集合包含的元素数量,也就是集合的实际长度
private int size;
还有一个从父类AbstractList继承过来的属性modCount,用于记录ArrayList集合的修改次数。结构修改是指更改列表的大小的结构修改,或以其他方式干扰列表进度的迭代可能产生不正确结果的方式。
protected transient int modCount = 0;
构造函数
创建集合时,如果不传入参数,则使用默认无参构建方法创建ArrayList对象,当进行第一次add的时候,elementData将会变成默认的长度:10,即容量扩为10。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
也可以在创建集合时,指定ArrayList的初始数组长度,即传入一个int型的参数,传入参数如果是大于0,则使用用户的参数初始化;如果传入的参数等于0,则创建的的是一个空对象的共享空数组实例;如果用户传入的参数小于0,则抛出异常:
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);
}
}
也可以在创建ArrayList的时候,传入一个指定的Collection对象,按照它们由集合的迭代器返回的顺序存入数组列表。
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
它的具体执行逻辑是:
1)将collection对象转换成数组,然后将数组的地址的赋给elementData。
2)更新size的值,同时判断size的大小,如果是size等于0,直接将空对象EMPTY_ELEMENTDATA的地址赋给elementData
3)如果size的值大于0,则执行Arrays.copy方法,把collection对象的内容(可以理解为深拷贝)copy到elementData中。
注意:this.elementData = arg0.toArray(); 这里执行的简单赋值时浅拷贝,所以要执行Arrays,copy做深拷贝
注意:JDK7 new无参构造的ArrayList对象时,直接创建了长度是10的Object[]数组elementData 。jdk7中的ArrayList的对象的创建类似于单例的饿汉式(先创建好,等待被使用),而jdk8中的ArrayList的对象的创建类似于单例的懒汉式(使用时才创建)。JDK8的内存优化也值得我们在平时开发中学习。
扩容机制
add方法
// 将指定的元素追加到此列表的末尾
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
添加元素之前,先调用ensureCapacityInternal方法,查看一下该方法的底层逻辑:
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 获取默认的容量和传入参数的较大值
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
传入到ensureCapacityInternal方法中的参数值是size加1,即minCapacity = size + 1,这是当前加入一个新元素后所需的容量,该方法也就是要确保当前数组是否有足够容量存下这个minCapacity数量的元素。
ensureCapacityInternal方法调用ensureExplicitCapacity之前,会先将当前最新的元素数量传入calculateCapacity方法,用于计算当前对象数组所需的容量,然后作为参数传给ensureExplicitCapacity方法。
在calculateCapacity方法内部,如果当我们使用无参构造函数创建数组列表时,没有指定容量大小,其内部对象数组的实例是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,当我们调用add方法第一次添加元素时,传入的minCapacity为1,在经过Math.max方法比较之后,对象数组所需的容量大小也就变为了10。
然后将对象数组的所需的容量大小传给ensureExplicitCapacity方法,用于判断是否需要扩容,如果当前所需的容量大于对象数组的长度,则调用grow方法进行扩容。
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
//调用grow方法进行扩容,调用此方法代表已经开始扩容了
grow(minCapacity);
}
用于标识arraylist修改记录的modcount会自增1,表示集合的结构修改了一次。
所以add方法的逻辑可以总结为以下几步:
(1)首先判断数组列表已经使用的长度加上1之后是否足够存下当前这个新元素
1)如果add添加的是数组列表的第一个元素,此时elementData.length为0 (因为还是一个空的list),此时执行了 ensureCapacityInternal()方法 , minCapacity会被设置为默认容量10。;
2)如果add添加的不是数组列表的第一个元素,而此时传入ensureCapacityInternal方法的参数值是已有元素数量加1(size+1),那么在calculateCapacity方法中就直接返回的是这个值;
(2)ensureExplicitCapacity方法接收calculateCapacity计算的minCapacity值,与当前对象数组的容量进行比较,如果minCapacity - elementData.length > 0则会进行扩容,否则不扩容。
(3)将元素e添加到对象数组中。
将指定的元素插入此列表中的指定位置。同时将当前位于该位置的元素(如果有的话)和任何它后面位置的元素右移(将它们的索引加一)。
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
//arraycopy()方法此时就是将index位置及其之后的元素全部向后挪动1个位置
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
该方法的执行逻辑是:
该方法先调用rangeCheckForAdd对index进行界限检查;
然后调用ensureCapacityInternal方法保证capacity足够大,同时增加该集合的修改记录数;
再将从index开始之后的所有成员后移一个位置;
将element插入index位置;最后size加1。
此处的界限检查函数要保证指定的元素插入位置既要大于0,也要位于小于等于当前数组列表含有的元素数量,其源码如下:
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
此处的size是ArrayList的大小(它包含的元素数)。
grow方法
// 要分配的最大数组大小
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 给数组列表扩容的核心方法,通过该方法会增加数组列表的容量,以确保至少能够容纳minCapacity参数指定的元素数。
private void grow(int minCapacity) {
// overflow-conscious code
// 旧容量
int oldCapacity = elementData.length;
// 新的容量,使用位运算比算术运算更加的快
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);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
从newCapacity = oldCapacity + (oldCapacity >> 1)这段代码可以看出,每次扩容时,首先采取的方法是将容量变为原先容量的1.5倍左右。进行1.5倍左右的扩容操作之后,会与minCapacity参数进行比较,如果仍小于minCapacity,那么就将minCapacity的值直接赋值给新容量变量。
如果新容量大于MAX_ARRAY_SIZE,则进入(执行) hugeCapacity()方法来比较minCapacity和 MAX_ARRAY_SIZE,如果minCapacity大于最大容量MAX_ARRAY_SIZE,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为MAX_ARRAY_SIZE即为Integer.MAX_VALUE - 8。
System.arraycopy()和Arrays.copyOf()
System.arraycopy
从指定的源数组复制数据,从源数组中指定的位置开始复制,将复制以后的数据复制到目标数组的指定位置。
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
其参数含义如下:
src:源数组
srcPos:要复制源数组中的数据的起始位置
dest:目标数组
destPos:将复制的数据放置到目标数组中的起始位置。
length:要复制的数组元素的数量
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) {
@SuppressWarnings("unchecked")
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;
}
copyOf有两个参数:
T[] original 源数组
int newLength 指定新数组的长度
copyOf底层调用的依旧是System.arraycopy方法,该方法不用调用者指定目标数组,而是自己在底层创建了一个用于存储源数组中被复制数据的数组copy。它进行数组复制时,是将源数组从索引位置为0的地方开始复制,复制到copy中,存储位置也是从0开始。要复制的元素数量是newLength和源数组元素数量二者之间的最小值。最终该方法会返回copy。
二者区别
arraycopy()
需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置
copyOf()
是系统自动在内部新建一个数组,并返回该数组。
案例
public static void main(String[] args) {
int[] a = {1,2,3,4,5};
int[] b = new int[10];
// System.arraycopy(a,0,b,0,10);
// java.lang.ArrayIndexOutOfBoundsException
//System.arraycopy(a,0,b,0,5);
// [1, 2, 3, 4, 5, 0, 0, 0, 0, 0]
System.arraycopy(a,2,b,0,3);
// [3, 4, 5, 0, 0, 0, 0, 0, 0, 0]
System.out.println(Arrays.toString(b));
int[] ints = Arrays.copyOf(a, 6);
System.out.println(ints.length);
System.out.println(Arrays.toString(ints));
/*
6
[1, 2, 3, 4, 5, 0]
*/
}
使用System.arraycopy方法时,一定要注意源数组中被复制的起始位置与要复制的元素数量之和,即srcPos+length要小于等于源数组的长度,否则就会发生数组越界异常。
对于Arrays.copyOf方法,如果要复制的元素数量大于源数组中长度,则会使用数组类型对应的默认值填充。数组的类型是在运行时确定的。
其他方法
get
获取指定位置上的元素。
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
set
参数index的值要大于等于0,小于当前数组列表中的元素数量。获取指定位置(index)元素,然后放到oldValue存放,将需要设置的元素放到指定的位置(index)上,然后将原来位置上的元素oldValue返回给用户。
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
contains
调用indexOf方法,遍历数组中的每一个元素作对比,如果找到对于的元素,则返回true,没有找到则返回false。
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
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;
}
remove(int index)
删除此列表中指定位置index处的元素, 将index之后的所有元素向左移动(它们的索引都减去1)。
public E remove(int index) {
// 判断索引index是否越界,合法范围:0 =< index < size
rangeCheck(index);
// 自增结构修改次数
modCount++;
// 将原来在index位置上的值取出来给oldValue
E oldValue = elementData(index);
// 计算出要左移的元素数量
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
return oldValue;
}
注意:调用这个方法不会缩减对象数组的容量,只是将最后一个数组元素置空而已。比如看下面的案例,创建一个ArrayList,添加完三个元素之后,此时ArrayList底层的对象数组elementData的容量应该是默认值10,当采用remove方法移除索引为1的元素之后,elementData数组的长度依旧是10,但是其中的元素数量已经变为2。
public static void main(String[] args) {
ArrayList<Object> objects = new ArrayList<>();
objects.add(1);
objects.add(1);
objects.add(1);
objects.set(0,2);
objects.remove(1);
System.out.println(objects.size());
}
remove(Object o)
循环遍历所有对象,得到对象所在索引位置,然后调用fastRemove方法,执行remove操作。如果该列表中有多个与指定对象相等的元素,那么该方法只会删除列表中出现的第一个与之相等的元素。如果列表不包含该元素,则它不会更改,此时返回false。如果此列表包含指定的元素,则在删除后,会返回true。
public boolean remove(Object o) {
if (o == null) {
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
}
clear
添加操作次数(modCount),将数组内的元素都置空,等待垃圾收集器收集,不减小数组容量。
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
trimToSize方法
将elementData中空余的空间(包括null值)去除,例如:数组长度为10,其中只有前三个元素有值,其他为空,那么调用该方法之后,数组的长度变为3.
public void trimToSize() {
// 修改结构记录数加1
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
toArray()
以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。返回的数组将是“安全的”,因为该列表不保留对它的引用。(换句话说,这个方法必须分配一个新的数组)。因此,调用者可以自由地修改返回的数组。此方法充当基于阵列和基于集合的API之间的桥梁。
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
需要遍历数组的方法
clear、remove(Object o)、contains