浅谈为什么倒序遍历List删除元素没有问题

要搞清楚这个问题,首先要知道如何正确的遍历List删除元素。注:下述代码完整版附在末尾。

先给出这次测试的list初始化结构:

                list.add("a");
		list.add("b");
		list.add("b");
		list.add("c");
		list.add("d");
		list.add("e");

目的是把其中的两个b元素给删除。有以下5中操作方法:


1.直接使用增强for循环,判断条件,删除元素;

代码:

       /**
	 * 方法一:遍历删除元素(错误版)
	 */
	public static void method1(){
		for(String s : list){
			if("b".equals(s)){
				list.remove(s);
			}
		}
		System.out.println("method1|list=" + list);
		// java.util.ConcurrentModificationException
	}

该种方法在遍历删除元素的时候会抛出ConcurrentmodificationException,具体为什么抛出,大家可以看源码。此处不重点讨论。

2.使用普通循环删除;

代码:

	/**
	 * 方法二:遍历删除元素(错误版)
	 */
	public static void method2(){
		for(int i=0;i<list.size();i++){
			if("b".equals(list.get(i))){
				list.remove(i);
			}
		}
		System.out.println("method2|list=" + list);
		// method2|list=[a, b, c, d, e]
	}

该版本遍历删除元素的时候,我们发现处理结果值删除了一个b元素,另外一个是没有删除的。

3.使用普通循环倒序删除(也是本次重要讨论的一个方法);

代码:

	/**
	 * 方法三:遍历删除元素(正确版)
	 */
	public static void method3(){
		for(int i=list.size()-1;i>=0;i--){
			if("b".equals(list.get(i))){
				list.remove(i);
			}
		}
		System.out.println("method3|list=" + list);
		// method3|list=[a, c, d, e]
	}

该方法就是我们此次要详细讨论的方法,处理结果就是我们的预期。

4.使用jdk1.8新增的Stream流操作;

代码:

        /**
	 * 方法四:遍历删除元素(正确版)需要JDK1.8以上
	 */
	public static void method4(){
		list = list.stream().filter(e -> !"b".equals(e)).collect(Collectors.toList());
		System.out.println("method4|list=" + list);
		// method4|list=[a, c, d, e]
	}

使用jdk1.8新增的流功能,操作很简单便捷,效率也可以,比较推荐此方式。

5.使用迭代器自身方法的remove删除元素。

代码:

	/**
	 * 方法五:遍历删除元素(正确版)
	 */
	public static void method5(){
		Iterator<String> it = list.iterator();
		while(it.hasNext()){
			String s = it.next();
			if("b".equals(s)){
				it.remove();
			}
		}
		System.out.println("method5|list=" + list);
		// method5|list=[a, c, d, e]
	}

该方法应该排在前面的,这个应该是最常用的方法了。使用迭代器自身的remove方法来进行元素删除。

---------------------------------我-------是-------分-------割-------线-------------------------------------

接下来就要详细讨论倒序遍历List删除元素的问题了。在此之前请允许我把正序删除元素为什么没有达到我们的预期给解释一下:

public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        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

        return oldValue;
    }

该方法是ArrayList按照索引删除元素的源代码。在删除元素之前,先判断了index是否越界,然后把modCount参数加一(改参数也是ConcurrentmodificationException异常产生的关键)。这里比较重要的是numMoved,该值为当前list大小减去index再减一。以我们创建的list为例:

size = 5;

index在获取到第一个b元素的时候为1(index从0开始计数);

所以,numMoved = 3;再看System.arraycopy方法,第一个参数是需要拷贝的源数组,第二个参数是要从源数组某个位置开始进行拷贝,第三个参数就是此次数组拷贝的目标数组,最后一个参数就是拷贝数组的长度。起作用见下图:


就是b元素删除后,把其后的元素向前补齐,把空间移到最后,然后放GC机制回收空间。但是此时b元素后的index都已经变化,删除的b元素index=1,接下来再遍历index=2时,对应的元素已经是c,所以有一个b元素没有删除,以至于与我们的预期不一样。

那么很明显,当我们倒序遍历元素的时候,无论b元素之后的元素怎么移动,之前的元素对应的index是不会发生变化的,所以在删除元素的时候不会发生问题。

package cn.ls011.thought.list;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;

public class TestList {
	private static List<String> list;
	
	static {
		list = new ArrayList<String>();
		list.add("a");
		list.add("b");
		list.add("b");
		list.add("c");
		list.add("d");
		list.add("e");
	}
	
	public static void main(String[] args) {
		method1();
		reset();
		method2();
		reset();
		method3();
		reset();
		method4();
		reset();
		method5();
	}
	
	/**
	 * 方法一:遍历删除元素(错误版)
	 */
	public static void method1(){
		for(String s : list){
			if("b".equals(s)){
				list.remove(s);
			}
		}
		System.out.println("method1|list=" + list);
		// java.util.ConcurrentModificationException
	}
	
	/**
	 * 方法二:遍历删除元素(错误版)
	 */
	public static void method2(){
		for(int i=0;i<list.size();i++){
			if("b".equals(list.get(i))){
				list.remove(i);
			}
		}
		System.out.println("method2|list=" + list);
		// method2|list=[a, b, c, d, e]
	}
	
	/**
	 * 方法三:遍历删除元素(正确版)
	 */
	public static void method3(){
		for(int i=list.size()-1;i>=0;i--){
			if("b".equals(list.get(i))){
				list.remove(i);
			}
		}
		System.out.println("method3|list=" + list);
		// method3|list=[a, c, d, e]
	}
	
	/**
	 * 方法四:遍历删除元素(正确版)需要JDK1.8以上
	 */
	public static void method4(){
		list = list.stream().filter(e -> !"b".equals(e)).collect(Collectors.toList());
		System.out.println("method4|list=" + list);
		// method4|list=[a, c, d, e]
	}
	
	/**
	 * 方法五:遍历删除元素(正确版)
	 */
	public static void method5(){
		Iterator<String> it = list.iterator();
		while(it.hasNext()){
			String s = it.next();
			if("b".equals(s)){
				it.remove();
			}
		}
		System.out.println("method5|list=" + list);
		// method5|list=[a, c, d, e]
	}
	
	/**
	 * 重置list列表
	 */
	private static void reset(){
		list.clear();
		list.add("a");
		list.add("b");
		list.add("b");
		list.add("c");
		list.add("d");
		list.add("e");
	}
}

综述:

list遍历删除元素还有存在一些“坑”的,稍不留神就会栽跟头。所以告诫自己,很多事情不能想当然,要仔细分析,拿出证据证明之。与大家共勉!

猜你喜欢

转载自blog.csdn.net/ls0111/article/details/80810101