问题背景

当我们要循环一个List中的元素,并且要删除某个元素的时候,一点需要注意了,其中深埋了好几个坑!

案例1

请看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void listRemove() {
List<String> list = new ArrayList<>();
list.add("marry");
list.add("marry");
list.add("tony");
list.add("minhow");
list.add("minhow");
System.out.println("原始list元素:"+ list.toString());

//移除等于marry的元素
for (int i = 0; i < list.size(); i++) {
String item = list.get(i);
if("marry".equals(item)) {
list.remove(i);
}
}
System.out.println("移除后的list元素:"+ list.toString());
}

输出结果:

1
2
原始list元素:[marry, marry, tony, minhow, minhow]
移除后的list元素:[marry, tony, minhow, minhow]

从输出结果看,移除marry元素后,并没有完全移除,为什么会这样呢?
要分析产生上述错误现象的原因,我们可以查下JDKArrayList源码,先看下ArrayList中的remove方法是怎样实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* Removes the first occurrence of the specified element from this list,
* if it is present. If the list does not contain the element, it is
* unchanged. More formally, removes the element with the lowest index
* <tt>i</tt> such that
* <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
* (if such an element exists). Returns <tt>true</tt> if this list
* contained the specified element (or equivalently, if this list
* changed as a result of the call).
*
* @param o element to be removed from this list, if present
* @return <tt>true</tt> if this list contained the specified element
*/
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;
}

查看源码找到fastRemove方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
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
}

可以看到执行System.arraycopy方法,导致删除元素时涉及到数组元素的移动,所以删除第一个marry后,集合中的元素个数减1,后面的元素往前移动1位,此时第二个marry的索引index=1,而此时的i++了,所以
list.get(i)获取到的数据应该是tony;同时list.size()的值也减少1;最终导致上面的结果出现。
再看看另外一个案例:

案例2

1
2
3
4
5
6
for (String str: list) {
if("marry".equals(str)) {
list.remove(str);
}
}
System.out.println("移除后的list元素:"+ list.toString());

输出结果直接报错了,抛ConcurrentModificationException异常:

1
Exception in thread "main" java.util.ConcurrentModificationException

foreach写法是对IterablehasNextnext方法的简写,问题同样处在上文的fastRemove方法中,可以看到第一行把modCount变量的值加1,但在ArrayList返回的迭代器:

1
2
3
public Iterator<E> iterator() {
return new Itr();
}

再找到Itr对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
private class Itr implements Iterator<E> {
/**
* Index of element to be returned by subsequent call to next.
*/
int cursor = 0;

/**
* Index of element returned by most recent call to next or
* previous. Reset to -1 if this element is deleted by a call
* to remove.
*/
int lastRet = -1;

/**
* The modCount value that the iterator believes that the backing
* List should have. If this expectation is violated, the iterator
* has detected concurrent modification.
*/
int expectedModCount = modCount;

public boolean hasNext() {
return cursor != size();
}

public E next() {
checkForComodification();
try {
int i = cursor;
E next = get(i);
lastRet = i;
cursor = i + 1;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();

try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}

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

这里会做迭代器内部修改次数检查,因为上面的remove(Object)方法把修改了modCount的值,所以才会报出并发修改异常。要避免这种情况的出现则在使用迭代器迭代时不要使用ArrayListremove,改为用Iteratorremove即可。

解决方案1-使用迭代器

1
2
3
4
5
6
7
8
9
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String str = it.next();
if ("marry".equals(str)) {
it.remove();
}
}

System.out.println("移除后的list元素:"+ list.toString());

结果:

1
移除后的list元素:[tony, minhow, minhow]

可以看到结果是正确的了,还有更简单的方法:

解决方案2

1
2
//基于jdk1.8移除等于marry的元素
list.removeIf(item -> "marry".equals(item));

结果跟上面一样的。

总结

如果基于jdk1.8的,可以使用最简单的方案,如果是低版本的,建议采用迭代器方法,效率高。

最后更新: 2019年07月20日 16:27

原始链接: http://blog.minhow.com/articles/java/list-question/

× 请我吃糖~
打赏二维码