迭代器模式
迭代器的优势比起直接遍历集合
迭代器模式的原理和实现
- 迭代器模式将集合对象的便利操作从集合类中拆分,放到迭代器类中,让两者职责更加单一。
-
来实现一个迭代器
先定义一个接口
// 接口定义方式一 public interface Iterator<E> { boolean hasNext(); void next(); E currentItem(); } // 接口定义方式二 public interface Iterator<E> { boolean hasNext(); E next(); }
第二中方式是将第一种的返回当前的元素都放在next函数中。第一种相对灵活一些,我们可以多次调用currentItem获取当前元素。
ArrayIterator
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(); //注意这里,cursor在指向最后一个元素的时候,hasNext()仍旧返回true。 } @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方法来创建对应的迭代器。
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(); } } }
迭代器的设计思路:迭代器中定义hasNext,currentItem,next三个最基本的方法。待遍历的容器对象通过依赖注入传递到迭代器类中。容器通过iterator方法创建迭代器。
迭代器模式的优势
首先看下Java的几种遍历:
List<String> names = new ArrayList<>(); names.add("xzg"); names.add("wang"); names.add("zheng"); // 第一种遍历方式:for循环 for (int i = 0; i < names.size(); i++) { System.out.print(names.get(i) + ","); } // 第二种遍历方式:foreach循环 for (String name : names) { System.out.print(name + ",") } // 第三种遍历方式:迭代器遍历 Iterator<String> iterator = names.iterator(); while (iterator.hasNext()) { System.out.print(iterator.next() + ","); // Java中的迭代器接口是第二种定义方式,next()既移动游标又返回数据 }
- 对于简单的数据结构,链表和数组,顺序循环遍历即可,for循环也可以满足,但是对于复杂的数据结构图和树,有很多遍历方式,Java通过封装预置这些遍历,减少了开发成。减少了本身代码的复杂度。
- 游标的存在可以让我们创建多个迭代器,互不影响。容器和迭代器职责更单一。
- 容器个迭代器都有抽象接口,基于接口编程更方便替换迭代器(比如,从前往后遍历链表切换成从后往前遍历链表,客户端代码只需要将迭代器类从 LinkedIterator 切换为 ReversedLinkedIterator 即可,其他代码都不需要修改);自己拓展新的数据结构或者算法可直接实现接口,更符合开闭原则。
遍历集合时为什么不能删减元素
在遍历时删除集合元素
-
在遍历时增删元素会导致元素被重复遍历或者遍历不到
-
但是这也不是一定发生,有时也能遍历出正常结果。
-
这种行为称为结果不可预期行为或者未决行为。
-
比如在list容器中,删除一个元素会让后续元素的都往前移位,而当前的游标已经挪到了下一个位置,所以下一个位置的元素挪到当前位置,而游标到了下一个位置,那原来下一个位置的元素就会被遍历不到。如下我们调用next,迭代器返回a,游标已经指向b,这时删除a会发生第三行的情况:游标仍然指向第二个位置但是,但是所有的元素往前一位。
-
当删除游标后的元素比如cd,就不会有这个问题,还是可以正常遍历。
-
所以说这个行为是未决行为。
-
同理,当我们调用一次next后,在游标之前添加元素,那么a被“挤到”后一位,此时cursor再次指向他,又会被遍历一遍。
如何应对遍历时改变集合导致的未决行为?
- 不允许增删元素在遍历时-》不太容易知道遍历结束的时间点。
- 在遍历时增删操作报错。
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();//抛出ConcurrentModificationException异常 } }
在定义迭代器的时候会传入一个expectedModCount,也就是当前的集合的修改次数。
在容器中会定义一个变量modCount,表示当前容器被修改(增删改)的次数;
每次在迭代器中调用next,getCurItem或者hasNext的时候就回去检查当前的modCount是否等于expectedModCount。如果中途被修改,那么这两个值不相等,则会马上抛出一个exception。
如何在遍历时安全删除集合元素?
- Java迭代器中提供了remove方法,但是只能删除游标指向的前一个元素。而且一个next之后只能跟一个remove多次调用也会报错。
- remove如何实现安全删除?
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; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such 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(); } } } }
迭代器中额外一步操作,将游标往前移动一位,让游标正确指向因为删除前移的元素。
并且将上一个元素置为负数以达到不能重复调用remove的目的。
迭代器是容器的内部类。remove作为迭代器类的方法而不是容器类的方法,是因为如果容器有多个迭代器,需要对不同的迭代器维护这些信息,会变得复杂。
如何实现一个支持“快照”功能的iterator?
问题描述
所谓“快照”,指我们为容器创建迭代器的时候,相当于给容器拍了一张快照(Snapshot)。之后即便我们增删容器中的元素,快照中的元素并不会做相应的改动。而迭代器遍历的对象是快照而非容器,这样就避免了在使用迭代器遍历的过程中,增删容器中的元素,导致的不可预期的结果或者报错。
具体的代码如下所示。容器 list 中初始存储了 3、8、2 三个元素。尽管在创建迭代器 iter1 之后,容器 list 删除了元素 3,只剩下 8、2 两个元素,但是,通过 iter1 遍历的对象是快照,而非容器 list 本身。所以,遍历的结果仍然是 3、8、2。
public ArrayList<E> implements List<E> { int size; E[] array = new E[size]; int index; @Override public void add(E obj) { // resize(); array[index++] = obj; } @Override public void remove(E obj) { array[index] = 0; } @Override public Iterator<E> iterator() { return new SnapshotArrayIterator(this); } } public class SnapshotArrayIterator<E> implements Iterator<E> { List<E> list; int cursor; public SnapshotArrayIterator(List list) { this.list = new ArrayList<>(list); cursor = 0; } @Override public boolean hasNext() { return list.size() - 1 == cursor; } @Override public E next() {//返回当前元素,并且游标后移一位 if (hasNext()) { return list.get(cursor++); } } }
解决方案1
-
在迭代器中定义一个成员变量snapshot,每当创建迭代器的时候都拷贝一份容器中的元素到快照中,后续操作都基于这个快照。
-
实现简单,但是成本较高,每次都需要完全复制一份原来的容器元素,内存消耗大。
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; } }
解决方案2
- 利用时间戳。对每一个元素保存添加时间和删除时间。对于一个快照来说,只有满足 addTimestamp< snapshotTimestamp<delTimestamp的元素才是属于这个迭代器的快照。
- 实际破坏了数组支持快速随机访问,变成了O(n)的访问。
- 可以通过再引入一个数组,支持随机访问(不要的数据直接删除,不做标记删除)。另一个数组做原来事情支持标记删除。
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) { for (int i = 0; i < totalSize; ++i) { if (elements[i].equals(obj)) { 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]; } }
public class SnapshotArrayIterator<E> implements Iterator<E> { private long snapshotTimestamp; private int cursorInAll; // 在整个容器中的下标,而非快照中的下标 private int leftCount; // 快照中还有几个元素未被遍历 private ArrayList<E> arrayList;
public SnapshotArrayIterator(ArrayList<E> arrayList) { this.snapshotTimestamp = System.currentTimeMillis(); this.cursorInAll = 0; this.leftCount = arrayList.actualSize();; this.arrayList = arrayList;
justNext(); // 先跳到这个迭代器快照的第一个元素 }
@Override public boolean hasNext() { return this.leftCount >= 0; // 注意是>=, 而非> }
@Override public E next() { E currentItem = arrayList.get(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++; } } }