Iterator学习记录
1. Java集合类图
图片来源:菜鸟教程
上图中,上图中实线边框的是实现类,折线边框的是抽象类,点线边框的是接口。
从图中我们可以看出,Java集合主要包含了Collection(主要存储元素集合)和Map(主要存储键值对集合)两种类型。
2. Iterator
2.1 Iterator介绍
从上图可以看出,Iterator是整个集合框架的起始点,因此,先学习这个Iterator。
Iterator是Java集合中的迭代器,也是设计模式中的迭代器模式的实现,在Java遍历一个集合,除了使用for循环和增强for循环外,还可以使用迭代器来遍历集合(forEach循环实际也是使用了迭代器来遍历)。
-
Iterator 简单使用
以
ArrayList
为例,展示Iterator的基本使用
private static void test() {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("1");
arrayList.add("2");
arrayList.add("3");
Iterator iterator = arrayList.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
可以看到,Iterator使用中,首先调用iterator()
方法,获取迭代器,然后,循环中使用hasNext()
方法判断是否含有下一个元素,使用next()
方法获取当前的元素。
2.2 Iterator源码
2.2.1 主要方法
Iterator中的主要方法有:
public interface Iterator<E> {
// 判断是否含有下一个元素
boolean hasNext();
// 获取下一个元素
E next();
// 移除元素,主要靠实现类实现
default void remove() {
throw new UnsupportedOperationException("remove");
}
// Java8 新增方法,方便直接遍历
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
在上边的使用中主要使用前两个方法,除了这两个方法外,Iterator中还有一个remove()
方法,主要依赖实现类实现;另外还有一个Java8新增的方法forEachRemaining()
可以直接使用该方法遍历集合,可以看到该方法的默认实现和上边例子中的使用相同。
forEachRemaining
的使用:
private static void test() {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("1");
arrayList.add("2");
arrayList.add("3");
Iterator iterator = arrayList.iterator();
iterator.forEachRemaining(a -> {
System.out.println(a);
});
}
2.2.2 Iterator原理理解
Iterator迭代器的基本模型,可以理解为一个介于结合元素的中间位置的游标。如下图所示,调用hasNext()
方法,会查看游标后边是否有数据,如果有就返回true,调用next()
方法,游标往后移一位,到1和2之间的位置,同时返回游标前的元素。(该游标只能单向移动)
2.2.3 Iterator源码实现
Iterator只是一个接口,具体方法的实现,还是在具体的集合类中,我们以ArrayList
为例,查看其具体实现,ArrayList
中使用了一个内部类Itr
实现了Iterator接口。
- 成员变量
private class Itr implements Iterator<E> {
int cursor; // 游标位置索引,默认为0
int lastRet = -1; // 游标上一次所在位置
// modCount:AbstractList中的成员变量,集合修改的次数;
// expectedModCount: 集合期望修改的次数,默认和modCount相等
int expectedModCount = modCount;
}
-
hasNext()
方法:在上边我们知道该方法,主要判断集合中是否还有元素,也就是判断游标后边是否还有元素,而根据成员变量中的几个元素,猜测可以使用对应的索引来判断,也就是
cursor
是否小于集合的长度,具体实现:
public boolean hasNext() {
return cursor != size;
}
-
next()
方法:在查看代码之前,猜测其实现方式:首先会将游标往后移,也就是
cursor++
, 然后,返回游标前的元素,在ArrayList
中就是游标未修改之前的索引位置元素(LinkedList
中没有实现Iterator接口)。
public E next() {
// 验证 expectedModCount 是否和 modCount相等
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;
// 返回之前索引位置的元素,并且修改lastRet的值
return (E) elementData[lastRet = i];
}
/**
* 验证 expectedModCount 是否和 modCount相等;
* modCount在修改集合(添加元素,删除元素时会修改)
*/
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
根据以上代码,首先需要判断expectedModCount
和modCount
是否相等,如果不相等会抛出异常,也就是如果获取了迭代器之后,又修改了集合,那么使用迭代器的next()
方法会报错;然后,就是常规操作,游标后移,返回之前索引的元素,为lastRet
赋值;
下边验证,获取迭代器之后,修改集合:
private static void test() {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("1");
arrayList.add("2");
arrayList.add("3");
Iterator iterator = arrayList.iterator();
arrayList.add("4");
iterator.forEachRemaining(a -> {
System.out.println(a);
});
}
运行之后,可以看到果然抛出了异常,因此,在使用迭代器时需要注意,不能在获取了迭代器之后仍然修改集合(添加、移除集合中的元素)。
Exception in thread "main" java.util.ConcurrentModificationException...
remove()
方法:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
// 判断 expectedModCount 和 modCount是否相等
checkForComodification();
try {
// 调用ArrayList中的remove方法,删除游标之前位置索引的元素,
// 也就是当前游标的前一个元素。
ArrayList.this.remove(lastRet);
// 游标移回上一次所在位置
cursor = lastRet;
// 游标上一次位置,置为 -1
lastRet = -1;
// 修改 expectedModCount 值,是它和modCount相等
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
根据上边的代码,首先,判断lastRet
是否小于0,然后,还是判断expectedModCount
和modCount
是否相等,然后调用ArrayList
的remove(int index)
方法,但是,删除之后,modCount
会被修改,因此需要让expectedModCount
和它再次相等,防止后边使用迭代器的相关方法报错;
每次移除的是游标上一次所在位置索引的元素,同时判断lastRet
不能小于0,而移除元素之后和最开始获取迭代器时,lastRet
都是-1;也就是说,获取迭代器之后,不能立马移除元素,也不能在使用remove()
方法后,不使用next()
方法移动游标;
同时移除之后,集合ArrayList
中的所有元素会往前移一位,如果游标的位置不修改,就会导致发生遗漏元素;因此,游标需要重新指向之前所在位置。
关于删除方法,还有一个常见问题,就是在循环中移除元素,这个问题,稍后在ArrayList
的学习整理中记录。