Java中的迭代器模式

迭代器模式

提供一种方式去访问一个容器元素中的各个对象,而又不暴露该对象的内部细节。

迭代器模式的结构

1、迭代器角色

负责定义访问和遍历元素的接口

2、具体迭代器角色

实现迭代器接口,并要记录遍历中的当前位置

3、容器角色

负责提供创建具体迭代器角色的接口

4、具体容器角色

实现创建具体迭代器角色的接口,这个具体迭代器角色与该容器的结构相关

为什么需要迭代器模式

列举一个简单的示例,遍历ArrayList、LinkedList、HashSet中各个元素:

 1 public static void main(String[] args) {
 2         List<Integer> arrayList = new ArrayList<Integer>();
 3         arrayList.add(1);
 4         arrayList.add(2);
 5         List<Integer> linkedList = new LinkedList<Integer>();
 6         linkedList.add(3);
 7         linkedList.add(4);
 8         HashSet<Integer> hashSet = new HashSet<Integer>();
 9         hashSet.add(5);
10         hashSet.add(6);
11         Iterator<Integer> iterator = null;
12         iterator = arrayList.iterator();
13         System.out.println("ArrayList:");
14         while (iterator.hasNext())
15         {
16             System.out.print(iterator.next() + "\t");
17         }
18         System.out.println("\nLinkedList:");
19         iterator = linkedList.iterator();
20         while (iterator.hasNext())
21         {
22             System.out.print(iterator.next() + "\t");
23         }
24         System.out.println("\nHashSet:");
25         iterator = hashSet.iterator();
26         while (iterator.hasNext())
27         {
28             System.out.print(iterator.next() + "\t");
29         }
30     }

运行结果:

ArrayList:
1    2    
LinkedList:
3    4    
HashSet:
5    6    

随便一个结合,是要实现了iterable接口,就可以用这样的方式遍历。开发者不需要知道集合中如何去遍历的细节,只管用类似的遍历方法就好了。

Java中的Iterable和Iterator

实现了Iterable接口,则表示某个对象是可被迭代的;Iterator接口相当于是一个迭代器,实现了Iterator接口,等于具体定义了这个可被迭代的对象时如何进行迭代的。参看Iterable接口的定义:

1 public interface Iterable<T> {
2 
3     /**
4      * Returns an iterator over a set of elements of type T.
5      * 
6      * @return an Iterator.
7      */
8     Iterator<T> iterator();
9 }
 1 public interface Iterator<E> {
 2     /**
 3      * Returns {@code true} if the iteration has more elements.
 4      * (In other words, returns {@code true} if {@link #next} would
 5      * return an element rather than throwing an exception.)
 6      *
 7      * @return {@code true} if the iteration has more elements
 8      */
 9     boolean hasNext();
10 
11     /**
12      * Returns the next element in the iteration.
13      *
14      * @return the next element in the iteration
15      * @throws NoSuchElementException if the iteration has no more elements
16      */
17     E next();
18 
19     /**
20      * Removes from the underlying collection the last element returned
21      * by this iterator (optional operation).  This method can be called
22      * only once per call to {@link #next}.  The behavior of an iterator
23      * is unspecified if the underlying collection is modified while the
24      * iteration is in progress in any way other than by calling this
25      * method.
26      *
27      * @implSpec
28      * The default implementation throws an instance of
29      * {@link UnsupportedOperationException} and performs no other action.
30      *
31      * @throws UnsupportedOperationException if the {@code remove}
32      *         operation is not supported by this iterator
33      *
34      * @throws IllegalStateException if the {@code next} method has not
35      *         yet been called, or the {@code remove} method has already
36      *         been called after the last call to the {@code next}
37      *         method
38      */
39     default void remove() {
40         throw new UnsupportedOperationException("remove");
41     }
42 
43     /**
44      * Performs the given action for each remaining element until all elements
45      * have been processed or the action throws an exception.  Actions are
46      * performed in the order of iteration, if that order is specified.
47      * Exceptions thrown by the action are relayed to the caller.
48      *
49      * @implSpec
50      * <p>The default implementation behaves as if:
51      * <pre>{@code
52      *     while (hasNext())
53      *         action.accept(next());
54      * }</pre>
55      *
56      * @param action The action to be performed for each element
57      * @throws NullPointerException if the specified action is null
58      * @since 1.8
59      */
60     default void forEachRemaining(Consumer<? super E> action) {
61         Objects.requireNonNull(action);
62         while (hasNext())
63             action.accept(next());
64     }
65 }

为什么一定要实现Iterable接口而不是直接实现Iterator接口?

Iterator接口的核心方法next()和hasNext()依赖于迭代器的当前迭代位置。如果直接实现Iterator接口,那么集合对象中就包含当前迭代位置的数据。集合在不同方法间被传递时,由于当前迭代位置不可预置,那么next()方法的结果会变成不可预知的。除非再为Iterator接口添加一个reset()方法,用来重置当前迭代位置。但即使这样,Collection也同时只能存在一个当前迭代位置。而Iterable,每次调用都返回一个从头开始计数的迭代器,多个迭代器时互不干扰。

 1 public class ArrayList<E> implements List<E>, Iterator<E>, RandomAccess, Cloneable, Serializable
 2 {
 3     /**
 4      * 序列化ID
 5      */
 6     private static final long serialVersionUID = -5786598508477165970L;
 7     
 8     private int size = 0;
 9     private transient Object[] elementData = null;
10     
11     public E next()
12     {
13         ...
14     }
15     
16     public boolean hasNext()
17     {
18         ...
19     }
20     ...
21 }

这么问题就来了,如果一个ArrayList实例被多个地方迭代,next()方法、hasNext()直接操作的是ArrayList中的资源,假如我在ArrayList中定义一个迭代位置的变量,那么对于不同调用处,这个迭代变量是共享的,线程A迭代的时候将迭代变量设置成了第5个位置,这时候切换到了线程B,对于线程B来讲,就从第5个位置开始遍历此ArrayList了,根本不是从0开始,如何正确迭代?

 1 public class ArrayListIterator implements Iterator<E>
 2 {
 3     int iteratorPostion = 0;
 4     
 5     /**
 6      * 判断是否后面还有元素
 7      */
 8     @Override
 9     public boolean hasNext()
10     {
11         if ((iteratorPostion + 1) > size)
12             return false;
13         return true;
14     }
15 
16     /**
17      * 返回之前一个元素的引用
18      */
19     @Override
20     public E next()
21     {
22         return (E)elementData[iteratorPostion++];
23     }
24     ...
25 } 

每次都返回一个返回一个ArrayListIterator实例出去:

1 /**
2  * 返回一个ArrayList的迭代器,可以通过该迭代器遍历ArrayList中的元素
3  */
4 public Iterator<E> iterator()
5 {
6     return new ArrayListIterator();
7 }

这就保证了,即使是多处同时迭代这个ArrayList,依然每处都是从0开始迭代这个ArrayList实例的。

迭代器模式的优缺点

迭代器模式的优点:

  • 简化了遍历方式,对于对象集合的遍历,还是比较麻烦的,对于数组或者有序列表,我们尚可以通过游标来取得,但用户需要在对集合了解很清楚的前提下,自行遍历对象,但是对于hash表来说,用户遍历起来就比较麻烦了。而引入了迭代器方法后,用户用起来就简单的多了。
  • 可以提供多种遍历方式,比如说对有序列表,我们可以根据需要提供正序遍历,倒序遍历两种迭代器,用户用起来只需要得到我们实现好的迭代器,就可以方便的对集合进行遍历了。
  • 封装性良好,用户只需要得到迭代器就可以遍历,而对于遍历算法则不用去关心。

迭代器模式的缺点:

  • 对于比较简单的遍历(像数组或者有序列表),使用迭代器方式遍历较为繁琐,大家可能都有感觉,像ArrayList,我们宁可愿意使用for循环和get方法来遍历集合。

总的来说: 迭代器模式是与集合共生共死的,一般来说,我们只要实现一个集合,就需要同时提供这个集合的迭代器,就像java中的Collection,List、Set、Map等,这些集合都有自己的迭代器。假如我们要实现一个这样的新的容器,当然也需要引入迭代器模式,给我们的容器实现一个迭代器。