ArrayList方法流程分析
ArrayList代码流程
从一个简单的例子开始
List<Integer> list = new ArrayList<>();
list.add(1);
构造方法
ArrayList()
无参构造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 默认数组长度 10
}
使用无参构造方法创建对象时,会创建一个默认长度为10
的Object
数组
ArrayList(int initialCapacity)
在创建对象时也可以传递初始的数组长度,根据这个合法的容量来创建对应长度的Object数组
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
}
ArrayList(Collection<? extends E> c)
创建对象时传递另外一个集合,引用从另外一个集合中继承数据
public ArrayList(Collection<? extends E> c) {
// 获取集合的Object数组
Object[] a = c.toArray();
// 集合元素个数大于0
if ((size = a.length) != 0) {
// 如果传入的集合c是ArrayList
if (c.getClass() == ArrayList.class) {
// 因为和当前类类型相同,那么c的数组数据类型也是Object,所以直接赋值
elementData = a;
} else {
// c的内部数据类型可能不是Object数组,
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// 创建一个空的Object数组
elementData = EMPTY_ELEMENTDATA;
}
}
add方法
提到ArrayList的add方法就不得不重点关注ArrayList数组的扩容方法
ensureCapacityInternal(int minCapacity)
根据capacity来判断当前数组是否需要扩容
// minCapacity,即添加元素后容量的大小
// 例如执行了一次add方法,那么minCapacity为size+1
// 执行了一次addAll()方法,那么minCapacity则为size+c.size()
private void ensureCapacityInternal(int minCapacity) {
// 计算容量
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 计算容量
// elementData为具体存储数据的数组
// minCapacity为执行了add方法,数组存放元素的数量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果当前elementData数组为空的话
// 判断minCapacity和默认的数组容量哪个大,返回其中大的一方
// ps: 默认的数组容量为`10`
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
// 此时minCapacity为计算后的容量大小
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 差值大于0,则表示当前 elementData 数组已经无法容纳下新的元素了
// 需要进行数组的扩容
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;
// 判断新数组长度是否大于最大长度
// ps: 最大长度为 Integer.MAX_VALUE - 8
// 即 2147483639
// hugeCapacity用于保证新数组的长度最大不会超过Integer.MAX_VALUE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 将旧数组中元素拷贝到新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
add(E e)
public boolean add(E e) {
// 判断是否需要扩容
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
// size+1如果大于当前数组容量,则进行数组扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
add(int index, E element)
将数据插入到指定位置
public void add(int index, E element) {
// 判断index是否越界,如果越界(小于0||大于length)则抛出数组越界异常
rangeCheckForAdd(index);
// 判断是否需要扩容
ensureCapacityInternal(size + 1);
// 将index位置开始的数据拷贝到index+1位置
// [1] [2] [3] [4] [5] [ ] [] []
// [1] [0] [2] [3] [4] [5] [] []
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 修改index位置的数据
elementData[index] = element;
size++;
}
addAll(Collection<? extends E> c)
将集合c中的全部元素添加到当前ArrayList中
public boolean addAll(Collection<? extends E> c) {
// 集合c元素拷贝
Object[] a = c.toArray();
int numNew = a.length;
// 判断是否需要扩容
ensureCapacityInternal(size + numNew); // Increments modCount
// 将集合c中的数据拷贝到数组中
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
addAll(int index, Collection<? extends E> c);
将集合c中的全部元素添加到当前ArrayList的index位置上
public boolean addAll(int index, Collection<? extends E> c) {
// 检查是否有数组越界
rangeCheckForAdd(index);
// 集合c数组拷贝
Object[] a = c.toArray();
int numNew = a.length;
// 检查是否需要扩容
ensureCapacityInternal(size + numNew); // Increments modCount
// numMoved即需要移动几个位置
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;
}
remove方法
remove(int index)
删除指定位置的元素,并返回被删除的元素
public E remove(int index) {
// 检查index是否发生数组越界
rangeCheck(index);
modCount++;
// 获取将要被删除的元素
E oldValue = elementData(index);
// 要移动数字的个数
// eg: size=10,index=5
// 删除5位置的元素,则需要将6~9位置的元素向前移动
// numMoved则为 size - index - 1 = 10 - 5 - 1 = 4
// 则有4个元素需要移动
// 0 1 2 3 4 5 6 7 8 9
// m
// |--4--|
int numMoved = size - index - 1;
// numMoved大于0,则表示删除的是数组中间的元素
// 如果numMoved等于0的话,则表示删除的是数组末尾的数字,不需要移动
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将最后位置的数据引用置为null
// 以确保GC回收时回收该索引指向的对象
elementData[--size] = null; // clear to let GC do its work
// 返回被删除位置的数据
return oldValue;
}
remove(Object o)
删除数据o
// 删除具体的数据存在两种情况
// 删除的数据o为null和不为null
// 如果为null的话则遍历元素数组,找到第一个为null的位置,将其删除
// 如果不为null,则遍历元素数组,找到第一个equals为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;
}
// 和普通的remove类似
private void fastRemove(int index) {
modCount++;
// 需要移动元素的个数
int numMoved = size - index - 1;
// 大于0表示删除的元素在元素数组中间
// 如果等于0则表示删除的是最后一个元素,不需要再进行数组的拷贝来覆盖
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将最后位置引用置为null,方便垃圾回收
elementData[--size] = null; // clear to let GC do its work
}
**`removeAll(Collection c)`**
removeAll是一个很有意思的方法,它可以删除当前集合与集合c相交的部分
如
集合A: {1, 2, 3, 4, 5 , 6}
集合B: {7, 6, 5, 9, 0}
那么A.removeAll(B)
的结果则为{1, 2, 3, 4},删除了相交部分{5, 6}
public boolean removeAll(Collection<?> c) {
// // 传入的集合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;
boolean modified = false;
try {
// 遍历数组
for (; r < size; r++)
// 如果集合c中没有当前元素的话
// 将当前r位置的元素写入到w位置
// 如果此时c中含有当前位置元素即elementData[r]
// 则会跳过这个元素
// eg: A: {1, 2, 3, 4, 5}
// B: {3, 4}
// 那么则会有A集合中{3, 4}元素依次被覆盖的情况
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
// r不等于size,表示try代码块发生了异常
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
// w不等于size,表示当前集合中删除了元素
// 需要将删除位置的引用置为null
if (w != size) {
// 将余下的引用置为null,方便GC
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
trimToSize方法
ArrayList中只有add时会检查是否需要扩容并进行容量的自动扩充,扩容为原先的1.5倍。
ArrayList不会自动的进行缩容,在remove时,只会将索引位置的引用置为空,方便垃圾回收不需要的对象,没有自动缩容的实现,要想进行容量的缩减,则需要手动的调用trimToSize
方法
public void trimToSize() {
modCount++;
// size为存储数据的个数
// elementData为存储数据的具体的数组
if (size < elementData.length) {
// 如果size小于数组长度,即当前数组没有存满的情况
// 如果size为0的话,则将数组置为空数组{},注意不是**null**
// 如果size不为0,则将数组空余部分都删减掉
// eg: List size=4 cap=10
// 即List内部数组长度为10,但是只存储了4个元素
// 那么则会将数组的cap缩减为4
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
modCount
在ArrayList中经常会看到对modCount的操作,modCount是用来记录对集合操作的次数
如果单线程情况下,操作、修改和遍历ArrayList不会出现什么问题,但是如果多线程的环境下可能就会出现异常情况。在多线程情况下,我们使用iterator迭代器对ArrayList进行遍历时,就会检测出异常。
ArrayList集合中有这样的一个内部类
private class Itr implements Iterator<E> {
int cursor; // 下一个元素的索引
int lastRet = -1; // 最后一个元素的索引
int expectedModCount = modCount; // expectedModCount期望的修改次数
// ......省略了其他方法或属性
// 检测在遍历集合时,集合是否被修改
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
一个例子
// java.util.ConcurrentModificationException
public void doTest() {
ArrayList<Integer> list1 = new ArrayList<>();
list1.add(1);
// 因为foreach本质上是使用的iterator迭代器进行遍历
// 而迭代器在获取下一个元素时会执行上面的 checkForComodification 方法
// 来检测当前集合是否被修改
// 在foreach循环中我们向集合中添加了一个元素
// 这就导致了modCount与exceptModCount不一致,导致了抛出了异常
for (Integer i: list1) {
list1.add(1);
}
}
注意!!!
在代码中要是使用普通的for循环来遍历ArrayList集合的话
建议是提前保存集合的size
一方面是不必每次循环都调用一次方法
另外一方面是如果在for循环体中执行的add方法的话,会导致ArrayList的size一直变化,i < list1.size()
的判断条件一直不满足,导致死循环!
// 死循环!!!
public void doTest() {
ArrayList<Integer> list1 = new ArrayList<>();
list1.add(1);
for (int i = 0; i < list1.size(); i++) {
list1.add(1); // 调用的add导致size+1,即使i+1也永远不会满足i<size
System.out.println(list1.size()); // ......
}
}