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等,这些集合都有自己的迭代器。假如我们要实现一个这样的新的容器,当然也需要引入迭代器模式,给我们的容器实现一个迭代器。