ArrayList 浅析

用户头像
章小传
关注
发布于: 2020 年 06 月 03 日
ArrayList浅析

ArrayList是Java开发者使用最多的集合容器之一。本篇文章通过源码的角度讲解ArrayList的原理。



ArrayList是List(有序集合)接口的一种 可调整大小的数组的 实现,底层数据结构是数组,存储元素是有序的,且可重复,可存储任何类型的元素,包括null。该类的实现大致相当于Vector,不过ArrayList是非线程安全的,如果多个线程并发访问ArrayList,并且至少有一个线程修改了容器的结构,那么必须要有额外的同步操作来保证ArrayList的线程安全问题。虽然Vector是线程安全的,不过由于Vector是JDK的历史遗留容器,已不推荐使用,如果想使用线程安全的List,可通过 Collections.synchroizedList 方法对ArrayList进行包装(该方式也适用于List接口的其他实现类),或者使用JUC中的并发容器 CopyOnWriteArrayList 。。

基本属性介绍

了解一个类,从属性开始

/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;

默认的初始化容量,即初始的数组大小,当创建ArrayList对象时,没有指定容量大小,即调用无参的构造方法,默认使用的就是这个容量大小。

/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

这两个空数组是为了区分该ArrayList对象是用无参构造创建的还是根据指定容量为0的构造方法创建的。这样才能知道第一次扩容的时候应该扩容多少。

/**
* The size of the ArrayList (the number of elements it contains).
*/
private int size;

表示ArrayList对象实际所存储的元素个数

/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access

实际存储数据的Object数组,该属性用transient表示,说明这个属性在序列化与反序列化时,是会被忽略的,真的是这样的吗?(下面讲)

/**
* The maximum size of array to allocate.
* 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;

可分配的数组的最大容量值,但实际上从扩容方法(下面讲)中可以看出来,ArrayList 的数组最大容量其实是 Integer.MAX_ALUE。那么为什么这里的最大值设计成Integer.MAX_VALUE - 8 ?首先,我们先看下这段注释:数组可分配的最大容量值,一些虚拟机在数组中保留一些head words,如果尝试分配更大 的内存,可能会造成OOM。从注释我们可以知道,减去的这个8是为了保留一些内存用来存放head words,那么为什么是8?而不是其他的数字呢?下面这段解释属片面看法,可做参考:



Java中的数组长度是只能用int来表示的,也就是如果内存允许,数组最大长度只能是Integer.MAX_VALUE。那么这里为什么要减8呢,这就要涉及到Java对象在内存中的结构,数组对象和标准Java对象类似,主要区别在于数组对象的对象头中有一个额外的元数据,用于表示数组的长度大小,即 array length ,减去的这个8就是为了保留一些内存用来存储数组长度这个值,至于为什么是8 ?array length 的数据长度会随着JVM架构不同而不同,在32位的JVM下,array length 就是32位,64位的JVM下就是 64 位,也就是8个字节,由于目前市面上大部分都是64位的,所以这里预留出了8个字节用来存储 array length。

protected transient int modCount = 0;

这个字段是继承自AbstractList类,表示的是该容器结构被修改的次数,如果该值被意外改变了,那么迭代器在执行next、remove、previous、set、add等方法时会抛出异常,以实现快速失败。

构造方法

ArrayList提供了三个构造方法

1. 无参构造

/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

文档注释翻译过来是,构造一个容量为10的空集合。但其实这里容量为10没有体现出来,这里只是构造了一个空的数组,这个时候数组的长度还是0,在第一次添加元素的时候会进行扩容,这时候才会构造一个容量为10的数组并添加元素。



大部分容器也有这种懒加载的设计思路,在构造方法里面不会初始化底层的数据结构,只有在第一次使用到的时候才会去初始化,防止资源浪费。

2. 指定容量的构造

/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
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,则跟上面的无参构造方法一样初始化为一个空的数组,EMPTYELEMENTDATA 和 DEFAULTCAPACITYEMPTY_ELEMENTDATA 这两个都是空数组,他们的区别就是区分该ArrayList对象是用无参构造创建的还是根据指定容量为0的构造方法创建的。

3. 参数为集合的构造

/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
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;
}
}

将传入的集合copy到当前创建的新集合中,如果传入的集合的长度为0,则创建一个空集合



注意,该方法没有做判空处理,所以如果传入的集合是null,会抛出空指针异常 NullPointerException



代码中有一行注释 c.toArray might (incorrectly) not return Object[] (see 6260652) ,这是一个JDK的bug,编号为 6260652 ,在JDK9中已经修复。意思就是 c.toArray 方法返回的不一定是 Object[],例如用 Arrays.asList(strs) 方式创建的List,在调用 toArray() 时返回的数组类型不一定是 Object[],会保留原来的类型,有兴趣可查看官方 JDK-6260652

增删改查

增加一个元素

public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}



private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}



private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

在ensureCapacityInternal方法中,先判断elementData是否是DEFAULTCAPACITYEMPTYELEMENTDATA,如果是则说明这是无参构造方法创建的容器,且是第一次添加元素,则会将数组初始化成DEFAULT_CAPACITY大小的数组,然后增加modCount值,接下来就是判断元素个数是否超过了数组大小,如果是则扩容。然后在数组最后一位新增元素,并且增加size。

public void add(int index, E element) {
rangeCheckForAdd(index); // 判断index是否越界
ensureCapacityInternal(size + 1); // Increments modCount!!
// 将指定下标位置以及之后的元素往后移动一位
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}

在指定位置上添加元素,并将指定位置以及之后的元素往后移动一位

还有addAll方法,原理类似。



修改一个元素

public E set(int index, E element) {
rangeCheck(index); // 判断下标是否越界
E oldValue = elementData(index);
elementData[index] = element; // 然后修改数组指定位置的值
return oldValue; // 并返回旧的值
}



删除一个元素

public E remove(int index) {
rangeCheck(index); // 判断下标是否越界
modCount++; // 增加modCount值
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; // 返回旧值
}



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
}

删除指定元素,遍历的方式,fastRemove 和 remove 类似,只是没有判断下标,以及没有返回值。



获取一个元素

@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}

public E get(int index) {
rangeCheck(index); // 判断下标是否越界

return elementData(index);
}

总结:增删改查这几个方法源码都比较简单,做一些判断及操作数组即可。新增一个元素和删除一个元素都会修改 modCount 值,即表示修改了容器的结构,而set方法不会修改 modCount 值,也就是set不会修改容器的结构,注意了! set 方法虽然修改了容器中元素的值,但是它不修改容器结构。

ArrayList的扩容

扩容代码如下

/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
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;
}

逻辑比较简单,先是在原来容量的基础上扩容50%,为了保证此次扩容一定能达到期望的容量值 minCapacity ,所以当扩容50%之后如果还小于 minCapacity,就直接拿 minCapacity 的值当新的容量值。



然后判断新的容量是否超过了最大容量值,即 MAX_ARRAY_SIZE ,如果超过了,就使用 MAX_ARRAY_SIZE 的值作为新的容量值。



前面讲到,ArrayList的最大容量值是 MAX_ARRAY_SIZE (Integer.MAX_VALUE - 8),即ArrayList的数组对象的最大容量值就是这个了,但是依然能够支持到 Integer.MAX_VALUE 大小的容量,什么时候支持呢?在期望的最小容量值,也就是 minCapacity 值也超过 MAX_ARRAY_SIZE 的时候,就使用 Integer.MAX_VALUE 的值作为新的容量值。



注意!扩容是创建一个新的数组对象,将旧数组的元素移动过去,然后再将elementData指向新的数组对象。



ArrayList还提供了一个公开的扩容方法,用户可以显式地调用该方法来扩容。

/**
* Increases the capacity of this <tt>ArrayList</tt> instance, if
* necessary, to ensure that it can hold at least the number of elements
* specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
public void ensureCapacity(int minCapacity) {
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的序列化

ArrayList实现了Serializable接口,表示这个类的对象是可以被序列化的,这个类的所有属性和方法都会被序列化。



再看下elementData的定义:

transient Object[] elementData; // non-private to simplify nested class access

该属性被 transient 修饰,说明在序列化的时候,该字段会被忽略



而elementData是主要存储数据的字段,为什么要忽略它呢?那序列化到文件或传输的时候,ArrayList的元素不是都没了吗?



再看下ArrayList的这两个方法:

private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}

当一个类实现了 Serializable接口,又定义了writeObject和readObject方法,那么在序列化和反序列化对象的时候会调用该类自己定义的序列化和反序列化方式,关键源码在 ObjectStreamClass 中

writeObjectMethod = getPrivateMethod(cl, "writeObject",
new Class<?>[] { ObjectOutputStream.class },
Void.TYPE);
readObjectMethod = getPrivateMethod(cl, "readObject",
new Class<?>[] { ObjectInputStream.class },
Void.TYPE);

通过反射去获取对象类中方法名叫 writeObject 和 readObject 的方法,如果存在就调用对象类的方法



我们看到ArrayList也写了这两个方法,那么在序列化和反序列化的时候就会调用自身的这两个方法。在writeObject和readObject方法中,去重新序列化和反序列化elementData这个属性



所以不是忽略掉 elementData 这个属性,而是自己去自定义序列化规则了。为什么要这么做呢?



因为ArrayList中elementData,是一个数组,扩容之后可能会存在没有被使用的内存,比如elementData目前扩容到数组长度为30了,可实际上存储的数据可能只有10个,如果使用默认的序列化方式,就会将那20个值为null的元素也序列化进去了,就造成了不必要的浪费,所以在自定义的方法中,根据elementData的实际存储的元素个数进行序列化。



所以你测试一下会发现,原本ArrayList的底层数组长度是10,然后存储了3个元素,"a","b","c",后面7个元素都是null,在进行序列化与反序列化之后得到的ArrayList对象的底层数组长度只有3了。

ArrayList的快速失败

/**
* The number of times this list has been <i>structurally modified</i>.
* Structural modifications are those that change the size of the
* list, or otherwise perturb it in such a fashion that iterations in
* progress may yield incorrect results.
*
* <p>This field is used by the iterator and list iterator implementation
* returned by the {@code iterator} and {@code listIterator} methods.
* If the value of this field changes unexpectedly, the iterator (or list
* iterator) will throw a {@code ConcurrentModificationException} in
* response to the {@code next}, {@code remove}, {@code previous},
* {@code set} or {@code add} operations. This provides
* <i>fail-fast</i> behavior, rather than non-deterministic behavior in
* the face of concurrent modification during iteration.
*
* <p><b>Use of this field by subclasses is optional.</b> If a subclass
* wishes to provide fail-fast iterators (and list iterators), then it
* merely has to increment this field in its {@code add(int, E)} and
* {@code remove(int)} methods (and any other methods that it overrides
* that result in structural modifications to the list). A single call to
* {@code add(int, E)} or {@code remove(int)} must add no more than
* one to this field, or the iterators (and list iterators) will throw
* bogus {@code ConcurrentModificationExceptions}. If an implementation
* does not wish to provide fail-fast iterators, this field may be
* ignored.
*/
protected transient int modCount = 0;

这个字段是继承至 AbstractList 类的,是实现快速失败的关键。



快速失败,即fail-fast,它是java集合的一种错误检测机制。当多钱程对集合进行结构上的改变或者集合在迭代元素时直接调用自身方法改变集合结构而没有通知迭代器时,有可能会触发fast-fail机制并抛出异常



在ArrayList的所有涉及结构变化的方法中都增加modCount的值,包括:add()、remove()、addAll()、removeRange()及clear()方法。这些方法每调用一次,modCount的值就加1。



在ArrayList创建迭代器的时候(iterator()方法或listIterator()方法),会将当前的modCount值赋予迭代器中的 expectedModCount 字段。



然后在迭代器中的方法(如next(),remove())都会去判断 ArrayList 的 modCount 值和迭代器的 expectedModCount ,如果不相等就会抛出异常。



所以如果修改了modCount值,那么再操作迭代器,就会报错了,即如果修改了ArrayList对象的结构,那么在这之前创建的迭代器都将失效。



SubList是ArrayList的一个内部类,它是截取ArrayList对象之后的返回值类,也有快速失败的机制。

RandomAccess接口

ArrayList 实现了RandomAccess接口,表示它是支持随机访问的。



RandomAccess接口是个空的接口,只是一个标识,表示ArrayList拥有随机访问的能力,但是即使不实现这个接口,ArrayList也是拥有随机访问能力的。ArrayList的随机访问能力源自于它存储数据的底层结构是数组,可以通过下标精准访问,且性能都是O(1)



通过RandomAccess接口的注释文档,可以知道用普通for循环遍历的方式比用迭代器的方式循环要快:

/**
* ...
* ...
* for typical instances of the class, this loop:
* <pre>
* for (int i=0, n=list.size(); i &lt; n; i++)
* list.get(i);
* </pre>
* runs faster than this loop:
* <pre>
* for (Iterator i=list.iterator(); i.hasNext(); )
* i.next();
* </pre>
* @since 1.4
* ...
* ...
*/
public interface RandomAccess {
}

所以如果是ArrayList容器,推荐使用 for 循环的方式遍历(foreach方式不算,foreach的底层也是使用迭代器遍历的)



在Colletions工具类二分查找方法中:

public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}

可以看到如果是 list 是实现了 RandomAccess 接口,则一定使用的是数组下标的方式查找,因为实现 RandomAccess 接口的 list 具有随机访问的能力,用数组下标的方式访问速度更快。

ArrayList的迭代器

ArrayList实现了两种迭代器,一种是 iterator() 方法,返回的是 Itr 迭代器,Itr是ArrayList的一个内部类:

public Iterator<E> iterator() {
return new Itr();
}
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator<E> {
int cursor; // 迭代器下一个要访问的元素数组下标(称为游标)
int lastRet = -1; // 迭代器前一个访问的元素数组下标,为-1表示迭代器没有开始访问元素或者迭代器前一次不是访问元素(比如删除元素)
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size; // 如果游标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]; // 将lastRet赋值为原来游标的值
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet); // 删除数组下标为lastRet的元素,即迭代器前一个访问的元素
cursor = lastRet; // 将游标重置为lastRet(即前一次访问的元素下标)
lastRet = -1; // 将lastRet重置为-1
expectedModCount = modCount; // 调用外部ArrayList的删除方法之后,modCount会被修改,这里将 expectedModCount 值重置一下,以免出现快速失败。所以调用Arraylist的删除方法会出现快速失败,而调用迭代器的删除方法则是安全的。
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
// 从游标位置开始往后访问所有元素,并全部执行指定方法,该方法结束之后,该迭代器也就结束了生命周期。
@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();
}
// 快速失败检测,很简单,将外部类ArrayList的 modCount 和迭代器的 expectedModCount 比较,如果不相等,则表示外部类ArrayList已经修改了容器结构,即该迭代器已经失效,抛出异常。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

AbstractList也有一个内部类叫Itr,是List的一个通用的迭代器实现,而ArrayList重写了Itr类,是对 AbstractList.Itr 的一个优化版本



另一个迭代器 ListItr 是 ArrayList.Itr 迭代器的一个扩展,功能更强大

public ListIterator<E> listIterator() {
return new ListItr(0);
}
/**
* An optimized version of AbstractList.ListItr
*/
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();
}
}
}

ArrayList.ListItr 迭代器继承了 Arraylist.Itr 迭代器,做了一些扩展,比如可以往前遍历,可以指定初始时的游标位置,也可以修改元素值和新增元素。



这种迭代器的设计方式,实际上是一种设计模式,叫迭代器(Iterator)模式,又叫做游标模式,它的含义是,提供一种方法访问一个容器对象中各个元素,而又不需暴露该对象的内部细节。从定义上看,迭代器是为容器而生,它本质上就是一种遍历的算法。因为容器的实现千差万别,很多时候会不知道如何去遍历一个集合对象的元素,所以Java通过提供 Iterator 和 Iterable 俩个接口来实现容器的可迭代性及迭代器的统一访问。



而Java中的 foreach 的这种写法,底层也是基于迭代器的,如果你的容器实现了 Iterable 接口,就能使用 foreach 的写法了

遍历删除ArrayList

如何正确地遍历删除ArrayList中的元素,主要有两种



1、普通for循环的方式遍历删除

List list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
list.add("f");
for (int i = 0; i < list.size(); i++) {
list.remove(i);
}
System.out.println(list);

打印出来的结果是 [b, d, f] ,原因是每次删除元素,被删除元素后面的元素都会往前移动一位,导致元素的位置发生了变化。

正确做法是反向遍历删除,这样就不会受到因为删除而移动元素所带来的影响。



2、迭代器方式遍历删除

List list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
list.add("f");
Iterator iterator = list.iterator();
while(iterator.hasNext()){
Object obj = iterator.next();
list.remove(obj);
}
System.out.println(list);

这种方式遍历删除,会抛出异常。原因是 调用 list.remove(obj) 的时候,会修改容器的结构,也就是修改了 modCount 值,导致 list 的 modCount 值跟迭代器中的 expectedModCount 不一致,造成 fail-fast 。

正确做法就是删除方法改成用迭代器提供的删除方法来删除 iterator.remove() ,迭代器的删除方法实际上也是调用 ArrayList 的删除方法,但是会重置 expectedModCount 为新的 modCount 值,所以不会出现 fail-fast 错误,源码如下:

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount; // 重置 expectedModCount
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

参考:

ArrayList源码分析超详细

Java中ArrayList最大容量为什么是Integer.MAX_VALUE-8?

JDK9修复的bug(JDK-6260652)是怎么回事?

用户头像

章小传

关注

不积跬步无以至千里,不积小流无以成江河。 2019.07.05 加入

还未添加个人简介

评论

发布
暂无评论
ArrayList浅析