从源码及字节码角度分析For-Each删除问题

前言

印象中list在循环中删除成员时是会抛出异常的,结果最近无意中看到了关于该操作的相关文章,就将循环删除的几种方式均试了一下,实际结果却大相径庭。

今天就借此将结果和原因好好梳理分析一下。

这里重点介绍ArrayList这个类型的数据结构。

示例代码说明

本篇使用到的示例代码如下,使用 ArrayList,并放入“1”、“2”、“3”这三个字符串。

在注释处替换为下方各方式内的示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.ArrayList;

public class TestForEach {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");

//此处替换为下方各种方式内的遍历代码



System.out.println(list);

}
}

Iterable.forEach(Consumer<? super E> action)

集合类均实现了 Iterable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package java.util
/**
* @see Set
* @see List
* @see Map
* @see SortedSet
* @see SortedMap
* @see HashSet
* @see TreeSet
* @see ArrayList
* @see LinkedList
* @see Vector
* @see Collections
* @see Arrays
* @see AbstractCollection
* @since 1.2
*/

public interface Collection<E> extends Iterable<E> {
···
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
42
43
44
45
46
47
package java.lang;

/**
* Implementing this interface allows an object to be the target of
* the "for-each loop" statement. See
* <strong>
* <a href="{@docRoot}/../technotes/guides/language/foreach.html">For-each Loop</a>
* </strong>
*
* @param <T> the type of elements returned by the iterator
*
* @since 1.5
* @jls 14.14.2 The enhanced for statement
*/
public interface Iterable<T> {

···

/**
* Performs the given action for each element of the {@code Iterable}
* until all elements have been processed or the action throws an
* exception. Unless otherwise specified by the implementing class,
* actions are performed in the order of iteration (if an iteration order
* is specified). Exceptions thrown by the action are relayed to the
* caller.
*
* @implSpec
* <p>The default implementation behaves as if:
* <pre>{@code
* for (T t : this)
* action.accept(t);
* }</pre>
*
* @param action The action to be performed for each element
* @throws NullPointerException if the specified action is null
* @since 1.8
*/
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}

···

}

示例

1
2
3
4
5
list.forEach(number -> {
if ("2".equals(number)) {
list.remove(number);
}
});

运行结果

1
2
3
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList.forEach(ArrayList.java:1260)
at TestForEach.main(TestForEach.java:10)

分析

可以看到,报了 java.util.ConcurrentModificationException错误,并提示了出错位置在 java.util.ArrayList.forEach(ArrayList.java:1260)

这是因为ArrayList重写了forEach方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}

其中最主要的一个就是modCount,这个是ArrayList父类AbstractList的成员变量,默认值为0,表示list成员结构性修改的次数(add、remove、clear、sort等操作会使modCount值+1)。

而我们回调内调用remove()方法,该方法内部又调用了fastRemove(),从而使得modCount++,导致modCount != expectedModCount成立后抛出异常。

1
2
3
4
5
6
7
8
9
10
11
/**
* The number of times this list has been <i>structurally modified</i>.
* Structural modifications are those that change the size of the
* list, or otherwise perturb it in such a fashion that iterations in
* progress may yield incorrect results.
*

···

*/
protected transient int modCount = 0;

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
42
/**
* Removes the first occurrence of the specified element from this list,
* if it is present. If the list does not contain the element, it is
* unchanged. More formally, removes the element with the lowest index
* <tt>i</tt> such that
* <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
* (if such an element exists). Returns <tt>true</tt> if this list
* contained the specified element (or equivalently, if this list
* changed as a result of the call).
*
* @param o element to be removed from this list, if present
* @return <tt>true</tt> if this list contained the specified element
*/
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 remove method that skips bounds checking and does not
* return the value removed.
*/
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
}

所以Arraylist在forEach回调内不能执行删除等会导致modCount值变化的操作。

而LinkedList等则没有重写forEach方法对modCount变量进行判断,所以这类在forEach回调内进行删除等操作是正常的。

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

示例

1
2
3
4
5
6
for (int i = 0; i < list.size(); i++) {
String number = list.get(i);
if ("1".equals(number)) {
list.remove(number);
}
}

结果

1
[2, 3]

能正常运行完毕,并输出当前list内的数据,可以看到字符串“1”已经被删除了。

字节码分析

这里字节码相对比较简单,就直接带过,详细的字节码分析放在For-Each小节内。

这里字节码所表示的意思就是和Java源码所直接表示的意思一样,仅仅是循环执行for循环方法体内的代码逻辑,字符串常量“1”与列表内取出的元素做比较,一致则将其从列表中移除。

1
javap -c TestForEach > TestForEach.c

得到文件:

TestForEach.c

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
42
43
44
45
46
47
48
49
50
51
52
Compiled from "TestForEach.java"
public class TestForEach {
public TestForEach();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
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: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
14: pop
15: aload_1
16: ldc #6 // String 2
18: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
21: pop
22: aload_1
23: ldc #7 // String 3
25: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
28: pop
29: iconst_0
30: istore_2
31: iload_2
32: aload_1
33: invokevirtual #8 // Method java/util/ArrayList.size:()I
36: if_icmpge 69
39: aload_1
40: iload_2
41: invokevirtual #9 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
44: checkcast #10 // class java/lang/String
47: astore_3
48: ldc #4 // String 1
50: aload_3
51: invokevirtual #11 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
54: ifeq 63
57: aload_1
58: aload_3
59: invokevirtual #12 // Method java/util/ArrayList.remove:(Ljava/lang/Object;)Z
62: pop
63: iinc 2, 1
66: goto 31
69: getstatic #13 // Field java/lang/System.out:Ljava/io/PrintStream;
72: aload_1
73: invokevirtual #14 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
76: return
}

for( T : Iterable)

这个就是增强for循环的写法,除了可以遍历数组,还可以用于遍历所有实现了Iterable 接口的对象。

示例

1
2
3
4
5
for (String number : list) {
if ("1".equals(number)) {
list.remove(number);
}
}

结果

1
2
3
4
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at TestForEach.main(TestForEach.java:10)

分析

可以看到,程序又报了 java.util.ConcurrentModificationException错误。

但是这次提示的错误信息与 Iterable.forEach(Consumer<? super E> action)内的不一样。

从这错误信息来分析,出错的地方是在ArrayList的内部类Itr内的一个方法。但是明明写的代码里面没有用到这个类啊,而且remove()方法内也是没有调用该类,那推测是这增强for循环在遍历时调用的。等同于入下代码:

1
2
3
4
5
6
for (Iterator<String> iterator = list.iterator();iterator.hasNext();){
String number = iterator.next();
if ("1".equals(number)) {
list.remove(number);
}
}

通过iterator.hasNext()判断是否还有下个元素,通过iterator.next()取出元素。

ArrayList#iterator()返回的就是Itr对象。

1
2
3
4
5
6
7
8
9
10
/**
* Returns an iterator over the elements in this list in proper sequence.
*
* <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>.
*
* @return an iterator over the elements in this list in proper sequence
*/
public Iterator<E> iterator() {
return new Itr();
}

java.util.ArrayList.Itr

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
* An optimized version of AbstractList.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() {}

public boolean hasNext() {
return cursor != size; // size是ArrayList的成员变量,用于记录当前列表内存放的元素数量
}

@SuppressWarnings("unchecked")
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];
}

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

@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

Itr类的逻辑比较简单,变量也有注释进行说明。

其中

  • cursor:下个元素下标

  • lastRet:上个返回元素的下标,-1表示没有这个元素

  • expectedModCount:记录ArrayList内modCount值,用于next()remove()forEachRemaining()内做检验

其中next()remove()方法在执行时,会先调用checkForComodification()进行判断modCount在此遍历期间是否有过改动,改动过(及ArrayList数据源有过结构性改动,比如增加、删除、元素排序等)就会抛出ConcurrentModificationException异常。

因为在for循环体内调用了了remove()方法,所以在next() -> checkForComodification()时会抛出ConcurrentModificationException异常。

字节码分析

反编译字节码文件

Java内置了一个javap工具,通过该工具就能将 .class文件进行反汇编,得到具体的字节码指令。

通过javap -help就能看到javap工具的所有使用方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
➜  ~ javap -help
用法: javap <options> <classes>
其中, 可能的选项包括:
-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置

其中 `-v参数能导出了整个class文件的详情;

-c参数,得到代码相关的JVM指令操作码。

1
javap -v TestForEach > TestForEach.v

得到TestForEach.v文件,其中.v后缀是为了和-v参数对应,方便辨别。文件内保存的是文本内容,文件后缀可以随意取。

TestForEach.v

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
Classfile /Users/nht/WorkSpace/java/bytecode/TestForEach.class
Last modified 2021-6-10; size 1119 bytes
MD5 checksum 3fb9f68c6d74f1a5bf9591aa607bf002
Compiled from "TestForEach.java"
public class TestForEach
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #17.#40 // java/lang/Object."<init>":()V
#2 = Class #41 // java/util/ArrayList
#3 = Methodref #2.#40 // java/util/ArrayList."<init>":()V
#4 = String #42 // 1
#5 = Methodref #2.#43 // java/util/ArrayList.add:(Ljava/lang/Object;)Z
#6 = String #44 // 2
#7 = String #45 // 3
#8 = Methodref #2.#46 // java/util/ArrayList.iterator:()Ljava/util/Iterator;
#9 = InterfaceMethodref #47.#48 // java/util/Iterator.hasNext:()Z
#10 = InterfaceMethodref #47.#49 // java/util/Iterator.next:()Ljava/lang/Object;
#11 = Class #50 // java/lang/String
#12 = Methodref #11.#51 // java/lang/String.equals:(Ljava/lang/Object;)Z
#13 = Methodref #2.#52 // java/util/ArrayList.remove:(Ljava/lang/Object;)Z
#14 = Fieldref #53.#54 // java/lang/System.out:Ljava/io/PrintStream;
#15 = Methodref #55.#56 // java/io/PrintStream.println:(Ljava/lang/Object;)V
#16 = Class #57 // TestForEach
#17 = Class #58 // java/lang/Object
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 LTestForEach;
#25 = Utf8 main
#26 = Utf8 ([Ljava/lang/String;)V
#27 = Utf8 number
#28 = Utf8 Ljava/lang/String;
#29 = Utf8 args
#30 = Utf8 [Ljava/lang/String;
#31 = Utf8 list
#32 = Utf8 Ljava/util/ArrayList;
#33 = Utf8 LocalVariableTypeTable
#34 = Utf8 Ljava/util/ArrayList<Ljava/lang/String;>;
#35 = Utf8 StackMapTable
#36 = Class #41 // java/util/ArrayList
#37 = Class #59 // java/util/Iterator
#38 = Utf8 SourceFile
#39 = Utf8 TestForEach.java
#40 = NameAndType #18:#19 // "<init>":()V
#41 = Utf8 java/util/ArrayList
#42 = Utf8 1
#43 = NameAndType #60:#61 // add:(Ljava/lang/Object;)Z
#44 = Utf8 2
#45 = Utf8 3
#46 = NameAndType #62:#63 // iterator:()Ljava/util/Iterator;
#47 = Class #59 // java/util/Iterator
#48 = NameAndType #64:#65 // hasNext:()Z
#49 = NameAndType #66:#67 // next:()Ljava/lang/Object;
#50 = Utf8 java/lang/String
#51 = NameAndType #68:#61 // equals:(Ljava/lang/Object;)Z
#52 = NameAndType #69:#61 // remove:(Ljava/lang/Object;)Z
#53 = Class #70 // java/lang/System
#54 = NameAndType #71:#72 // out:Ljava/io/PrintStream;
#55 = Class #73 // java/io/PrintStream
#56 = NameAndType #74:#75 // println:(Ljava/lang/Object;)V
#57 = Utf8 TestForEach
#58 = Utf8 java/lang/Object
#59 = Utf8 java/util/Iterator
#60 = Utf8 add
#61 = Utf8 (Ljava/lang/Object;)Z
#62 = Utf8 iterator
#63 = Utf8 ()Ljava/util/Iterator;
#64 = Utf8 hasNext
#65 = Utf8 ()Z
#66 = Utf8 next
#67 = Utf8 ()Ljava/lang/Object;
#68 = Utf8 equals
#69 = Utf8 remove
#70 = Utf8 java/lang/System
#71 = Utf8 out
#72 = Utf8 Ljava/io/PrintStream;
#73 = Utf8 java/io/PrintStream
#74 = Utf8 println
#75 = Utf8 (Ljava/lang/Object;)V
{
public TestForEach();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LTestForEach;

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
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: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
14: pop
15: aload_1
16: ldc #6 // String 2
18: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
21: pop
22: aload_1
23: ldc #7 // String 3
25: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
28: pop
29: aload_1
30: invokevirtual #8 // Method java/util/ArrayList.iterator:()Ljava/util/Iterator;
33: astore_2
34: aload_2
35: invokeinterface #9, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
40: ifeq 71
43: aload_2
44: invokeinterface #10, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
49: checkcast #11 // class java/lang/String
52: astore_3
53: ldc #4 // String 1
55: aload_3
56: invokevirtual #12 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
59: ifeq 68
62: aload_1
63: aload_3
64: invokevirtual #13 // Method java/util/ArrayList.remove:(Ljava/lang/Object;)Z
67: pop
68: goto 34
71: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream;
74: aload_1
75: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
78: return
LineNumberTable:
line 6: 0
line 7: 8
line 8: 15
line 9: 22
line 18: 29
line 19: 53
line 20: 62
line 22: 68
line 56: 71
line 58: 78
LocalVariableTable:
Start Length Slot Name Signature
53 15 3 number Ljava/lang/String;
0 79 0 args [Ljava/lang/String;
8 71 1 list Ljava/util/ArrayList;
LocalVariableTypeTable:
Start Length Slot Name Signature
8 71 1 list Ljava/util/ArrayList<Ljava/lang/String;>;
StackMapTable: number_of_entries = 3
frame_type = 253 /* append */
offset_delta = 34
locals = [ class java/util/ArrayList, class java/util/Iterator ]
frame_type = 33 /* same */
frame_type = 250 /* chop */
offset_delta = 2
}
SourceFile: "TestForEach.java"

其中重点是Constant pool及其下方打括号{}内的方法表集合。

Constant pool

Constant pool意为常量池。主要存放的是两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量类似于java中的常量概念,如文本字符串,final常量等,而符号引用则属于编译原理方面的概念,包括以下三种:

  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符号(Descriptor)
  • 方法的名称和描述符

查看第一个常量池数据:

1
2
3
4
5
#1 = Methodref          #17.#40        // 
#17 = Class #58 // java/lang/Object
#18 = Utf8 <init>
#19 = Utf8 ()V
#40 = NameAndType #18:#19 // "<init>":()V

这是一个方法定义,指向了第17和第40个常量,以此类推,最终组成了右边注释的内容

1
java/lang/Object."<init>":()V

表示的是实例构造器。

()内部是入参,这里为空,表示没有入参。

V表示void,代表没有返回类型。

关于字节码的类型对应如下:

标识字符 含义
B 基本类型byte
C 基本类型char
D 基本类型double
F 基本类型float
I 基本类型int
J 基本类型long
S 基本类型short
Z 基本类型boolean
V 特殊类型void
L 对象类型,以分号结尾,如Ljava/lang/Object;
对于数组类型,每一位使用一个前置的”[“字符来描述,如定义一个java.lang.String[][]类型的维数组,将被记录为”[[Ljava/lang/String;”

方法表集合分析

在常量池之后打括号{}内的是对类内部的方法描述。

1
2
3
4
0: new           #2                  // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1

其中0、3、4表示 new ArrayList();

new: 创建ArrayList对象,并将其引用引用值压入栈顶(栈内:ref)

dup:复制栈顶数值并将复制值压入栈顶(栈内:ref ref)

invokespecial:调用了构造方法初始化实例并使用了一个引用(栈内:ref)

astore_1:弹出栈顶数据,并保存到局部变量表1处。

7:存储到局部变量表1处。

1
2
3
4
 8: aload_1
9: ldc #4 // String 1
11: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
14: pop

本组指令表示将字符串“1”添加到列表内。

8:将局部变量表1处的列表压入栈内

9:将字符串“1”压入栈内

11:调用add方法,此时会消耗掉栈内的两个值(将栈顶字符串通过add方法添加到列表内),并压入add方法的Boolean类型的返回结果

14:出栈,将栈顶由11操作压入的值出栈(丢弃)

15~28操作与此一致:

15~21:将字符串“2”加入列表内;

22~28:将字符串“3”加入列表内;

1
2
3
29: aload_1
30: invokevirtual #8 // Method java/util/ArrayList.iterator:()Ljava/util/Iterator;
33: astore_2

本组表示 Iterator iterator = list.iterator();

aload_1:将局部变量表1处的列表压入栈内

invokevirtual:调用ArrayList.iterator方法获取Iterator对象并压入栈内(ArrayList内获取到的是Itr对象)

astore_2:将栈顶的Itr对象保存到局部变量表2处

1
2
34: aload_2
35: invokeinterface #9, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z

本组表示 :iterator.hasNext()

aload_2:将局部变量表2处的Itr对象压入栈顶

invokeinterface:调用hasNext接口,并将boolean结果压入栈顶

1
2
3
4
5
40: ifeq          71
43: aload_2
44: invokeinterface #10, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
49: checkcast #11 // class java/lang/String
52: astore_3

ifeq:if语句,栈顶内为true这执行下条指令(这里是43);反之,执行71处指令

43~52:表示调用iterator.next(),并将取到的结果强转成String类型后保存到局部变量表3处

1
2
3
4
5
6
7
8
9
53: ldc           #4                  // String 1
55: aload_3
56: invokevirtual #12 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
59: ifeq 68
62: aload_1
63: aload_3
64: invokevirtual #13 // Method java/util/ArrayList.remove:(Ljava/lang/Object;)Z
67: pop
68: goto 34

字符串“1”和局部变量表3处的值入栈,调用equals方法后判断栈顶值,true则执行下条指令(这里是62);反之,执行68处指令,goto 34,跳到34指令处执行,即再次开始判断hasNext。

62~67:将局部变量表3的值从列表内移除。

再执行68处goto指令。

而前面提到的71处指令如下:

1
2
3
4
71: getstatic     #14                 // Field java/lang/System.out:Ljava/io/PrintStream;
74: aload_1
75: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
78: return

表示调用System.out.println将列表数据打印。

return:结束程序。

综上分析,上面的分析过程是正确的。

1
2
3
4
5
for (String number : list) {
if ("1".equals(number)) {
list.remove(number);
}
}

等同于

1
2
3
4
5
6
for (Iterator<String> iterator = list.iterator();iterator.hasNext();){
String number = iterator.next();
if ("1".equals(number)) {
list.remove(number);
}
}

特例

1
2
3
4
5
for (String number : list) {
if ("2".equals(number)) {
list.remove(number);
}
}

在使用for( T : Iterable)方式时,有一种特殊用例,会使得遍历时能正常删除,就是删除的是list倒数第二项时。

经过上面的分析,我们只需回看下ArrayList.Itr的其中两个成员方法:ArrayList.Itr.hasNext()ArrayList.Itr.remove() 以及ArrayList.remove()就可以知道原因了。

ArrayList.Itr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

public boolean hasNext() {
return cursor != size; // size是ArrayList的成员变量,用于记录当前列表内存放的元素数量
}

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

ArrayList

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
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()取出的是list最后第二项后,成员变量cursor+1后的值等于size-1

而调用list.remove()(remove() -> fastRemove())方法后,可以看到–size

所以此时Itr.cursor == sizeItr.hasNext()返回的是false,直接退出了循环,没有对list的最后一项进行判断。

但是不推荐怎么使用。

其他正确的删除方式

除了上面删除几种方式外,还有如下几种正确的方式:

显示使用迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String number;  
for (Iterator<String> iterator = list.iterator();iterator.hasNext();){
number = iterator.next();
if ("1".equals(number)) {
iterator.remove();
}
}


Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
number = iterator.next();
if ("1".equals(number)) {
iterator.remove();
}
}

Collection.removeIf(Predicate<? super E> filter)

1
list.removeIf(number -> number.equals("1"));

Collection接口内给出了默认实现:

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
/**
* Removes all of the elements of this collection that satisfy the given
* predicate. Errors or runtime exceptions thrown during iteration or by
* the predicate are relayed to the caller.
*
* @implSpec
* The default implementation traverses all elements of the collection using
* its {@link #iterator}. Each matching element is removed using
* {@link Iterator#remove()}. If the collection's iterator does not
* support removal then an {@code UnsupportedOperationException} will be
* thrown on the first matching element.
*
* @param filter a predicate which returns {@code true} for elements to be
* removed
* @return {@code true} if any elements were removed
* @throws NullPointerException if the specified filter is null
* @throws UnsupportedOperationException if elements cannot be removed
* from this collection. Implementations may throw this exception if a
* matching element cannot be removed or if, in general, removal is not
* supported.
* @since 1.8
*/
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}

也是使用迭代器进行删除。

但是Arraylist类重写了该实现,提高了删除效率:

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
42
43

@Override
public boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
// figure out which elements are to be removed
// any exception thrown from the filter predicate at this stage
// will leave the collection unmodified
int removeCount = 0;
final BitSet removeSet = new BitSet(size);
final int expectedModCount = modCount;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
@SuppressWarnings("unchecked")
final E element = (E) elementData[i];
if (filter.test(element)) {
removeSet.set(i);
removeCount++;
}
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}

// shift surviving elements left over the spaces left by removed elements
final boolean anyToRemove = removeCount > 0;
if (anyToRemove) {
final int newSize = size - removeCount;
for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
i = removeSet.nextClearBit(i);
elementData[j] = elementData[i];
}
for (int k=newSize; k < size; k++) {
elementData[k] = null; // Let gc do its work
}
this.size = newSize;
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}

return anyToRemove;
}

总结

  1. for( T : Iterable) 完全等同于

    1
    2
    3
    4
    for (Iterator<T> iterator = list.iterator();iterator.hasNext();){
    T number = iterator.next();
    //这里执行具体操作
    }
  2. 能够正常遍历时删除的方式有

    • for(int i =0; i < list.size(); i ++)
    • 迭代器方式
    • Collection.removeIf(Predicate<? super E> filter)
  3. for( T : Iterable) 或 迭代器内使用List.remove()时,若仅删除最后第二项时,不会抛出异常

参考

轻松看懂Java字节码

字节码增强技术探索

坚持原创技术分享,您的支持是对我最大的鼓励!