思考
代码片段1
public class ItrLearn {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
for (String temp : list) {
if ("1".equals(temp)) {
list.remove(temp);
}
}
}
}
代码片段2
public class ItrLearn {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
for (String temp : list) {
if ("2".equals(temp)) {
list.remove(temp);
}
}
}
}
你能不运行代码分别说出上面的代码片段1和代码片段2的运行结果吗?为什么会出现上面的情况呢?下面就将要介绍一些关于为什么foreach中不能用remove、建议不要用for遍历集合而应该使用迭代器遍历集合的细节。
Iterator 与 Iterable
首先介绍一些Iterator和Iterable的区别,Iterator定义的是实现Iterator接口的类应该完成的工作。Iterable定义的是实现Iterable接口的必须能够返回一个Iterator接口,这里有一点工厂方法的意思。Iterable接口其实是为了实现foreach而提供的,在Java中foreach其实就是Java提供的一个语法糖,要使用foreach的类必须实现Iterable接口。
为什么要使用foreach的类就必须实现Iterable接口呢?下面会通过对上面的代码片段1反编译为字节码来分析。和Iterable语法糖类似的还有Closable接口,实现了Closable的接口就可以使用jdk1.7中提供的try-with-resources来自动关闭资源。
虽然这里没有什么关系,但是还是提一下和Iterator与Iterable形似的Comparator与 Comparable,Comparator是比较器,就是给定2个对象按照Comparator定义的规则比较2个对象的大小。所以Comparator提供的接口是int compare(T o1, T o2);而Comparable是可比较的就是实现Comparable的类是可以比较的,指定一个对象和自身比较的规则,所以Comarable的接口是public int compareTo(T o);
反编译与问题分析
为了了解更多的关于foreach语法糖的细节我们把上面的代码片段1反编译为字节码来查看字节码上的实现。使用javap命令反编译ItrLearn.class文件并且输出到itr.txt文件中。
javap -verbose ItrLearn.class >> itr.txt
itr.txt中的内容:
Classfile /E:/studio/learn/target/classes/cn/freemethod/base/ItrLearn.class
Last modified 2017-1-18; size 1006 bytes
MD5 checksum c02cb7dc2d0eee96837f532dc0208fbc
Compiled from "ItrLearn.java"
public class cn.freemethod.base.ItrLearn
SourceFile: "ItrLearn.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // cn/freemethod/base/ItrLearn
#2 = Utf8 cn/freemethod/base/ItrLearn
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcn/freemethod/base/ItrLearn;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Class #17 // java/util/ArrayList
#17 = Utf8 java/util/ArrayList
#18 = Methodref #16.#9 // java/util/ArrayList."<init>":()V
#19 = String #20 // 1
#20 = Utf8 1
#21 = InterfaceMethodref #22.#24 // java/util/List.add:(Ljava/lang/Object;)Z
#22 = Class #23 // java/util/List
#23 = Utf8 java/util/List
#24 = NameAndType #25:#26 // add:(Ljava/lang/Object;)Z
#25 = Utf8 add
#26 = Utf8 (Ljava/lang/Object;)Z
#27 = String #28 // 2
#28 = Utf8 2
#29 = InterfaceMethodref #22.#30 // java/util/List.iterator:()Ljava/util/Iterator;
#30 = NameAndType #31:#32 // iterator:()Ljava/util/Iterator;
#31 = Utf8 iterator
#32 = Utf8 ()Ljava/util/Iterator;
#33 = InterfaceMethodref #34.#36 // java/util/Iterator.next:()Ljava/lang/Object;
#34 = Class #35 // java/util/Iterator
#35 = Utf8 java/util/Iterator
#36 = NameAndType #37:#38 // next:()Ljava/lang/Object;
#37 = Utf8 next
#38 = Utf8 ()Ljava/lang/Object;
#39 = Class #40 // java/lang/String
#40 = Utf8 java/lang/String
#41 = Methodref #39.#42 // java/lang/String.equals:(Ljava/lang/Object;)Z
#42 = NameAndType #43:#26 // equals:(Ljava/lang/Object;)Z
#43 = Utf8 equals
#44 = InterfaceMethodref #22.#45 // java/util/List.remove:(Ljava/lang/Object;)Z
#45 = NameAndType #46:#26 // remove:(Ljava/lang/Object;)Z
#46 = Utf8 remove
#47 = InterfaceMethodref #34.#48 // java/util/Iterator.hasNext:()Z
#48 = NameAndType #49:#50 // hasNext:()Z
#49 = Utf8 hasNext
#50 = Utf8 ()Z
#51 = Utf8 args
#52 = Utf8 [Ljava/lang/String;
#53 = Utf8 list
#54 = Utf8 Ljava/util/List;
#55 = Utf8 temp
#56 = Utf8 Ljava/lang/String;
#57 = Utf8 LocalVariableTypeTable
#58 = Utf8 Ljava/util/List<Ljava/lang/String;>;
#59 = Utf8 StackMapTable
#60 = Class #52 // "[Ljava/lang/String;"
#61 = Utf8 SourceFile
#62 = Utf8 ItrLearn.java
{
public cn.freemethod.base.ItrLearn();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/freemethod/base/ItrLearn;
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #16 // class java/util/ArrayList
3: dup
4: invokespecial #18 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: ldc #19 // String 1
11: invokeinterface #21, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
16: pop
17: aload_1
18: ldc #27 // String 2
20: invokeinterface #21, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
25: pop
26: aload_1
27: invokeinterface #29, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
32: astore_3
33: goto 63
36: aload_3
37: invokeinterface #33, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
42: checkcast #39 // class java/lang/String
45: astore_2
46: ldc #19 // String 1
48: aload_2
49: invokevirtual #41 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
52: ifeq 63
55: aload_1
56: aload_2
57: invokeinterface #44, 2 // InterfaceMethod java/util/List.remove:(Ljava/lang/Object;)Z
62: pop
63: aload_3
64: invokeinterface #47, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
69: ifne 36
72: return
LineNumberTable:
line 9: 0
line 10: 8
line 11: 17
line 12: 26
line 13: 46
line 14: 55
line 12: 63
line 17: 72
LocalVariableTable:
Start Length Slot Name Signature
0 73 0 args [Ljava/lang/String;
8 65 1 list Ljava/util/List;
46 17 2 temp Ljava/lang/String;
LocalVariableTypeTable:
Start Length Slot Name Signature
8 65 1 list Ljava/util/List<Ljava/lang/String;>;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 36
locals = [ class "[Ljava/lang/String;", class java/util/List, top, class java/util/Iterator ]
stack = []
frame_type = 26 /* same */
}
从上面的字节码中我们很容易发现foreach被编译为了通过Iterable接口的iterator获取了Iterator接口,然后使用Iterator接口来遍历集合。现在知道为什么使用foreach的类必须要实现Iterable接口了吧。
现在我们来看一下为什么代码片段2会抛出java.util.ConcurrentModificationException异常,而代码片段1不会。 我们以ArrayList为例,ArrayList实现了Iterable接口,ArrayList的iterator实现返回的Iterator接口是一个内部类Itr,我们来分析一下这个Itr的代码:
ArrayList#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;
public boolean hasNext() {
return cursor != size;
}
@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();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
为了更好的理解这个类,我们先从设计的角度来看一下这个类,这里其实使用到了适配器模式的变形,这里的目标接口(target)是Iterator,Adaptee是ArrayList,Adapter是ArrayList的内部类Itr。这里变形的部分是Adapter角色Itr没有继承ArrayList也没有通过合成复用,而是通过内部类来实现数据的共享和方法的复用。
如果对于适配器模式不熟悉也没有关系,我们简化一下问题的模型,直接通过堆栈信息定位到错误的原因,从代码片段2的堆栈信息我们知道抛出异常是在next方法中调用checkForComodification();抛出的异常,异常的原因是modCount != expectedModCount,那么为什么modCount和expectedModCount会不相等,在Itr初始化的时候就执行了 modCount = expectedModCount,然后在Itr中hasNext(),next()方法都不会改变modCount,在Itr的remove中调用了外部类的remove方法改变了modCount之后都重新执行了赋值操作expectedModCount = modCount;显然代码片段2中也是在单线程的情景中,不存在其他线程改变了modCount的值。
如果你没有发现问题的原因,那么你可能被我有意误导了,我就是因为定式思维误区而掉入这个坑中。foreach代码被编译成了使用Iterator遍历是没有问题,问题在于list.remove(temp);在这一句中我们并没有使用Iterator的remove方法,而是调用的外部类ArrayList的remove方法。ArrayList的remove方法改变了modCount,但是外部类的方法没有办法给内部类的expectedModCount重新赋值,那也不是它的责任。所以引起了modCount和expectedModCount的不等,而在下一次next()方法中执行checkForComodification();方法是抛出java.util.ConcurrentModificationException异常。
现在知道为什么不要在foreach中变量集合时不要使用集合对象执行remove操作了吧,不仅仅是remove操作,任何改变modCount的操作都可能引起java.util.ConcurrentModificationException异常。
那么问题有来了,为什么代码片段1没有抛出异常呢?更一般的总结,如果条件的位置为倒数第2个则不会抛出异常。
如果没有理清楚,请看下面2张图片:
图1:Iterator索引示意图1
图2:Iterator索引示意图2
如图1,加上我们有6个元素,现在我们要删除倒数第2个元素(下标为4的元素),因为调用的是外部类ArrayList的remove操作,所以不会改变内部类Itr的成员cursor,但是会改变size(外部类ArrayList的成员,内部类Itr只是共享),所以执行删除操作之后就变为了如图2所示的样子。现在cursor和size相等了,那么在下一次执行hasNext直接返回false遍历结束,没有机会执行next()方法,所以也不会执行checkForComodification()方法抛出异常了。从图中也可以看到其实并没有遍历完成就结束了。
总结
- foreach本质上就是Iterator,所以只是遍历的话可以直接使用foreach方式
- foreach虽然使用的是Iterator方式但是没有办法获取Iterator的引用,所以为了不直接在foreach中使用集合对象执行remove,add等会修改modCount的操作还是使用Iterator方式吧。
- 在并发操作中为Iterator对象执行加锁操作
- ArrayList的iterator使用的是适配器模式的变形,通过内部类的方式实现,而不是通过继承复用或者合成复用方式实现的