关于Iterator你掉坑里了吗

思考

代码片段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张图片: Iterator遍历1

图1:Iterator索引示意图1

Iterator遍历2

图2:Iterator索引示意图2

如图1,加上我们有6个元素,现在我们要删除倒数第2个元素(下标为4的元素),因为调用的是外部类ArrayList的remove操作,所以不会改变内部类Itr的成员cursor,但是会改变size(外部类ArrayList的成员,内部类Itr只是共享),所以执行删除操作之后就变为了如图2所示的样子。现在cursor和size相等了,那么在下一次执行hasNext直接返回false遍历结束,没有机会执行next()方法,所以也不会执行checkForComodification()方法抛出异常了。从图中也可以看到其实并没有遍历完成就结束了。

总结

  1. foreach本质上就是Iterator,所以只是遍历的话可以直接使用foreach方式
  2. foreach虽然使用的是Iterator方式但是没有办法获取Iterator的引用,所以为了不直接在foreach中使用集合对象执行remove,add等会修改modCount的操作还是使用Iterator方式吧。
  3. 在并发操作中为Iterator对象执行加锁操作
  4. ArrayList的iterator使用的是适配器模式的变形,通过内部类的方式实现,而不是通过继承复用或者合成复用方式实现的

转载于:https://my.oschina.net/u/2474629/blog/828136

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值