设计模式——迭代器模式(遍历王者荣耀和英雄联盟英雄信息)

本文主要讲述迭代器模式,并使用遍历不同数据结构的王者荣耀和英雄联盟英雄作为例子帮助大家理解,最后附上阿离美图一张!

image-20181219220835618

一、概述

迭代器模式(Iterator pattern):提供一种方法顺序访问一个聚合对象中的各个元素,而又不用暴露聚合底层的实现。

迭代器模式比较常见的设计模式,对于熟悉java集合的我们来说,会经常用到迭代器。当我们需要写一个方法来遍历集合,又不想针对不同的集合实现不同的方法,就可以使用迭代器来完成。

迭代器模式主要解决两个问题
1.聚集元素在访问和遍历时,不必要暴露底层数据结构(without exposing its representation)
2.当为一个新的聚集定义遍历操作时,不需要改变接口

二、结构

UML图:

image-20181218220839386

主要角色:

聚合的接口(Aggregate):提供一个共同的接口,给所有的聚合使用,将客户代码从聚合的实现中解耦。

聚合的具体实现(ConcreateAggregate):这个具体实现会持有一个对象的集合,并实现createIterator()方法,通过这个方法可以返回一个对象的集合的迭代器。

迭代器接口(Iterator):这是一个迭代器接口,我们可以在里面定义一些方法,利用这些方法可以在集合元素之间游走。可以使用java自带的java.util.Iterator,也可以根据我们自己需要来定义一个Iterator。

迭代器具体实现(ConcreteIterator):由具体聚合提供一个工厂方法来实现具体迭代器,这个迭代器负责管理目前遍历的位置。

三、输出所有王者荣耀和英雄联盟的英雄信息

源代码

假设当前有这样一个需求,我们需要一个小助手,可以同时向用户展示王者荣耀和英雄联盟中的英雄信息,但是两款游戏维护英雄使用数据结构不同,一个数组,现在我们用迭代器来完成这个功能

定义英雄的实体类,包括姓名、性别、描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Hero {
private String name;
private String sex;
private String desc;

public Hero(String name, String sex, String desc) {
this.name = name;
this.sex = sex;
this.desc = desc;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getSex() {
return sex;
}

public void setSex(String sex) {
this.sex = sex;
}

public String getDesc() {
return desc;
}

public void setDesc(String desc) {
this.desc = desc;
}

@Override
public String toString() {
return "Hero{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", desc='" + desc + '\'' +
'}';
}
}

创建王者荣耀英雄类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class HOKHeros {
static final int MAX_HERO_COUNT = 10;
int heroCount = 0;
private Hero[] hokHeros = new Hero[MAX_HERO_COUNT];

public HOKHeros() {
Hero gsl = new Hero("公孙离", "妹子", "我觉得阿离是最漂亮的女英雄~");
Hero lbqh = new Hero("鲁班七号", "男", "智商250");
Hero wzt = new Hero("武则天", "女", "贼贵");
hokHeros[0] = gsl;
hokHeros[1] = lbqh;
hokHeros[2] = wzt;
this.heroCount = 3;
}

public Hero[] getHokHeros() {
return hokHeros;
}

public void addHero(Hero hero) {
if (heroCount >= MAX_HERO_COUNT) {
System.out.println("已经不能放更多英雄了!");
} else {
hokHeros[this.heroCount] = hero;
this.heroCount += 1;
}
}

public Iterator<Hero> createIterator(){
return new HOKHerosIterator(hokHeros);
}
}

数组没有继承迭代器接口,所以我们自己实现一个!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class HOKHerosIterator implements Iterator<Hero> {
private int position = 0;
private Hero[] heroes;

public HOKHerosIterator(Hero[] heroes) {
this.heroes = heroes;
}

@Override
public boolean hasNext() {
if (heroes.length <= position || heroes[position] == null) {
return false;
}
return true;
}

@Override
public Hero next() {
Hero hero = heroes[position];
position += 1;
return hero;
}


}

创建英雄联盟英雄类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class LOLHeros {
private ArrayList<Hero> heroes;

public LOLHeros() {
heroes = new ArrayList<>();
Hero gl = new Hero("盖伦", "男", "初始英雄");
Hero rw = new Hero("锐雯", "女", "贼秀");
Hero ys = new Hero("亚索", "男", "很厉害的样子");
heroes.add(gl);
heroes.add(rw);
heroes.add(ys);
}

public ArrayList<Hero> getHeroes() {
return heroes;
}
public void addHere(Hero hero){
heroes.add(hero);
}
public Iterator<Hero> createIterator(){
return heroes.iterator();
}
}

小助手

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class HeroHelper {
private Iterator iterator;

public HeroHelper(Iterator iterator) {
this.iterator = iterator;
}

public HeroHelper() {
}

public void showHeros() {
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}

public void setIterator(Iterator iterator) {
this.iterator = iterator;
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
public static void main(String[] args) {
HOKHeros hokHeros = new HOKHeros();
LOLHeros lolHeros = new LOLHeros();

HeroHelper heroHelper = new HeroHelper();
heroHelper.setIterator(hokHeros.createIterator());
System.out.println("------");
heroHelper.showHeros();
heroHelper.setIterator(lolHeros.createIterator());
heroHelper.showHeros();
}
}
结果
Hero{name='公孙离', sex='妹子', desc='我觉得阿离是最漂亮的女英雄~'}
Hero{name='鲁班七号', sex='男', desc='智商250'}
Hero{name='武则天', sex='女', desc='贼贵'}
------
Hero{name='盖伦', sex='男', desc='初始英雄'}
Hero{name='锐雯', sex='女', desc='贼秀'}
Hero{name='亚索', sex='男', desc='很厉害的样子'}

从上面可以看出,迭代器需要两个核心方法hashNext(),next(),在java中还提供了remove()方法,根据需要我们可以自己实现迭代器,例如上文中,王者荣耀是用数组来储存英雄,我们手动实现了一个迭代器。

小助手并不需要清楚每款游戏在底层是如何储存英雄,但是只要可以创建迭代器,小助手就可以帮我们遍历英雄列表。

想象如果两款游戏没有实现迭代器接口,是不是就得在小助手里自己手写两个不同的for循环?

如果新加入了doto的英雄列表,它是用Hashtable来维护英雄列表的,我们现在就可以让它提供createItorator就可以,但若不使用迭代器,是不是就需要对小助手进行修改?

java中的集合

java的集合并未将迭代器单独创建一个类,而是将迭代器作为集合的内部类,这样迭代器可以自由操作集合内元素,即保证了聚合对象的封装又能实现迭代器模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public classArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
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();
}
}

@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
}

像这个ArrayList,内部主干是一个数组,但是别为对外提供获取该数组的方法,不过利用内部类依然实现了迭代器模式。

如果读过ArrayList源码,会注意modCount这个成员变量,用来记录ArrayList被修改次数(add,remove,set这类方法都会给modCount加1),在迭代器中checkForComodification(),用来实现fail-fast机制,当我们一个线程在使用迭代器遍历时,如果另一个线程对集合进行了修改,就会抛出异常。

四、优缺点

优点

  • 客户端不需要要了解具体底层实现,可以使用迭代器进行遍历

  • 符合开闭原则,当我们需要新添加一个聚合时,不需要对客户端进行修改,只需要使聚合实现迭代器接口就可以

  • 封装良好,客户端只需要迭代器就可以遍历,而不需要知道具体的迭代器算法

缺点

  • 对于简单的遍历,实现迭代器较为繁琐,用户可能更喜欢用for来进行遍历。

五、总结

迭代器模式在平时遍码过程中使用的频率并不高,但是了解迭代器却能加深我们对集合的了解。在java类中,除了Iterator外,Iterable、ListIterator也值得我们去了解~有兴趣的同学可以进行阅读。

掌握迭代器模式要把握住在不了解聚合底层实现的情况下进行遍历这个核心即可,一般来说,如果我们需要实现一个集合,就需要提供这个集合的迭代器。

另外,迭代器模式与工厂模式结合可以发挥巨大的威力!

希望本文对想要学习迭代器的小伙伴有所帮助,最后附离妹美图一张!

公孙离2

本文首发于cdream个人博客

欢迎转载,转载请注明出处!


本文参考

  1. Head First 设计模式,Eric Freeman &Elisabeth Freeman with Kathy Sierra & Bert Bates
  2. Iterator pattern,wiki
  3. 《JAVA与模式》之迭代子模式
0%