ArrayList源码解析
概要
概括的说,ArrayList
是一个动态数组,他是线程不安全的,允许元素为null。
其底层数据结构依然是数组,它实现了List<E>
,RandomAccess
, Cloneable
, Serializable
接口,其中RandomAccess
代表了其拥有快速随机访问的能力,ArrayList
可以以O(1)的时间复杂度去根据下表访问元素。
RandomAccess
: 标记接口, 标记实现该接口的集合使用索引遍历比迭代器更快.
Serializable
: 标记接口, 标记实现该接口的类可以序列化。
Cloneable
: 标记接口, 标记实现该接口的类可以调用 clone 方法, 否则会抛出CloneNotSupportedException
(克隆不被支持)异常.
因其底层数据结构是数组,所以可想而知,它是占据一块连续的内存空间(容量就是数组的length
),所以它也有数组的缺点,空间效率不高。
由于数组的内存连续,可以根据下标以O1的时间读写(改查)元素,因此时间效率很高。
当集合中的元素超出这个容量,便会进行扩容操作。扩容操作也是ArrayList
的一个性能消耗比较大的地方,所以若我们可以提前预知数据的规模,应该通过public ArrayList(int initialCapacity) {}
构造方法,指定集合的大小,去构建ArrayList
实例,以减少扩容次数,提高效率。
或者在需要扩容的时候,手动调用public void ensureCapacity(int minCapacity) {}
方法扩容。
不过该方法是ArrayList
的API,不是List
接口里的,所以使用时需要强转:
((ArrayList)list).ensureCapacity(30)
;
当每次修改结构时,增加导致扩容,或者删,都会修改modCount
。
成员变量
// 默认初始容量为10
private static final int DEFAULT_CAPACITY = 10;
// 有参构造函数的数组容量为0时,赋值给elementData的空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认无参构造函数里的空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 存储集合元素的底层实现:真正存放元素的数组
transient Object[] elementData;
// 当前数组元素的数量
private int size;
// 数组最大容量
private static final int MAX_ARRAY_SIZE = 2147483639;
构造函数
// 默认构造函数
public ArrayList() {
//默认构造方法只是简单的将空数组赋值给了elementData
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//带初始容量的构造方法
public ArrayList(int initialCapacity) {
//如果初始容量大于0,则新建一个长度为initialCapacity的Object数组.
//注意这里并没有修改size(对比第三个构造函数)
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//如果容量为0,直接将EMPTY_ELEMENTDATA赋值给elementData
this.elementData = EMPTY_ELEMENTDATA;
} else {
//容量小于0,直接抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//利用别的集合类来构建ArrayList的构造函数
public ArrayList(Collection<? extends E> c) {
//直接利用Collection.toArray()方法得到一个对象数组,并赋值给elementData
elementData = c.toArray();
//因为size代表的是集合元素数量,所以通过别的集合来构造ArrayList时,要给size赋值
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
//这里是当c.toArray出错,没有返回Object[]时,利用Arrays.copyOf 来复制集合c中的元素到elementData数组中
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
//如果集合c元素数量为0,则将空数组EMPTY_ELEMENTDATA赋值给elementData
this.elementData = EMPTY_ELEMENTDATA;
}
}
常用API
增加
每次add之前,都会判断add后的容量,是否需要扩容。
- 添加单个元素
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;//在数组末尾追加一个元素,并修改size
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//利用 == 可以判断数组是否是用默认构造函数初始化的,如果是的话,minCapacity改为10和minCapacity中较大的那个
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;//如果确定要扩容,会修改modCount
// 如果容量比当前数组的容量大,那么进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//需要扩容的话,默认扩容一半
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);//默认扩容一半
if (newCapacity - minCapacity < 0)//如果还不够 ,那么就用 能容纳的最小的数量。(add后的容量)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);//拷贝,扩容,构建一个新数组,
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // 整数溢出
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
// 在index的位置添加一个元素,index开始的数组,往后移动一位
public void add(int index, E element) {
rangeCheckForAdd(index);//越界判断 如果越界抛异常
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index); //将index开始的数据 向后移动一位
elementData[index] = element;
size++;
}
- 添加多个元素
// 将指定集合中的所有元素按指定结合的迭代器返回的顺序追加到此列表的末尾
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew);
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
// 从指定位置开始, 将指定集合中的元素插入此列表. 该位置及其之后的元素后移.
public boolean addAll(int index, Collection<? extends E> c) {
// 判断索引是否超出界限, 超出抛出 IndexOutOfBoundsException 异常.
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // 确认是否需要扩容
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;
}
总结:
add、addAll
- 先判断是否越界,是否需要扩容
- 如果扩容,就复制数组
- 然后设置对应下标元素值
注意:
- 如果需要扩容的话,默认扩容一半。如果扩容一半不够,就用目标的size作为扩容后的容量。
- 在扩容成功后,会修改modeCount。
删除
public E remove(int index) {
rangeCheck(index); //判断是否越界
modCount++; //修改modeCount 因为结构改变了
E oldValue = elementData(index); //读出要删除的值
int numMoved = size - index - 1;
// 如果不是最后一个元素
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);//用复制 覆盖数组数据
// 置空原尾部数据 不再强引用, 可以GC掉
// 如果没有elementData[--size]==null, 可能会导致内存泄漏, 若没有这一步操作, 该内存一直指向之前的元素, GC 不会认为它是垃圾, 故无法回收内存造成内存泄漏.
elementData[--size] = null;
return oldValue;
}
//根据下标从数组取值 并强转
E elementData(int index) {
return (E) elementData[index];
}
//删除该元素在数组中第一次出现的位置上的数据。 如果有该元素返回true,如果false。
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);//根据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++;//修改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 //置空 不再强引用
}
//批量删除
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);//判空
return batchRemove(c, false);
}
//批量移动
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;//w 代表批量删除后 数组还剩多少元素
boolean modified = false;
try {
//高效的保存两个集合公有元素的算法
for (; r < size; r++)
if (c.contains(elementData[r]) == complement) // 如果 c里不包含当前下标元素,
elementData[w++] = elementData[r];//则保留
} finally {
// 出现异常会导致 r !=size , 则将出现异常处后面的数据全部复制覆盖到数组里。
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r; //修改 w 数量
}
if (w != size) {//置空数组后面的元素
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;//修改modCount
size = w;// 修改size
modified = true;
}
}
return modified;
}
从这里我们也可以看出,当用来作为删除元素的集合里的元素多于被删除集合时,也没事,只会删除它们共同拥有的元素。
总结:
删除操作一定会修改modCount,且可能涉及到数组的复制,相对低效。
修改
不会修改modCount,相对于增删,修改是高效的操作。
// 用指定元素替换列表中指定位置的元素.
public E set(int index, E element) {
rangeCheck(index);//越界检查
E oldValue = elementData(index); //取出旧元素
elementData[index] = element;//用新元素覆盖旧元素
return oldValue;//返回旧元素
}
查找
不会修改modCount,相对于增删,查找是高效的操作。
// 告诉编译器忽略 unchecked 警告信息
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
// 获取列表指定位置的元素.
public E get(int index) {
// 越界检查
rangeCheck(index);
return elementData(index);
}
// 返回指定元素第一次出现的索引,如果列表中没有此元素, 返回 -1
public int indexOf(Object o) {
if (o == null) {
// 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;
}
// 返回指定元素最后一次出现的索引,如果列表中没有此元素, 返回 -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;
}
清空 clear
会修改modeCount
public void clear() { modCount++;//修改modCount for (int i = 0; i < size; i++) //将所有元素置null,方便GC去清理 elementData[i] = null; size = 0; //修改size }
包含 contain
// indexOf() 普通的for循环寻找值,只不过会根据目标对象是否为null分别循环查找。public boolean contains(Object o) { return indexOf(o) >= 0;}// 返回指定元素第一次出现的索引,如果列表中没有此元素, 返回 -1public 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;}
判空 isEmpty
public boolean isEmpty() { return size == 0;}
缩容 trimToSize
ArrayList虽然没有自动缩容,但是提供了trimToSize方法让使用者可以进行手动扩容。
public void trimToSize() { // 如果elementData的长度大于size,则说明有剩余空间,可以进行缩容 ++this.modCount; if (this.size < this.elementData.length) { this.elementData = this.size == 0 ? EMPTY_ELEMENTDATA : Arrays.copyOf(this.elementData, this.size); }}
转为数组 toArray
// 按正确顺序包含此列表中所有元素的数组,返回的数组为 elementData 的复制// 修改返回的数组不会对 elementData 造成影响.public Object[] toArray() { // 直接复制 elementData, 然后返回 return Arrays.copyOf(elementData, size);}// 返回运行时指定数组类型的数组.// 如果指定数组的容量小于 size, 新建一个容量为size的数组返回.// 否则将数据复制到指定数组返回.@SuppressWarnings("unchecked")public <T> T[] toArray(T[] a) { if (a.length < size) // 利用反射, 返回一个大小为size , 类型和 a 相同的新数组. return (T[]) Arrays.copyOf(elementData, size, a.getClass()); // 将数据复制到 a 中 System.arraycopy(elementData, 0, a, 0, size); // 将 a[size] 置为 null(如果有的话), 对以后确定列表的长度很有用, 但只在调用方知道列表中不包含任何 null 元素时才有用. if (a.length > size) a[size] = null; return a;}
System.arrayCopy()参数意义
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)Object src : 原数组int srcPos : 从原数据的起始位置开始Object dest : 目标数组int destPos : 目标数组的开始起始位置int length : 要copy的数组的长度
问题
elementData 为什么用 transient 修饰?
Java的ArrayList
中,定义了一个数组elementData
用来装载对象的,具体定义如下:
transient Object[] elementData;
transient用来表示一个域不是该对象序行化的一部分,当一个对象被序行化的时候,transient
修饰的变量的值是不包括在序行化的表示中的。但是ArrayList
又是可序行化的类,elementData
是ArrayList
具体存放元素的成员,用transient
来修饰elementData
,岂不是反序列化后的ArrayList
丢失了原先的元素?
其实玄机在于ArrayList中的两个方法:
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{}private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { }
ArrayList
在序列化的时候会调用writeObject,直接将size和element写入ObjectOutputStream
;
反序列化时调用readObject,从ObjectInputStream
获取size和element,再恢复到elementData
。
那么为什么不直接用elementData来序列化?
而是采用上述的方式来实现序列化呢?原因在于elementData
是一个缓存数组,它通常会预留一些容量,等容量不够的时候再扩容容量,那么有些空间可能就没有实际存储元素,我们不想在序列化的时候把这些空数据序列化,所以对elementData
添加transient
,使用自定义的方式进行序列化。
为什么 hugeCapacity() 会返回 Integer.MAX_VALUE?
private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // 整数溢出 throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;}
/** * The maximum size of array to allocate (unless necessary). * Some VMs reserve some header words in an array. * Attempts to allocate larger arrays may result in * OutOfMemoryError: Requested array size exceeds VM limit */private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
是因为部分虚拟机在数组中保存header words的头部字节需要占用8个字节,在这些虚拟机中分配大于MAX_ARRAY_SIZE
的空间会导致OOM,而在其他虚拟机中则可以正常扩容。
而grow
方法的目的是:提高容量以便至少满足最少的minCapacity
容量。
因为数组理论上的长度就是Integer.MAX_VALUE
,但并不是说一定因为个别的JVM就不让扩容到整数最大值的长度。