设计模式之迭代器模式(十四)

前言

今天介绍的这个设计模式,是我认为日常代码最常用的一个设计模式—迭代器模式。所以这篇文章将使用JDK集合类框架来介绍迭代器模式。

在我们使用聚合类例如各种集合框架ArrayList、LinkedList、HashMap等等的集合类遍历时都会用到。那么此时可以引申出几个问题:

  1. 迭代器模式是什么?
  2. 为什么要用迭代器模式?它解决了哪些问题?
  3. 使用迭代器模式的好处?

那么,接下来就带着问题继续看下去。

什么是迭代器模式

定义

迭代器模式(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的源码可以看出,要实现迭代器模式,有几个要点:

  1. 抽象一个接口 ,此接口定义获取迭代器的方法(类似java.lang.Iterable)
  2. 抽象一个迭代器 ,此迭代器定义所有遍历会用到的方法(类似java.util.Iterator)
  3. 多个实现类都实现刚刚抽象出来的接口,并且实现获取迭代器的方法 (类似java.util.ArrayList#iterator)
  4. 实现抽象的迭代器 ,迭代器中可实现自己特有的迭代方法。一般都在需要迭代的对象内部以内部类的方式进行定义,好处是不用写过多的类,把内部实现直接隐藏在需要遍历的对象内部,隐藏细节。(类似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) {
	//这里只遍历了一次集合
}

从以上代码中,我们进行以下总结:

  1. 迭代器模式很大程度简化了代码量 。JVM字节码底层将会优化foreach语法,将调用foreach中的对象的iterator各个方法进行遍历,所以,实现了Iterable接口的对象都可以进行foreach。

  2. 不需要关注聚合对象的内部结构来进行遍历 。示例是arrayList,本身就是数组结构,但如果是linkedList呢?链表结构的遍历方式是不一样的,那么如何去遍历?当然你也可以先转换成数组,然后遍历数组,不过就是太麻烦了。

  3. 性能问题 。在我们普通遍历模式,是先将集合对象转换为数组,这个过程将在底层对数据进行一次遍历,然后我们拿到数组,又对其进行遍历,将产生两次的遍历,性能哪个好是可想而知的。但如果是ArrayList是可以解决这个遍历两次的问题,你可以不转换成数组,因为它本来就是数组,你可以直接for循环然后利用下标调用get(index)方法去顺序遍历,数组由于其有序性和连续性是可以马上获得到第五个对象的,这是没有问题的,但如果是linkedList呢?如果这样一个个利用下标遍历,性能问题是很严重的,例如你需要获取链表中第五个数组,它需要从头指针一个个访问下一个指向的对象,直到第五个才可以拿到你想要的对象。又例如HashSet,你如何去根据下标遍历呢?由于Hash特性,存放的数据都是无序的,所以此遍历方法不适合HashSet,由于迭代器模式的出现,我们将可以更加方便遍历一个Set:

    //这里的Map对象的entrySet方法返回的是一个Set
    for (Map.Entry<Integer, String> entry : hashMap.entrySet()) {
        //某种角度上看,也使Map更加方便遍历了
    }
    
  4. 更安全地变更聚合对象内部数据 。这里还是以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变量将动态更新,所以迭代器可以安全的修改聚合遍历内部数据。

总结

看到这里,我相信开头的问题将一一得到解答:

  1. 迭代器模式是什么?

    答:是一个统一迭代方式的一种设计模式。

  2. 为什么要用迭代器模式?它解决了哪些问题?

    答:更透明的遍历各种集合,不需要关心集合内部结构。只需要认识Iterator即可遍历聚合对象。

  3. 使用迭代器模式的好处?

    • 性能优化(不需要遍历两次数组),其中LinkedList强烈推荐使用迭代器模式遍历,若使用get方法下标遍历,性能上会有很大的问题。
    • 安全修改内部数据。
    • 简化代码。
    • 更简单的遍历方式。

由于其统一了迭代方式,使原本不太方便遍历的Hash类集合对象也变得容易遍历了。

发布了84 篇原创文章 · 获赞 85 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/qq_41737716/article/details/88669297
今日推荐