从实现去理解迭代器模式
还记得刚刚接触Java的时候,学习到集合这一章节的时候,实在是理解不了迭代器这一概念,只能是囫囵吞枣地记住迭代器是用来遍历集合对象中的元素的(其实当时对集合也是一知半解)。当时主要不能理解的是为什么一个看似与集合没有联系的东西能够操纵集合。其实现在看来迭代器能够给我这种印象,正说明了迭代器设计者的成功,因为这证明了其透明性对于我这样的初学者来说是真的非常透明,完全感受不到迭代器具体的存在,所见到的只是孤零零的hasNext()、next()这样的调用方法,而为什么通过这些对外的方法能够迭代访问集合元素则是我心头一直萦绕的疑问(难道遍历一个集合的方法不应该在集合中吗?)。
回过头来看,正如马士兵老师所说,许多设计模式并不是语法上的区别,更多的是语义上的区别。我觉得迭代器模式也同样适用于这句话,细扣下去发现迭代器模式也没有什么独特的,简单来说,也是遵循了一些基本的编程原则。首先是面向接口编程,接口提供一种规范,其所有的实现类都在一个相同的框架中,而拥有这个同一框架也有利于多态的应用。其次是接口的设计尽量细一些,使得每一个接口只管理自己该管的功能,不要试图将所有的功能放到一个大的接口中去。最终迭代器模式实现了集合的存储数据与遍历数据的分离。
话不多说,既然本文尝试从实现一个集合的角度去理解为什么要这样设计迭代器?那么就以最简单的线性表来开始我们的实现之旅吧。
ArrayList、LinkedList简单实现:
package com.xy.iterator2; import com.xy.iterator2.Collection; import com.xy.iterator2.Iterator;; /** * 实现一个简单版本的数组列表 * @author sunmin * @date:2018年7月19日 下午10:56:52 */ public class ArrayList implements Collection{ private Object[] objects; private int size = 0; public ArrayList() { this(10); } public ArrayList(int size) { super(); objects = new Object[size]; } public boolean add(Object o) { if (size == objects.length) { Object[] newObjects = new Object[objects.length * 2]; // JDK的数组扩容实现更为复杂和合理一些,这里简单说明是在数组扩容就好 System.arraycopy(objects, 0, newObjects, 0, objects.length); objects = newObjects; } objects[size ++] = o; return true; } public Object get(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("Illegal Index: "+ index); } return objects[index]; } public int size() { return size; } }
package com.xy.iterator2; import com.xy.iterator2.Collection; /** * 实现一个简单版本的链表 * @author sunmin * @date:2018年7月19日 下午10:56:08 */ public class LinkedList implements Collection{ private static class Node { private Object item; private Node next; public Node(Object item, Node next) { super(); this.item = item; this.next = next; } public Object getItem() { return item; } public void setItem(Object item) { this.item = item; } public Node getNext() { return next; } public void setNext(Node next) { this.next = next; } } private Node head; private Node tail; private int size; public boolean add(Object o) { Node node = new Node(o, null); if (head == null) { head = node; tail = node; } tail.next = node; tail = node; size++; return true; } public int size() { return size; } public Object get(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("Illegal Index: "+ index); } Node target = head; for (int i = 0; i < index; i++) { target = target.next; } return target.item; } }
如上所示,我们简单实现了ArrayList,LinkedList的add方法,其实这里了解其内部一个是使用数组,一个是使用链表来存储数据即可。下面我们就来测试这两个集合类,分别往里面存数据以及遍历集合中的数据。
public static void main(String[] args) { // 往集合中存储数据 Collection collection = new ArrayList(); for (int i = 0; i < 15000; i++) { collection.add("cat " + i); } // 遍历数据 for (int i = 0; i < collection.size(); i++) { } }
由于有统一的往集合添加数据的方法add,那么这里往集合中存储数据是很简单的,利用多态,如果要换成往LinkedList中添加数据只要改动一点,将new ArrayList()改成new LinkedList(),其他代码不动。但是遍历集合中数据的时候,就没有这么简单了,由于ArrayList和LInkedList中存储数据的方式不同,数组可以用索引来获取元素,但是链表就只能一次遍历了。如果有更多的集合,那么这样遍历集合中数据需要改动的代码就更多了。此时,我们应该想到接口的好处----规范,如果设计一个接口,有一个遍历集合的抽象方法,每个集合都去实现这个抽象方法,那么集合就有了统一的遍历数据的对外API了。
迭代器接口:
package com.xy.iterator2; public interface Iterator { boolean hasNext(); Object next(); }
按照我们正常的理解是让集合实现这个接口,那么在集合类中就有了具体的hasNext和next方法,这样集合就能对外提供遍历集合元素的统一方法了(或者说像有些更极端的同学所想直接在集合的接口Collection中定义hasNext和next,但是不要这样做,让接口尽可能苗条一点吧)。此时,这里就到了迭代器的设计与我们常规的思路不同的地方了,迭代器的设计者为了让存储数据与迭代数据实现真正的分离,不让集合类去实现迭代器接口,而是重新定义一个迭代器实现类,让这个类去完成数据迭代的工作,而集合类则安心完成数据存储的工作,不要操心迭代访问聚合中的数据的问题。
集合类不需要提供迭代的实现,但是用户用到一个集合,然后还想遍历集合,当然这个工作是由相应的迭代器实现类完成,但是用户怎么知道怎么创建一个对象(用户甚至不需要知道迭代器实现类在哪里,叫什么名字),这时集合应该有一个统一的方法,用户知道我一调用这个方法,一定会给我返回一个相应的迭代器对象。好了,现在你应该知道集合为什么会有iterator()方法了吧。这里多说一句,为什么Collection接口要继承Iterable接口,我想这是为了在应用多态的时候接口引用collection能够调用集合类对象的iterator,毕竟父类(接口)引用变量能够调用的方法只能是自己定义过的。
实现:
package com.xy.iterator2; import com.xy.iterator2.Collection; /** * 实现一个简单版本的链表 * @author sunmin * @date:2018年7月19日 下午10:56:08 */ public class LinkedList implements Collection{ private static class Node { private Object item; private Node next; public Node(Object item, Node next) { super(); this.item = item; this.next = next; } public Object getItem() { return item; } public void setItem(Object item) { this.item = item; } public Node getNext() { return next; } public void setNext(Node next) { this.next = next; } } private Node head; private Node tail; private int size; public boolean add(Object o) { Node node = new Node(o, null); if (head == null) { head = node; tail = node; } tail.next = node; tail = node; size++; return true; } public int size() { return size; } public Object get(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("Illegal Index: "+ index); } Node target = head; for (int i = 0; i < index; i++) { target = target.next; } return target.item; } @Override public Iterator iterator() { return new LinkedListIterator(); } class LinkedListIterator implements Iterator { Node currentNode = head; @Override public boolean hasNext() { if (currentNode == tail) { return false; } return true; } @Override public Object next() { currentNode = currentNode.next; return currentNode.item; } } }
这里将迭代器实现类定义为集合类的一个内部类,目的是为了访问到集合类中的成员变量size。当然设计成外部类也是可以的,通过get方法来获取size值。
多说一句: 链表使用迭代器进行数据的迭代访问效率比使用集合中的get(int index)方法高得多,原因在于迭代器会记住当前的位置,而链表的get方法不会,它是从头依次遍历链表找到目标节点,下一个在get时又从头遍历。这是非常低效的,比如说先get(3),从0遍历到3节点获得目标节点,然后get(5),这个方法又会愚蠢的从0遍历到5获得目标节点,而不知道从3遍历到5。所以说链表中这个方法的设计是很不合理的,只是链表要实现接口中的所有方法,不得已而为之的一个低效实现,而我们最好不去使用它,用迭代器吧。
下面是我的一个测试,看看链表的get方法到底多低效。
// 遍历容器 long start = System.currentTimeMillis(); Iterator iterator = collection.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } /*for (int i = 0; i < collection.size(); i++) { System.out.println(collection.get(i)); }*/ long end = System.currentTimeMillis(); System.out.println("time: " + (end - start));
测试发现,使用迭代器耗时大约都在170 ms左右,而使用get方法耗时达到2700 ms以上,可以看出,,对于链表使用迭代器遍历结合效率要比get方法高得多。
总结:
迭代器模式最大的关注点就是将数据的存储与数据的遍历分开来。单独实现一个迭代器类来进行数据遍历,而不是夹杂在集合类中。
通过将迭代与存储分离,有利于以后扩展新的迭代方式,只要重新实现一个新的迭代器类,而不必对集合类作任何改动。
实现了迭代访问集合中数据的透明化,调用者完全不必知道是怎么实现的,特别是使用一个新的的迭代器实现类,还是像使用之前的迭代器一样使用就好。
由于将集合的数据存储和数据迭代分离,成倍增加的类的数量,这可能是其一个缺点。
===========================================================================================================================================
本文只是我现阶段的学习心得总结而成,内容可能不够深入,由于水平所限,不保证所有内容正确,欢迎有同学在评论中指正,万分感谢!
保证每一个字的原创性!
作为一个程序员,我所能做的就是每一天都在进步,面对技术保持一颗赤子之心,这是我人生现阶段全部的追求。
===========================================================================================================================================
浙公网安备 33010602011771号