我们知道,在Java语言当中对集合的遍历方式大致分为3种:fori,foreach,iterator。假如给定一个ArrayList,对其中的某些指定元素进行循环遍历查找并且删除的话,它们之间又有什么不同呢?
首先构造一个ArrayList:
private static List<String> list = new ArrayList<>(5);
static {
list.add("add");
list.add("delete");
list.add("delete");
list.add("update");
list.add("query");
}
1. fori 删除(顺序会漏删,倒序可正常)
顺序删除
@Test
public void foriDelete(){
for (int i = 0; i < list.size(); i++) {
if ("delete".equals(list.get(i))){
list.remove(i);
}
}
System.out.println("顺序删除之后的结果为-->"+list.toString());
//顺序删除之后的结果为-->[add, delete, update, query]
}
上述可以看出,其中有一个delete字符串没有被删除掉,这是因为ArrayList本身是个数组,在进行遍历的时候,如果删除某个元素,则后续的元素需要整体往前移动,当循环到i=1的时候,找到了第一个delete字符串,然后删除,第2个delete以及之后的字符串会向前移动,这个时候第2个delete就在数组的1位置了。当循环到i=2的时候,正好把原来位置的delete给略过去了,导致漏删:
针对这个问题,我们可以依据删除的特点进行倒序删除,这样就可以找到delete字符串的正确位置了:
倒序删除
@Test
public void foriDelete(){
for (int i = list.size() - 1; i >= 0; i--) {
if ("delete".equals(list.get(i))){
list.remove(i);
}
}
System.out.println("倒序删除之后的结果为-->"+list.toString());
//倒序删除之后的结果为-->[add, update, query]
}
2. foreach 删除(删除一个元素停止遍历可正常,多个元素删除有CME问题)
@Test
public void foreachDelete(){
for(String s:list){
if(s.equals("delete")){
list.remove(s);
}
}
System.out.println("foreach删除之后的结果为-->"+list.toString());
//删除出错:java.util.ConcurrentModificationException
}
首先得明确一点,foreach的原理其实是编译器会将其编译为迭代器Iterator的形式进行遍历,以下是对.class文件反编译之后的代码:
@Test
public void foreachDelete() {
Iterator var1 = list.iterator();
while(var1.hasNext()) {
String s = (String)var1.next();
if (s.equals("delete")) {
list.remove(s);
}
}
}
但与真正的使用迭代器遍历的方式有点不同,删除的时候不是使用迭代器的remove方法,而是用的ArrayList的remove方法。为什么会产生ConcurrentModificationException呢?这是由于,ArrayList本身不是线程安全的,在使用迭代器遍历查询的时候,会有一个检查机制,来确保一个线程在遍历的时候,其他线程不会删除该集合中的元素。遍历代码如下:
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];
}
/**确保List在同一时刻不会有多个线程进行删除*/
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
而当使用foreach方式删除元素的时候,调用ArrayList的remove方法如下:
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
}
当在第一次找到delete字符串,并进行删除的时候,会对modCount++。如果没有停止该遍历,则在下次循环的时候,会校验modCount与 expectedModCount是否相等。若不等,则抛出并发修改异常。
所以,foreach循环删除元素,是可以的,但有个条件:只能删除一个元素,并立即停止遍历,在remove下加上break即可。
3. iterator 删除(可正常删除)
@Test
public void iteratorDelete(){
Iterator<String> it = list.iterator();
while(it.hasNext()){
String x = it.next();
if(x.equals("delete")){
it.remove();
}
}
System.out.println("iterator删除之后的结果为-->"+list.toString());
//iterator删除之后的结果为-->[add, update, query]
}
ArrayList在使用迭代器Iterator进行删除的时候,逻辑如下:
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();
}
}
可以看出来,在进行删除的时候,会将modCount赋值给expectedModCount,所以不会导致两者不等。只要不是数组越界,就不会报出ConcurrentModificationException了。
通过以上的分析可以看出,在不同的场景下需要选择合适的方式,当然,Iterator遍历删除是不需要担心的。