ArrayList源码解析
一、概述
ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null元素,底层通过数组实现。除该类未实现同步外,其余跟Vector大致相同。每个ArrayList都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。这里的数组是一个Object数组,以便能够容纳任何类型的对象。
size(), isEmpty(), get(), set()方法均能在常数时间内完成,add()方法的时间开销跟插入位置有关,addAll()方法的时间开销跟添加元素的个数成正比。其余方法大都是线性时间。 为追求效率,ArrayList没有实现同步(synchronized),如果需要多个线程并发访问,用户可以手动同步,也可使用Vector替代,也可以使用集合工具类变成安全的。
java引入了一个可以保证读和写都是线程安全的集合(读写分离集合):CopyOnWriteArrayList
二、源码解读
继承和实现的接口
public class ArrayList<E> extends AbstractList<E>
// 实现RandomAccess接口,支持随机访问(也就是根据下标访问)
// 实现Cloneable接口, 可以进浅拷贝
// 实现Serializable接口,可以进行序列化和反序列化
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
底层数据结构
//ArrayList的存储数据数组,根据传入的容量来根据设置
// transient 表示不进行序列化
transient Object[] elementData; // non-private to simplify nested class access
属性
private static final long serialVersionUID = 8683452581122892189L;
//默认的初始容量
private static final int DEFAULT_CAPACITY = 10;
// 空数组,如果传入的容量为0时使用
private static final Object[] EMPTY_ELEMENTDATA = {};
//空数组,在第一次add也就是第一次插入数据的时候进行初始化容量(10)
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//ArrayList的存储数据数组,根据传入的容量来根据设置
// transient 表示不进行序列化
transient Object[] elementData; // non-private to simplify nested class access
// 数组的元素个数
private int size;
// 用来计算和限制容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
构造方法
// 传入容量的初始化
public ArrayList(int initialCapacity) {
// 如果传入的数据大于0,数组初始化使用传入的数据
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
// 如果传入的数据为0,则初始化为空数组
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
// 如果不传入,设置为空数组,在第一次add的时候进行初始化,默认容量为10
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//传入一个集合,进行初始化
public ArrayList(Collection<? extends E> c) {
//转化为数组
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// 检查c.toArray()返回的是不是Object[]类型,如果不是,重新拷贝成Object[].class类型
if (elementData.getClass() != Object[].class)
// 进行复制
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 如果长度为0, 设置为空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
add(), addAll()
- add(E e) 平均时间复杂度为O(1)
- add(int index, E e)需要先对元素进行移动,然后完成插入操作,也就意味着该方法有着线性的时间复杂度。 addAll()方法能够一次添加多个元素,根据位置不同也有两个版本,一个是在末尾添加的
- addAll(Collection<? extends E> c)方法,一个是从指定位置开始插入的addAll(int index, Collection<? extends E> c)方法。跟add()方法类似,在插入之前也需要进行空间检查,如果需要则自动扩容;如果从指定位置插入,也会存在移动元素的情况。
- addAll()的时间复杂度不仅跟插入元素的多少有关,也跟插入的位置相关。
// 添加元素
public boolean add(E e) {
// 确保容量够,看是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
// 添加到指定索引的位置
public void add(int index, E element) {
// 检查索引是否合法
rangeCheckForAdd(index);
// 确保容量够
ensureCapacityInternal(size + 1); // Increments modCount!!
// 移动index之后的元素
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
// 根据传入的集合,来添加元素
public boolean addAll(Collection<? extends E> c) {
// 转换为数组
Object[] a = c.toArray();
int numNew = a.length;
// 确保容量够,看是否需要扩容
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
// 根据传入的集合和索引,来添加元素
public boolean addAll(int index, Collection<? extends E> c) {
// 检查索引是否合法
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
// 确保容量够,看是否需要扩容
ensureCapacityInternal(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
// 移动元素
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
扩容
每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。
数组扩容通过一个公开的方法ensureCapacity(int minCapacity)来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。 数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。
// 确保空间够用
public void ensureCapacity(int minCapacity) {
//如果数组不等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA则设置为0,否则设置为10,默认的初始容量
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
// 如果要求的最小容量大于当前的数组的容量得调用方法进行扩容
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
// 计算需要的最小容量
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));
}
// 确保空间够用
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;
// 如果新容量大于AX_ARRAY_SIZE= Integer.MAX_VALUE - 8 防止OOM异常
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;
}
检查索引
//索引合法检查
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//索引合法检查
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
get()获取元素
由于底层数组是Object[],得到元素后需要进行类型转换。
E elementData(int index) {
return (E) elementData[index];
}
// 根据索引获取元素
public E get(int index) {
// 检查索引是否合法
rangeCheck(index);
return elementData(index);
}
set()
底层是一个数组ArrayList的set()方法也就变得非常简单,直接对数组的指定位置赋值即可
// 根据索引设置元素的值
public E set(int index, E element) {
// 检查索引是否合法
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
remove()
remove()方法也有两个版本,一个是remove(int index)删除指定位置的元素,另一个是remove(Object o)删除第一个满足o.equals(elementData[index])的元素。删除操作是add()操作的逆过程,需要将删除点之后的元素向前移动一个位置。需要注意的是为了让GC起作用,必须显式的为最后一个位置赋null值。
// 根据索引删掉元素
public E remove(int index) {
// 检查索引是否合法
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
// 移动index之后的元素
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//清除该位置的引用,让GC起作用
elementData[--size] = null;
return oldValue;
}
// 根据元素的值,删除元素
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)
// 移动index之后的元素
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//清除该位置的引用,让GC起作用
elementData[--size] = null;
}
从源码中得出,ArrayList删除元素的时候并没有缩容。
trimToSize()
ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过trimToSize方法来实现
//将底层数组的容量调整为当前列表保存的实际元素的大小的
public void trimToSize() {
//操作数用于快速失败
modCount++;
if (size < elementData.length) {
//如果数组为空,设置为空数组,否则复制数组
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
retainAll(Collection<?> c)
// 删除不在这个集合的其他元素
public boolean retainAll(Collection<?> c) {
//判断是否为null
Objects.requireNonNull(c);
return batchRemove(c, true);
}
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
// 遍历整个数组,如果c中包含该元素,则把该元素放到写指针的位置(以complement为准)
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// 如果c.contains()抛出了异常,则把未读的元素都拷贝到写指针之后
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// 将写指针之后的元素置为空,帮助GC
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
removeAll(Collection<?> c)
// 删除集合里面包含的元素
public boolean removeAll(Collection<?> c) {
//判断是否为null
Objects.requireNonNull(c);
return batchRemove(c, false);
}
indexOf(), lastIndexOf()
// 根据元素查找索引的位置
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 int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
Fail-Fast机制
ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险