java设计模式-迭代子模式(Iterator)

定义

迭代子模式又叫游标(Cursor)模式,是对象的行为模式。迭代子模式可以顺序的访问一个聚集中的元素而不必暴露聚集的内部表象(internal representation)。

聚集和JAVA聚集

多个对象聚在一起形成的总体称为聚集(Aggregate),聚集对象是能够包含一组对象的容器对象。聚集依赖于聚集结构的抽象化,具有复杂化和多样性。数组就是最基本的聚集,也是其他JAVA聚集对象的设计基础。

Java聚集对象时实现了共同的java.util.Collection接口的对象,是Java语言对聚集概念的直接支持。从1.2版开始,Java语言提供了很多种聚集,包括VectorArrayListHashSetHashMapHashtable等,这些都是Java聚集的例子。

迭代子模式的结构

迭代子模式具有两种实现方式,分别为白箱聚集于外禀迭代子黑箱聚集于内禀迭代子

白箱聚集于外禀迭代子

如果一个聚集的接口提供了可以用来修改聚集元素的方法,这个接口就是所谓的宽接口

如果聚集对象为所有对象提供同一个接口,也就是宽接口的话,当然会满足迭代子模式对迭代子对象的要求。但是,这样会破坏对聚集对象的封装。这种提供宽接口的聚集叫做白箱聚集。聚集对象向外界提供同样的宽接口。如下图所示:

白箱聚集

由于聚集自己实现迭代逻辑,并向外部提供适当的接口,使得迭代子可以从外部控制聚集元素的迭代过程。这样一来迭代子所控制的仅仅是一个游标而已,这种迭代子叫做游标迭代子(Cursor Iterator)。由于迭代子是在聚集结构之外的,因此这样的迭代子又叫做外禀迭代子(Extrinsic Iterator)

现在看一看白箱聚集与外禀迭代子的实现。一个白箱聚集向外界提供访问自己内部元素的接口(称作遍历方法或者Traversing Method),从而使外禀迭代子可以通过聚集的遍历方法实现迭代功能。

因为迭代的逻辑是由聚集对象本身提供的,所以这样的外禀迭代子角色往往仅仅保持迭代的游标位置。

一个典型的由白箱聚集于外禀迭代子组成的系统如下图所示,在这个实现中具体迭代子角色是一个外部类,而具体聚集对象角色向外界提供遍历聚集元素的借口。

由白箱聚集于外禀迭代子组成的系统

迭代子模式涉及到以下几个角色:

  • 抽象迭代子角色(Iterator):此抽象角色定义出遍历的对象所需的借口。
  • 具体迭代子角色(ConcreteIterator):此角色实现了Iterator接口,并保持迭代过程中的游标位置。
  • 聚集角色(Aggregate):此抽象角色给出创建迭代子Iterator对象的接口。
  • 具体聚集角色(ConcreteAggregate):实现了创建迭代子Iterator对象的接口,返回一个合适的具体迭代子对象实例。
  • 客户端角色(Client):持有对聚集及其迭代子对象的引用,调用迭代子对象的迭代接口,也有可能通过迭代子操作聚集元素的增加和删除。

示例代码

抽象聚集角色类,这个角色规定出所有的聚集必须实现的接口。迭代子模式要求聚集对象必须有一个工厂方法,也就是createIterator()方法,以向外界提供迭代子对象的实例。

public abstract class Aggregate {
    /**
     * 工厂方法,创建相应迭代子对象的接口
     * @return
     */
    public abstract Iterator createIterator();
}

聚提聚集角色类,实现了抽象聚集角色类所要求的借口,也就是createIterator()方法。此外,还有方法getElement()向外界提供聚集元素,而方法size()向外界提供聚集的大小等。

public class ConcreteAggregate extends Aggregate {
    private Object[] objArray = null;
    /**
     * 构造方法,传入聚合对象的具体内容
     * @param objArray
     */
    public ConcreteAggregate(Object[] objArray) {
        this.objArray = objArray;
    }
    @Override
    public Iterator createIterator() {
        return new ConcreteIterator(this);
    }
    
    /**
     * 取值方法,向外界提供聚集元素
     * @param index
     * @return
     */
    public Object getElement(int index) {
        if (index < objArray.length) {
            return objArray[index];
        } else {
            return null;
        }
    }
     /**
      * 取值方法,向外界提供聚集的大小
      * @return
      */
    public int size() {
        return objArray.length;
    }
}

抽象迭代子角色类

public interface Iterator {
    /**
     * 迭代方法,移动至第一个元素
     */
    public void first();
    /**
     * 迭代方法,移动至下一个元素
     */
    public void next();
    /**
     * 迭代方法,是否为最后一个元素
     */
    public boolean isDone();
    /**
     * 迭代方法,返回当前元素
     */
    public Object currentItem();
}

具体迭代子角色类

public class ConcreteIterator implements Iterator {
    /**
     * 持有被迭代的具体的聚合对象
     */
    private ConcreteAggregate agg;
    /**
     * 内部索引,记录当前迭代到的索引位置
     */
    private int index = 0;
    /**
     * 记录当前索引对象的大小
     */
    private int size = 0;
    
    /**
     * 构造函数,传入具体的聚合对象,并获取聚合对象大小
     * @param agg
     */
    public ConcreteIterator(ConcreteAggregate agg) {
        this.agg = agg;
        this.size = agg.size();
        this.index = 0;
    }
    /**
     * 移动至第一个元素
     */
    public void first() {
        this.index = 0;
    }

    /**
     * 迭代方法,移动到下一个元素
     */
    public void next() {
        if (this.index < this.size) {
            this.index++;
        }
    }

    /**
     * 迭代方法,是否是最后一个元素
     */
    public boolean isDone() {
        return (this.index >= this.size);
    }

    /**
     * 迭代方法,返还当前位置元素
     */
    public Object currentItem() {
        return this.agg.getElement(this.index);
    }
}

上面的例子首先创建了一个聚集类角色,然后调用聚集类对象的工厂方法createIterator()以得到一个迭代子对象。在得到迭代子对象后,客户端进行迭代过程处理,打印出所有的聚集元素。

外禀迭代子的意义

一个常常会问的为题就是:既然白箱聚集已经向外界提供了遍历方法,客户端已经可以自行进行迭代了,为什么还要应用迭代子模式,并创建一个迭代子对象进行迭代呢?

客户端当然可以自行进行迭代,不一定非要一个迭代子对象。但是,迭代子对象和迭代模式会将迭代过程抽象化,将作为迭代消费者的客户端和迭代负责人的迭代子责任分隔开,使得两者都可以独立的烟花。在聚集对象的种类发生变化,或者迭代的方法发生改变时,迭代子作为一个中间层可以吸收变化的因素,从而避免修改客户端或者聚集本身。

此外,如果系统同时需要针对几个不同的聚集对象进行迭代,而这些聚集对象所提供的遍历方法有所不同是,使用迭代子模式和一个外界的迭代子对象是有意义的。具有同一个迭代接口的不同迭代子对象处理具有不同遍历接口的聚集对象,使得系统可以使用一个统一的迭代接口进行所有的迭代。

黑箱聚集与内禀迭代子

如果一个聚集的接口没有提供修改聚集元素的方法,这样的接口就是所谓的窄接口

聚集对象为迭代子对象提供一个宽接口,而为其他对象提供一个窄接口。换而言之,聚集对象的内部接口应当对迭代子对象适当公开,以便迭代子对象能够对聚集对象有足够的了解,从而可以进行迭代操作。但是,聚集对象应当避免向其他对象提供这些方法,因为其他对象应当经过迭代子对象进行这些工作,而不是直接操控聚集对象。

黑箱聚集于内禀迭代子

在Java语言中,实现双重接口的方法就是将迭代子类设计成聚集类的内部成员类。这样迭代子对象可以像聚集对象的内部成员一样访问聚集对象的内部结构。下面给出一个示意性的实现,说明这种双重接口的结构是怎样产生的,以及使用双重接口之后迭代子模式的实现方案。这种同时保证聚集对象的封装和迭代子功能的实现方案叫做黑箱实现方案

由于迭代子是聚集的内部类,迭代子可以自由访问聚集的元素,所以迭代子可以自行实现迭代功能并控制对聚集元素的迭代逻辑。由于迭代子是在聚集的结构之内定义的,因此这样的迭代子又叫做内禀迭代子(Intrinsic Iterator)

为了说明黑箱方案的细节,这里给出一个示意性的黑箱实现。在这个实现里,聚集类ConcreteAggregate含有一个内部成员类ConcreteIterator,也就是实现了抽象迭代子接口的具体迭代子类,同时聚集并不向外界提供访问自己内部元素的方法。

示意性的黑箱实现

示例代码

抽象聚集角色类,这个角色规定出所有的聚集必须实现的接口。迭代子模式要求聚集对象必须有一个工厂方法,也就是createIterator()方法,以向外界提供迭代子对象的实例。

public abstract class Aggregate {
    /**
     * 工厂方法,创建相应迭代子对象的接口
     * @return
     */
    public abstract Iterator createIterator();
}

抽象迭代子角色类

public interface Iterator {
    /**
     * 迭代方法,移动至第一个元素
     */
    public void first();
    /**
     * 迭代方法,移动至下一个元素
     */
    public void next();
    /**
     * 迭代方法,是否为最后一个元素
     */
    public boolean isDone();
    /**
     * 迭代方法,返回当前元素
     */
    public Object currentItem();
}

具体聚集角色类,实现了抽象聚集角色所要求的接口,也就是createIterator()方法。此外,聚集类有一个内部成员类ConcreateIterator,这个内部类实现了抽象迭代子角色所规定的接口;而工厂方法createIterator()所返还的就是这个内部成员类的实例。

public class ConcreteAggregate extends Aggregate {
    private Object[] objArray = null;

    /**
     * 构造方法,传入聚合对象的具体内容
     * 
     * @param objArray
     */
    public ConcreteAggregate(Object[] objArray) {
        this.objArray = objArray;
    }

    @Override
    public Iterator createIterator() {
        return new ConcreteIterator();
    }

    /**
     * 内部成员类,具体迭代子类
     * @author chenshuaishuai
     *
     */
    private class ConcreteIterator implements Iterator {
        /**
         * 内部索引,记录当前迭代到的索引位置
         */
        private int index = 0;
        /**
         * 记录当前聚集对象的大小
         */
        private int size = 0;
        /**
         * 构造函数,设置聚集对象大小和起始索引
         */
        public ConcreteIterator() {
            this.size = objArray.length;
            this.index = 0;
        }
        /**
         * 移动至第一个元素
         */
        @Override
        public void first() {
            this.index = 0;
        }

        /**
         * 迭代方法,移动到下一个元素
         */
        @Override
        public void next() {
            if (this.index < this.size) {
                this.index++;
            }
        }

        /**
         * 迭代方法,是否是最后一个元素
         */
        @Override
        public boolean isDone() {
            return (this.index >= this.size);
        }

        /**
         * 迭代方法,返还当前位置元素
         */
        @Override
        public Object currentItem() {
            return objArray[this.index];
        }
        
    }
}

客户端类

public class Client {
    public void operation() {
        Object[] objArray = { "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eigth" };
        Aggregate agg = new ConcreteAggregate(objArray);
        Iterator iterator = agg.createIterator();
        while (!iterator.isDone()) {
            System.out.println(iterator.currentItem());
            iterator.next();
        }
    }
    
    public static void main(String[] args) {
        Client client = new Client();
        client.operation();
    }
}

上面的例子首先创建了一个聚集类实例,然后调用聚集对象的工厂方法createIterator()得到一个迭代子对象。在得到迭代子的实例后,客户端开始迭代过程,打印出所有的聚集元素。

主动迭代子和被动迭代子

主动迭代子和被动迭代子又称作外部迭代子和内部迭代子。

所谓主动(外部)迭代子,指的是由客户端来控制迭代下一个元素的步骤,客户端会明显调用迭代子的next()等迭代方法,在遍历过程中向前进行。

所谓被动(内部)迭代子,指的是由迭代子自己来控制迭代下一个元素的步骤。因此,如果想要在迭代的过程中完成工作的话,客户端需要把操作传递给迭代子,迭代子在迭代的时候会在每个元素上执行这个操作,类似于Java的回调机制。

总体来说外部迭代器比内部迭代器要灵活一些,因此我们常见的实现多属于主动迭代子。

静态迭代子和动态迭代子

  • 静态迭代子由聚集对象创建,并持有聚集对象的一份快照(snapshot),在产生后这个快照的内容就不再变化。客户端可以继续修改原聚集的内容,但是迭代子对象不会反映出聚集的新变化。
    静态迭代子的好处是它的安全性和简易性,换而言之,静态迭代子易于实现,不容易出现错误。但是由于静态迭代子将原聚集复制了一份,因此它的短处是对时间和内存资源的消耗。
  • 动态迭代子则与静态迭代子完全相反,在迭代子被产生之后,迭代子保持着对聚集元素的引用,因此,任何对于聚集元素内容的修改都会在迭代子对象上反映出来。
    完整的动态迭代子不容易实现,但是简化的动态迭代子并不难实现,大多数的Java设计师遇到的迭代子都是这种简化的动态迭代子。为了说明什么是简化的动态迭代子,首先需要介绍一个新的概念:Fail Fast

Fail Fast

如果一个算法开始之后,它的运算环境发生变化,是的算法无法进行必须的调整时,这个算法就应当发出故障信号。这就是Fail Fast的含义。

如果聚集对象的元素在一个动态迭代子的迭代过程中发生变化是,迭代过程会受到影响而变得不能进行。这时候,迭代子就应当立即抛出一个异常。这种迭代子就是实现了Fail Fast功能的迭代子。

Fail Fast在Java聚集中的应用

Java语言以接口java.util.Iterator的方式支持迭代子模式,Collection接口要求提供iterator()方法,此方法在调用时返还一个Iterator类型的对象。而作为Collection接口的子类型,AbstractList的内部成员类Itr就是实现了Iterator接口的类。

Itr类的源代码如下所示:

    private class Itr implements Iterator<E> {
        /**
         * Index of element to be returned by subsequent call to next.
         */
        int cursor = 0;

        /**
         * Index of element returned by most recent call to next or
         * previous.  Reset to -1 if this element is deleted by a call
         * to remove.
         */
        int lastRet = -1;

        /**
         * The modCount value that the iterator believes that the backing
         * List should have.  If this expectation is violated, the iterator
         * has detected concurrent modification.
         */
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size();
        }

        public E next() {
            checkForComodification();
            try {
                int i = cursor;
                E next = get(i);
                lastRet = i;
                cursor = i + 1;
                return next;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                AbstractList.this.remove(lastRet);
                if (lastRet < cursor)
                    cursor--;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
                throw new ConcurrentModificationException();
            }
        }

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

Itr类的源代码中可以看到,方法checkForComodification()会检查聚集的内容是否刚刚被外界直接修改过(不是通过迭代子提供的方法修改的)。如果在迭代开始后,聚集的内容被外界绕过迭代子对象而世界修改的话,这个方法会立即抛出ConcurrentModificationException异常。

这就是说,AbstractList.Itr迭代子是一个Fail Fast的迭代子。

迭代子模式的优点

  1. 迭代子模式简化了聚集的借口。迭代子具备了一个遍历接口,这样聚集的接口就不必具备遍历接口。
  2. 每一个聚集对象都可以有一个或多个迭代子对象,每一个迭代子对象的迭代状态可以是彼此独立的。因此,一个聚集对象可以同时有几个迭代在进行之中。
  3. 由于遍历方法被封装在迭代子角色里面,因此迭代的算法可以独立于聚集角色变化。

参考

《JAVA与模式》之迭代子模式

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 1 场景问题# 1.1 工资表数据的整合## 考虑这样一个实际应用:整合工资表数据。 这个项目的背景是这样的,项目...
    七寸知架构阅读 2,424评论 0 53
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,293评论 18 399
  • 设计模式汇总 一、基础知识 1. 设计模式概述 定义:设计模式(Design Pattern)是一套被反复使用、多...
    MinoyJet阅读 3,820评论 1 15
  • 警告!警告!你2016年余额已经不足10%!!! 进入12月以后,很多微信公众号文章中都出现了这样的字样。很多人在...
    药山阅读 1,191评论 1 51
  • 是的,如果不记下来,梦会被遗忘。 我把记事本app打开,笔电合起来摆在床头,从而可以在第一时间噼里啪啦敲下我的梦境...
    P哥阅读 172评论 2 0