Skip to content

Files

Latest commit

c4e46db · Oct 12, 2021

History

History
1389 lines (983 loc) · 40.1 KB

File metadata and controls

1389 lines (983 loc) · 40.1 KB

五、循环结构

经常需要一次又一次地重复一系列动作。例如,我们可能希望显示存储在数组中的组织中员工的信息。数组的每个元素都可能包含对Employee对象的引用。对对象方法的调用将放置在循环构造中。

在 Java 中,有四种循环构造可用:

  • 声明

  • 对于每个语句

  • While statement

    Dwhile 语句

此外,在循环中使用 break 和 continue 语句来控制循环的行为。break 语句用于过早退出或短路回路,并在break 语句一节中讨论。正如我们在第 3 章切换语句部分中所观察到的,决策构造了,在切换语句中也使用了中断。continue 语句用于绕过循环中的语句并继续执行循环。在继续陈述一节中介绍。我们还将研究 Java 中标签的使用,尽管它们应该少用。

循环体根据循环结构进行特定次数的迭代。迭代是描述这种执行的常用术语。

循环使用控制信息来确定循环体将执行多少次。对于大多数循环,都有一组初始值、一组要在主体末尾执行的操作以及一个将停止循环执行的终端条件。并不是所有的循环都包含所有这些部分,因为其中一些部分要么缺失,要么隐含。终止条件几乎总是存在的,因为这是终止循环迭代所必需的。如果缺少终端条件,将创建无限循环。

无限循环是指那些不使用语句(如 break 语句)就永远不会终止的循环。尽管名称不同,无限循环不会无限期地执行,因为它们总是在某个点终止。在不方便或难以提供循环终止条件作为基本循环构造的一部分的情况下,它们非常有用。

我们还将介绍嵌套循环的使用以及与循环相关的各种陷阱。本节还介绍了编程逻辑的开发,以帮助提供创建程序逻辑的方法。

对于声明

for 语句用于已知需要执行循环的次数时。for 循环有两种变体。第一种是在本节中讨论的传统形式。for-each 语句是第二种形式,是在 Java5 中引入的。在中针对每个报表部分进行了讨论。

for 声明由以下三部分组成:

  • 初始操作
  • 终端条件
  • 结束循环操作

for 循环的一般形式如下所示:

for (<initial-expression>;<terminal-expression>;<end-loopoperation>)
  //statements;

for 循环的主体通常是块语句。初始操作发生在循环的第一次迭代之前,只执行一次。结束循环操作在每次循环执行结束时进行。终端条件确定循环何时终止,是一个逻辑表达式。它在循环的每次重复开始时执行。因此,如果第一次评估终端条件时,其评估为 false,则 for 循环的主体可以执行零次。

变量通常用作初始操作、终端条件和结束回路操作的一部分。变量要么声明为循环的一部分,要么声明为循环的外部变量。下面的代码片段是将变量i声明为循环的一部分的示例。for statement and scope部分介绍了使用外部变量的示例:

for (int i = 1; i <= 10; i++) {
   System.out.print(i + "  ");
}
System.out.println();

在本例中,我们在循环体中使用了一条语句。变量i被分配一个初始值 1,并且每次循环执行时递增 1。循环执行 10 次,产生 1 行输出。i++是一种更简洁的表述i = i + 1。输出应如下所示:

1  2  3  4  5  6  7  8  9  10 

以下示例使用 for 语句计算从164的整数的平方:

for (int i = 1; i <= 64; i++) {
  System.out.println (i + " squared is = " + i * i);
  }

输出的部分列表如下所示:

1 平方等于 1

2 平方等于 4

3 平方等于 9

4 平方等于 16

。。。

循环变量的初始值可以是任何值。此外,结束循环操作可以根据需要减少或修改变量。在下一个示例中,从101显示数字:

for (int i = 10; i > 0; i--) {
   System.out.print(i + "  ");
}
System.out.println();

此序列的输出如下所示:

10  9  8  7  6  5  4  3  2  1 

一个常见的操作是计算累积和,如下面的代码序列所示。这个例子在计时就是一切一节中有更详细的讨论:

int sum = 0;
for(i = 1; i <= 10; i++) {
  sum += i;
}
System.out.println(sum);

sum的值应为55

逗号运算符

逗号运算符可以用作 for 语句的一部分,用于添加其他变量以在循环中使用和/或控制循环。它用于分隔 for 循环的初始表达式部分和结束循环操作部分。逗号运算符的用法如下所示:

for(int i = 0, j = 10; j > 5; i++, j--) {
   System.out.printf("%3d  %3d%n",i , j);
}

注意在printf语句中使用了%n格式说明符。这指定应生成新行字符。此外,这种新的行分隔符是特定于平台的,使应用更具可移植性。

执行时,此代码序列将产生以下输出:

0   10
1    9
2    8
3    7
4    6

为循环声明了两个变量,ij。变量i被初始化为0,而j被初始化为10。在循环结束时,i增加1,而j减少1。只要j大于5就执行循环。

我们本可以使用更复杂的终端条件,如以下代码片段所示:

for(int i = 0, j = 10; j > 5 && i < 3; i++, j--) {
   System.out.printf("%3d  %3d%n",i , j);
}

在本例中,循环将在第三次迭代后终止,产生以下输出:

  0   10
  1    9
  2    8

单独声明变量是非法的,如下所示:

for(int i = 0, int j = 10; j > 5; i++, j--) {

将生成一个语法错误,如下所示。只提供消息的第一部分,因为它很长。这也说明了 Java 和大多数其他编程语言生成的错误消息的神秘性质:

<identifier> expected
'.class' expected
...

声明和范围的定义

for 语句使用的索引变量可以具有不同的作用域,具体取决于其声明方式。我们可以使用它来控制循环的执行,然后根据需要在循环外部使用变量。下面重复 for 循环的第一个示例。在此代码序列中,i变量的范围仅限于 for 循环的主体:

for (int i = 1; i <= 10; i++) {
   System.out.println(i);
}
System.out.println();

另一种方法将i声明为循环外部,如下所示:

int i;
for (i = 1; i <= 10; i++) {
  System.out.print(i + " ");
}
System.out.println();

这两个 for 循环是等效的,因为它们都在一行上显示数字 1 到 10。它们在i变量的范围上有所不同。在第一个示例中,范围仅限于循环体。尝试在循环之外使用变量(如以下代码所示)将导致语法错误:

for (int i = 1; i <= 10; i++) {
   System.out.println(i);
}
System.out.println(i);

错误消息如下:

cannot find symbol
 symbol:   variable i

在第二个示例中,循环终止后,变量将保留其值,并可供后续使用。以下示例说明了这一点:

int i;
for (i = 1; i <= 10; i++) {
  System.out.print(i + "  ");
}
System.out.println();
System.out.println(i);

此序列的输出如下所示:

1  2  3  4  5  6  7  8  9  10 
11

范围将在第 2 章Java 数据类型及其用法中的范围和生命周期部分进行更详细的讨论。

用于回路变化的

for 循环可能有一个由多个语句组成的主体。必须记住,for 循环体由一条语句组成。下面演示了在循环中使用多个语句。该循环将读取一系列数字,并每行打印一个。它将继续,直到读入负值,然后退出循环。java.util.Scanner类用于从输入源读入数据。在这种情况下,它使用System.in指定键盘作为其输入源:

Scanner scanner = new Scanner(System.in);
int number = 0;

for (int i = 0; number >= 0; i++) {
   System.out.print("Enter a number: ");
   number = scanner.nextInt();
   System.out.printf("%d%n", number);
}

执行此代码序列的一个可能输出如下:

Enter a number: 3
3
Enter a number: 56
56
Enter a number: -5
-5

不需要初始操作、终端条件或结束循环操作。例如,在退出循环时,以下语句将使用分配给i的值5执行i++语句 5 次:

int i = 0;
for (;i<5;) {
   i++;
}

在以下示例中,循环体将永远执行,从而创建一个无限循环:

int i = 0;
for (;;i++)
   ;

以下 for 循环也是如此:

int i = 0;
for(;;) 
   ;

这被称为一个无限循环,而将在无限循环一节中详细介绍。

当您知道循环将执行多少次时,通常使用 for 循环。控制整数变量通常用作数组的索引或用于循环体中的计算目的。

每个语句的

for each 语句是随着 Java5 的发布而引入的。它有时被称为增强型 for 循环。使用 for each 语句的优点包括:

  • 不需要为计数器变量提供结束条件
  • 它更简单,可读性更强
  • 该语句为编译器优化提供了机会
  • 泛型的使用被简化了

for-each 语句与集合和数组一起使用。它提供了一种更简单的方法来迭代实现了java.util.Iterable接口的数组或类的每个成员。由于Iterable接口是java.util.Collection接口的超级接口,for each 语句可以与实现Collection接口的类一起使用。

此语句的语法与常规 for 语句相似,只是括号中的内容不同。内容包括数据类型,后跟变量、冒号,然后是数组名或集合,如下图所示:

for (<dataType variable>:<collection/array>)
   //statements;

它在集合中的用法在中有说明,使用带有列表的 for-each 语句。在以下序列中,声明、初始化一个整数数组,并使用 for-each 语句显示数组的每个元素:

int numbers[] = new int[10];

for (int i = 0; i < 10; i++) {
   numbers[i] = i;
}

for (int element : numbers) {
   System.out.print(element + "  ");
}    
System.out.println();

数字数组的元素被初始化为它们的索引。请注意,使用了 for 语句。这是因为我们无法在 for-each 语句中直接访问索引变量。前面代码段中的 for-each 语句被读取为表示数字中的每个元素。在循环的每次迭代中,element对应于数组的一个元素。它从第一个元素开始,以最后一个元素结束。该序列的输出如下所示:

**```java 0 1 2 3 4 5 6 7 8 9


对于带有数组的 for each 语句,使用有缺点。不可能执行以下操作:

*   修改数组或列表中的当前位置
*   直接迭代多个数组或集合

例如,使用前面的示例,如果我们尝试使用以下代码修改包含 5 的数组元素,则不会导致语法错误。但它也不会修改相应的数组元素:

```java
for (int element : numbers) {
   if (element == 5) {
      element = -5;
   }
}

for (int element : numbers) {
   System.out.print(element + "  ");
}
System.out.println();

该序列的输出如下所示:

0 1 2 3 4 5 6 7 8 9

如果要使用一个循环访问两个不同的数组,则不能使用 for-each 循环。例如,如果要将一个数组复制到另一个数组,则需要使用 For 循环,如下所示:

int source[] = new int[5];
int destination[] = new int[5];

for(int number : source) {
   number = 100;
}

for(int i = 0; i < 5; i++) {
   destination[i] = source[i];
}

虽然我们使用 for each 来初始化源数组,但一次只能寻址单个数组。因此,在第二个循环中,我们被迫使用 for 语句。

使用 for each 语句和列表

我们将从开始,用ArrayList说明 for-each 语句的用法。ArrayList类实现了List接口,扩展了Collection接口。接口的使用和声明在第 6 章类、构造器和方法中有更详细的说明。由于 for-each 语句可以用于实现Collection接口的类,因此我们也可以将其用于ArrayList类。在下一节中,我们将创建自己的Iterable类:

ArrayList<String> list = new ArrayList<String>();

list.add("Lions and");
list.add("tigers and");
list.add("bears.");
list.add("Oh My!");

for(String word : list) {
   System.out.print(word + "  ");
}
System.out.println();

正如您可能预测的那样,输出如下所示:

Lions and tigers and bears. Oh My!

在本例中,for-each 的用法与其在数组中的用法没有太大区别。我们只是使用了ArrayList的名称,而不是数组名称。

将 for-each 语句与列表一起使用具有与前面使用数组时类似的限制:

  • 在遍历列表时,可能无法从列表中删除元素
  • 无法修改列表中的当前位置
  • 无法迭代多个集合

remove方法可以抛出UnsupportedOperationException异常。这是可能的,因为Iteratable接口的Iterator的实现可能没有实现remove方法。这将在下一节中详细阐述。

ArrayList的情况下,我们可以删除一个元素,如下代码片段所示:

for(String word : list) {
   if(word.equals("bears.")) {
      list.remove(word);
      System.out.println(word + " removed");
   }
}

for(String word : list) {
   System.out.print(word + "  ");
}
System.out.println();

for-each 语句用于遍历列表。当bears.字符串被发现时,它被移除。前面序列的输出如下所示:

Lions and tigers and bears. Oh My! 
bears. removed
Lions and tigers and Oh My!

我们无法从 for each 语句中修改列表。例如,下面的代码序列试图修改word并向list添加一个字符串。该列表将不受影响:

for(String word : list) {
   if(word.equals("bears.")) {
      word = "kitty cats";
      list.add("kitty cats");
   }
}

虽然尝试修改word变量没有任何作用,但它不会生成异常。add方法并非如此。在前面的 for-each 语句中使用时,它将生成一个java.util.ConcurrentModificationException异常。

与数组一样,不可能使用 for-each 语句一次迭代多个集合。由于 for-each 语句只支持一个引用变量,因此一次只能访问一个列表。

如果需要从列表中删除元素,请使用迭代器而不是 for-each 语句。

实现迭代器接口

如前所述,任何实现Iterable接口的类都可以与 for-each 语句一起使用。为了说明这一点,我们将创建两个类:

  • MyIterator:这个实现了Iterator接口,支持一个简单的迭代
  • MyIterable:此使用MyIterator来支持在 for-each 语句中使用它

首先,让我们检查下面的MyIterator类。该类将遍历数字 1 到 10。它通过比较value变量与 10 的上限,并在其hasNext方法中返回truefalse来实现这一点。next方法只是返回并增加当前值。不支持remove方法:

import java.util.Iterator;

public class MyIterator implements Iterator<Integer> {
   private int value;
   private final int size;

   public MyIterator() {
      value = 1;
      size = 10;
   }

   @Override
   public boolean hasNext() {
      return value<=size;
   }

   @Override
   public Integer next() {
      return value++;
   }

   @Override
   public void remove() {
      throw new UnsupportedOperationException(
            "Not supported yet.");
   }
}

MyIterable类实现Iterable接口。该接口由一个方法iterator组成。在这个类中,它使用MyIterator类的一个实例来提供一个Iterator对象:

import java.util.Iterator;

public class MyIterable implements Iterable<Integer> {
   private MyIterator iterator;

   public MyIterable() {
      iterator = new MyIterator();
   }

   @Override
   public Iterator<Integer> iterator() {
      return iterator;
   }
}

我们可以使用以下代码序列测试这些类:

MyIterable iterable = new MyIterable();

for(Integer number : iterable) {
   System.out.print(number + "  ");
}
System.out.println();

输出将显示从 1 到 10 的数字,如下所示:

1 2 3 4 5 6 7 8 9 10

并不总是需要使用Iterator方法对集合进行迭代。在许多情况下,for-each 语句提供了一种更加方便和简单的技术。

针对每种语句的使用问题

在为每个语句处理时,有几个问题需要注意:

  • 如果数组/集合为 null,则会出现 null 指针异常
  • 它适用于参数数量可变的方法

空值

如果数组/集合为空,则会出现空指针异常。考虑下面的例子。我们创建了一个字符串数组,但未能初始化第三个元素:

String names[] = new String[5];
names[0] = "Will Turner";
names[1] = "Captain Jack Sparrow";
names[3] = "Barbossa";
names[4] = "Elizabeth Swann";

我们可以使用 for-each 语句显示名称,如下所示:

for(String name : names) {
   System.out.println(name);
}

如下图所示,对于缺失的条目,输出将显示null。这是因为println方法检查其参数是否为空值,如果为空值,则打印null

Will Turner
Captain Jack Sparrow
null
Barbossa
Elizabeth Swann

但是,如果我们对下面的名称应用toString方法,我们将在第三个元素上得到java.lang.NullPointerException

for(String name : names) {
   System.out.println(name.toString());
}

这是经过验证的,如以下输出所示:

Will Turner
Captain Jack Sparrow
java.lang.NullPointerException

参数数量可变

for-each 语句在使用可变数量参数的方法中运行良好。在第 6 章类、构造器和方法中的变量数量部分,可以找到使用变量数量的方法的更详细说明。

在下面的方法中,我们传递数量可变的整数参数。接下来,我们计算这些整数的累积和并返回总和:

public int total(int ... array) {
   int sum = 0;
   for(int number : array) {
      sum+=number;
   }
   return sum;
}

当使用以下调用执行此操作时,我们将得到150作为输出:

result = total(1,2,3,4,5);
result = total();

但是,我们需要小心不要传递null值,因为这将导致java.lang.NullPointerException,如以下代码片段所示:

result = total(null);

尽可能使用 for each 循环,而不是 for 循环。** **# while 语句

while 语句提供了重复执行语句块的另一种方式。当块的执行次数未知时,经常使用。它的一般形式包括while关键字,后跟一组括号,其中包含一个逻辑表达式,然后是一个语句。只要逻辑表达式的计算结果为 true,循环体就会执行:

while (<boolean-expression>) <statements>;

一个简单的示例复制了第一个 for 循环示例,其中我们在一行上显示数字 1 到 10:

int i = 1;
while(i <= 10) {
   System.out.print(i++ + "  ");
}
System.out.println();

结果如下:

1 2 3 4 5 6 7 8 9 10

下面的示例有点复杂,它计算number变量的因子:

int number;
int divisor = 1;
Scanner scanner = new Scanner(System.in);
System.out.print("Enter a number: ");
number = scanner.nextInt();
while (number >= divisor) {
   if ((number % divisor) == 0) {
      System.out.printf("%d%n", divisor);
   }
   divisor++;
}

当输入为6执行时,我们得到以下输出:

Enter a number: 6
1
2
3
6

下表说明了语句序列的操作:

|

迭代计数

|

除数

|

数字

|

输出

| | --- | --- | --- | --- | | 1. | 1. | 6. | 1. | | 2. | 2. | 6. | 2. | | 3. | 3. | 6. | 3. | | 4. | 4. | 6. |   | | 5. | 5. | 6. |   | | 6. | 6. | 6. | 6. |

在下面的示例中,当用户键入负数时,循环将终止。在此过程中,它计算输入的数字的累积和:

int number;
System.out.print("Enter a number: ");
number = scanner.nextInt();
while (number > 0) {
   sum += number;
   System.out.print("Enter a number: ");
   number = scanner.nextInt();
}
System.out.println("The sum is " + sum);

注意这个示例是如何复制提示用户输入数字所需的代码的。如下一节所述,可以使用 do while 语句更优雅地处理该问题。以下输出说明了此代码对一系列数字的执行:

Enter a number: 8
Enter a number: 12
Enter a number: 4
Enter a number: -5
The sum is 24

while 语句对于所需循环迭代次数未知的循环非常有用。while 循环的主体将一直执行,直到循环表达式变为 false。当终端条件相当复杂时,它也很有用。

while 语句的一个重要特性是在循环开始时对表达式求值。因此,如果逻辑表达式的第一次计算结果为 false,则可能永远不会执行循环体。

do while 语句

do while 语句与 while 循环类似,只是循环的主体始终至少执行一次。它由以下内容组成:do关键字后跟一条语句,while关键字,然后是一个用括号括起来的逻辑表达式:

do <statement> while (<boolean-expression>);

通常,do while 循环的主体(由语句表示)是块语句。下面的代码片段演示了 do 语句的用法。这是对上一节中使用的等效 while 循环的改进,因为它避免了在循环开始之前提示输入数字:

int sum = 0;
int number;
Scanner scanner = new Scanner(System.in);
do {
   System.out.print("Enter a number: ");
   number = scanner.nextInt();
   if(number > 0 ) {
     sum += number;
   }
} while (number > 0);
System.out.println("The sum is " + sum);

执行时,您应该获得类似以下内容的输出:

Enter a number: 8
Enter a number: 12
Enter a number: 4
Enter a number: -5
The sum is 24

do while 语句与 while 语句不同,因为表达式的计算发生在循环的末尾。这意味着此语句将至少执行一次。

此语句的使用频率不如 for 或 while 语句,但在循环底部的测试最好的情况下非常有用。下一个语句序列将确定整数中的位数:

int numOfDigits;
System.out.print("Enter a number: ");
Scanner scanner = new Scanner(System.in);
int number = scanner.nextInt();
numOfDigits = 0;
do {
   number /= 10;
   numOfDigits++;
} while (number != 0);
System.out.printf("Number of digits: %d%n", numOfDigits);

此序列的输出如下所示:

Enter a number: 452
Number of digits: 3

数值452的结果如下表所示:

|

迭代计数

|

数字

|

数字

| | --- | --- | --- | | 0 | 452 | 0 | | 1. | 45 | 1. | | 2. | 4. | 2. | | 3. | 0 | 3. |

中断声明

break 语句的作用是终止当前循环,无论是一段时间、for、for each 还是 do while 语句。它也用于 switch 语句中。break 语句将控制传递给循环后面的下一个语句。break 语句由break关键字组成。

考虑以下语句序列的效果,该语句重复地提示用户在无限循环中执行命令。当用户输入Quit命令时,循环终止:

String command;
while (true) {
   System.out.print("Enter a command: ");
   Scanner scanner = new Scanner(System.in);
   command = scanner.next();
   if ("Add".equals(command)) {
      // Process Add command
   } else if ("Subtract".equals(command)) {
      // Process Subtract command
   } else if ("Quit".equals(command)) {
      break;
   } else {
      System.out.println("Invalid Command");
   }
}

注意equals方法是如何使用的。equals方法是针对字符串文字执行的,该命令用作其参数。这种方法避免了在命令包含空值时产生的NullPointerException。由于字符串文本从不为 null,因此永远不会发生此异常。

continue 语句

continue 语句用于将控制从循环内部转移到循环的末尾,但不像 break 语句那样退出循环。continue 语句由关键字continue组成。

执行时,它强制对循环的逻辑表达式求值。在以下语句序列中:

while (i < j) {
   …
   if (i < 0) {
      continue;
   }
   …
}

如果i小于0,它将绕过回路主体的其余部分。如果循环条件i<j的计算结果不为 false,则将执行循环的下一次迭代。

continue 语句通常用于消除通常需要的嵌套级别。如果未使用 continue 语句,则前面的示例如下所示:

while (i < j) {
   …
   if (i < 0) {
      // Do nothing
   } else {
      …
   }
}

嵌套循环

循环可以相互嵌套。允许 for、for each、while 或 do while 循环的任何嵌套组合。这有助于解决许多问题。下面的示例计算二维数组中一行的元素之和。它首先将每个元素初始化为其索引的总和。然后显示阵列。接下来是嵌套循环,用于计算和显示每行元素的总和:

final int numberOfRows = 2;
final int numberOfColumns = 3;
int matrix[][] = new int[numberOfRows][numberOfColumns];

for (int i = 0; i < matrix.length; i++) {
   for (int j = 0; j < matrix[i].length; j++) {
      matrix[i][j] = i + j;
   }
}

for (int i = 0; i < matrix.length; i++) {
   for(int element : matrix[i]) {
      System.out.print(element + "  ");
   }
   System.out.println();
}  

for (int i = 0; i < matrix.length; i++) {
   int sum = 0;
   for(int element : matrix[i]) {
      sum += element;
   }
   System.out.println("Sum of row " + i + " is " +sum);
}

注意使用length方法来控制循环的执行次数。如果数组的大小发生变化,这将使代码更易于维护。执行时,我们得到以下输出:

0 1 2 
1 2 3 
Sum of row 0 is 3
Sum of row 1 is 6

请注意,在显示数组并计算行和时,使用 for-each 语句。这简化了计算。

break 和 continue 语句也可以在嵌套循环中使用。但是,它们将仅与当前回路一起使用。也就是说,内部循环的中断只会中断内部循环,而不会中断外部循环。正如我们将在下一节中看到的,我们可以使用标签将外部循环从内部循环中分离出来。

在下面对最后一个嵌套循环序列的修改中,当总和超过 2 时,我们将中断内部循环:

for (int i = 0; i < matrix.length; i++) {
   int sum = 0;
   for(int element : matrix[i]) {
      sum += element;
      if(sum > 2) {
         break;
      }
   }
   System.out.println("Sum of row " + i + " is " +sum);
}

执行此嵌套循环将更改最后一行的总和,如下所示:

Sum of row 0 is 3
Sum of row 1 is 3

break 语句将我们带出了内部循环,但没有带出外部循环。如果在外循环的直接体中有一个相应的 break 语句,我们就可以跳出外循环。continue 语句的行为方式与内部循环和外部循环类似。

使用标签

标签是程序中位置的名称。它们可用于改变控制的流量,应谨慎使用。在前面的示例中,我们无法使用 break 语句中断最内部的循环。然而,标签可以用来将我们从多个循环中分离出来。

在下面的示例中,我们将在外循环前面放置一个标签。在内部循环中,当 i大于 0 时,我们执行 break 语句,在计算第一行的和之后,有效地终止外部循环。标签由名称和冒号组成:

outerLoop:
for(int i = 0; i < 2; i++) {
   int sum = 0;
   for(int element : matrix[i]) {
      sum += element;
      if(i > 0) {
         break outerLoop;
      }
   }
   System.out.println("Sum of row " + i + " is " +sum);
}

该序列的输出如下所示:

Sum of row 0 is 3

我们也可以使用带有标签的 continue 语句来获得类似的效果。

应避免使用标签,因为它们可能导致代码无法阅读且难以维护。

无限循环

无限循环将永远执行,除非使用诸如 break 语句之类的语句来强制终止。无限循环对于避免循环的尴尬逻辑条件非常有用。

无限 while 循环应使用true关键字作为其逻辑表达式:

while (true) {
   // body
}

for 循环可以简单到为 for 语句的每个部分使用 null:

for (;;) {
   // body
}

从不终止的循环通常对大多数程序都没有价值,因为大多数程序最终都应该终止。但是,大多数无限循环设计为使用 break 语句终止,如下所示:

while (true) {
   // first part
   if(someCondition) {
      break;
   }
   // last part
}

这种技术相当常见,用于简化程序的逻辑。考虑在一个时代阅读的必要性,当年龄是负的时候终止。有必要为 age 指定一个非负值,以确保循环至少执行一次:

int age;
age = 1;
Scanner scanner = new Scanner(System.in);
while (age > 0) {
   System.out.print("Enter an age: ");
   age = scanner.nextInt();
   // use the age
}

另一个选项是复制用户提示和用于在循环开始前读取的语句:

System.out.print("Enter an age: ");
age = scanner.nextInt();
while (age > 0) {
   System.out.print("Enter an age: ");
   age = scanner.nextInt();
   // use the age
}

在循环开始之前,必须为 age 指定任意值,或者必须复制代码。两种方法都不令人满意。

但是,使用无限循环会产生更干净的代码。不需要指定任意值,也不需要复制代码:

while (true) {
   System.out.print("Enter an age: ");
   age = scanner.nextInt();
   if (age < 0) {
      break;
   }
   // use the age
}

虽然在许多情况下需要无限循环,但当程序员不小心时也会发生无限循环,从而导致意外的结果。一种常见的方法是在没有有效终止条件的情况下构建 for 循环,如下所示:

for(int i = 1; i > 0; i++) {
   // Body
}

循环从i的值 1 开始,并在循环的每次迭代期间将i增加 1。终止条件表明循环不会终止,因为i只会变大,因此总是大于 0。但是,最终变量将溢出,i将变为负值,循环将终止。这可能需要多长时间取决于机器的执行速度。

这个故事的寓意是“小心循环”。无限循环既可以是解决某些问题的有用构造,也可以是无意中使用的有问题构造。

时间就是一切

一个常见的编程需求是执行某种求和。在前面的例子中,我们已经计算了几个数字序列的和。虽然求和过程相对简单,但对于新手程序员来说可能很困难。更重要的是,我们将在这里使用它来深入了解编程过程。

编程本质上是一个抽象的过程。程序员需要查看静态代码列表并推断其动态执行。这对很多人来说都很困难。帮助开发人员编写代码的一种方式是考虑以下三个问题:

  • 我们想做什么?
  • 我们想怎么做?
  • 我们想什么时候做?

在这里,我们将询问并应用这三个问题的答案来解决求和问题。然而,这些问题同样适用于其他编程问题。

让我们重点计算一组学生的平均年龄,这将涉及求和过程。假设年龄存储在age数组中,然后初始化,如下面的代码段所示:

final int size = 5;
int age[] = new int[size];
int total;
float average;

age[0] = 23;
age[1] = 18;
age[2] = 19;
age[3] = 18;
age[4] = 21;

然后可按如下方式计算总和:

total = 0;
for (int number : age) {
   total = total + number;
}
average = total / (age.length * 1.0f);

请注意,total被显式指定为零值。for 循环的每次迭代都会将下一个age添加到total。循环完成时,total将除以数组的长度乘以1.0f来计算平均值。通过使用数组长度,如果数组大小更改,则不需要更改代码表达式。必须乘以1.0f以避免整数除法。下表说明了循环执行时变量的值:

|

循环计数

|

|

全部的

| | --- | --- | --- | | 0 | - | 0 | | 1. | 1. | 23 | | 2. | 2. | 41 | | 3. | 3. | 60 | | 4. | 4. | 78 | | 5. | 5. | 99 |

让我们从三个基本问题的角度来考察这个问题,如下所示:

  • 我们想做什么:我们想计算一个部门的总工资。

  • How do we want to do it: This has a multipart answer. We know we need a variable to hold the total pay, total, and that it needs to be initialized to 0.

    total = 0;

    我们还了解计算累计和的基本操作如下:

    total = total + number;

    循环需要使用数组的每个元素,以便使用 for-each 语句:

    for (int number : age) {
       …
    }

    我们有解决问题的基础。

  • 我们想什么时候做:这种情况下的“何时”建议了三种基本选择:

    • 在循环前
    • 在循环中
    • 在循环后

我们的解决方案的三个部分可以以不同的方式组合。基本操作需要在循环内部,因为它需要执行多次。只执行一次基本操作不会得到我们想要的答案。

变量total需要用 0 初始化。这是怎么做到的?我们通过使用赋值语句来实现这一点。什么时候应该这样做?循环前、循环中或循环后?在循环之后这样做是愚蠢的。循环完成后,total应该包含答案,而不是零。如果我们在循环内部将其初始化为 0,那么在循环的每次迭代中,total被重置回0。这就让我们把语句放在循环之前,作为唯一有意义的选项。我们要做的第一件事是将0分配给total

大多数问题的解决方案似乎总是有变化的。例如,我们可以使用 while 循环而不是 For-each 循环。+=运算符可用于缩短基本操作。使用这些技术的一个潜在解决方案引入了索引变量:

int index = 0;
total = 0;

while(index < age.length) {
   total += age[index++];
}
average = total / (age.length * 1.0f);

显然,对于一个特定的问题,并不总是有一个最佳的解决方案。这使得编程过程既具有创造性,又具有潜在的乐趣。

陷阱

与大多数编程结构一样,循环也有自己的潜在陷阱。在本节中,我们将讨论可能给不谨慎的开发人员带来问题的领域。

当程序员在每条语句后使用分号时,会出现一个常见问题。例如,以下语句由于额外的分号而导致无限循环:

int i = 1;
while(i < 10) ;
  i++;

行上的分号本身就是空语句。这句话毫无意义。但是,在本例中,它构成 while 循环的主体。increment 语句不是 while 循环的一部分。这是 while 循环后面的第一条语句。缩进虽然可取,但不会使语句成为循环的一部分。因此,i永远不会递增,逻辑控制表达式将始终返回 true。

未能对循环体使用 block 语句可能是一个问题。在下面的例子中,我们试图计算从 1 到 5 的数的乘积之和。但是,这不能正常工作,因为循环体仅包含产品的计算。summation 语句缩进时不属于循环体的一部分,只执行一次:

int sum = 0;
int product = 0;
for(int i = 1; i <= 5; i++)
   product = i * i;;
   sum += product;

循环的正确实现使用如下所示的 block 语句:

int sum = 0;
int product = 0;
for(int i = 1; i <= 5; i++) {
   product = i * i;;
   sum += product;
}

对循环体使用 block 语句始终是一个好策略,即使循环体由单个语句组成。

在以下序列中,循环体由多个语句组成。但是,i从未递增。这也将导致无限循环,除非更改限制或遇到 break 语句:

int i = 0;
while(i<limit) {
  // Process i
}

事实上,如果不注意浮点运算的发生方式,即使是简单的循环也可能是无限循环。在本例中,0.1随着循环的每次迭代被添加到x。循环应该在x正好等于1.1时停止。这永远不会发生,因为某些值的浮点数存储方式存在问题:

float x = 0.1f;
while (x != 1.1) {
   System.out.printf("x = %f%n", x);
   x = x + 0.1f;
}

数字0.1不能精确地存储在基数 2 中,正如小数 1/3 的十进制等价物不能精确表示(0.333333…)。将此数字重复添加到x的结果将产生一个不完全为1.1的数字。比较,x != 1.1将返回 true,循环将永远不会结束。printf语句的输出未显示此差异:

x = 0.900000
x = 1.000000
x = 1.100000
x = 1.200000
x = 1.300000

在进行涉及自动装箱的操作时要小心。根据实现的不同,如果频繁发生装箱和取消装箱,可能会导致性能下降。

虽然不一定是陷阱,但请记住,逻辑表达式可能会短路。也就是说,逻辑 AND 或 or 操作的最后一部分可能不会根据第一部分的计算返回的值进行计算。这将在第 3 章决策构造中的短路评估部分详细讨论。

请记住,数组、字符串和大多数集合都是基于零的。忘记在0处启动循环将忽略第一个元素。

始终使用 block 语句作为循环体。

总结

在本章中,我们研究了 Java 对循环的支持。我们已经演示了 for、for each、while 和 do while 语句的用法。这些演示让我们深入了解了它们的正确用法、应该使用它们的时间和不应该使用它们的时间。

显示了 break 和 continue 语句的使用以及标签的使用。我们看到了 break 语句的实用性,特别是在支持无限循环方面。虽然应该避免使用标签,但标签在打破深度嵌套循环时非常有用。

检查了各种陷阱,并研究了求和过程的创建,以深入了解一般编程问题。具体来说,它解决了代码段应该放在哪里的问题。

现在,我们已经了解了循环,我们准备检查类、方法和数据封装的创建,这是下一章的主题。

涵盖的认证目标

在本章中,我们讨论了以下认证目标:

  • 创建和使用 while 循环
  • 创建和使用 for 循环,包括增强的 for 循环
  • 创建和使用 do/while 循环
  • 比较循环构造
  • 使用“中断并继续”

此外,我们还提供了这些目标的额外覆盖范围:

  • 定义变量的范围
  • 使用运算符和决策构造
  • 声明并使用 ArrayList

测试你的知识

  1. Given the following declarations, which of the following statement will compile?

    int i = 5;
    int j = 10;

    A.while(i < j) {}

    Bwhile(i) {}

    Cwhile(i = 5) {}

    Dwhile((i = 12)!=5) {}

  2. Given the following declaration of an array, which statement will display each element of the array?

    int arr[] = {1,2,3,4,5};

    A.for(int n : arr[]) { System.out.println(n); }

    Bfor(int n : arr) { System.out.println(n); }

    Cfor(int n=1; n < 6; n++) { System.out.println(arr[n]); }

    Dfor(int n=1; n <= 5; n++) { System.out.println(arr[n]); }

  3. Which of the following do/while loops will compile without errors?

    A.

    int i = 0;
    do {
    	System.out.println(i++);
    } while (i < 5);

    B

    int i = 0;
    do
    	System.out.println(i++);
    while (i < 5);

    C

    int i = 0;
    do 
    	System.out.println(i++);
    while i < 5;

    D

    i = 0;
    do
    	System.out.println(i);
    	i++;
    while (i < 5);
  4. Which of the following loops are equivalent?

    A.

    for(String n : list) { 
    	System.out.println(n);
    }

    B

    for(int n = 0; n < list.size(); n++ ){ 
    	System.out.println(list.get(n));
    }

    C

    Iterator it = list.iterator();
    while(it.hasNext()) {
    	System.out.println(it.next());
    }
  5. What will be output by the following code?

    int i;
    int j;
    for (i=1; i < 4; i++) {
       for (j=2; j < 4; j++) {
          if (j == 3) {
             continue;
          }
          System.out.println("i: " + i + " j: " + j);
       }
    }

    A.i: 1 j: 2i: 2 j: 2i: 3 j: 2

    Bi: 1 j: 3i: 2 j: 3i: 3 j: 3

    Ci: 1 j: 1i: 2 j: 1i: 3 j: 1**