ArrayList遍历删除方式总结


我们知道,在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遍历删除是不需要担心的。

  • 7
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值