迭代器模式的原理和实现 迭代器模式(Iterator Design Pattern),也叫作游标模式(Cursor Design Pattern)。在开篇中我们讲到,它用来遍历集合对象。这里说的“集合对象”也可以叫“容器”“聚合对象”,实际上就是包含一组对象的对象,比如数组、链表、树、图、跳表。迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一。
迭代器是用来遍历容器的,所以,一个完整的迭代器模式一般会涉及容器和容器迭代器两部分内容。为了达到基于接口而非实现编程的目的,容器又包含容器接口、容器实现类,迭代器又包含迭代器接口、迭代器实现类。对于迭代器模式,我画了一张简单的类图,你可以看一看,先有个大致的印象。
 
线性数据结构包括数组和链表,在大部分编程语言中都有对应的类来封装这两种数据结构,在开发中直接拿来用就可以了。假设在这种新的编程语言中,这两个数据结构分别对应 ArrayList 和 LinkedList 两个类。除此之外,我们从两个类中抽象出公共的接口,定义为 List 接口,以方便开发者基于接口而非实现编程,编写的代码能在两种数据存储结构之间灵活切换。
现在,我们针对 ArrayList 和 LinkedList 两个线性容器,设计实现对应的迭代器。按照之前给出的迭代器模式的类图,我们定义一个迭代器接口 Iterator,以及针对两种容器的具体的迭代器实现类 ArrayIterator 和 ListIterator。
1 2 3 4 5 6 7 8 9 10 11 12 13 public  interface  Iterator <E> {  boolean  hasNext () ;   void  next () ;   E currentItem () ; } public  interface  Iterator <E> {  boolean  hasNext () ;   E next () ; } 
 
terator 接口有两种定义方式。在第一种定义中,next() 函数用来将游标后移一位元素,currentItem() 函数用来返回当前游标指向的元素。在第二种定义中,返回当前元素与后移一位这两个操作,要放到同一个函数 next() 中完成。第一种定义方式更加灵活一些,比如我们可以多次调用 currentItem() 查询当前元素,而不移动游标。所以,在接下来的实现中,我们选择第一种接口定义方式。
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 public  class  ArrayIterator <E> implements  Iterator <E> {  private  int  cursor;   private  ArrayList<E> arrayList;   public  ArrayIterator (ArrayList<E> arrayList)  {     this .cursor = 0 ;     this .arrayList = arrayList;   }   @Override    public  boolean  hasNext ()  {     return  cursor != arrayList.size();    }   @Override    public  void  next ()  {     cursor++;   }   @Override    public  E currentItem ()  {     if  (cursor >= arrayList.size()) {       throw  new  NoSuchElementException ();     }     return  arrayList.get(cursor);   } } public  class  Demo  {  public  static  void  main (String[] args)  {     ArrayList<String> names = new  ArrayList <>();     names.add("xzg" );     names.add("wang" );     names.add("zheng" );          Iterator<String> iterator = new  ArrayIterator (names);     while  (iterator.hasNext()) {       System.out.println(iterator.currentItem());       iterator.next();     }   } } 
 
在上面的代码实现中,我们需要将待遍历的容器对象,通过构造函数传递给迭代器类。实际上,为了封装迭代器的创建细节,我们可以在容器中定义一个 iterator() 方法,来创建对应的迭代器。为了能实现基于接口而非实现编程,我们还需要将这个方法定义在 List 接口中。具体的代码实现和使用示例如下所示:
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 public  interface  List <E> {  Iterator iterator () ;    } public  class  ArrayList <E> implements  List <E> {     public  Iterator iterator ()  {     return  new  ArrayIterator (this );   }    } public  class  Demo  {  public  static  void  main (String[] args)  {     List<String> names = new  ArrayList <>();     names.add("xzg" );     names.add("wang" );     names.add("zheng" );          Iterator<String> iterator = names.iterator();     while  (iterator.hasNext()) {       System.out.println(iterator.currentItem());       iterator.next();     }   } } 
 
迭代器模式的优势 一般来讲,遍历集合数据有三种方法:for 循环、foreach 循环、iterator 迭代器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 List<String> names = new  ArrayList <>(); names.add("xzg" ); names.add("wang" ); names.add("zheng" ); for  (int  i  =  0 ; i < names.size(); i++) {  System.out.print(names.get(i) + "," ); } for  (String name : names) {  System.out.print(name + "," ) } Iterator<String> iterator = names.iterator(); while  (iterator.hasNext()) {  System.out.print(iterator.next() + "," ); } 
 
实际上,foreach 循环只是一个语法糖而已,底层是基于迭代器来实现的。也就是说,上面代码中的第二种遍历方式(foreach 循环代码)的底层实现,就是第三种遍历方式(迭代器遍历代码)。这两种遍历方式可以看作同一种遍历方式,也就是迭代器遍历方式。
从上面的代码来看,for 循环遍历方式比起迭代器遍历方式,代码看起来更加简洁。那我们为什么还要用迭代器来遍历容器呢?为什么还要给容器设计对应的迭代器呢?原因有以下三个。
首先,对于类似数组和链表这样的数据结构,遍历方式比较简单,直接使用 for 循环来遍历就足够了。但是,对于复杂的数据结构(比如树、图)来说,有各种复杂的遍历方式。比如,树有前中后序、按层遍历,图有深度优先、广度优先遍历等等。如果由客户端代码来实现这些遍历算法,势必增加开发成本,而且容易写错。如果将这部分遍历的逻辑写到容器类中,也会导致容器类代码的复杂性。
前面也多次提到,应对复杂性的方法就是拆分。我们可以将遍历操作拆分到迭代器类中。比如,针对图的遍历,我们就可以定义 DFSIterator、BFSIterator 两个迭代器类,让它们分别来实现深度优先遍历和广度优先遍历。其次,将游标指向的当前位置等信息,存储在迭代器类中,每个迭代器独享游标信息。这样,我们就可以创建多个不同的迭代器,同时对同一个容器进行遍历而互不影响。
最后,容器和迭代器都提供了抽象的接口,方便我们在开发的时候,基于接口而非具体的实现编程。当需要切换新的遍历算法的时候,比如,从前往后遍历链表切换成从后往前遍历链表,客户端代码只需要将迭代器类从 LinkedIterator 切换为 ReversedLinkedIterator 即可,其他代码都不需要修改。除此之外,添加新的遍历算法,我们只需要扩展新的迭代器类,也更符合开闭原则。
简单总结一下优势:
迭代器模式封装集合内部的复杂数据结构,开发者不需要了解如何遍历,直接使用容器提供的迭代器即可; 
迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一; 
迭代器模式让添加新的遍历算法更加容易,更符合开闭原则。除此之外,因为迭代器都实现自相同的接口,在开发中,基于接口而非实现编程,替换迭代器也变得更加容易。 
 
在遍历的同时增删集合元素会发生什么? 在通过迭代器来遍历集合元素的同时,增加或者删除集合中的元素,有可能会导致某个元素被重复遍历或遍历不到。不过,并不是所有情况下都会遍历出错,有的时候也可以正常遍历,所以,这种行为称为结果不可预期行为或者未决行为,也就是说,运行结果到底是对还是错,要视情况而定。
在遍历的时候删除游标后面的元素或在游标的后面添加元素,就不会存在任何问题
如何应对遍历时改变集合导致的未决行为? 当通过迭代器来遍历集合的时候,增加、删除集合元素会导致不可预期的遍历结果。有两种比较干脆利索的解决方案:一种是遍历的时候不允许增删元素,另一种是增删元素之后让遍历报错。
实际上,第一种解决方案比较难实现,我们要确定遍历开始和结束的时间点。遍历开始的时间节点我们很容易获得。我们可以把创建迭代器的时间点作为遍历开始的时间点。但是,遍历结束的时间点又不好确认。
实际上,第二种解决方法更加合理。Java 语言就是采用的这种解决方案,增删元素之后,让遍历报错。接下来,我们具体来看一下如何实现。怎么确定在遍历时候,集合有没有增删元素呢?我们在 ArrayList 中定义一个成员变量 modCount,记录集合被修改的次数,集合每调用一次增加或删除元素的函数,就会给 modCount 加 1。当通过调用集合上的 iterator() 函数来创建迭代器的时候,我们把 modCount 值传递给迭代器的 expectedModCount 成员变量,之后每次调用迭代器上的 hasNext()、next()、currentItem() 函数,我们都会检查集合上的 modCount 是否等于 expectedModCount,也就是看,在创建完迭代器之后,modCount 是否改变过。
如果两个值不相同,那就说明集合存储的元素已经改变了,要么增加了元素,要么删除了元素,之前创建的迭代器已经不能正确运行了,再继续使用就会产生不可预期的结果,所以我们选择 fail-fast 解决方式,抛出运行时异常,结束掉程序,让程序员尽快修复这个因为不正确使用迭代器而产生的 bug。
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 public  class  ArrayIterator  implements  Iterator  {  private  int  cursor;   private  ArrayList arrayList;   private  int  expectedModCount;   public  ArrayIterator (ArrayList arrayList)  {     this .cursor = 0 ;     this .arrayList = arrayList;     this .expectedModCount = arrayList.modCount;   }   @Override    public  boolean  hasNext ()  {     checkForComodification();     return  cursor < arrayList.size();   }   @Override    public  void  next ()  {     checkForComodification();     cursor++;   }   @Override    public  Object currentItem ()  {     checkForComodification();     return  arrayList.get(cursor);   }      private  void  checkForComodification ()  {     if  (arrayList.modCount != expectedModCount)         throw  new  ConcurrentModificationException ();   } } public  class  Demo  {  public  static  void  main (String[] args)  {     List<String> names = new  ArrayList <>();     names.add("a" );     names.add("b" );     names.add("c" );     names.add("d" );     Iterator<String> iterator = names.iterator();     iterator.next();     names.remove("a" );     iterator.next();   } } 
 
如何在遍历的同时安全地删除集合元素?像 Java 语言,迭代器类中除了前面提到的几个最基本的方法之外,还定义了一个 remove() 方法,能够在遍历集合的同时,安全地删除集合中的元素。不过,需要说明的是,它并没有提供添加元素的方法。毕竟迭代器的主要作用是遍历,添加元素放到迭代器里本身就不合适。我个人觉得,Java 迭代器中提供的 remove() 方法还是比较鸡肋的,作用有限。它只能删除游标指向的前一个元素,而且一个 next() 函数之后,只能跟着最多一个 remove() 操作,多次调用 remove() 操作会报错。
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 public  class  ArrayList <E> {  transient  Object[] elementData;   private  int  size;   public  Iterator<E> iterator ()  {     return  new  Itr ();   }   private  class  Itr  implements  Iterator <E> {     int  cursor;            int  lastRet  =  -1 ;      int  expectedModCount  =  modCount;     Itr() {}     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 ();       }     }   } } 
 
在上面的代码实现中,迭代器类新增了一个 lastRet 成员变量,用来记录游标指向的前一个元素。通过迭代器去删除这个元素的时候,我们可以更新迭代器中的游标和 lastRet 值,来保证不会因为删除元素而导致某个元素遍历不到。如果通过容器来删除元素,并且希望更新迭代器中的游标值来保证遍历不出错,我们就要维护这个容器都创建了哪些迭代器,每个迭代器是否还在使用等信息,代码实现就变得比较复杂了。
实现一个支持“快照”功能的迭代器 理解这个问题最关键的是理解“快照”两个字。所谓“快照”,指我们为容器创建迭代器的时候,相当于给容器拍了一张快照(Snapshot)。之后即便我们增删容器中的元素,快照中的元素并不会做相应的改动。而迭代器遍历的对象是快照而非容器,这样就避免了在使用迭代器遍历的过程中,增删容器中的元素,导致的不可预期的结果或者报错。
接下来,我举一个例子来解释一下上面这段话。具体的代码如下所示。容器 list 中初始存储了 3、8、2 三个元素。尽管在创建迭代器 iter1 之后,容器 list 删除了元素 3,只剩下 8、2 两个元素,但是,通过 iter1 遍历的对象是快照,而非容器 list 本身。所以,遍历的结果仍然是 3、8、2。同理,iter2、iter3 也是在各自的快照上遍历,输出的结果如代码中注释所示。
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 List<Integer> list = new  ArrayList <>(); list.add(3 ); list.add(8 ); list.add(2 ); Iterator<Integer> iter1 = list.iterator(); list.remove(new  Integer (2 )); Iterator<Integer> iter2 = list.iterator(); list.remove(new  Integer (3 )); Iterator<Integer> iter3 = list.iterator(); while  (iter1.hasNext()) {  System.out.print(iter1.next() + " " ); } System.out.println(); while  (iter2.hasNext()) {  System.out.print(iter2.next() + " " ); } System.out.println(); while  (iter3.hasNext()) {  System.out.print(iter3.next() + " " ); } System.out.println(); 
 
下面是针对这个功能需求的骨架代码,其中包含 ArrayList、SnapshotArrayIterator 两个类。
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 public  ArrayList<E> implements  List <E> {        @Override    public  void  add (E obj)  {        }      @Override    public  void  remove (E obj)  {        }      @Override    public  Iterator<E> iterator ()  {     return  new  SnapshotArrayIterator (this );   } } public  class  SnapshotArrayIterator <E> implements  Iterator <E> {        @Override    public  boolean  hasNext ()  {        }      @Override    public  E next ()  {        } } 
 
解决方案一 在迭代器类中定义一个成员变量 snapshot 来存储快照。每当创建迭代器的时候,都拷贝一份容器中的元素到快照中,后续的遍历操作都基于这个迭代器自己持有的快照来进行。具体的代码实现如下所示:
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 package  com.mono.monochrome.iterator.snapshot1;import  java.util.Iterator;public  class  ArrayList <E> extends  java .util.ArrayList<E>{    @Override      public  java.util.Iterator<E> iterator ()  {         return  new  SnapshotArrayIterator <>(this );     }     public  class  SnapshotArrayIterator <E> implements  Iterator <E> {         private  int  cursor;         private  ArrayList<E> snapshot;         public  SnapshotArrayIterator (ArrayList<E> arrayList)  {             this .cursor = 0 ;             this .snapshot = new  ArrayList <>();             this .snapshot.addAll(arrayList);         }         @Override          public  boolean  hasNext ()  {             return  cursor < snapshot.size();         }         @Override          public  E next ()  {             E  currentItem  =  snapshot.get(cursor);             cursor++;             return  currentItem;         }     } } public  class  Demo  {    public  static  void  main (String[] args)  {         ArrayList<Integer> list = new  ArrayList <>();         list.add(3 );         list.add(8 );         list.add(2 );         Iterator<Integer> iter1 = list.iterator();         list.remove(2 );         Iterator<Integer> iter2 = list.iterator();         list.remove(3 );         Iterator<Integer> iter3 = list.iterator();                  while  (iter1.hasNext()) {             System.out.print(iter1.next() + " " );         }         System.out.println();                  while  (iter2.hasNext()) {             System.out.print(iter2.next() + " " );         }         System.out.println();                  while  (iter3.hasNext()) {             System.out.print(iter3.next() + " " );         }         System.out.println();     } } 
 
这个解决方案虽然简单,但代价也有点高。每次创建迭代器的时候,都要拷贝一份数据到快照中,会增加内存的消耗。如果一个容器同时有多个迭代器在遍历元素,就会导致数据在内存中重复存储多份。不过,庆幸的是,Java 中的拷贝属于浅拷贝,也就是说,容器中的对象并非真的拷贝了多份,而只是拷贝了对象的引用而已。
解决方案二 我们可以在容器中,为每个元素保存两个时间戳,一个是添加时间戳 addTimestamp,一个是删除时间戳 delTimestamp。当元素被加入到集合中的时候,我们将 addTimestamp 设置为当前时间,将 delTimestamp 设置成最大长整型值(Long.MAX_VALUE)。当元素被删除时,我们将 delTimestamp 更新为当前时间,表示已经被删除。
注意,这里只是标记删除,而非真正将它从容器中删除。
同时,每个迭代器也保存一个迭代器创建时间戳 snapshotTimestamp,也就是迭代器对应的快照的创建时间戳。当使用迭代器来遍历容器的时候,只有满足 addTimestamp<snapshotTimestamp<delTimestamp 的元素,才是属于这个迭代器的快照。
如果元素的 addTimestamp>snapshotTimestamp,说明元素在创建了迭代器之后才加入的,不属于这个迭代器的快照;如果元素的 delTimestamp<snapshotTimestamp,说明元素在创建迭代器之前就被删除掉了,也不属于这个迭代器的快照。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package  com.mono.monochrome.iterator.snapshot2;import  java.util.Iterator;public  interface  List <E> {    void  add (E obj) ;     void  remove (E obj) ;     Iterator<E> iterator () ; } 
 
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 package  com.mono.monochrome.iterator.snapshot2;import  java.util.Iterator;public  class  ArrayList <E> implements  List <E> {    private  static  final  int  DEFAULT_CAPACITY  =  10 ;     private  int  actualSize;      private  int  totalSize;      private  Object[] elements;     private  long [] addTimestamps;     private  long [] delTimestamps;     public  ArrayList ()  {         this .elements = new  Object [DEFAULT_CAPACITY];         this .addTimestamps = new  long [DEFAULT_CAPACITY];         this .delTimestamps = new  long [DEFAULT_CAPACITY];         this .totalSize = 0 ;         this .actualSize = 0 ;     }     @Override      public  void  add (E obj)  {         elements[totalSize] = obj;         addTimestamps[totalSize] = System.currentTimeMillis();         delTimestamps[totalSize] = Long.MAX_VALUE;         totalSize++;         actualSize++;     }     @Override      public  void  remove (E obj)  {         try  {             Thread.sleep(10 );         } catch  (InterruptedException e) {             throw  new  RuntimeException (e);         }         for  (int  i  =  0 ; i < totalSize; ++i) {             if  (elements[i].equals(obj) && delTimestamps[i] == Long.MAX_VALUE) {                 delTimestamps[i] = System.currentTimeMillis();                 actualSize--;             }         }     }     public  int  actualSize ()  {         return  this .actualSize;     }     public  int  totalSize ()  {         return  this .totalSize;     }     public  E get (int  i)  {         if  (i >= totalSize) {             throw  new  IndexOutOfBoundsException ();         }         return  (E) elements[i];     }     public  long  getAddTimestamp (int  i)  {         if  (i >= totalSize) {             throw  new  IndexOutOfBoundsException ();         }         return  addTimestamps[i];     }     public  long  getDelTimestamp (int  i)  {         if  (i >= totalSize) {             throw  new  IndexOutOfBoundsException ();         }         return  delTimestamps[i];     }     @Override      public  Iterator<E> iterator ()  {         return  new  SnapshotArrayIterator <>(this );     }     class  SnapshotArrayIterator <E> implements  Iterator <E> {         private  long  snapshotTimestamp;         private  int  cursorInAll;          private  int  leftCount;          private  ArrayList<E> arrayList;         public  SnapshotArrayIterator (ArrayList<E> arrayList)  {             try  {                 Thread.sleep(10 );             } catch  (InterruptedException e) {                 throw  new  RuntimeException (e);             }             this .snapshotTimestamp = System.currentTimeMillis();             this .cursorInAll = 0 ;             this .leftCount = arrayList.actualSize();             this .arrayList = arrayList;             justNext();          }         @Override          public  boolean  hasNext ()  {             return  cursorInAll < arrayList.totalSize();         }         @Override          public  E next ()  {             E  currentItem  =  arrayList.get(cursorInAll);             cursorInAll++;             justNext();             return  currentItem;         }         private  void  justNext ()  {             while  (cursorInAll < arrayList.totalSize()) {                 long  addTimestamp  =  arrayList.getAddTimestamp(cursorInAll);                 long  delTimestamp  =  arrayList.getDelTimestamp(cursorInAll);                 if  (snapshotTimestamp > addTimestamp && snapshotTimestamp < delTimestamp) {                     leftCount--;                     break ;                 }                 cursorInAll++;             }         }     } } 
 
实际上,上面的解决方案相当于解决了一个问题,又引入了另外一个问题。ArrayList 底层依赖数组这种数据结构,原本可以支持快速的随机访问,在 O(1) 时间复杂度内获取下标为 i 的元素,但现在,删除数据并非真正的删除,只是通过时间戳来标记删除,这就导致无法支持按照下标快速随机访问了。如果你对数组随机访问这块知识点不了解,可以去看我的《数据结构与算法之美》专栏,这里我就不展开讲解了。
现在,我们来看怎么解决这个问题:让容器既支持快照遍历,又支持随机访问?
解决的方法也不难,我稍微提示一下。我们可以在 ArrayList 中存储两个数组。一个支持标记删除的,用来实现快照遍历功能;一个不支持标记删除的(也就是将要删除的数据直接从数组中移除),用来支持随机访问。