List迭代过滤操作注意点

时间:2023-03-09 04:27:01
List迭代过滤操作注意点

今天在写一段很简单的代码,本来以为肯定没什么问题,然后直接跑的时候,吆,简单的一个List的操作报错了。仔细一看代码,确实有问题,但是一般真的是如果稍微不小心就会犯下面这种愚蠢的操作。

这里我把代码贴出来:

public static void main(String[] args)
{
List<Integer> list = new ArrayList<>(1);
list.add(1);
for (Integer a : list)
{
if (a == 1)
{
list.remove(a);
}
}
list.forEach(System.out::println);
}

上面的代码报错,我贴错误出来:

List迭代过滤操作注意点

List迭代过滤操作注意点



然后第一时间觉得有问题,这种遍历然后删除的操作应该要使用迭代器。然后我修改后改成了下面代码:

public static void main(String[] args)
{
List<Integer> list = new ArrayList<>(1);
list.add(1);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext())
{
Integer next = iterator.next();
if (next == 1)
{
list.remove(next);
}
}
list.forEach(System.out::println);
}

结果一运行同样报错,哎吆,一不小心还是直接去删除List了,然后再次修改才没了问题。最后修改的代码如下:

public static void main(String[] args)
{
List<Integer> list = new ArrayList<>();
list.add(1);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext())
{
Integer next = iterator.next();
if (next == 1)
{
iterator.remove();
}
}
list.forEach(System.out::println);
}

所以决心好好的研究下这个List在迭代过程中的删减操作为什么很容易报错。打开前面2次报错的代码异常出现的地方,可以清楚的看见报错的原因。

 

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

modCount,expectedModCount这2个变量是什么搞不懂,所以还是好好研究下吧。在这里我们看那段用迭代器遍历然后直接删除list中一个对象的那段代码,我们来研究一下:

打开JDK源码看一下Arraylist的add和remove操作:

public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return 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;
}
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
}

前面我们已经知道,不管是使用fore循环还是说使用迭代器,List内部操作的都是hasnext()和next()方法。这里贴出源码:

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];
} 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();
}
} @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();
}
}

上面的源码人家JDK里面的注释写的已经很清楚了,这里我们来整理一下:

cursor:表示下一个要访问的元素的索引,从next()方法的具体实现就可看出

lastRet:表示上一个访问的元素的索引

expectedModCount:表示对ArrayList修改次数的期望值,它的初始值为modCount。

modCount:AbstractList类中的一个成员变量,默认是0。

该值表示对List的修改次数,查看ArrayList的add()和remove()方法就可以发现,每次调用add()方法或者remove()方法就会对modCount进行加1操作。

所以我们在对List做迭代操作的过程中,如果这个时候来添加或者删除这个List,这个时候expectedModCount是原来的初始化时候的modCount值,但是modCount这个时候都自增改变了值了,所以肯定就报错了。具体如下图:

List迭代过滤操作注意点



OK,现在这个时候我们已经知道了报错的原因了,那么为什么在直接使用interator迭代器来删除就没问题呢?

我们自己看一下Iterator源码里面的remove()方法,就明白了。

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification(); try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
//下面这行是亮点,重新设值expectedModCount了。
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

好了,现在错误原因已经清楚了,和公司CTO聊了下这个问题,人家的原话是这么说的:

操作List在迭代的时候最好只是单纯的迭代,而不要试图去影响原来的那个List,特别是原来那个List的size长度。如果有需要最好也new一个新的list来处理过滤出来的数据最好。而且如果是单纯的要做查询就用Arraylist,如果要做插入和删除操作,最好用likenList。

我个人觉得说的很好很正确,以后编码时候如果要做过滤一个List这种操作时候,最好新new一个容器来搬数据,不要试图直接操作原来那个List。