ArrayList的两种删除方法


基本上,在JDK1.5之后接触Java的人,或者说在使用Java foreach之后都会对List的删除感到困惑,特别是莫名其妙的java.util.ConcurrentModificationException异常。比如如下的代码:
		List list = new ArrayList();
		list.add("wong");
		list.add("song");
		list.add("zhao");
		list.add("lv");
		
		for (String s : list) {
			if (s.equals("wong")) {
				list.remove(s);
			}
		}

这段代码在删除除了倒数第二个元素之外的任意元素的时候,会报ConcurrentModificationException异常,而删除倒数第二个元素的元素时,则一切正常。

当时,我对Java的了解不够深入,所以认为list的remove方法是不安全的,删除的时候尽量使用Iterator的remove方法,代码如下:

		Iterator it = list.iterator();
		for(;it.hasNext();){
			String s = it.next();
			if(s.equals("wong")){
				it.remove();
			}
		}

这段代码对于删除list中的元素是安全的,这也使得我对Iterator的好感倍增,直到有一天我看到下面的代码:

		for (int i = 0; i < list.size(); i++) {
			String s = list.get(i);
			if (s.equals("wong")) {
				list.remove(s);
			}
		}
没错,这也是安全的,而且用的是list的remove方法,我当时就。。。

不懂就看源码,反正是开源的,先说list的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;
    }
可以看出来,list的remove方法最后调用的是fastRemove方法,其代码如下:

    private void fastRemove(int index) {
        modCount++;//抛出ConcurrentModificationException的罪魁涡首
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // Let gc do its work
    }
可以看出来:fastRemove方法首先修改了modCount,使其自增,然后移动被删除元素后的元素,最后改变size,gc最后一个元素。
那么这个modCount是干什么的呢?JDK API上给出的解释是“已 从结构上修改 此列表的次数”。结构上修改的意思是“从结构上修改是指更改列表的大小,或者打乱列表,从而使正在进行的迭代产生错误的结果"。也就是说,modCount是为了避免Iterator因为列表list被修改而带来的问题,Itertor在使用期next方法时候,会先检查modCount与expectedModCount(前者是list中的属性,后者是Iterator的属性),如果两者不相等,则抛出那个ConcurrentModificationException异常。这样是为什么list中的remove方法放在foreach中会报错,因为list的remove方法仅仅增加了modCount的值,而不会对Iterator中的expectedModCount进行维护。如果你想要使用list的remove方法,那么你必须放弃Iterator的遍历方式(foreach就是使用Iterator的遍历方式,这也是JDK1.5的一大亮点),使用index的遍历方式。

现在,我们回到第一段代码,为什么在删除倒数第二个元素的时候不会报错呢?原因在于Iterator的hasNext方法:

	public boolean hasNext() {
            return cursor != size();
	}
在Iterator使用next方法后,Iterator中的属性cursor(游标)会指向下一个index,也就是最后一个元素的index:size-1,,而当我们使用list的remove方法,成功的修改了modCount,删除了元素,改变了size的大小,使其自减,这个时候再调用hasNext方法,就会返回false。也就是说,当使用第一段代码,删除倒数第二个元素时,虽然成功删除了倒数第二个元素,但因为没有修改Iterator中的cursor属性,导致Iterator认为已经遍历完了list,最终跳出循环,没有调用next方法,所以也就没有抛出ConcurrentModificationException异常。

类似的情况也发生在第三代码中,当使用index遍历,使用list的remove方法删除元素时,fastRemove方法会将list中被删除元素后的元素前移,这会使得游标i指向预期元素的后一个元素,从而使得遍历不完整。但由于这个游标是暴露的,我们可以对其维护,只需要:

		for (int i = 0; i < list.size(); i++) {
			String s = list.get(i);
			if (s.equals("wong")) {
				list.remove(s);
				i--;
			}
		}
就可以了。当然,有些时候,我们可能为了性能,将上面的代码写成如下的形式:

		int size = list.size();
		for (int i = 0; i < size; i++) {
			String s = list.get(i);
			System.out.println(s);
			if (s.equals("wong")) {
				list.remove(s);
				i--;
				size--;//修改此值,避免数组越界
			}
		}
如果使用这样的写法,请不要忘记修改size的值,否则会发生IndexOutOfBoundsException数组越界的错误。【完】



你可能感兴趣的:(Java,Core)