java 8 个流的内部迭代情况

How Internal iteration looks like for java 8 streams

我试图了解外部迭代器与内部迭代器之间的区别,其中外部迭代器使用迭代器枚举其元素

List<String> alphabets = Arrays.asList(new String[]{"a","b","b","d"});
         
for(String letter: alphabets){
   System.out.println(letter.toUpperCase());
}

以上代码在后台执行如下操作

List<String> alphabets = Arrays.asList(new String[]{"a","b","b","d"});    
Iterator<String> iterator = alphabets.listIterator();
while(iterator.hasNext()){
     System.out.println(iterator.next().toUpperCase());
}

但是对于内部迭代,一切都在后台完成,这对我来说是一个黑盒子,我想深入研究它。

就像下面的代码一样,迭代在后台发生,但究竟发生了什么,与 foreach 循环相比有何不同?

List<String> alphabets = Arrays.asList(new String[]{"a","b","b","d"});
alphabets.stream().forEach(l -> l.toUpperCase());

这是我对外部迭代和内部迭代的理解。如有不妥请指正

内部迭代的全部意义在于可以用不同的方式实现迭代逻辑。

Iterable 的默认实现类似于

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

所以当一个可迭代的,例如集合,不提供 forEach 实现,此继承实现与外部迭代相同。

覆盖此默认实现的原因有多种。例如,Collections.synchronized…(…) 方法之一返回的包装器具有如下所示的实现:

@Override
public void forEach(Consumer<? super E> consumer) {
    synchronized (mutex) {c.forEach(consumer);}
}

它委托原始集合的 forEach 实现,做它做的任何事情,但是在持有互斥量的同时执行整个操作。换句话说,与代码必须关心锁定自身的外部迭代不同,此实现免费提供同步。

另一个有趣的例子是在 Collections.unmodifiable…(…) 包装器中。

@Override
public void forEach(Consumer<? super E> action) {
    c.forEach(action);
}

乍一看优势不明显。但是,正如您在问题中所展示的,外部迭代是使用 Iterator 完成的,即使在使用 for-each 语法时也是如此。 Iterator 有一个 remove 方法,因此每次对不可修改的包装器执行外部迭代时,原始集合的迭代器必须包装在另一个迭代器中,以防止调用者使用 remove().

相比之下,内部迭代不是按合同修改集合。所以包装器可以委托给原始集合的 forEach 方法来做正确的事情。如果目标方法完全使用迭代器,则不需要迭代器周围的包装器。

例如这是 ArrayListforEach 方法:

@Override
public void forEach(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    @SuppressWarnings("unchecked")
    final E[] elementData = (E[]) this.elementData;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        action.accept(elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

因此,它不是基于迭代器的循环,而是遍历索引并直接访问其数组。

请注意,在 Collection 等可迭代对象上调用 forEach 不同于在 Stream 上调用 forEach。流允许链接各种中间操作,影响终端操作开始时将发生的事情。当您不链接中间操作而只是调用 forEach(…) 时,该调用可能会根据流实现的判断执行等同于 collection.spliterator() .forEachRemaining(…) 的操作。

这与在集合上调用 forEach 的代码路径不同,但对于合理的集合实现,这些代码路径的作用基本相同。 比较了 ArrayList 的不同变体,虽然存在细微差别,但迭代(使用数组索引)的基本方法是相同的。