java迭代器解析

437 阅读2分钟

起因

今天补刷力扣每日一题,出现了一道迭代器有关的题目(284)。让我不禁思考,Java里的经常使用的迭代器我竟然没有看过源码,所以通过这篇文章去学习总结下迭代器的Java实现。

作用

自顶向下的去理解迭代器,先思考他的作用和意义,不要一上来就扣他的源码。

我们平常使用List集合时,想要遍历元素,我们一般采取什么办法呢

遍历List的方法

//传统下标方法
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}
//for-each方法
for (int a : list) {
    System.out.println(a);
}

一般我们写算法第一种用的比较多,因为有比较复杂的各种判断和下标记录之类的;写业务第二种比较多,因为foreach的写法更简洁。当然Jdk8之后还有lambda写法,我就不写了,因为不是我这篇文章的重点。

而迭代器的写法我相信大部分人都是不爱写的,因为我也不爱写。。

Iterator iterator = list.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}

相比较之下,比较麻烦,甚至IDE都会建议你改写为foreach写法

但是一般的业务可能会有过滤、新增元素的需求,也就是增删需求,采取非迭代器的方式就不可取了。

遍历过程中增删List

对于第一种下标类写法,我们如果删除元素会出现什么问题呢,我们举一个例子

List<Integer> list = new ArrayList<>();
for(int i=1;i<=10;i++){
    list.add(i);
}
for (int i = 0; i < list.size(); i++) {
    list.remove(i);
}
System.out.println(list);
//输出结果为
//[2, 4, 6, 8, 10]

与我们预料的结果不太一样,以为会全部删除掉,结果只删除了奇数。

其实是因为当删除了i位置的元素后,因为线性表的特性,后面的元素会前移,i此时已经在下一个元素的位置了,但是for循环仍将i++,导致有元素没有被删掉,这种情况ide也会有警告提示。

解决方法其实就是在删除之后,做一个i--,但其实这么写是非常丑陋的,业务代码最好不要加一些奇怪的变量来混淆逻辑。


对于foreach来说,foreach在遍历过程中严禁改变集合的长度,进行对集合的删除或添加等操作。

所以想要优雅的删除元素,就得需要使用到迭代器了

while (iterator.hasNext()) {
    iterator.next();
    iterator.remove();
}

就一句remove就可以了。

源码解析

Iterator

首先介绍下Iterator接口,我们使用的所有迭代器都需要实现这个接口,他只有几个常见的方法

public interface Iterator<E>{
    //判断是否还有下一个元素
    boolean hasNext();
    //遍历到下一个元素
    E next();
    //溢出这个元素
    default void remove();
    //jdk8新增的方法,类似于stream流的foreach执行,对集合中剩余的元素进行操作
    default void forEachRemaining(Consumer<? super E> action)
}

非常简单,使我们可以自己轻松的实现一个简单的迭代器。

所以我们来看下jdk源码里ArrayList里迭代器的实现类。

Itr

ArrayList的内部类

 private class Itr implements Iterator<E> {
        int cursor;       
        int lastRet = -1; 
        int expectedModCount = modCount;
​
        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();
            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
​
    }

简单来说就是通过两个指针cursorlastRet

lastRet在cursor的左面。每次next(),这两个指针都会往右移动一格。当调用remove的时候,实际上还是调用的list的remove()方法,只不过做了一个不感知的操作,让lastRet指针=-1。使再次remove的时候抛出异常。

总结来说,迭代器remove的作用就是使使用者感知不到数组底层删除元素后对数组底层位置造成的变化。

ListIterator和ListItr

其实我平常一直是用普通的迭代器,今天仔细看了源码才知道,ArrayList还有一个ListIterator迭代器。

 private class ListItr extends Itr implements ListIterator<E> {
 
 }

ListIterator接口实现了Iterator的接口,相比之下,增加许多更多样化的接口,例如addsetprevious等等,大体思路差不多我就不细讲了。

Iterable

Iterable接口和Iterator接口有点像,所以千万不要把他俩弄混。。

上文提到的Iterator接口是迭代器接口,这个Iterable接口,表示可迭代。修饰的对象不一样

他是所有集合类的顶级接口,例如Collection就继承了Iterable接口。我们看下这个接口的方法

public interface Iterable<T> {
    //眼熟的名称。jdk1.5时只有这一个方法
    Iterator<T> iterator();
}

其实就是要求每个集合都实现iterator()方法,可以生成迭代器。

我们上文提到的for-each增强其实底层就是通过迭代器去实现的。也就是说,集合只要实现了Iterable接口就可以使用foreach去遍历。


小结一下

 //ArrayList继承Iterable接口,实现iterator()方法,生成迭代器
 Iterator<Integer> iterator = list.iterator();
 //这里的迭代器实际是Itr内部类,实现了Iterator接口

\