Java 迭代器介绍

Java 迭代器介绍

迭代器模式

迭代器模式是一个典型的设计模式,提供一种方法访问一个容器对象中各个元素,而又不暴露该对象的内部细节。因为屏蔽了细节,可以针对不同实现的容器,提供一致的标准化的访问方法。

迭代器模式有四个部分组成:

  1. 抽象容器 Aggregate
  2. 具体容器 ConcreteAggregate
  3. 抽象迭代器 Iterator
  4. 迭代器实现 ConcreteIterator
迭代器模式类图

Java 迭代器

框架

Java 中的容器接口 Collection 继承了迭代器接口 Iterable,迭代器接口中的 iterator() 方法返回 Iterator 类型对象。这里 Collection 相当于迭代器模式中的抽象容器 Aggregate;Iterator 相当于迭代器模式中的抽象迭代器 Iterator。

public interface Collection<E> extends Iterable<E> {
    Iterator<E> iterator();
    // 省略其他
}

public interface Iterable<T> {
    Iterator<T> iterator();
}

public interface Iterator<E> {
    // 判断是否还有下一个元素
    boolean hasNext();
    // 获取下一个元素
    E next();
    // 移除一个元素
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
}

java 中 List 、Set、Queue 容器都会继承实现 Collection 类,提供迭代器接口,我们以 List 为例看一下迭代器的实现。

List 容器框架

List 的迭代器实现

我们可以看一下 ArrayList 中迭代器的实现:

  1. Itr 实现了 hasNext、next、remove 三个方法
  2. 遍历靠下标 cursor 的递增,其实现和我们自己遍历数组一样(迭代器好处是屏蔽细节、标准化)
  3. remove 之后 lastRet 置为 -1,调用一次 next 后只能调用一次 remove,否则会抛异常
  4. 调用 next 之前也要调用 hasNext 判断,否则 cursor 大于等于 size 会抛异常
  5. 迭代器遍历过程中,只能使用其 remove 方法移除元素,如果使用了 List 本身的 add、remove 方法,会导致下次调用 next 方法时抛出异常。原因是迭代器 Itr 里保存了 List 修改次数 modCount 的值到局部变量 expectedModCount 里,不通过迭代器的修改会导致两者不一致,导致 checkForComodification 方法抛异常。
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    public Iterator<E> iterator() {
        return new Itr();
    }

    /**
     * An optimized version of AbstractList.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;

        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();
            }
        }

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

ArrayList 中的迭代器在容器变更后会失效,那么有没有不失效的呢?我们可以看看 CopyOnWriteArrayList 的迭代器实现:

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }

    private static class COWIterator<E> implements ListIterator<E> {
        /** Snapshot of the array **/
        private final Object[] snapshot;
        /** Index of element to be returned by subsequent call to next.  */
        private int cursor;

        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }

        public boolean hasNext() {
            return cursor < snapshot.length;
        }

        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }
    }    
}

CopyOnWriteArrayList 迭代器 COWIterator 遍历的是创建迭代器时的数据快照,这样后续容器的元素增加和减少,就不会导致迭代器的失效了。请注意这里并没有记录容器元素的实际大小,容器元素数目完全取决于数组 snapshot 的长度。我们知道在 ArrayList 中,为了减少扩容次数,扩容时数组长度会比实际所需的长,导致数组长度和容器元素个数不一致。CopyOnWriteArrayList 没有这个问题吗?确实是的,我们看一下其添加元素时,扩容的源代码:

  1. 每次扩容,新数组长度等于老数组长度加1,长度刚刚好。
  2. 每次添加元素,都需要扩容,效率低下。
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

扩展迭代器 ListIterator

List 容器中,不仅提供了普通迭代器 Iterator 的实现,还有一个功能更强的迭代器 ListIterator:

  1. 除了正向遍历之外,还提供了反向遍历的功能
  2. 除了 remove 之外,提供了 set 和 add,当然通过迭代器的这些方法修改数据,都不应该导致迭代器失效
public interface ListIterator<E> extends Iterator<E> {
    boolean hasNext();
    E next();
    boolean hasPrevious();
    E previous();
    int nextIndex();
    int previousIndex();
    void remove();
    void set(E e);
    void add(E e);  
}

总结

迭代器模式,体现了标准化遍历容器,屏蔽内部实现细节的编程思想。

普通迭代器在遇到容器修改时会失效,但是一些线程安全类实现的迭代器比如 COWIterator 是不会因容器修改而失效的。

参考文献

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

推荐阅读更多精彩内容

  • java笔记第一天 == 和 equals ==比较的比较的是两个变量的值是否相等,对于引用型变量表示的是两个变量...
    jmychou阅读 1,477评论 0 3
  • Java源码研究之容器(1) 如何看源码 很多时候我们看源码, 看完了以后经常也没啥收获, 有些地方看得懂, 有些...
    骆驼骑士阅读 980评论 0 22
  • 1 场景问题# 1.1 工资表数据的整合## 考虑这样一个实际应用:整合工资表数据。 这个项目的背景是这样的,项目...
    七寸知架构阅读 2,496评论 0 53
  • 夜已深,雨后的月亮慢慢掰开云层露出真容,而我习惯在深夜思念您的心依旧难以平静,不知您是否已经入睡,不知您的那篇天空...
    娜因有您阅读 201评论 0 0
  • 数算恩福 1.感恩妈妈,我读初二那年,视力变的模糊,看不清黑板上的字,那时我的一位堂哥有去县城,妈妈就叫他帮我买眼...
    lrryy阅读 136评论 0 1