ArrayList遍历时删除元素有哪些方法?

首先结论如下: 第1种方法 - 普通for循环正序删除(结果:会漏掉元素判断) 第2种方法 - 普通for循环倒序删除(结果:正确删除) 第3种方法 - for-each循环删除(结果:抛出异常) 第4种方法 - Iterator遍历,使用ArrayList.remove()删除元素(结果:抛出异常) 第5种方法 - Iterator遍历,使用Iterator的remove删除元素(结果:正确删除) 下面让我们来详细探究一下原因吧! 首先初始化一个数组arrayList,假设我们要删除等于3的元素。 ```java public static void main(String[] args) { ArrayList<Integer> arrayList = new ArrayList(); arrayList.add(1); arrayList.add(2); arrayList.add(3); arrayList.add(3); arrayList.add(4); arrayList.add(5); removeWayOne(arrayList); } ``` ### 第1种方法 - 普通for循环正序删除(结果:会漏掉对后一个元素的判断) ```java for (int i = 0; i < arrayList.size(); i++) { if (arrayList.get(i) == 3) {//3是要删除的元素 arrayList.remove(i); //解决方案: 加一行代码i = i - 1; 删除元素后,下标减1 } System.out.println("当前arrayList是"+arrayList.toString()); } //原ArrayList是[1, 2, 3, 3, 4, 5] //删除后是[1, 2, 3, 4, 5] ``` 输出结果: ```java 当前arrayList是[1, 2, 3, 3, 4, 5] 当前arrayList是[1, 2, 3, 3, 4, 5] 当前arrayList是[1, 2, 3, 4, 5] 当前arrayList是[1, 2, 3, 4, 5] 当前arrayList是[1, 2, 3, 4, 5] ``` 可以看到少删除了一个3, 原因在于调用remove删除元素时,remove方法调用System.arraycopy()方法将后面的元素移动到前面的位置,也就是第二个3会移动到数组下标为2的位置,而在下一次循环时,i+1之后,i会为3,不会对数组下标为2这个位置进行判断,所以这种写法,在删除元素时,被删除元素a的后一个元素b会移动a的位置,而i已经加1,会忽略对元素b的判断,所以如果是连续的重复元素,会导致少删除。 **解决方案** 可以在删除元素后,执行i=i-1,使得下次循环时再次对该数组下标进行判断。 ### 第2种方法 - 普通for循环倒序删除(结果:正确删除) ```java for (int i = arrayList.size() -1 ; i>=0; i--) { if (arrayList.get(i).equals(3)) { arrayList.remove(i); } System.out.println("当前arrayList是"+arrayList.toString()); } ``` 输出结果: ```java 当前arrayList是[1, 2, 3, 3, 4, 5] 当前arrayList是[1, 2, 3, 3, 4, 5] 当前arrayList是[1, 2, 3, 4, 5] 当前arrayList是[1, 2, 4, 5] 当前arrayList是[1, 2, 4, 5] 当前arrayList是[1, 2, 4, 5] ``` 这种方法可以正确删除元素,因为调用remove删除元素时,remove方法调用System.arraycopy()将被删除元素a后面的元素向前移动,而不会影响元素a之前的元素,所以倒序遍历可以正常删除元素。 ### 第3种方法 - for-each循环删除(结果:抛出异常) 抛出异常的根本原因在于for-each是使用Iterator来实现遍历的,调用ArrayList.remove()方法会将modCount+1,而Iterator内部的expectedModCount确没有更新,这样在进行下次循环时调用Iterator.next()会对modCount和expectedModCount进行比较,不一致就会抛出ConcurrentModificationException异常。 ```java public static void removeWayThree(ArrayList<Integer> arrayList) { for (Integer value : arrayList) { if (value.equals(3)) {//3是要删除的元素 arrayList.remove(value); } System.out.println("当前arrayList是"+arrayList.toString()); } } ``` 输出结果: ```java 当前arrayList是[1, 2, 3, 3, 4, 5] 当前arrayList是[1, 2, 3, 3, 4, 5] 当前arrayList是[1, 2, 3, 4, 5] Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901) at java.util.ArrayList$Itr.next(ArrayList.java:851) at com.test.ArrayListTest1.removeWayThree(ArrayListTest1.java:50) at com.test.ArrayListTest1.main(ArrayListTest1.java:24) ``` 会抛出ConcurrentModificationException异常,主要在于for-each的底层实现是使用ArrayList.iterator的hasNext()方法和next()方法实现的,我们可以使用反编译进行验证,对包含上面的方法的类使用以下命令反编译验证 ```java javac ArrayTest.java//生成ArrayTest.class文件 javap -c ArrayListTest.class//对class文件反编译 ``` 得到removeWayThree方法的反编译代码如下: ```java public static void removeWayThree(java.util.ArrayList<java.lang.Integer>); Code: 0: aload_0 1: invokevirtual #12 // Method java/util/ArrayList.iterator:()Ljava/util/Iterator; 4: astore_1 5: aload_1 6: invokeinterface #13, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z 调用Iterator.hasNext()方法 11: ifeq 44 14: aload_1 15: invokeinterface #14, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;调用Iterator.next()方法 20: checkcast #9 // class java/lang/Integer 23: astore_2 24: aload_2 25: iconst_3 26: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #10 // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z 32: ifeq 41 35: aload_0 36: aload_2 37: invokevirtual #15 // Method java/util/ArrayList.remove:(Ljava/lang/Object;)Z 40: pop 41: goto 5 44: return ``` 可以很清楚得看到Iterator.hasNext()来判断是否还有下一个元素,和Iterator.next()方法来获取下一个元素。而因为在删除元素时,remove()方法会调用fastRemove()方法,其中会对modCount+1,代表对数组进行了修改,将修改次数+1。 ```java 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 } ``` 而当删除完元素后,进行下一次循环时,会调用下面源码中Itr.next()方法获取下一个元素,会调用checkForComodification()方法对ArrayList进行校验,判断在遍历ArrayList是否已经被修改,由于之前对modCount+1,而Iterator中的expectedModCount还是初始化时ArrayList.Itr对象时赋的值,所以会不相等,然后抛出ConcurrentModificationException异常。 那么有什么办法可以让expectedModCount及时更新呢? 可以看到下面Itr的源码中,在Itr.remove()方法中删除元素后会对 expectedModCount更新,所以我们在使用删除元素时使用Itr.remove()方法来删除元素就可以保证expectedModCount的更新了,具体看第5种方法。 ```java //使用Iterator遍历元素的方法 /* Iterator遍历时使用next()方法返回下一个元素,主要通过将游标cursor+1,获得下一个元素 调用remove()删除元素时,主要删除lastRet下标对应的元素,并且将cursor设置为lastRet的值,因为后面的元素向前面的空位移动了一位 Iterator遍历过程中,在一次循环中也不能多次调用remove()方法,因为每次remove()后就会将lastRet设置为-1,本次循环中再remove就会抛异常,必须等调用next()方法后对lastRet重新赋值。 */ public void tranverse() { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); list.add(2); list.add(3); list.add(3); list.add(4); list.add(5); Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()) { Integer value = iterator.next(); System.out.println(value); if (value.equals(3)) { iterator.remove(); iterator.remove();//在循环中多次调用iterator的remove方法会抛出异常 iterator.remove(); } } } ``` Iterator的源代码 ```java private class Itr implements Iterator<E> { int cursor; // 游标 int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount;//期待的modCount值 public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { checkForComodification();//判断expectedModCount与当前的modCount是否一致 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]; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount;//更新expectedModCount } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } } ``` ### 第4种方法 - Iterator遍历,使用ArrayList.remove()删除元素(结果:抛出异常) ```java Iterator<Integer> iterator = arrayList.iterator(); while (iterator.hasNext()) { Integer value = iterator.next(); if (value.equals(3)) {//3是要删除的元素 arrayList.remove(value); } System.out.println("当前arrayList是"+arrayList.toString()); } ``` 输出结果: ```java 当前arrayList是[1, 2, 3, 3, 4, 5] 当前arrayList是[1, 2, 3, 3, 4, 5] 当前arrayList是[1, 2, 3, 4, 5] Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901) at java.util.ArrayList$Itr.next(ArrayList.java:851) at com.test.ArrayListTest1.removeWayFour(ArrayListTest1.java:61) at com.test.ArrayListTest1.main(ArrayListTest1.java:25) ``` 第4种方法其实是第3种方法在编译后的代码,所以第四种写法也会抛出ConcurrentModificationException异常。这种需要注意的是,每次调用iterator的next()方法,会导致游标向右移动,从而达到遍历的目的。所以在单次循环中不能多次调用next()方法,不然会导致每次循环时跳过一些元素,我在一些博客里面看到了一些错误的写法,比如这一篇[《在ArrayList的循环中删除元素,会不会出现问题?》](https://juejin.cn/post/6844903672481054733)文章中: ![image.png](https://images.codewd.com/d1a1974df4ff912f1dd29b08b94d6e35.png) 先调用iterator.next()获取元素,与elem进行比较,如果相等,再调用list.remove(iterator.next());来移除元素,这个时候的iterator.next()其实已经不是与elem相等的元素了,而是后一个元素了,我们可以写个demo来测试一下 ```java ArrayList<Integer> arrayList = new ArrayList(); arrayList.add(1); arrayList.add(2); arrayList.add(3); arrayList.add(4); arrayList.add(5); arrayList.add(6); arrayList.add(7); Integer elem = 3; Iterator iterator = arrayList.iterator(); while (iterator.hasNext()) { System.out.println(arrayList); if(iterator.next().equals(elem)) { arrayList.remove(iterator.next()); } } ``` 输出结果如下: ```java [1, 2, 3, 4, 5, 6, 7] [1, 2, 3, 4, 5, 6, 7] [1, 2, 3, 4, 5, 6, 7] [1, 2, 3, 5, 6, 7] Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901) at java.util.ArrayList$Itr.next(ArrayList.java:851) at com.test.ArrayListTest1.main(ArrayListTest1.java:29) ``` 可以看到移除的元素其实不是3,而是3之后的元素,因为调用了两次next()方法,导致游标多移动了。所以应该使用Integer value = iterator.next();将元素取出进行判断。 ### 第5种方法 - Iterator遍历,使用Iterator的remove删除元素(结果:正确删除) ```java Iterator<Integer> iterator = arrayList.iterator(); while (iterator.hasNext()) { Integer value = iterator.next(); if (value.equals(3)) {//3是需要删除的元素 iterator.remove(); } } ``` 输出结果: ```java 当前arrayList是[1, 2, 3, 3, 4, 5] 当前arrayList是[1, 2, 3, 3, 4, 5] 当前arrayList是[1, 2, 3, 4, 5] 当前arrayList是[1, 2, 4, 5] 当前arrayList是[1, 2, 4, 5] 当前arrayList是[1, 2, 4, 5] ``` 可以正确删除元素。 跟第3种和第4种方法的区别在于是使用iterator.remove();来移除元素,而在remove()方法中会对iterator的expectedModCount变量进行更新,所以在下次循环调用iterator.next()方法时,expectedModCount与modCount相等,不会抛出异常。