迭代

遍历方式

第一种:for 循环。

for (int i = 0; i < list.size(); i++) {
    System.out.print(list.get(i) + ",");
}

第二种:迭代器。

Iterator it = list.iterator();
while (it.hasNext()) {
    System.out.print(it.next() + ",");
}

第三种:for-each。

for (String str : list) {
    System.out.print(str + ",");
}

第一种我们略过,第二种用的是 Iterator,第三种看起来是 for-each,其实背后也是 Iterator,看一下反编译后的代码(如下所示)就明白了。

Iterator var3 = list.iterator();

while(var3.hasNext()) {
    String str = (String)var3.next();
    System.out.print(str + ",");
}

for-each 只不过是个语法糖,让我们开发者在遍历 List 的时候可以写更少的代码,更简洁明了。

Iterator

Iterator 是个接口,用来改进 Enumeration 接口:

public interface Iterator<E> {
    boolean hasNext();  // 判断集合中是否存在下一个对象
    E next();  // 返回集合中的下一个对象,并将访问指针移动一位
    
    // 删除集合中调用next()方法返回的对象
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
}

Iterable

JDK 1.8 时,Iterable 接口中新增了 forEach 方法。该方法接受一个 Consumer 对象作为参数,用于对集合中的每个元素执行指定的操作。该方法的实现方式是使用 for-each 循环遍历集合中的元素,对于每个元素,调用 Consumer 对象的 accept 方法执行指定的操作。

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);  //非空检查
    for (T t : this) {
        action.accept(t);
    }
}

由于 Iterable 接口是 Java 集合框架中所有集合类型的基本接口,因此该方法可以被所有实现了 Iterable 接口的集合类型使用。它对 Iterable 的每个元素执行给定操作,具体指定的操作需要自己写Consumer接口通过accept方法回调出来。

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
list.forEach(integer -> System.out.println(integer));

写得更浅显易懂点,就是:

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
list.forEach(new Consumer<Integer>() {
    @Override
    public void accept(Integer integer) {
        System.out.println(integer);
    }
});

如果我们仔细观察ArrayList 或者 LinkedList 的“户口本”就会发现,并没有直接找到 Iterator 的影子。反而找到了 Iterable!

事实上,List 的关系图谱中并没有直接使用 Iterator,而是使用 Iterable 做了过渡。

public interface Iterable<T> {
    Iterator<T> iterator();
}

正因此,我们可以使用迭代器遍历List:Iterator it = list.iterator();

为什么要套一层?为什么不直接将 Iterator 中的核心方法 hasNext、next 放到 Iterable 接口中呢?

从英文单词的后缀语法上来看,(Iterable)able 表示这个能不能迭代,而 (Iterator)tor 表示如何迭代能不能怎么做显然不能混在一起,否则就乱的一笔。还是各司其职的好。

原则上,只要一个 List 实现了 Iterable 接口,那么它就可以使用 for-each 这种方式来遍历,那具体该怎么遍历,还是要看它自己是怎么实现 Iterator 接口的。

实现方式

对于ArrayList ,它重写了 Iterable 接口的 iterator 方法。返回的对象 Itr 是个内部类,实现了 Iterator 接口,并且按照自己的方式重写了 hasNext、next、remove 等方法。

public Iterator<E> iterator() {
    return new Itr();
}

private class Itr implements Iterator<E> {
    // ...
}

Map 就没办法直接使用 for-each,因为 Map 没有实现 Iterable 接口,只有通过 map.entrySet()map.keySet()map.values() 这种返回一个 Collection 的方式才能 使用 for-each。

LinkedList 并没有直接重写 Iterable 接口的 iterator 方法,而是由它的父类 AbstractSequentialList 来完成。

public Iterator<E> iterator() {
    return listIterator();
}

LinkedList 重写了 listIterator 方法:

public ListIterator<E> listIterator(int index) {
    checkPositionIndex(index);
    return new ListItr(index);
}

这里又套了一层迭代器 ListIterator,它继承了 Iterator 接口,在遍历List 时可以从任意下标开始遍历,而且支持双向遍历。ListIterator 就只支持 List

public interface ListIterator<E> extends Iterator<E> {
    boolean hasNext();
    E next();
    boolean hasPrevious();
    E previous();
}

for-each陷阱

for-each 删除元素报错代码:

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");

for (String str : list) {
	if ("沉默王二".equals(str)) {
		list.remove(str);
	}
}

System.out.println(list);

for-each 本质上是个语法糖,底层是通过迭代器 Iterator 配合 while 循环实现的,来看一下反编译后的字节码。

List<String> list = new ArrayList();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");
Iterator var2 = list.iterator();

while(var2.hasNext()) {
    String str = (String)var2.next();
    if ("沉默王二".equals(str)) {
        list.remove(str);
    }
}

System.out.println(list);

再挖一层ArrayList 的 iterator 方法:

public Iterator<E> iterator() {
    return new Itr();
}

private class Itr implements Iterator<E> {
    int cursor;             // 下一个元素的索引
    int lastRet = -1;       // 上一个返回元素的索引;如果没有则为 -1
    int expectedModCount = modCount; // ArrayList 的修改次数

    Itr() { }  // 构造函数

    public boolean hasNext() { // 判断是否还有下一个元素
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() { // 返回下一个元素
        checkForComodification(); // 检查 ArrayList 是否被修改过
        int i = cursor; // 当前索引
        Object[] elementData = ArrayList.this.elementData; // ArrayList 中的元素数组
        if (i >= elementData.length) // 超出数组范围
            throw new ConcurrentModificationException(); // 抛出异常
        cursor = i + 1; // 更新下一个元素的索引
        return (E) elementData[lastRet = i]; // 返回下一个元素
    }
}

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

new Itr() 的时候 expectedModCount 被赋值为 modCount,而 modCount 是 ArrayList 中的一个计数器,用于记录 ArrayList 对象被修改的次数。ArrayList 的修改操作包括添加、删除、设置元素值等。每次对 ArrayList 进行修改操作时,modCount 的值会自增 1。

list 执行了 3 次 add 方法:

  • add 方法调用 ensureCapacityInternal 方法

  • ensureCapacityInternal 方法调用 ensureExplicitCapacity 方法

  • ensureExplicitCapacity 方法中会执行 modCount++

所以 modCount 的值在经过三次 add 后为 3,于是 new Itr() 后 expectedModCount 的值也为 3(回到前面去看一下 Itr 的源码)。

执行 list.remove(str)时:

  • remove 方法调用 fastRemove 方法

  • fastRemove 方法中会执行 modCount++

modCount 的值变成了 4。

第二次遍历时,会执行 Itr 的 next 方法(String str = (String) var3.next();),next 方法就会调用 checkForComodification 方法。该方法对 modCount 和 expectedModCount 进行了比较,发现两者不等,就抛出了 ConcurrentModificationException 异常。这种机制可以保证迭代器在遍历 ArrayList 时,不会遗漏或重复元素,同时也可以在多线程环境下检测到并发修改问题。

结论:不要在 for-each 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式

正确地删除元素

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");

Iterator<String> itr = list.iterator();
// 区别于Iterator var2 = list.iterator();

while (itr.hasNext()) {
	String str = itr.next();
    // 区别于String str = (String)var2.next();
	if ("沉默王二".equals(str)) {
		itr.remove();
        // 区别于list.remove(str);
	}
}

之前执行 list.remove(str)时:

  • remove 方法调用 fastRemove 方法

  • fastRemove 方法中会执行 modCount++

现在执行itr.remove()时,加了一步,删除完会执行 expectedModCount = modCount,保证了 expectedModCount 与 modCount 的同步。

最后更新于