好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

Java中ArrayList在foreach里remove的问题详析

前言

arraylist就是传说中的动态数组,用msdn中的说法,就是array的复杂版本,它提供了如下一些好处:

动态的增加和减少元素 实现了icollection和ilist接口 灵活的设置数组的大小

都说arraylist在用foreach循环的时候,不能add元素,也不能remove元素,可能会抛异常,那我们就来分析一下它具体的实现。我目前的环境是java8。

有下面一段代码:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

public class testforeachlist extends basetests {

 

  @test

  public void testforeach() {

  list<string> list = new arraylist<>();

  list.add( "1" );

  list.add( "2" );

  list.add( "3" );

 

  for (string s : list) {

  }

  }

 

}

代码很简单,一个arraylist添加3个元素,foreach循环一下,啥都不干。那么foreach到底是怎么实现的呢,暴力的方法看一下,编译改类,用 javap -c testforeachlist 查看class文件的字节码,如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

javap -c testforeachlist

warning: binary file testforeachlist contains collection.list.testforeachlist

compiled from "testforeachlist.java"

public class collection.list.testforeachlist extends com.ferret.basetests {

  public collection.list.testforeachlist();

  code:

  0 : aload_0

  1 : invokespecial # 1   // method com/ferret/basetests."<init>":()v

  4 : return

 

  public void testforeach();

  code:

  0 : new # 2   // class java/util/arraylist

  3 : dup

  4 : invokespecial # 3   // method java/util/arraylist."<init>":()v

  7 : astore_1

  8 : aload_1

  9 : ldc # 4   // string 1

  11 : invokeinterface # 5 , 2 // interfacemethod java/util/list.add:(ljava/lang/object;)z

  16 : pop

  17 : aload_1

  18 : ldc # 6   // string 2

  20 : invokeinterface # 5 , 2 // interfacemethod java/util/list.add:(ljava/lang/object;)z

  25 : pop

  26 : aload_1

  27 : ldc # 7   // string 3

  29 : invokeinterface # 5 , 2 // interfacemethod java/util/list.add:(ljava/lang/object;)z

  34 : pop

  35 : aload_1

  36 : invokeinterface # 8 , 1 // interfacemethod java/util/list.iterator:()ljava/util/iterator;

  41 : astore_2

  42 : aload_2

  43 : invokeinterface # 9 , 1 // interfacemethod java/util/iterator.hasnext:()z

  48 : ifeq 64

  51 : aload_2

  52 : invokeinterface # 10 , 1 // interfacemethod java/util/iterator.next:()ljava/lang/object;

  57 : checkcast # 11   // class java/lang/string

  60 : astore_3

  61 : goto 42

  64 : return

}

可以勉强读,大约是调用了list.iterator,然后根据iterator的hasnext方法返回结果判断是否有下一个,根据next方法取到下一个元素。

但是是总归是体验不好,我们是现代人,所以用一些现代化的手段,直接用idea打开该class文件自动反编译,得到如下内容:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

public class testforeachlist extends basetests {

  public testforeachlist() {

  }

 

  @test

  public void testforeach() {

  list<string> list = new arraylist();

  list.add( "1" );

  list.add( "2" );

  list.add( "3" );

 

  string var3;

  for (iterator var2 = list.iterator(); var2.hasnext(); var3 = (string)var2.next()) {

  ;

  }

 

  }

}

体验好多了,再对比上面的字节码文件,没错

?

1

2

3

for (iterator var2 = list.iterator(); var2.hasnext(); var3 = (string)var2.next()) {

  ;

  }

这就是脱掉语法糖外壳的foreach的真正实现。

接下来我们看看这三个方法具体都是怎么实现的:

iterator

arraylist的iterator实现如下:

?

1

2

3

4

5

6

7

8

9

10

public iterator<e> iterator() {

  return new itr();

}

 

private class itr implements iterator<e> {

  int cursor; // index of next element to return

  int lastret = - 1 ; // index of last element returned; -1 if no such

  int expectedmodcount = modcount;

  //省略部分实现

}

itr是arraylist中的内部类,所以 list.iterator() 的作用是返回了一个itr对象赋值到var2,后面调用 var2.hasnext() , var2.next() 就是itr的具体实现了。

这里还值的一提的是expectedmodcount, 这个变量记录被赋值为modcount, modcount是arraylist的父类abstractlist的一个字段,这个字段的含义是list结构发生变更的次数,通常是add或remove等导致元素数量变更的会触发modcount++。

下面接着看 itr.hasnext()``var2.next() 的实现。

itr.hasnext 和 itr.next 实现

hasnext很简单

?

1

2

3

public boolean hasnext() {

  return cursor != size;

  }

当前index不等于size则说明还没迭代完,这里的size是外部类arraylist的字段,表示元素个数。

在看next实现:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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];

  }

 

final void checkforcomodification() {

  if (modcount != expectedmodcount)

  throw new concurrentmodificationexception();

  }

next方法第一步 checkforcomodification() ,它做了什么? 如果 modcount != expectedmodcount 就抛出异常concurrentmodificationexception。modcount是什么?外部类arraylist的元素数量变更次数;expectedmodcount是什么?初始化内部类itr的时候外部类的元素数量变更次数。

所以,如果在foreach中做了add或者remove操作会导致程序异常concurrentmodificationexception。这里可以走两个例子:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

@test (expected = concurrentmodificationexception. class )

public void testlistforeachremovethrow() {

list<string> list = new arraylist<>();

list.add( "1" );

list.add( "2" );

list.add( "3" );

 

for (string s : list) {

list.remove(s);

}

}

 

@test (expected = concurrentmodificationexception. class )

public void testlistforeachaddthrow() {

list<string> list = new arraylist<>();

list.add( "1" );

list.add( "2" );

list.add( "3" );

 

for (string s : list) {

list.add(s);

}

}

单元测试跑过,都抛了concurrentmodificationexception。

checkforcomodification() 之后的代码比较简单这里就不分析了。

倒数第二个元素的特殊

到这里我们来捋一捋大致的流程:

获取到itr对象赋值给var2

判断hasnext,也就是判断 cursor != size ,当前迭代元素下标不等于list的个数,则返回true继续迭代;反之退出循环

next取出迭代元素

checkforcomodification() ,判断 modcount != expectedmodcount ,元素数量变更次数不等于初始化内部类itr的时元素变更次数,也就是在迭代期间做过修改就抛concurrentmodificationexception。 如果检查通过cursor++

下面考虑一种情况:remove了倒数第二个元素会发生什么?代码如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

@test

public void testlistforeachremoveback2notthrow() {

  list<string> list = new arraylist<>();

  list.add( "1" );

  list.add( "2" );

  list.add( "3" );

 

  for (string s : list) {

  system.out.println(s);

  if ( "2" .equals(s)) {

  list.remove(s);

  }

  }

}

猜一下会抛出异常吗?答案是否定的。输出为:

1
2

发现少了3没有输出。 分析一下

在倒数第二个元素"2"remove后,list的size-1变为了2,而此时itr中的cur在next方法中取出元素"2"后,做了加1,值变为2了,导致下次判断hasnext时,cursor==size,hasnext返回false,最终最后一个元素没有被输出。

如何避坑

foreach中remove 或 add 有坑,

在foreach中做导致元素个数发生变化的操作(remove, add等)时,会抛出concurrentmodificationexception异常 在foreach中remove倒数第二个元素时,会导致最后一个元素不被遍历

那么我们如何避免呢?不能用foreach我们就用fori嘛,如下代码:

?

1

2

3

4

5

6

7

8

9

10

11

12

@test

  public void testlistforimiss() {

  list<string> list = new arraylist<>();

  list.add( "1" );

  list.add( "2" );

  list.add( "3" );

 

  for ( int i = 0 ; i < list.size(); i++) {

   system.out.println(list.get(i));

   list.remove(i);

  }

  }

很明显上面是一个错误的示范,输出如下:

1
3

原因很简单,原来的元素1被remove后,后面的向前拷贝,2到了原来1的位置(下标0),3到了原来2的位置(下标1),size由3变2,i+1=1,输出list.get(1)就成了3,2被漏掉了。

下面说下正确的示范:

方法一,还是fori,位置前挪了减回去就行了, remove后i--:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

@test

  public void testlistforiright() {

  list<string> list = new arraylist<>();

  list.add( "1" );

  list.add( "2" );

  list.add( "3" );

 

  for ( int i = 0 ; i < list.size(); i++) {

   system.out.println(list.get(i));

   list.remove(i);

   i--; //位置前挪了减回去就行了

  }

  }

方法二,不用arraylist的remove方法,用itr自己定义的remove方法,代码如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

@test

  public void testiteratorremove() {

  list<string> list = new arraylist<>();

  list.add( "1" );

  list.add( "2" );

  list.add( "3" );

 

  iterator<string> itr = list.iterator();

  while (itr.hasnext()) {

   string s = itr.next();

   system.out.println(s);

   itr.remove();

  }

  }

为什么itr自己定义的remove就不报错了呢?看下源码:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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();

   }

  }

依然有 checkforcomodification()校验,但是看到后面又重新赋值了,所以又相等了。

ok,以上就是全部内容。介绍了foreach中list remove的坑,以及如何避免。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。

原文链接:https://www.cnblogs.com/chrischennx/p/9610853.html

查看更多关于Java中ArrayList在foreach里remove的问题详析的详细内容...

  阅读:41次