Java的迭代器与迭代子模式

概括

Java集合容器是Java的一个重要组成部分,而迭代器(Iterator)就是对外提供访问集合元素的一种方式。

访问数组元素的方式

访问数组的方式并不陌生,如下

public class Test {

    public static void main(String[] args) {
        int[] arr = {2, 0, 1, 8, 0, 6, 0, 7};
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}

或者也可以使用语法糖的写法

public class Test {

    public static void main(String[] args) {
        int[] arr = {2, 0, 1, 8, 0, 6, 0, 7};
        for (int a : arr) {
            System.out.println(a);
        }
    }
}

对于后者,如果查看.class反编译的文件可以发现如下代码

public class Test {
    public Test() {
    }

    public static void main(String[] args) throws IOException {
        int[] arr = new int[]{2, 0, 1, 8, 0, 6, 0, 7};
        int[] var2 = arr;
        int var3 = arr.length;

        for(int var4 = 0; var4 < var3; ++var4) {
            int a = var2[var4];
            System.out.println(a);
        }

    }
}

不难发现,反编译出来的代码实际上就是传统的下标访问方式

语法糖作用于数组或者继承了Iterable<T>的接口上,数组已经验证了,接下来验证一下后者,拿最常用的ArrayList来举示例

public class Test {

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(2, 0, 1, 8, 0, 6, 0, 7);
        for (Integer i : list) {
            System.out.println(i);
        }
    }
}

反编译后的代码

public class Test {
    public Test() {
    }

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(2, 0, 1, 8, 0, 6, 0, 7);
        Iterator var2 = list.iterator();

        while(var2.hasNext()) {
            Integer i = (Integer)var2.next();
            System.out.println(i);
        }

    }
}

从代码中可以看出,编译后语法糖被转化成了迭代器(Iterator)的访问模式,还顺便验证了泛型擦除

还以一种类似于传统数组的访问方式,反编译后基本没有变化

public class Test {

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(2, 0, 1, 8, 0, 6, 0, 7);
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
}

到这里就会问,两种访问方式的效率怎么样?

来看一下测试代码

public class Test {

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();

        // 向list中装入50000个随机数
        putRandomNumbers(list, 50000);

        long start, end;

        /*
         * fori下标访问
         */
        start = System.currentTimeMillis();
        for (int i = 0; i < list.size(); i++) {
            list.get(i);
        }
        end = System.currentTimeMillis();
        System.out.println("fori: " + (end - start) + "ms");

        /*
         * foreach(迭代器)访问
         */
        start = System.currentTimeMillis();
        for (Integer i : list) {

        }
        end = System.currentTimeMillis();
        System.out.println("foreach: " + (end - start) + "ms");
    }


    public static void putRandomNumbers(List<Integer> list, int length) {
        Random r = new Random();
        for (int i = 0; i < length; i++) {
            list.add(r.nextInt());
        }
    }
}

运行结果是

——————

fori: 5ms

foreach: 6ms

——————

如果数组大小改为500000

——————

fori: 14ms

foreach: 20ms

——————

一次测试可能不怎么可靠,我测了很多次fori都是在10~20ms,而foreach在15ms~25ms上,暂且认为fori比foreach快吧

那么问题来了,既然传统的fori比foreach(迭代器)快,还要迭代器来干什么?

现在改一下上面的代码,就把ArrayList改成LinkedList,其他不变,再看看运行结果

(ps:数组长度为50000)

——————

fori: 3817ms

foreach: 10ms

——————

差距一下就出来了,下面是从源码来分析为什么两种list的fori效率差距为何如此之大

对于ArrayList,get方法的源码如下

先对下标做检查(防止越界)

    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

再跳到elementData()中

    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }

明显这个get方法就是直接访问数组的下标,访问数组单个元素,时间复杂度O(1)

再来看看LinkedList

    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

跳转到node方法

    Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

可见LinkedList内部是一个双向链表,当访问某一个元素时,先检查下标在链表的前半段还是后半段,如果在前半段,从前往后依次访问,否则从后往前依次访问,时间复杂度O(n/2),姑且也算作O(n)吧

这样一来,用fori的方式遍历List,调用n次get方法,对于ArrayList的时间复杂度就是O(n),而LinkedList就变成了O(n^2)!所以会导致fori访问上ArrayList和LinkedList的速度差距如此之大。

迭代器与迭代子模式

遍历一个数组和遍历一个链表的方式是不一样的,而在Java中,集合不只是有ArrayList和LinkedList,还有各种Set和Map,我们希望能够有统一的方式来遍历一个集合。迭代器恰好为我们解决了这个问题

迭代器的引入使得在遍历集合时,客户端(使用集合的一端)可以不必关心集合是以怎样的方式遍历的,因为集合对外提供了统一的获取迭代器接口,当集合内部结构需要做修改时,也要修改对应的迭代器接口的逻辑,但是客户端的遍历逻辑是不需要变动的,这样一来保证了开-闭原则,这也就是所说的迭代子模式

迭代子模式定义

在软件构建过程中,集合对象内部结构常常变化各异,但对于这些集合对象,我们希望在不暴露其内部结构的同时,可以让外部客户代码透明地访问其中包含的元素;同时这种“透明遍历”也为同一种算法在多种集合对象上进行操作提供了可能。使用面向对象技术将这种遍历机制抽象为“迭代器对象”为“应对变化中的集合对象”提供了一种优雅的方式。迭代子(Iterator)模式又叫游标(Cursor)模式,是对象的行为模式。迭代子模式可以顺序地访问一个聚集中的元素而
不必暴漏
聚集的内部表象。

迭代子模式有两种,白箱聚集与外禀迭代子,黑箱聚集与内禀迭代子

白箱还黑箱取决于集合本身有没有对外提供除了迭代器以外访问元素的接口,像Java集合里面size,get这些方法,有的话就是白箱,如果除了迭代器接口没有其他能够访问集合元素接口的就是黑箱,关于迭代子模式更多细节,可以看看这篇文章

https://www.cnblogs.com/java-my-life/archive/2012/05/22/2511506.html

点赞