java set遍历删除元素_「原理探究」你问我ArrayList遍历时删除元素的正确姿势是什么?...

简介

我们在项目开发过程中,经常会有需求需要删除ArrayList中的某个元素,而使用不正确的删除方式,就有可能抛出异常。或者在面试中,会遇到面试官询问遍历时如何正常删除元素。所以在本篇文章中,我们会对几种删除元素的方式进行测试,并对原理进行研究,希望可以帮助到大家!

ArrayList遍历时删除元素的几种姿势b8d4b6e5c9c41d6cde9d95f06b3f9905.png

首先结论如下:第1种方法 - 普通for循环正序删除(结果:会漏掉元素判断)

第2种方法 - 普通for循环倒序删除(结果:正确删除)

第3种方法 - for-each循环删除(结果:抛出异常)

第4种方法 - Iterator遍历,使用ArrayList.remove()删除元素(结果:抛出异常)

第5种方法 - Iterator遍历,使用Iterator的remove删除元素(结果:正确删除)

下面让我们来详细探究一下原因吧!

首先初始化一个数组arrayList,假设我们要删除等于3的元素。public static void main(String[] args) {

ArrayList 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循环正序删除(结果:会漏掉元素判断)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]

输出结果:当前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循环倒序删除(结果:正确删除)for (int i = arrayList.size() -1 ; i>=0; i--) {

if (arrayList.get(i).equals(3)) {

arrayList.remove(i);

}

System.out.println("当前arrayList是"+arrayList.toString());

}

输出结果:当前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循环删除(结果:抛出异常)public static void removeWayThree(ArrayList arrayList) {

for (Integer value : arrayList) {

if (value.equals(3)) {//3是要删除的元素

arrayList.remove(value);

}

System.out.println("当前arrayList是"+arrayList.toString());

}

}

输出结果:当前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()方法实现的,我们可以使用反编译进行验证,对包含上面的方法的类使用以下命令反编译验证javac ArrayTest.java//生成ArrayTest.class文件

javap -c ArrayListTest.class//对class文件反编译

得到removeWayThree方法的反编译代码如下:public static void removeWayThree(java.util.ArrayList);

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。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,而expectedModCount还是初始化时ArrayList.Itr对象时赋的值,所以会不相等,然后抛出ConcurrentModificationException异常。

那么有什么办法可以让expectedModCount及时更新呢?

可以看到下面Itr的源码中,在Itr.remove()方法中删除元素后会对 expectedModCount更新,所以我们在使用删除元素时使用Itr.remove()方法来删除元素就可以保证expectedModCount的更新了,具体看第5种方法。private class Itr implements Iterator {

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()删除元素(结果:抛出异常)Iterator iterator = arrayList.iterator();

while (iterator.hasNext()) {

Integer value = iterator.next();

if (value.equals(3)) {//3是要删除的元素

arrayList.remove(value);

}

System.out.println("当前arrayList是"+arrayList.toString());

}

输出结果:当前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)

第3种方法在编译后的代码,其实是跟第4种是一样的,所以第四种写法也会抛出ConcurrentModificationException异常。这种需要注意的是,每次调用iterator的next()方法,会导致游标向右移动,从而达到遍历的目的。所以在单次循环中不能多次调用next()方法,不然会导致每次循环时跳过一些元素,我在一些博客里面看到了一些错误的写法,比如这一篇《在ArrayList的循环中删除元素,会不会出现问题?》文章中:

先调用iterator.next()获取元素,与elem进行比较,如果相等,再调用list.remove(iterator.next());来移除元素,这个时候的iterator.next()其实已经不是与elem相等的元素了,而是后一个元素了,我们可以写个demo来测试一下ArrayList 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());

}

}

输出结果如下:[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删除元素(结果:正确删除)Iterator iterator = arrayList.iterator();

while (iterator.hasNext()) {

Integer value = iterator.next();

if (value.equals(3)) {//3是需要删除的元素

iterator.remove();

}

}

输出结果:当前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相等,不会抛出异常。

HashMap遍历时删除元素的几种姿势

首先结论如下:第1种方法 - for-each遍历HashMap.entrySet,使用HashMap.remove()删除(结果:抛出异常)。

第2种方法-for-each遍历HashMap.keySet,使用HashMap.remove()删除(结果:抛出异常)。

第3种方法-使用HashMap.entrySet().iterator()遍历删除(结果:正确删除)。

下面让我们来详细探究一下原因吧!

HashMap的遍历删除方法与ArrayList的大同小异,只是api的调用方式不同。首先初始化一个HashMap,我们要删除key包含"3"字符串的键值对。HashMap hashMap = new HashMap();

hashMap.put("key1",1);

hashMap.put("key2",2);

hashMap.put("key3",3);

hashMap.put("key4",4);

hashMap.put("key5",5);

hashMap.put("key6",6);

第1种方法 - for-each遍历HashMap.entrySet,使用HashMap.remove()删除(结果:抛出异常)for (Map.Entry entry: hashMap.entrySet()) {

String key = entry.getKey();

if(key.contains("3")){

hashMap.remove(entry.getKey());

}

System.out.println("当前HashMap是"+hashMap+" 当前entry是"+entry);

}

输出结果:当前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 当前entry是key1=1

当前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 当前entry是key2=2

当前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 当前entry是key5=5

当前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 当前entry是key6=6

当前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4} 当前entry是key3=3

Exception in thread "main" java.util.ConcurrentModificationException

at java.util.HashMap$HashIterator.nextNode(HashMap.java:1429)

at java.util.HashMap$EntryIterator.next(HashMap.java:1463)

at java.util.HashMap$EntryIterator.next(HashMap.java:1461)

at com.test.HashMapTest.removeWayOne(HashMapTest.java:29)

at com.test.HashMapTest.main(HashMapTest.java:22)

第2种方法-for-each遍历HashMap.keySet,使用HashMap.remove()删除(结果:抛出异常)Set keySet = hashMap.keySet();

for(String key : keySet){

if(key.contains("3")){

keySet.remove(key);

}

System.out.println("当前HashMap是"+hashMap+" 当前key是"+key);

}

输出结果如下:当前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 当前key是key1

当前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 当前key是key2

当前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 当前key是key5

当前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 当前key是key6

当前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4} 当前key是key3

Exception in thread "main" java.util.ConcurrentModificationException

at java.util.HashMap$HashIterator.nextNode(HashMap.java:1429)

at java.util.HashMap$KeyIterator.next(HashMap.java:1453)

at com.test.HashMapTest.removeWayTwo(HashMapTest.java:40)

at com.test.HashMapTest.main(HashMapTest.java:23)

第3种方法-使用HashMap.entrySet().iterator()遍历删除(结果:正确删除)Iterator> iterator  = hashMap.entrySet().iterator();

while (iterator.hasNext()) {

Map.Entry entry = iterator.next();

if(entry.getKey().contains("3")){

iterator.remove();

}

System.out.println("当前HashMap是"+hashMap+" 当前entry是"+entry);

}

输出结果:当前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 当前entry是key1=1

当前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 当前entry是key2=2

当前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 当前entry是key5=5

当前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 当前entry是key6=6

当前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 当前entry是key4=4

当前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4} 当前entry是deletekey=3

第1种方法和第2种方法抛出ConcurrentModificationException异常与上面ArrayList错误遍历-删除方法的原因一致,HashIterator也有一个expectedModCount,在遍历时获取下一个元素时,会调用next()方法,然后对expectedModCount和modCount进行判断,不一致就抛出ConcurrentModificationException异常。abstract class HashIterator {

Node next;        // next entry to return

Node current;     // current entry

int expectedModCount;  // for fast-fail

int index;             // current slot

HashIterator() {

expectedModCount = modCount;

Node[] t = table;

current = next = null;

index = 0;

if (t != null && size > 0) { // advance to first entry

do {} while (index < t.length && (next = t[index++]) == null);

}

}

public final boolean hasNext() {

return next != null;

}

final Node nextNode() {

Node[] t;

Node e = next;

if (modCount != expectedModCount)

throw new ConcurrentModificationException();

if (e == null)

throw new NoSuchElementException();

if ((next = (current = e).next) == null && (t = table) != null) {

do {} while (index < t.length && (next = t[index++]) == null);

}

return e;

}

public final void remove() {

Node p = current;

if (p == null)

throw new IllegalStateException();

if (modCount != expectedModCount)

throw new ConcurrentModificationException();

current = null;

K key = p.key;

removeNode(hash(key), key, null, false, false);

expectedModCount = modCount;

}

}

PS:ConcurrentModificationException是什么?56eff31c81f029e9edcaee79aa6c0747.png

根据ConcurrentModificationException的文档介绍,一些对象不允许并发修改,当这些修改行为被检测到时,就会抛出这个异常。(例如一些集合不允许一个线程一边遍历时,另一个线程去修改这个集合)。

一些集合(例如Collection, Vector, ArrayList,LinkedList, HashSet, Hashtable, TreeMap, AbstractList, Serialized Form)的Iterator实现中,如果提供这种并发修改异常检测,那么这些Iterator可以称为是"fail-fast Iterator",意思是快速失败迭代器,就是检测到并发修改时,直接抛出异常,而不是继续执行,等到获取到一些错误值时在抛出异常。

异常检测主要是通过modCount和expectedModCount两个变量来实现的,modCount集合被修改的次数,一般是被集合(ArrayList之类的)持有,每次调用add(),remove()方法会导致modCount+1

expectedModCount期待的modCount,一般是被Iterator(ArrayList.iterator()方法返回的iterator对象)持有,一般在Iterator初始化时会赋初始值,在调用Iterator的remove()方法时会对expectedModCount进行更新。(可以看看上面的ArrayList.Itr源码)

然后在Iterator调用next()遍历元素时,会调用checkForComodification()方法比较modCount和expectedModCount,不一致就抛出ConcurrentModificationException。

单线程操作Iterator不当时也会抛出ConcurrentModificationException异常。(上面的例子就是)

总结

因为ArrayList和HashMap的Iterator都是上面所说的“fail-fast Iterator”,Iterator在获取下一个元素,删除元素时,都会比较expectedModCount和modCount,不一致就会抛出异常。

所以当使用Iterator遍历元素(for-each遍历底层实现也是Iterator)时,需要删除元素,一定需要使用 Iterator的remove()方法 来删除,而不是直接调用ArrayList或HashMap自身的remove()方法,否则会导致Iterator中的expectedModCount没有及时更新,之后获取下一个元素或者删除元素时,expectedModCount和modCount不一致,然后抛出ConcurrentModificationException异常。047478e91b218126b59fe067226c5037.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值