迭代器的这些细节你真的懂吗?

为什么使用迭代器

迭代器是一种设计模式,主要用来对集合中的数据进行遍历,在很多编程语言中,都将迭代器作为了一个基础类库,直接提供出来,由此可见迭代器模式已经成为容器实现内部元素遍历的范式。

迭代器的接口的定义,通常有两种方式

第一种:

public interface Iterator<E> { 
	 boolean hasNext(); 
	 void next(); 
	 E currentItem();
 }

第二种:

public interface Iterator { 
	boolean hasNext(); 
	E next();
}

在第一种定义中:

hasNext() 表示被遍历的集合中是否还有未被遍历的元素,作为终止遍历的判断条件。

next() 表示将遍历元素的游标向后移动。

currentItem() 表示游标指向的元素。

在第二种定义中:

hasNext() 表示的含义和第一种相同。

next() 表示返回当前元素,同时将遍历元素的游标向后移动。

相比第二种实现方式,第一种可以通过多次调用currentItem()方法,查询当前元素,而不移动游标,但是实现的过程中需要比第二种方式实现更多的方法。在jdk中的Iterator的定义采用了第二种。

为什么要使用迭代器作为容器内部元素遍历的范式呢?

1.使用迭代器可以实现更加复杂的遍历方式,针对不同遍历方式的需求,可以实现不同的迭代器,方便代码的扩展。

2.使用迭代器可以实现集合类和集合的遍历操作之间的解耦,将集合的遍历操作独立于集合类之外,只存在于迭代器中,让集合类的职责更加单一,使代码的可读性更高。

3.实现接口隔离,让客户端只依赖自己需要的接口,比如客户端只需要对集合的遍历功能,那么此时可以只依赖Iterator接口即可,而不用依赖集合类相关的类库,减少客户端的复杂度。

迭代器该怎么使用

使用迭代器的进行集合遍历的规范代码:

public void iteratorDemo(){

        List<String> strList = new ArrayList<String>();
        strList.add("a");
        strList.add("b");

        Iterator<String> iterator = strList.iterator();
        while(iterator.hasNext()) {
            String item = iterator.next();
            System.out.println(item);
        }
    }

在对集合元素进行简单遍历时候,使用iterator的方式相比使用for循环,反而更复杂,使用for循环进行集合遍历的代码如下:

public void forDemo() {

        List<String> strList = new ArrayList<String>();
        strList.add("a");
        strList.add("b");

        for (int i = 0; i < strList.size(); i++) {
            String item = strList.get(i);
            System.out.println(item);
        }
    }

为了使迭代器使用方式更加简洁易用,java在编译器层面,通过语法糖的方式对迭代器进行了优化,也就是我们常说的foreach 遍历。优化后的可以通过以下方式使用迭代器:

 public void iteratorOptDemo() {
        List<String> strList = new ArrayList<String>();
        strList.add("a");
        strList.add("b");

        for (String item : strList) {
            System.out.println(item);
        }
    }

iterator使用方式优化后,使用起来更加的简洁易用,甚至比普通的for循环的方式更加简单。

增强for循环为什么被称为语法糖?

iterator语法糖的实现过程:

为了验证 foreach只是iterator遍历方式的语法糖,可以通过查看foreach遍历的字节码进行验证,以下是上文中foreach代码的字节码实现;

public void iteratorOptDemo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: ldc           #4                  // String a
        11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        16: pop
        17: aload_1
        18: ldc           #6                  // String b
        20: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        25: pop
        26: aload_1
        27: invokeinterface #7,  1            // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
        32: astore_2
        33: aload_2
        34: invokeinterface #8,  1            // InterfaceMethod java/util/Iterator.hasNext:()Z
        39: ifeq          62
        42: aload_2
        43: invokeinterface #9,  1            // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
        48: checkcast     #10                 // class java/lang/String
        51: astore_3
        52: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
        55: aload_3
        56: invokevirtual #12                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        59: goto          33
        62: return

在第27行字节码,通过 iterface 指令获取 list的的迭代器iteraor对象,然后通过第34行字节码,通过调用 iterator的hasnext方法判断是否还有可比可遍历的元素,如果有的话,通过43行字节码,调用 next方法获取当前可用的item,也就是说foreach代码在运行时,依旧还是使用了迭代器,只不过在源码层面,对使用方式进行了简化。

迭代器的最佳实践

在使用集合类的迭代器进行集合元素遍历时,不要对集合内部元素进行增加或者删除,否则回产生一些不可预料的问题发生。

我们知道ArrayList的底层实现是数组,当对ArrayList中的元素进行新增或者删除时,会引起数组中元素移动,这样可能会导致数据遍历出现问题。

如果在遍历过程中,对集合进行增加元素操作会产生以下结果:

1.如果在游标指定元素前增加元素时,会导致当前被遍历的元素重复访问。

2.如果在游标指定元素后增加元素的话,对遍历无影响。

为了方便理解,可以参考下图:

在这里插入图片描述
在迭代器游标访问元素b时,在元素b的位置插入了新的元素e,那么元素b就会被后移一位,那么接下来,元素b还会被再次访问,导致元素的重复访问。

使用迭代器遍历集合中,增删元素,会带来不可预期的结果,实际上不可预期的结果要比直接出错更危险,因为这种不可预期的结果,会导致非常难以发现的bug。为了避免这种问题的发生,java中集合类在实现iterator时,采用了 fail-fast的处理方式,集合元素在被iterator遍历的过程中,如果检测到集合元素数量发生了变化,那么就会抛出 ConcurrentModificationException。

以arrayList中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;

        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];
        }
        
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

在容器中,变量modCount 记录了,当前容器中元素修改的次数。再给容器创建迭代器时,会将modCount赋值给迭代器中的expectedModCount ,变量expectedModCount 用来监测容器中元素的变化。在next和hasNext方法中,都会通过方法checkForComodification来比较expectedModCount 和modCount的值是否相同,如果相同,则说明在迭代器遍历的过程中,容器中的元素没有发生变化,否则,就会抛出ConcurrentModificationException异常,来防止上文中描述的不可预测的问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值