本文已参与「新人创作礼」活动,一起开启掘金创作之路。
List是有序的Collection,ArrayList是最常用的List实现类。是一个非线程安全的集合。
1 ArrayList的数据基础
1.1 数据缓冲区(buffer)
Arraylist通过在内部维护一个Object数组来作为容器存储集合元素。这个数组被称为数据缓冲区。对于数据缓冲区,并不是每新增一个元素就进行一次数组的扩充。而是按照指定规则一次性扩充一定长度。
transient Object[] elementData; // ArrayList内部定义的作为数据缓冲区的数组
1.2 容量(capacity)
数据缓冲区的长度即为容量(capacity)。
1.3 大小(size)
集合中含有元素的数量即为大小(size),size实时维护。
private int size;
容量(capacity)与大小(size)是不同的概念:容量capacity是指容器的大小(数据缓冲区的长度),而size则表示容器中存放元素的数量。
1.4 modCount
ArrayList中维护了定义自父类AbstractList的整型属性modCount,用来记录集合本身发生的元素变化。通过遍历前记录modCount并在遍历过程中进行比对的方式来确认遍历过程中集合是否发生了改变。
protected transient int modCount = 0;
2 ArrayList的初始化
2.1 无参构造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
ArrayList的无参构造函数只做了一件事,将数据缓冲区elementData赋值为常量DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
根据源码可以看到DEFAULTCAPACITY_EMPTY_ELEMENTDATA只是一个空数组。这意味着ArrayList的无参构造方法并未直接申请一段连续内存来进行数据缓冲区的初始化,ArrayList的初始容量为0,ArrayList的数据缓冲区真正的初始化是在第一次执行Add操作时通过扩容完成的。是一种懒加载的方式。
2.2 带有初始容量的构造方法
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时则直接以常量EMPTY_ELEMENTDATA作为数据缓冲区:
private static final Object[] EMPTY_ELEMENTDATA = {};
可以看到EMPTY_ELEMENTDATA 和DEFAULTCAPACITY_EMPTY_ELEMENTDATA一样都是一个空数组,但是其代表的意义不同,EMPTY_ELEMENTDATA 表示一个指定0容量的数据缓冲区,DEFAULTCAPACITY_EMPTY_ELEMENTDATA则表示着一个未初始化的数据缓冲区。在扩容时能体现出其实际使用区别。
容量不可为负数,所以当指定容量小于0时抛出异常IllegalArgumentException。
2.2 带有初始元素集合的构造方法
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;
}
}
带有初始元素集合的ArrayList构造方法,直接用Collection的toArray方法获得初始集合的数组形式,并将其作为数据缓冲区。初始容量就是入参数据集合的元素数量,为了避免有的集合的toArray方法返回的数组不为Object[]类型,对toArray返回的数组进行类型判断,如果不是Object[]类型,则进行数组拷贝以保证数据缓冲区为Object[]类型的数组。
当元素数量为0时则会将EMPTY_ELEMENTDATA替代toArray返回的空数组作为数据缓冲区,这么做的目的一是保持一致性,二是使得toArray返回的空数组能被尽快回收,以回收堆空间。
3 ArrayList的扩容
3.1 内部私有的容量担保检查
ArrayList新增元素时,都会对当前容量进行担保检查,如果容量不足(数据缓冲区没有剩余的空间),那么就要内部自动进行数据缓冲区的扩容。
ArrayList的内部容量担保检查通过私有的ensureCapacityInternal(int minCapacity)方法进行:
3.1.M.1 ensureCapacityInternal(int minCapacity)
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
ensureCapacityInternal(int minCapacity)的入参minCapacity为数据缓冲区需要保证的最小容量。
ArrayList内部主要有两处调用ensureCapacityInternal(int minCapacity),分别是add方法和addAll方法,add方法的minCapacity为原容量+1,addAll的minCapacity为原容量+新增的元素数量。可以看到都是为了保证有足够的空间保证操作能正常完成。除此之外在readObject方法里面也有调用,不过目的是一样的,申请足够的空间完成本次元素新增操作。
再往下看,if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA),如果数据缓冲区等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则意味着这是一个未经初始化的数据缓冲区,需要重新计算minCapacity 。取值DEFAULT_CAPACITY和入参minCapacity作为新的minCapacity。
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
DEFAULT_CAPACITY 的值为10。这一步是保证内部扩容后的容量最小为10,也是为什么说ArrayList的初始容量是10的原因。这么做是为了避免容量过小时的频繁扩容。
接下来调用ensureExplicitCapacity(int minCapacity)方法,minCapacity作为入参:
3.1.M.1-1 ensureExplicitCapacity(int minCapacity)
private void ensureExplicitCapacity(int minCapacity) {
// modCount增加标记ArrayList发生改变
modCount++;
// overflow-conscious code
// 要保证缓冲区大小比数据量大
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
第一步进行了一个modCount++的操作,modCount在ArrayList内部被定义为:
protected transient int modCount = 0;
modCount被用来记录ArrayList产生改变的次数,为了保证遍历时数据的一致性,在ArrayList遍历过程中,如果对ArrayList进行修改的话就会抛出ConcurrentModificationException异常,这个异常的监测机制就是通过modCount实现的,在遍历之前会记录当前modCount,遍历过程中会一直监测modCount,如果modCount发生改变则停止遍历,遍历结束时判断modCount是否和遍历前一致,不一致就会抛出ConcurrentModificationException。
下一步将minCapacity与当前容量比较,如果当前容量满足需求,则结束。否则通过grow方法进行扩容:
3.1.M.1-1-1 grow(int minCapacity)
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
* - 进行容量扩充
*/
private void grow(int minCapacity) {
// overflow-conscious code
// 原数据缓冲区大小
int oldCapacity = elementData.length;
// 在原数据缓冲区大小的基础上扩充原大小的1/2
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);
}
可以看到每次默认的扩容量为原容量的1/2。但是如果基础扩容后的容量还达不到目标容量,就直接以入参容量值做为扩容目标容量。
目标容量需要进行溢出检查,与MAX_ARRAY_SIZE进行比较:
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
ArrayList中MAX_ARRAY_SIZE定义为Integer.MAX_VALUE - 8,如果目标容量大于MAX_ARRAY_SIZE ,则会作为巨大容量重新调整,进入hugeCapacity方法中:
3.1.M.1-1-1-1 hugeCapacity:
private static int hugeCapacity(int minCapacity) {
// 目标容量进行检查,如果小于0抛异常
// 其实是校验目标容量是否大于Integer.MAX_VALUE,这是因为通过运算得出的结果比Integer.MAX_VALUE大那么根据int的溢出原则就会得到小于0的值
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 如果目标容量大于MAX_ARRAY_SIZE就重置为Integer.MAX_VALUE,否则返回MAX_ARRAY_SIZE
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
因为数组的最大容量就是Integer.MAX_VALUE,因此hugeCapacity(int minCapacity)方法首先对目标容量进行校验,溢出的话就抛出OutOfMemoryError。否则就根据条件返回Integer.MAX_VALUE或者MAX_ARRAY_SIZE。
接下来回到3.1.M.1-1-1 grow(int minCapacity),使用Arrays.copyOf完成数据缓冲区的扩容。
3.2 公开的容量担保检查
除了在新增数据时内部私有的容量担保检查方法外,ArrayList还提供了一个公共的ensureCapacity(int minCapacity)方法给外部,让开发人员可以通过这个方法来进行自己的容量担保检查:
3.2.M.1 ensureCapacity(int minCapacity)
public void ensureCapacity(int minCapacity) {
// 如果数据缓冲区处于未初始化的状态,那么依然不初始化
// 否则的话最小容量定义为DEFAULT_CAPACITY
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);
}
}
此方法进行了两个校验:一是如果ArrayList的数据缓冲区处于未初始化的状态,则无论入参是多少,都不进行初始化。 二是:指定的容量如果小于DEFAULT_CAPACITY,也不进行容量容量担保检查。这么做是为了节省空间以及性能考虑,一个未初始化的ArrayList说明其处于未使用状态,不必为其先行分配内存空间。而如果指定容量小于DEFAULT_CAPACITY,则可能会频繁的触发扩容操作,而扩容又是一个较为耗时的操作。
接下来如果入参指定的容量合理的话,就会进入3.1.M.1-1 ensureExplicitCapacity(int minCapacity) 方法,此方法在3.1 内部私有的容量担保检查已进行过分析,这里就不再重复了。
4 ArrayList的缩容
缩容并不是指将集合元素删除,而是指回收未使用的空间。数组的内存要求是一段连续的内存空间,无论数组内有没有值,对应长度的内存空间都会被占用。而从上面扩容的部分可以知道,ArrayList的扩容并不是新增一个元素就扩容一个数组长度,而是一次性扩容多个长度。这是一种以空间换性能的方法。但是这也意味着大多数情况下,容量都是大于集合中元素的数量的,未使用的部分就造成了浪费。根据1/2扩容的原则,扩容次数越多,浪费的内存空间也就越大。
为了应对此种情况,ArrayList提供了trimToSize方法进行缩容操作,提前回收未使用的内存空间:
public void trimToSize() {
// modCount增加标记ArrayList发生改变
modCount++;
// - 如果缓冲区大小比数据量大-数据量为0时直接赋值缓冲区EMPTY_ELEMENTDATA,
// 否则按照数据量创建一个新的数组作为数据缓冲区,老的数据缓冲区会被GC掉
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
modCount++记录标记,然后如果元素数为0就直接赋值数据缓冲区为EMPTY_ELEMENTDATA,这么做也是为了让之前的空数据尽快被回收,不为0的情况就使用Arrays.copyOf进行size长度的赋值,保持元素数量和数据缓冲区长度一致,从而完成缩容操作。
5 ArrayList的元素新增
5.M.1 add(E e)
添加指定元素到List的末尾
public boolean add(E e) {
// 确保数据缓冲区的大小,上面有其相关的操作(这里会有modCount的+1)
ensureCapacityInternal(size + 1); // Increments modCount!!
// 将指定元素放到size位,然后size+1
elementData[size++] = e;
return true;
}
add(E e)此方法第一步进行调用了3.1.M.1 ensureCapacityInternal(int minCapacity) 进行容量担保检查以确保有足够的空间,因为3.1.M.1 ensureCapacityInternal(int minCapacity) 存在modCount++的操作,add(E e)也具有了modCount++的属性。担保检查之后将指定元素放到size位,然后size+1,完成元素的新增:放到size位是因为数组下标是从0开始的,元素数量为size时,下标实际使用到size-1.
5.M.2 add(int index, E element)
插入指定元素到指定位置。
public void add(int index, E element) {
// 越界检查(For Add)
rangeCheckForAdd(index);
// 确保数据缓冲区的大小(这里会有modCount的+1)
ensureCapacityInternal(size + 1); // Increments modCount!!
// 将index及之后的数据向后推移一位(index至end赋值到index+1至end+1的位置)
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 将指定数据放到指定位置
elementData[index] = element;
// size +1
size++;
}
方法开始调用rangeCheckForAdd进行越界检查:
5.M.2.1 rangeCheckForAdd(int index)
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
根据方法名可以得知此方法属于对Add的越界检查,index需要大于0,且需要不大于size。可以等于size的。指定size下标add时间相当于放到List的末尾。
回到5.M.2 add(int index, E element),使用rangeCheckForAdd(int index)进行越界检查后调用了3.1.M.1 ensureCapacityInternal(int minCapacity) 进行容量担保检查,接下来是一个System.arraycopy的操作,意思是从index开始将元素后移以空出index的位置,接下来就是赋值elementData[index]以及进行size++。
5.M.3 addAll(Collection<? extends E> c)
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
// 确保数据缓冲区的大小
ensureCapacityInternal(size + numNew); // Increments modCount
// 将c的数据赋值到elementData中
System.arraycopy(a, 0, elementData, size, numNew);
// 更新size
size += numNew;
return numNew != 0;
}
添加指定集合中的元素到List当中(从末尾添加),和5.M.1 add(E e) 相差无几,不同的是不使用下标赋值,而是直接使用System.arraycopy进行数组复制。
5.M.4 addAll(int index, Collection<? extends E> c)
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;
// 如果指定的index不在末尾
if (numMoved > 0)
// 数据向后移动腾出位置
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
// 将c中的数据依次放到腾出的位置上
System.arraycopy(a, 0, elementData, index, numNew);
// 更新size
size += numNew;
return numNew != 0;
添加指定集合中的元素到List当中(从指定位置添加)。和5.M.1 add(E e) 相差无几。区被在于单元素操作,改为数组操作。通过System.arraycopy来进行数组元素的移动以及复制。
6 ArrayList的元素删除
6.M.1 remove(int index)
/**
1. - 根据下标移除数据[触发modCount]
*/
public E remove(int index) {
// 下标越界检查
rangeCheck(index);
// modCount增加标识结构改变
modCount++;
// 获得这个下标的旧值
E oldValue = elementData(index);
int numMoved = size - index - 1;
// 如果不是最后一个就把index之后的数据整体前移
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 置空最后一位,同时更新size
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
此方法根据下标移除数据[触发modCount],并且返回移除的元素。
方法开头通过rangeCheck(int index)来对入参下标进行范围校验.
6.M.1.1 rangeCheck(int index)
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
很简单的逻辑,如果index大于等于size就抛出IndexOutOfBoundsException异常。remove的越界检查就和add的越界检查5.M.2.1 rangeCheckForAdd(int index) 不一样。remove的越界检查下标不允许等于size。这是因为add时,index为size即意味着数据插到末尾处。而remove则要求index必须有元素,因此其取值范围为[0,size-1]。
回到6.M.1 remove(int index),因为要返回移除的元素,所以要记录原值。
接下来从index+1处开始,用System.arraycopy将元素前移一位,这样做会覆盖掉index处的元素,相当于一个移除操作。最后例行的修正size,进行减1的操作。
除此之外,elementData[--size] = null还进行了置null的操作。这是因为System.arraycopy是复制操作,这么做一则可以释放elementData[oldSize-1]处的空间,二是如果元素属于引用类型,那么移动之后集合的最后一个元素会有两个强引用,分别来自elementData[oldSize-1]与elementData[newSize-1]。而这时只有elementData[newSize-1]是实际需要的有效强引用。为了使这个元素能够更合理的被GC,因此要断开elementData[oldSize-1]对其的强引用。
6.M.2 remove(Object o)
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;
}
从此列表中删除指定元素的第一个匹配项,如果存在则返回true,不存在则返回false。 本方法分成两段,但是逻辑都是一样的,区别只是对于null采用==判断避免空指针,非null采用equals判断。
从0开始遍历,如果相等则进入fastRemove方法进行移除。
6.M.2.1 fastRemove(int index)
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
}
可以看到fastRemove和remove(index)操作是差不多的,只是少了越界检查(这是因为fastRemove属于私有方法,ArrayList内部调用此方法时都会保证index不会越界)和旧值返回。
6.M.3 clear()
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
清空集合内的所有元素。这一步通过size=0就可以实现了。而把循环把数据缓冲区的每一位都置为null的原因在6.M.1 remove(int index) 部分已经解释过,是为了释放空间以及得到更合理的GC。
6.M.4 removeRange(int fromIndex, int toIndex)
protected void removeRange(int fromIndex, int toIndex) {
modCount++;
int numMoved = size - toIndex;
System.arraycopy(elementData, toIndex, elementData, fromIndex,
numMoved);
// clear to let GC do its work
int newSize = size - (toIndex-fromIndex);
for (int i = newSize; i < size; i++) {
elementData[i] = null;
}
size = newSize;
}
从此列表中删除其索引介于fromIndex、和toIndex之间的所有元素。删除的元素范围为[fromIndex, toIndex)。 通过System.arraycopy将数据缓冲区中的元素进行移位。并将多出来的长度置为null来快速GC掉无效数据,范围越界时抛出IndexOutOfBoundsException[触发modCount].实质上就是数据缓冲区的数组覆盖移位。和6.M.1 remove(int index) 逻辑类似,不过是覆盖一个下标以及覆盖一段下标的区别。
另外这个方法是protected 级别的。
6.M.5 removeAll(Collection<?> c)
/**
2. - 移除指定元素,使用到batchRemove
*/
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
此方法从此列表中删除指定集合中包含的所有元素,有元素移除返回true,无元素被移除返回false。先对c进行非空判断,然后进入batchRemove方法:
6.M.5.1 batchRemove(Collection<?> c, boolean complement)
/**
3. - 通过complement来判断是保留还是移除c中的元素。
4. - 具体执行为遍历数据缓冲区的每个元素,判断c中是否含有指定元素。如果符合条件则保留该元素。
5. - 因为保留的数据量一定是小于等于原数据量的,因此依然采用覆盖数组的方式
*/
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++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
// 这个地方主要是针对异常情况,异常点之后的数据会全部保存
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
// 如果w不等于size,说明有元素被移除,此时将后面的元素置为null以快速GC。
/*后面置为null并不是防止数据干扰,因为有size的存在,用户根本访问不到后面的数据*/
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
这个方法用来批量删除元素,含有两个参数,第一个入参Collection<?> c表示范围,第二个参数boolean complement表示方式,true时表示移除c中不含有的元素,false时表示移除c中含有的元素。
因为是多元素移除,采用了快慢指针法,r是快指针,一直往后移动,w作为慢指针,c.contains(elementData[r]) == complement满足时,直接将r处的元素放到w处,并且w向后移动一位。
为了避免c.contains(elementData[r]) 出现异常,将数据的修正放到了finally中:
第一步判断了r != size,只有发生异常才会出现这种情况,将从r之后的数据移动到w处及之后,这一步是为了弥补因为前面快慢指针可能出现的数据重复错乱,但是这也意味则r之后的数据并未经过判断,使得可能移除操作并不完全。
第二步判断了w != size,这意味着存在元素被移除了,修正size以及modCount,以及为了合理GC进行w处之后的的置null操作。
并将返回值modified置为true,意为确实有元素被移除了。
6.M.6 retainAll(Collection<?> c)
/**
6. - 保留指定元素,使用到batchRemove
*/
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
和6.M.5 removeAll(Collection<?> c) 相反,此方法从此列表中删除指定集合中不包含的元素,有元素移除返回true,无元素被移除返回false。也是调用 6.M.5.1 batchRemove(Collection<?> c, boolean complement) ,通过入参不同实现的。
6.M.7 removeIf(Predicate<? super E> filter)
// 移除所有符合条件的元素.:先通过filter找到所有的下标,然后再通过遍历覆盖的方式来进行remove-确实有移除操作的话触发modCount
@Override
public boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
//找出要删除的元素,且在此阶段从筛选器谓词引发的任何异常都将使集合保持不变
int removeCount = 0;
final BitSet removeSet = new BitSet(size);
final int expectedModCount = modCount;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
@SuppressWarnings("unchecked")
final E element = (E) elementData[i];
if (filter.test(element)) {
// 将i位置为true
removeSet.set(i);
removeCount++;
}
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
// 将幸存的元素左移到被移除元素留下的空间上
final boolean anyToRemove = removeCount > 0;
if (anyToRemove) {
final int newSize = size - removeCount;
for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
// 获取下一个为flase的下标,即下一个幸存元素的下标
i = removeSet.nextClearBit(i);
elementData[j] = elementData[i];
}
for (int k=newSize; k < size; k++) {
elementData[k] = null; // Let gc do its work
}
this.size = newSize;
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
return anyToRemove;
}
removeIf通过Predicate进行匹配,移除所有符合条件的元素。不同于 6.M.5 removeAll(Collection<?> c) 边匹配边移除的方式,此方法采用先先遍历记录下标再移除的方法,这样做的好处是不会出现6.M.5 removeAll(Collection<?> c) 中遍历一半出异常,导致一部分数据已处理,一部分数据未扫描的情况。此方法若匹配过程中出现异常,会使得原集合保持不变。
此方法使用BitSet记录那些被匹配到需要移除的元素下标。BitSet是一个用位概念(只是用位概念,内部不是直接使用的类,而是用的数组)记录数据的工具类。new BitSet(size)创建一个可以记录size位,默认每一位都是false的工具对象。遍历过程中使用filter进行匹配,如果符合条件,则通过BitSet.set(i)将对应位设置为true,这样遍历完成就能获得所有符合条件的元素下标。
如果removeCount 大于0,说明的确有元素需要移除。BitSet.nextClearBit()会返回下一个为false的位下标,也就是下一个幸存元素的下标,再利用快慢指针的方式进行幸存元素移动。最后为了合理GC依然要将newSize及其之后进行置null处理,并修正size。
因为removeIf的下标两段处理模式,如果处理过程中集合产生改变,就意味着下标可能不在准确,因此每一段均对modCount进行验证,如果modCount在处理期间改变,就需要抛出异常。
7 ArrayList的元素获取
7.M.1 get(int index)
/**
7. - 获得指定下标的数据
*/
public E get(int index) {
// 下标越界检查
rangeCheck(index);
// 获得数据缓存区的数据
return elementData(index);
}
使用6.M.1.1 rangeCheck(int index) 进行入参下标检查,然后直接返回数据缓冲区相对于下标的元素。
7.M.2 indexOf(Object o)
/**
8. - 获得第一个与指定元素相等的下标
*/
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;
}
如果用null的话用等号,非null用equals。采用for循环遍历数据缓冲区中有数据的区域,一但找到符合的便返回其下标(也就是说会返回第一个相等元素的下标),未找到返回-1。
7.M.3 lastIndexOf(Object o)
/**
9. - 获得最后一个与指定元素相等的下标
*/
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;
}
如果用null的话用等号,非null用equals。采用for循环遍历数据缓冲区中有数据的区域,与7.M.2 indexOf(Object o) 不同的是进行倒序寻找,一但找到符合的便返回其下标,未找到返回-1。
7.M.4 contains(Object o)
/**
10. - 是否含有某元素,可以看到ArrayList是通过IndexOf来判断是否含有对应元素的
*/
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
直接调用7.M.2 indexOf(Object o) , 返回值合法就说明值是存在的。
8 ArrayList的元素修改
8.M.1 set(int index, E element)
此方法设置指定下标为指定数据,并返回旧数据,值得注意的是此操作不会改变modCount)
public E set(int index, E element) {
// 下标越界检查
rangeCheck(index);
// 获得旧数据oldValue
E oldValue = elementData(index);
// 用新数据覆盖旧数据
elementData[index] = element;
// 返回旧数据
return oldValue;
}
方法开始进入6.M.1.1 rangeCheck(int index) 方法进行下标越界检查,rangeCheck校验index需要小于size,否则就抛出IndexOutOfBoundsException,这个校验意味着5.M.1 set(int index, E element) 只能执行覆盖更新,而无法新增。
8.M.2 replaceAll(UnaryOperator<E> operator)
// 通过operator对所有的元素进行替换操作(遍历缓存区,覆盖数组元素)-触发modCount
@Override
@SuppressWarnings("unchecked")
public void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final int expectedModCount = modCount;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
elementData[i] = operator.apply((E) elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
很简单的直接遍历,然后用operator对每个元素进行修改的动作,会做modCount 检查,以避免修改到非目标元素。
9 ArrayList的视图与迭代器
ArrayList拥有两种迭代器,分别为通过public Iterator<E> iterator()获得的Iterator正向迭代器,以及通过public ListIterator<E> listIterator()与public ListIterator<E> listIterator(int index)获得的ListIterator双向迭代器。
9.1 Iterator正向迭代器
/**
11. - ArrayList的元素迭代器
12. - 就是通过记录下标的方式来遍历集合。cursor用来记录下一个下标值,lastRet记录上一次返回的下标值
13. - expectedModCount用来记录属于迭代器的modCount,以便于并发情况下抛出ConcurrentModificationException
*/
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
// 通过下次要返回的下标就能知道是否还有数据
public boolean hasNext() {
return cursor != size;
}
// 移动下标返回元素
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
// 会调用ArrayList的remove来移除元素。同时也会重置lastRet使得不能连续remove,每次remove之前都需要调用next。
// 重置expectedModCount也是为什么在迭代期间使用迭代器的remove不会触发ConcurrentModificationException的原因
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
// 对迭代器剩下的元素执行特定操作(Consumer用来定义操作),迭代到最后一位
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
// 用来检查迭代期间是否发生了集合内元素改动
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
Itr实现了Iterator,逻辑就是通过记录下标的方式来遍历集合。cursor用来记录下一个下标值,lastRet记录上一次返回的下标值,再加上一个数据合法性校验和modCount校验。
为什么便利期间使用Iterator的remove不会触发ConcurrentModificationException 呢,可以看一下Itr实现的remove方法,除了调用ArrayList本身的remove方法进行数据移除外,还通过expectedModCount = modCount重置了expectedModCount ,这使得虽然在ArrayList.this.remove(lastRet)中修改了modCount,但是这里又进行了重新修正,因此在下一次通过checkForComodification进行校验时,expectedModCount 依然等于modCount,因此不会抛出异常。
9.2 ListIterator双向迭代器
ListItr 是比Itr拥有更强大功能的迭代器,可以进行反向迭代,并对集合进行替换和新增。
private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
super();
cursor = index;
}
// 是否存在前一个元素
public boolean hasPrevious() {
return cursor != 0;
}
// 下一个元素的下标
public int nextIndex() {
return cursor;
}
// 前一个元素的下标
public int previousIndex() {
return cursor - 1;
}
// 前一个元素
@SuppressWarnings("unchecked")
public E previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[lastRet = i];
}
// 替换上一次返回的元素
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.set(lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
// 在当前位置新增元素
public void add(E e) {
checkForComodification();
try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
除了继承了正向迭代器Itr外,还实现了ListIterator接口。一样是通过记录下标的方式来向前向后迭代。 hasPrevious与previous的使用方法和hasNext与next的使用方法相同。
除此之外,双向迭代器还提供了set(E e)方法对上次返回的数据进行替换,或者add(E e)在下一个要返回的元素位置进行新增。因为同样有expectedModCount 的重置操作,所以在迭代期间使用属于迭代器的add(E e)方法进行新增,也不会触发ConcurrentModificationException。
9.3 视图 - SubList内部类
ArrayList可以通过subList方法对获得此列表中指定的fromIndex(包含)和toIndex(独占)之间部分的视图 [fromIndex,toIndex) 。(如果fromIndex和toIndex相等,则返回的列表为空)。
// 返回一个SubList
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
// 基础数据合法性校验
static void subListRangeCheck(int fromIndex, int toIndex, int size) {
if (fromIndex < 0)
throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
if (toIndex > size)
throw new IndexOutOfBoundsException("toIndex = " + toIndex);
if (fromIndex > toIndex)
throw new IllegalArgumentException("fromIndex(" + fromIndex +
") > toIndex(" + toIndex + ")");
}
视图集合通过内部类SubList实现,SubList拥有正常List的所有特性,他就是集合的一种。
SubList的数据缓冲区直接使用原ArrayList的数据缓冲区,只是记录了开头和结尾下标并计算偏移量的方式对原数据缓冲区进行访问。原集合和subList之间的对集合的任意改动会互相影响
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
private final int parentOffset;
private final int offset;
int size;
// 构造函数,定义父ArrayList以及进行偏移量计算
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
public E set(int index, E e) {
rangeCheck(index);
checkForComodification();
E oldValue = ArrayList.this.elementData(offset + index);
ArrayList.this.elementData[offset + index] = e;
return oldValue;
}
/**略去List的剩下常用操作,实现都与ArrayList类似,或者是直接调用父ArrayList的相关方法。只是会对操作下标进行计算,并在会使对modCount改变的地方进行modCount修正***/
}
如果使用了SubList,那么在使用期间如果对ArrayList进行(如修改,删除),则要和迭代器一样,通过SubList的set或者remove方法,否则就会抛出ConcurrentModificationException。这是因为SubList的modCount校验方法:
private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
其校验的是父modCount 与当前SubList的modCount,如果通过ArrayList执行了remove操作,那么就会校验失败触发异常,而SubList的remove方法则会在remove之后进行modCount修正,如remove方法:
public E remove(int index) {
rangeCheck(index);
checkForComodification();
E result = parent.remove(parentOffset + index);
this.modCount = parent.modCount;
this.size--;
return result;
}
rangeCheck和checkForComodification属于基础校验,接着调用其parent的remove方法,parent即为被切割的ArrayList实例,SubList构造时作为构造入参传入。ArrayList的remove方法会改变modCount,但是SubList会在调用parent的remove方法之后通过this.modCount = parent.modCount进行修正,这样就不会再下次checkForComodification校验时触发异常。而如果直接调用ArrayList的remove方法,缺少修正过程,那么在下次使用当前SubList时就会校验失败。
9.4 可分割迭代器 - ArrayListSpliterator内部类
JDK1.8引入了Spliterator(splitable iterator可分割迭代器),Spliterator接口是Java为了更方便的并行遍历集合中的元素而提供的迭代器接口。JDK1.8开始每个实现Collection接口的类都拥有自己的Spliterator。如果未自定义,则Collection接口的default Spliterator<E> spliterator()会提供默认的实现。
ArrayList的Spliterator可以通过spliterator方法获得:
@Override
public Spliterator<E> spliterator() {
return new ArrayListSpliterator<>(this, 0, -1, 0);
}
ArrayList通过其内部类ArrayListSpliterator来实现自己的Spliterator:
static final class ArrayListSpliterator<E> implements Spliterator<E> {
private final ArrayList<E> list;
private int index; // current index, modified on advance/split
private int fence; // -1 until used; then one past last index
private int expectedModCount; // initialized when fence set
//新建一个ArrayListSpliterator,指定数据缓存区,开始和结束下标,以及指定ModCount
ArrayListSpliterator(ArrayList<E> list, int origin, int fence,
int expectedModCount) {
this.list = list; // OK if null unless traversed
this.index = origin;
this.fence = fence;
this.expectedModCount = expectedModCount;
}
// 在第一次使用时初始化围栏数值
private int getFence() { // initialize fence to size on first use
int hi; // (a specialized variant appears in method forEach)
ArrayList<E> lst;
// 小于0说明没有初始化
if ((hi = fence) < 0) {
// 原集合为空的围栏直接定为0
if ((lst = list) == null)
hi = fence = 0;
else {
// 否则的话就初始化expectedModCount与fence
expectedModCount = lst.modCount;
// 没有初始化,说明还没分割,那fence 就是size
hi = fence = lst.size;
}
}
return hi;
}
// 进行切割-创一个拥有新下标和围栏的ArrayListSpliterator。 trySplit的详细解释请查看Spliterator
public ArrayListSpliterator<E> trySplit() {
// 初始化检查,获得结束下标hi,开始下标lo,与进行中位计算获得mid
int hi = getFence(), lo = index, mid = (lo + hi) >>> 1;
// 若果开始下标lo比中位下标mid还大,就说明没得分割了,直接返回null
return (lo >= mid) ? null : // divide range in half unless too small
// 分成两部分,建立一个新的实例管理lo-mid, mid-hi依然由自身管理
new ArrayListSpliterator<E>(list, lo, index = mid,
expectedModCount);
// expectedModCount也是使用统一
}
// 对当前下标元素进行指定操作.且会触发modCount。也就是说原集合有变动会导致抛出ConcurrentModificationException异常。
public boolean tryAdvance(Consumer<? super E> action) {
if (action == null)
throw new NullPointerException();
// 初始化检查并获得hi
int hi = getFence(), i = index;
if (i < hi) {
// index后挪一位
index = i + 1;
@SuppressWarnings("unchecked") E e = (E)list.elementData[i];
action.accept(e);
if (list.modCount != expectedModCount)
throw new ConcurrentModificationException();
return true;
}
return false;
}
// 对剩余元素进行指定操作(index会直接记录到末尾)
public void forEachRemaining(Consumer<? super E> action) {
int i, hi, mc; // hoist accesses and checks from loop
ArrayList<E> lst; Object[] a;
if (action == null)
throw new NullPointerException();
// 实际有元素
if ((lst = list) != null && (a = lst.elementData) != null) {
// 初始化检查
if ((hi = fence) < 0) {
mc = lst.modCount;
hi = lst.size;
}
else
mc = expectedModCount;
// 从i开始遍历 index一步到位置为hi,而不是一步步+1,
if ((i = index) >= 0 && (index = hi) <= a.length) {
for (; i < hi; ++i) {
@SuppressWarnings("unchecked") E e = (E) a[i];
action.accept(e);
}
if (lst.modCount == mc)
return;
}
}
throw new ConcurrentModificationException();
}
// 返回当前切割器的剩余元素量
public long estimateSize() {
return (long) (getFence() - index);
}
// 返回当前切割器的特征值
public int characteristics() {
return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED;
}
}
首先其构造方法ArrayListSpliterator(ArrayList<E> list, int origin, int fence,int expectedModCount)入参中list表示原集合,origin表示起始下标,fence表示栅栏值,即为结束下标,expectedModCount是一个期望modCount。但是看ArrayList的spliterator()方法返回的对象为new ArrayListSpliterator<>(this, 0, -1, 0);fence为-1,expectedModCount为0,并不是实际值,这是因为ArrayListSpliterator采用的是懒加载的方式,只有真正开始使用ArrayListSpliterator那一刻才会进行初始化,这就使得ArrayListSpliterator在实际使用之前原ArrayList还是可以继续随意修改的,但是一旦开始使用,fence与expectedModCount被初始化,那么原ArrayList就不能随意修改了。
trySplit()进行分割,首先用getFence()进行初始化fence与expectedModCount,并获得hi,然后利用>>>进行除操作获得中位,将当前分段分为两部分:一个新的实例管理lo-mid, mid-hi依然由自身管理。
tryAdvance(Consumer<? super E> action)对当前元素执行指定动作并后移一位。
forEachRemaining(Consumer<? super E> action)本段剩下的所有元素执行指定动作,并将index置为hi,意为所有元素均已处理。
10 总结
- Arraylist通过在内部维护一个Object数组来作为容器存储集合元素,使其查询快速,但是增删缓慢
- Arraylist是一个有序集合,这个有序是指其元素顺序与插入顺序一致
- Arraylist无参构造时默认初始容量为10,但是是采取的懒加载的方式,其他方式的构造也会确保第一次扩容后最小容量为10
- Arraylist每次扩容原容量的1/2,即新容量=原容量*1.5
- Arraylist不是线程安全的集合
- Arraylist不允许在迭代的过程中直接修改集合数据,但是可以通过迭代器进行间接修改
PS:
开发成长之旅 [持续更新中...]
上篇导航:10: JAVA AIO - 掘金 (juejin.cn)
下篇导航:12: 从源码看LinkedList:一文看懂LinkedList - 掘金 (juejin.cn)
欢迎关注...