有关ListIterator和Iterator的理解——丑九怪
迭代器存在的意义
- java中容器类有很多,大部分内部结构都不太一样,当我们需要对我们所使用的容器类进行遍历时,就不可避免的需要先知道这个容器的内部结构,才可以的对它进行遍历操作。可是有些容器内部结构复杂,这就给遍历容器带来了困难。所以jdk中提出了迭代器的思想:所有容器都会实现迭代器,在各自容器内部实现遍历过程并对外提供遍历容器的手段。对外方便的同时,也保护了容器的内部结构不被外面知道。这就是我理解的跌迭代器。
有关Iterator
首先先看一下Iterator接口的内容
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
解释Iterator中的方法
- hasNext();方法
这个方法返回boolean,用来判断容器内是否还有下一个元素 - next();方法
返回了一个E,意思是返回容器中的下一个元素 - remove();方法
删除容器中的最后一个元素,但是在接口中的默认处理方式时抛出一个“不支持操作”异常 - forEachRemaining(Consumer<? super E> action);方法
这个方法是jdk1.8新加入的方法,参数是一个Consumer(有关Consumer可以参考我的博文有关1.8中的函数式编程接口——丑九怪),这个方法的作用,就是根据外面在action中所设置的过程(函数),通过对容器的遍历,实现对容器内对应元素的造作。如果还是不太明白,请参考完上面的博文再来研究这个方法。
有关ListIterator
同样先看一下接口内容
public interface ListIterator<E> extends Iterator<E> {
// Query Operations 查询操作
boolean hasNext();
E next();
boolean hasPrevious();
E previous();
int nextIndex();
int previousIndex();
// Modification Operations 修改造作
void remove();
void set(E e);
void add(E e);
}
很明显可以得到两个信息
1、ListIterator接口继承于Iterator接口
2、ListIterator中的操作分为两类,查询操作和修改操作
解析ListIterator中的方法
有关查询的方法
- hasNext();方法
这个方法返回boolean,判断是否还有下一个元素 - next();方法
这个方法返回容器内的下一个元素 - hasPrevious();方法
他和hasNext()方法刚好相反,他检查是否有上一个元素,相当于倒序 - previous();方法
这个方法和next()方法相反,返回容器内的上一个元素 - nextIndex()方法
返回下一个元素的下标 - previousIndex();方法
返回上一个元素的下标
有关修改的方法
- remove();方法
移除容器中的最后一个元素,容器大小发生改变。这个方法覆盖了Iterator中的remove()方法 - set(E e);方法
重新给容器中最后一个元素赋值为e,但是容器的结构以及大小没有发生改变 - add(E e);方法
在容器最后追加一个元素,改变了容器大小
Iterator和ListIterator的异同
通过上面的总结,他们的异同已经明了:
- Iterator和ListIterator都可以对容器进行遍历操作,不同的地方在于ListIterator提供了倒序便利的手段——hasPrevious()方法和previous()方法的组合使用就可以做到倒序遍历
- Iterator本身对容器大小结构的修改关闭,它的remove()方法会抛出异常。可以这么理解,就是不允许改变容器的大小结构。而对于ListIterator来说,他在继承并修改Iterator接口的remove()方法同时,还提供了add(E e)方法,就是说ListIterator允许对容器进行大小结构上的修改
Iterator和ListIterator在源码中的具体实现以及解析
在源码中的迭代器会根据容器的类型有各自的实现方式,这里以ArrayList为例,简要说明一下ArrayList中迭代器的工作原理。由于代码较长,这里只贴出部分较核心内容。
- 在ArrayList中有一个字段modCount,每当调用了ArrayList中能够使容器发生改变的方法,就会将modCount加一
- Iterator接口在ArrayList中以被一个叫做Itr的内部类实现,remove()方法被重写,本质上是调用Arra中的remove()方法。在内部类Itr中,有两个成员非常关键,expectedModCount和lastRet。expectedModCount被赋初值与modCount相等,lastRet赋初值为-1.
- 一般情况下,我们用以下方式遍历
List<Integer> strList = new ArrayList<>();
init(strList); // 这里初始化的代码只是简单地向list中添加元素
Iterator<Integer> intIterator = strList.iterator(); // 取得Iterator
while (intIterator.hasNext()) {
System.out.println(intIterator.next());
}
在此过程中,任何其他线程对此容器内元素的任何操作都是不允许的。这一点体现在next()方法中(事实上,凡是能使容器内容发生改变的方法都可以体现)。在方法调用最开始,会先检查expectedModCount和modCount是否相等。如果存在其他线程对这个容器进行了操作,会相应地修改modCount的值,导致expectedModCount和modCount不相等,这会让方法抛出ConcurrentModificationException异常。这样做保证了迭代器的安全运行。
public E next() {
checkForComodification(); // 这个方法检查expectedModCount和modCount是否相等
...... // 省略的代码
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
- 对于lastRet,一方面他是迭代器中标明当前位置的“指针”,另一方面,我认为他有类似标记的功能。每当执行使容器结构发生改变的方法时,会同时将lastRet改为-1,再次调用这种方法时,方法内部会先判断lastRet是否为-1,如果lastRet的值为-1,代表容器结构发生了改变,无法确定当前所遍历到的元素。这种情况下会抛出IllegalStateException()异常。而next()方法会重新给lastRet赋值为遍历到的最后一个元素的下标,也就是重新校准lastRet的值,这种机制也保证了迭代器的正常运行。remove()方法中有相关代码,这里贴出:
public void remove() {
if (lastRet < 0) // 判断lastRet的值是否为-1
throw new IllegalStateException();
...... // 省略的代码
}
public E next() {
...... // 省略的代码
cursor = i + 1;
return (E) elementData[lastRet = i]; // 这里给lastRet赋值为当前元素下标,相当于重新校准
}
如果没有这个机制,同时外面不停地对集合用迭代器进行删除,迭代器内部所记录的下标数据就会出现混乱
- ListIterator中的实现方式和Iterator中的大同小异,理解了上述说法,自学ListIterator并非难事