前言
今天介绍的这个设计模式,是我认为日常代码最常用的一个设计模式—迭代器模式。所以这篇文章将使用JDK集合类框架来介绍迭代器模式。
在我们使用聚合类例如各种集合框架ArrayList、LinkedList、HashMap等等的集合类遍历时都会用到。那么此时可以引申出几个问题:
- 迭代器模式是什么?
- 为什么要用迭代器模式?它解决了哪些问题?
- 使用迭代器模式的好处?
那么,接下来就带着问题继续看下去。
什么是迭代器模式
定义
迭代器模式(Iterator),提供一种方法顺序访问一个聚合对象中的各种元素,而又不暴露该对象的内部表示。—百度百科
为了更权威一点,这里摘抄了百度百科对迭代器模式的定义。
从迭代器模式的定义上来看,它解决了遍历聚合对象的问题。而又不暴露对象的内部表示这句话怎么理解?
public static void main(String[] args) {
List<String> arrayList = new ArrayList<String>();
List<String> linkedList = new LinkedList<String>();
Set<String> hashSet = new HashSet<String>();
initCollection(arrayList, linkedList, hashSet, hashMap);
//不管是怎样的聚合对象,都可以生产出同一类型的遍历对象
Iterator arrayIterator = arrayList.iterator();
Iterator linkedIterator = linkedList.iterator();
Iterator hashSetIterator = hashSet.iterator();
//所以遍历时,我们不管遍历的是哪个的聚合对象,只需要关注Iterator即可
foreachCollection(arrayIterator);
foreachCollection(linkedIterator);
foreachCollection(hashSetIterator);
}
private static void foreachCollection(Iterator iterator) {
//这里只需要关注Iterator即可
//我不需要知道我遍历的是ArrayList还是LinkedList,同样可以做到遍历效果
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
//略过init集合类方法...
}
从代码上我觉得可以很好的理解,迭代器模式是如何不暴露对象内部表示的。
例如arrayList
内部是一个数组,linkedList
内部是一个链表,如果没有迭代器模式的话,我们遍历的时候通常数组是以下标遍历,链表是以头指针一直往下寻找下一个指针对象的方式进行遍历,此时如果要遍历聚合对象,有时候需要遍历数组,有时候需要遍历链表,此时怎么办?难道arrayList遍历时写一个专门遍历数组的方法,linkedList遍历时写一个专门遍历链表的方法吗?显然太冗余且不健壮。以上代码中的foreachCollection 方法很好的诠释了迭代器模式是怎样解决以上的问题的。我不需要知道我遍历的是否是数组,是否是链表,我只需要关注迭代器对象就行了,至于如何遍历是迭代器对象的定义,所以,不暴露聚合对象内部的数据是如何表示的,客户端代码只需要关注Iterator
。
实现
这里我从百度百科copy了一个类图,这里解析一下。
首先ConcreteAggregate
的位置就是聚合类对象例如List,首先它实现了Aggregate
接口,此接口用于生产Iterator
,在集合类框架中就是Iterable
接口,这种设计模式有点类似工厂模式 。然后抽象出了一个遍历对象Iterator
,这个对象定义了许多遍历方法,比如是否有下一个、取出下一个对象、删除下一个对象。而Collection
类全部都可以生产出自己特有的Iterator
,在集合框架中,一般以内部类的方式实现,有兴趣的读者可以阅读源码,调用Aggregate
中定义的这个创建Iterator
方法即可获取Iterator对象,这里以ArrayList
源码做示例:
//from java.util.ArrayList
//实现了刚刚说的Aggregate定义的方法
public Iterator<E> iterator() {
//返回一个内部类
return new Itr();
}
//这里使用内部类的方式定义自己特有的Iterator对象
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;
}
//遍历方法
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();
}
}
//略...
}
从ArrayList的源码可以看出,要实现迭代器模式,有几个要点:
- 抽象一个接口 ,此接口定义获取迭代器的方法(类似java.lang.Iterable)
- 抽象一个迭代器 ,此迭代器定义所有遍历会用到的方法(类似java.util.Iterator)
- 多个实现类都实现刚刚抽象出来的接口,并且实现获取迭代器的方法 (类似java.util.ArrayList#iterator)
- 实现抽象的迭代器 ,迭代器中可实现自己特有的迭代方法。一般都在需要迭代的对象内部以内部类的方式进行定义,好处是不用写过多的类,把内部实现直接隐藏在需要遍历的对象内部,隐藏细节。(类似java.util.ArrayList.Itr这个内部类)
使用迭代器模式的好处
以上将迭代器模式的定义以及如何实现都表述了一遍,接下来我们来探究为什么使用迭代器模式。
其实这个部分答案上面也可以看出来,其中有一点原因就是为了统一聚合对象遍历的方法,正如定义所表述的:不需要关注聚合对象的内部实现,也做到了集合对象与客户端的解耦,可以更方便遍历集合对象。
至于还有没有其他什么好处,下面可以看一段代码:
List<String> arrayList = new ArrayList<String>();
initCollection(arrayList);
//如果没有迭代器模式,将如何遍历聚合对象?
//首先将集合对象转换为数组
Object[] array = arrayList.toArray();
//for循环遍历这个数组
for (int i = 0; i < array.length; i++) {
//这种方式遍历集合,实际上遍历了两次数组
}
//迭代器模式遍历对象(直接foreach)
for (String s : arrayList) {
//这里只遍历了一次集合
}
从以上代码中,我们进行以下总结:
-
迭代器模式很大程度简化了代码量 。JVM字节码底层将会优化foreach语法,将调用foreach中的对象的iterator各个方法进行遍历,所以,实现了Iterable接口的对象都可以进行foreach。
-
不需要关注聚合对象的内部结构来进行遍历 。示例是arrayList,本身就是数组结构,但如果是linkedList呢?链表结构的遍历方式是不一样的,那么如何去遍历?当然你也可以先转换成数组,然后遍历数组,不过就是太麻烦了。
-
性能问题 。在我们普通遍历模式,是先将集合对象转换为数组,这个过程将在底层对数据进行一次遍历,然后我们拿到数组,又对其进行遍历,将产生两次的遍历,性能哪个好是可想而知的。但如果是ArrayList是可以解决这个遍历两次的问题,你可以不转换成数组,因为它本来就是数组,你可以直接for循环然后利用下标调用get(index)方法去顺序遍历,数组由于其有序性和连续性是可以马上获得到第五个对象的,这是没有问题的,但如果是linkedList呢?如果这样一个个利用下标遍历,性能问题是很严重的,例如你需要获取链表中第五个数组,它需要从头指针一个个访问下一个指向的对象,直到第五个才可以拿到你想要的对象。又例如HashSet,你如何去根据下标遍历呢?由于Hash特性,存放的数据都是无序的,所以此遍历方法不适合HashSet,由于迭代器模式的出现,我们将可以更加方便遍历一个Set:
//这里的Map对象的entrySet方法返回的是一个Set for (Map.Entry<Integer, String> entry : hashMap.entrySet()) { //某种角度上看,也使Map更加方便遍历了 }
-
更安全地变更聚合对象内部数据 。这里还是以ArrayList做示例,假如你使用下标进行遍历ArrayList,需要使用到删除操作,例如遍历到下标为3的时候需要删除这个数据,此时将引发数据越界异常,因为当删除数据之后,ArrayList会调整内部数组,在你遍历的过程中,将会访问到实际不存在的下标,所以遍历过程中判断以及删除操作需要使用迭代器:
Iterator<String> iterator = arrayList.iterator(); while (iterator.hasNext()){ String value = iterator.next(); if (value.equals("xxx")) { iterator.remove(); } }
使用这样的方式将不会引发数组越界问题。至于实现原理这里简单带过,有兴趣的读者可以阅读Iterator实现类的源码进行更多的了解。
首先如果是使用下标方式遍历,在你删除数据之后,其实ArrayList将调整内部数组长度,而程序只知道遍历原先长度,所以将会导致访问到不存在的下标问题。
那么迭代器是如何解决这个问题的呢?其实这和迭代器遍历的方式有关,迭代器遍历的方式类似指针的方式,例如ArrayList中迭代器的实现,每次调用next方法获取下一个数据的时候,其实也是以下标的方式获取,只不过这个下标“会变化”。每次next的时候下标会+1,获取数组下标位置的数据,所以这个下标就像指针一样,每次next下标(指针)都会下移。但如果调用remove的话,这个下标(指针)会不动,例如删除下标为5的数据,此时指针还是指向5(因为数组会前移,所以下一次调用next的数据和没删除之前的next的数据是一样的),这样就可以实现删除也照样遍历的效果。至于为什么不会越界,这是因为每次遍历的时候都会调用hasNext方法去看看是否指针指到头了(没数据了),底层中是去查找了ArrayList中size变量,由于remove时调用了ArrayList的remove方法,所以ArrayList中的size变量将动态更新,所以迭代器可以安全的修改聚合遍历内部数据。
总结
看到这里,我相信开头的问题将一一得到解答:
-
迭代器模式是什么?
答:是一个统一迭代方式的一种设计模式。
-
为什么要用迭代器模式?它解决了哪些问题?
答:更透明的遍历各种集合,不需要关心集合内部结构。只需要认识Iterator即可遍历聚合对象。
-
使用迭代器模式的好处?
- 性能优化(不需要遍历两次数组),其中LinkedList强烈推荐使用迭代器模式遍历,若使用get方法下标遍历,性能上会有很大的问题。
- 安全修改内部数据。
- 简化代码。
- 更简单的遍历方式。
由于其统一了迭代方式,使原本不太方便遍历的Hash类集合对象也变得容易遍历了。