JavaRush /Java 博客 /Random-ZH /For 和 For-Each 循环:关于我如何迭代、被迭代、但未被迭代的故事
Viacheslav
第 3 级

For 和 For-Each 循环:关于我如何迭代、被迭代、但未被迭代的故事

已在 Random-ZH 群组中发布

介绍

循环是编程语言的基本结构之一。例如,在Oracle网站上有一个章节“ Lesson: Language Basics ”,其中循环有一个单独的课程“ The for Statement ”。让我们回顾一下基础知识:循环由三个表达式(语句)组成:初始化(初始化)、条件(终止)和增量(增量):
For 和 For-Each 循环:一个关于我如何迭代、迭代但不迭代的故事 - 1
有趣的是,它们都是可选的,这意味着如果我们愿意,我们可以写:
for (;;){
}
确实,在这种情况下我们会得到一个无限循环,因为 我们没有指定退出循环(终止)的条件。在执行整个循环之前,初始化表达式仅执行一次。始终值得记住的是,循环有其自己的范围。这意味着初始化终止增量和循环体看到相同的变量。使用花括号总是可以轻松确定范围。括号内的所有内容在括号外不可见,但括号外的所有内容在括号内可见。 初始化只是一个表达式。例如,您通常可以调用不返回任何内容的方法,而不是初始化变量。或者直接跳过它,在第一个分号之前留一个空格。以下表达式指定终止条件。只要为true,就会执行循环。如果为false,则不会开始新的迭代。如果你看下图,我们在编译过程中会遇到错误,IDE 会抱怨:循环中的表达式无法访问。由于循环中不会有单次迭代,因此我们将立即退出,因为 错误的:
For 和 For-Each 循环:关于我如何迭代、迭代但不迭代的故事 - 2
值得关注终止语句中的表达式:它直接决定您的应用程序是否会出现无限循环。 增量是最简单的表达方式。它在循环的每次成功迭代后执行。并且这个表达式也可以被跳过。例如:
int outerVar = 0;
for (;outerVar < 10;) {
	outerVar += 2;
	System.out.println("Value = " + outerVar);
}
从示例中可以看出,循环的每次迭代我们都会以 2 为增量递增,但前提是该值outerVar小于 10。此外,由于increment 语句中的表达式实际上只是一个表达式,因此可以包含任何东西。因此,没有人禁止使用减量而不是增量,即 减少价值。您应该始终监视增量的写入。+=先执行增加,然后执行赋值,但如果在上面的示例中我们写相反的内容,我们将得到一个无限循环,因为变量outerVar永远不会收到更改的值:在这种情况下,它将在=+赋值之后计算。顺便说一下,视图增量也是一样的++。例如,我们有一个循环:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length; ++i) {
	System.out.println(names[i]);
}
该循环有效并且没有任何问题。但后来重构人来了。他不理解增量,只是这样做了:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length;) {
	System.out.println(names[++i]);
}
如果数值前面出现增量符号,则表示它会先增加,然后返回到所指示的位置。在此示例中,我们将立即开始从数组中提取索引 1 处的元素,跳过第一个元素。然后在索引 3 处我们将崩溃并出现错误“ java.lang.ArrayIndexOutOfBoundsException ”。正如您可能已经猜到的,这之前之所以有效,只是因为增量是在迭代完成后调用的。当将此表达式转移到迭代时,一切都崩溃了。事实证明,即使在一个简单的循环中,你也会弄得一团糟)如果你有一个数组,也许有一种更简单的方法来显示所有元素?
For 和 For-Each 循环:关于我如何迭代、迭代但不迭代的故事 - 3

对于每个循环

从 Java 1.5 开始,Java 开发人员for each loop在 Oracle 站点的指南中为我们提供了一种名为“ The For-Each Loop ”或版本1.5.0的设计。一般来说,它看起来像这样:
For 和 For-Each 循环:关于我如何迭代、迭代但不迭代的故事 - 4
您可以阅读 Java 语言规范 (JLS) 中对此构造的描述,以确保它并不神奇。这种结构在“ 14.14.2. 增强的 for 语句”一章中进行了描述。正如您所看到的,foreach循环可以与数组和实现java.lang.Iterable接口的数组一起使用。也就是说,如果您确实需要,您可以实现java.lang.Iterable接口,并且foreach 循环可以与您的类一起使用。你会立即说:“好吧,它是一个可迭代对象,但数组不是一个对象。算是吧。” 你会错的,因为...... 在Java中,数组是动态创建的对象。语言规范告诉我们:“在 Java 编程语言中,数组是对象。” 一般来说,数组有点 JVM 的魔力,因为…… 该数组的内部结构是未知的,并且位于 Java 虚拟机内部的某个位置。有兴趣的可以阅读stackoverflow上的答案:“ How does array class work in Java? ” 事实证明,如果我们不使用数组,那么我们必须使用实现Iterable 的东西。例如:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (String name : names) {
	System.out.println("Name = " + name);
}
在这里你可以记住,如果我们使用集合(java.util.Collection),多亏了这一点,我们得到了精确的Iterable。如果一个对象有一个实现 Iterable 的类,那么当调用迭代器方法时,它有义务提供一个迭代器来迭代该对象的内容。例如,上面的代码将具有类似这样的字节码(在 IntelliJ Idea 中,您可以执行“查看”->“显示字节码”:
For 和 For-Each 循环:一个关于我如何迭代、迭代但不迭代的故事 - 5
正如你所看到的,实际上使用了迭代器。如果不是foreach 循环,我们就必须编写如下内容:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (Iterator i = names.iterator(); /* continue if */ i.hasNext(); /* skip increment */) {
	String name = (String) i.next();
	System.out.println("Name = " + name);
}

迭代器

正如我们在上面看到的,Iterable接口表示,对于某个对象的实例,您可以获得一个迭代器,用它可以迭代内容。同样,这可以说是SOLID的单一职责原则。数据结构本身不应该驱动遍历,但它可以提供应该驱动的遍历。Iterator 的基本实现是,它通常被声明为一个内部类,可以访问外部类的内容并提供外部类中包含的所需元素。ArrayList以下是迭代器如何返回元素的 类的示例:
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];
}
正如我们所看到的,在迭代器的帮助下ArrayList.this访问外部类及其变量elementData,然后从那里返回一个元素。因此,获取迭代器非常简单:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Iterator<String> iterator = names.iterator();
它的工作归结为这样一个事实:我们可以检查是否还有元素(hasNext方法),获取下一个元素(next方法)和remove方法,该方法删除通过next接收到的最后一个元素。删除方法是可选的,不保证一定会实现。事实上,随着 Java 的发展,接口也在发展。因此,在Java 8中,还出现了一种方法forEachRemaining,允许你对迭代器未访问到的剩余元素执行一些操作。迭代器和集合有什么有趣的地方?例如,有一个类AbstractList。这是一个抽象类,是ArrayListand的父类LinkedList。我们对modCount这样的字段很感兴趣。每次更改都会更改列表的内容。那么这对我们来说有什么关系呢?事实上,迭代器确保在操作期间迭代它的集合不会改变。如您所知,列表迭代器的实现与modcount位于同一位置,即在类中AbstractList。让我们看一个简单的例子:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
names.add("modcount++");
System.out.println(iterator.next());
这是第一个有趣的事情,虽然不是主题。实际上Arrays.asList返回它自己的特殊的一个ArrayListjava.util.Arrays.ArrayList)。它没有实现添加方法,因此它是不可修改的。JavaDoc 中对此进行了描述:固定大小。但事实上,它不仅仅是固定大小的。它也是不可变的,即不可改变的;删除也不起作用。我们也会得到一个错误,因为...... 创建迭代器后,我们记住了其中的modcount。然后我们“从外部”(即不通过迭代器)更改集合的状态并执行迭代器方法。因此,我们得到错误:java.util.ConcurrentModificationException。为了避免这种情况,迭代期间的更改必须通过迭代器本身执行,而不是通过访问集合来执行:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next();
iterator.remove();
System.out.println(iterator.next());
如您所知,如果iterator.remove()您以前不这样做iterator.next(),那么是因为。迭代器没有指向任何元素,那么我们会得到一个错误。在示例中,迭代器将转到John元素,将其删除,然后获取Sara元素。这里一切都会好起来,但运气不好,再次存在“细微差别”)java.util.ConcurrentModificationExceptionhasNext()仅当它返回true时才会发生。也就是说,如果通过集合本身删除最后一个元素,迭代器不会掉落。有关更多详细信息,最好查看“ #ITsubbotnik JAVA 部分:Java 谜题”中有关 Java 谜题的报告。我们开始如此详细的对话的原因很简单,当for each loop...... 我们最喜欢的迭代器是在底层使用的。所有这些细微差别也适用于此。唯一的问题是,我们将无法访问迭代器,并且无法安全地删除元素。顺便说一下,正如您所理解的,状态在创建迭代器时被记住。并且安全删除仅在调用它的地方起作用。也就是说,这个选项将不起作用:
Iterator<String> iterator1 = names.iterator();
Iterator<String> iterator2 = names.iterator();
iterator1.next();
iterator1.remove();
System.out.println(iterator2.next());
因为对于iterator2来说,通过iterator1的删除是“外部的”,也就是说,它是在外部的某个地方执行的,而他对此一无所知。关于迭代器的话题,我也想指出这一点。专门为接口实现制作了一个特殊的扩展迭代器List。他们给他起了名字ListIterator。它不仅允许您向前移动,还可以向后移动,并且还允许您找出上一个元素和下一个元素的索引。此外,它还允许您替换当前元素或在当前迭代器位置和下一个迭代器位置之间的位置插入新元素。正如您所猜测的,由于实现了索引访问, ListIterator所以允许这样做。List
For 和 For-Each 循环:关于我如何迭代、迭代但不迭代的故事 - 6

Java 8 和迭代

Java 8 的发布让许多人的生活变得更加轻松。我们也没有忽略对象内容的迭代。要理解它是如何工作的,您需要对此说几句话。Java 8 引入了java.util.function.Consumer类。这是一个例子:
Consumer consumer = new Consumer() {
	@Override
	public void accept(Object o) {
		System.out.println(o);
	}
};
Consumer是一个函数式接口,这意味着接口内部只有 1 个未实现的抽象方法,需要在指定该接口的实现的那些类中强制实现。这让你可以使用 lambda 这样神奇的东西。本文不是要讨论这个,但我们需要了解为什么我们可以使用它。因此,使用 lambdas,上面的Consumer可以这样重写: Consumer consumer = (obj) -> System.out.println(obj); 这意味着 Java 看到一个名为 obj 的东西将被传递到输入,然后会针对这个 obj 执行 -> 之后的表达式。至于迭代,现在我们可以这样做:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Consumer consumer = (obj) -> System.out.println(obj);
names.forEach(consumer);
如果你去看看这个方法forEach,你会发现一切都非常简单。这是我们最喜欢的for-each loop
default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
}
还可以使用迭代器漂亮地删除元素,例如:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Predicate predicate = (obj) -> obj.equals("John");
names.removeIf(predicate);
在本例中,removeIf方法采用的输入不是Consumer,而是Predicate。它返回布尔值。在这种情况下,如果谓词说“ true ”,那么该元素将被删除。有趣的是,这里并不是一切都是显而易见的))那么,你想要什么?会议上需要给人们创造谜题的空间。例如,我们使用以下代码来删除迭代器在迭代后可以到达的所有内容:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next(); // Курсор на John
while (iterator.hasNext()) {
    iterator.next(); // Следующий элемент
    iterator.remove(); // Удалor его
}
System.out.println(names);
好的,这里一切正常。但我们毕竟记得 Java 8。因此,我们尝试简化一下代码:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next(); // Курсор на John
iterator.forEachRemaining(obj -> iterator.remove());
System.out.println(names);
真的变得更美了吗?但是,将会出现java.lang.IllegalStateException。原因是...Java 中的一个错误。事实证明它是固定的,但是在 JDK 9 中。这里是 OpenJDK 中任务的链接:Iterator.forEachRemaining vs. 迭代器.删除。当然,这已经讨论过:Why iterator.forEachRemaining does not remove element in Consumer lambda? 那么,另一种方法是直接通过 Stream API:
List<String> names = new ArrayList(Arrays.asList("John", "Sara", "Jack"));
Stream<String> stream = names.stream();
stream.forEach(obj -> System.out.println(obj));

结论

正如我们从上面的所有材料中看到的,循环for-each loop只是迭代器之上的“语法糖”。不过,现在很多地方都在使用它。此外,任何产品都必须谨慎使用。例如,一个无害的人forEachRemaining可能隐藏着令人不快的惊喜。而这也再次证明了单元测试的必要性。良好的测试可以识别代码中的此类用例。您可以观看/阅读有关该主题的内容: #维亚切斯拉夫
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION