5. 循环
循环是用来控制语句块重复执行的一种结构。循环的概念是程序设计的基础。Java 提供了三种类型的循环语句:while 循环、do - while 循环和 for 循环。
5.1 while 循环
while 循环在条件为真的情况下,重复地执行语句。
while 循环的语法如下 :
while( 循环继续条件){
// 循环体
语句 (组);
}
下图给出了 while 循环的流程图。循环中包含的重复执行的语句部分称为循环体(loop body)。循环体的每一次执行都被认为是一次循环的迭代(或重复)。每个循环都含有循环继续条件,循环继续条件是一个布尔表达式,控制循环体的执行。在循环体执行前总是先计算循环条件以决定是否执行它。若条件为 true, 则执行循环体;若条件为 false,则终止整个循环,并且程序控制转移到 while 循环后的下一条语句。
确切地知道循环体需要执行的次数。所以,使用一个控制变量 count 来对执行次数计数。这种类型的循环称为计数器控制的循环(counter - controlled loop)。
注意:循环继续条件应该总是放在圆括号内。只有当循环体只包含一条语句或不包含语句时,循环体的花括号才可以省略。
注意:要保证循环继续条件最终可以变为 false,以便程序能够结束。一个常见的程序设计错误是无限循环(也就是说,循环会永远执行下去)。如果程序运行了不寻常的长时间而不结束,可能其中有无限循环。如果你是从命令窗口运行程序的,按 CTRL+C 键来结束。
警告:程序员经常会犯的错误就是使循环多执行一次或少执行一次。这种情况通常称为差一错误( off - by - one error)。例如:下面的循环会将 Welcome to ]ava 显示 101 次,而不是100 次。这个错误出在条件部分,所以条件应该是 count<100 而不是 count<=100。
int count = 0;
while (count <= 100){
System.out.println("Weicome to Java!");
count++;
}
5.1.1 循环设计策略
编写一个正确的循环对编程新手来说,并不是件容易的事。编写循环时应该考虑如下三个步骤:
第一步:确定需要重复的语句。
第二步:将这些语句放在一个循环中,如下所示:
while(true){
语句组 ;
}
第三步:为循环继续条件编码,并为控制循环添加适合的语句。
while( 循环继续条件){
语句组 ;
用于控制循环的附件语句 ;
}
5.1.2 使用标记值控制循环
另一种控制循环的常用技术是在读取和处理一个集合的值时指派一个特殊值。这个特殊的输人值也称为标记值(sentinel value), 用以表明循环的结束。如果一个循环使用标记值来控制它的执行,它就称为标记位控制的循环(sentinel - controlled loop)。
注意:在循环控制中,不要使用浮点值来比较值是否相等。因为浮点值都是某些值的近似值,使用它们可能导致不精确的循环次数和不准确的结果。
考虑下面计算 1+0.9+0.8+ . . .+0.1的代码:
double item = 1; double sum = 0;
while (item != 0) { // No guarantee item will be 0
sum += item;
item -= 0.1;
}
System.out.println(sum);
变量 item 从1开始,每执行一次循环体就减去 0.1。当 item 变为 0 时循环应该终止。但是,因为浮点數在算术上是近似的,所以不能确保 item 会变成真正的 0。从表面上看,这个循环似乎没问題,但实际上它是一个无限循环。
5.1.3 输入和输出重定向
在前面的例子中,如果要输人大量的数值,那么从键盘上输入是非常繁琐的事。可以将这些数据用空格隔开,保存在一个名为 input.txt 的文本文件中,然后使用下面的命令运行这个程序:
java SentinelValue < input.txt
这个命令称为输入重定向( input redirection)。程序从文件 input.txt 中读取输入,而不是让用户在运行时从键盘输入数据。假设文件内容是:
2 3 4 5 6 7 8 9 12 23 32
23 45 67 89 92 12 34 35 3 1 2 4 0
程序将得到 sum 值为 518。
类似地,还有输出重定向( output redirection), 输出重定向将输出发送给文件,而不是将它们显示在控制台上。输出重定向的命令为:
java ClassName > output.txt
可以在同一命令中同时使用输入重定向和输出重定向。例如,下面的命令从文件 input , txt 中获取输入,并将输出发送给文件 output.txt:
java SentinelValue <input.txt> output.txt
5.2 do - while 循环
do - while 循环和 while 循环基本一样,不同的是它先执行循环体一次,然后判断循环继续条件。
do - while 循环是 while 循环的变体。它的语法如下:
do {
// 循环体 ;
语句(组 );
} while ( 循环继续条件);
执行流程图如下所示首先执行循环体,然后计算循环继续条件。如果计算结果为 true, 则重复执行循环体;如果为 false, 则终止 do - while 循环。while 循环与 do - while 循环的差别在于:计算循环继续条件和执行循环体的先后顺序不同。有时候,选择其中一种会比另一种更方便。
提示:如果循环中的语句至少需要执行一次,建议使用 do-while 循环。前面程序TestDoWhile 中 do-while 循环的情形就是如此。如果使用 while 循环,那么这些语句必须在循环前和循环内都出现。
5.3 for 循环
for 循环具有编写循环的简明语法。
经常会用到下面的通用形式编写循环:
i = initialValue; // Initialize loop control variable
while (i < endValue)
// Loop body
...
i++; // Adjust loop control variable
}
可以使用 for 循环简化前面的循环:
for (i = initialValue; i < endValue; i++) {
// Loop body
...
}
通常,for 循环的语法如下所示:
for (初始操作;循环鏈续条件;每次迭代后的操作){
// 循环体;
语句(组);
}
for 循环的流程图如图所示。
for 循环语句从关键字 for 开始,然后是用双括号括住的循环控制结构体。这个结构体包括初始动作、循环继续条件和每次迭代后的动作。控制结构体后紧跟着花括号括起来的循环体。初始动作、循环继续条件和每次迭代后的动作都要用分号分隔。一般情况下,for 循环使用一个变量来控制循环体的执行次数,以及什么时候循环终止。这个变量称为控制变量(control variable)。初始化动作是指初始化控制变量,每次迭代后的动作通常会对控制变量做自增或自减,而循环继续条件检验控制变量是否达到终止值。例如,下面的 for 循环打印 Welcome to Java! 100 次:
int i;
for(i = 0;i < 100;i++){
System.out.println("Weicowe to Java!");
}
循环控制变量可以在 for 循环中声明和初始化。下面就是一个例子:
for(int = 0;i<100;i++){
System.out.println("Weicome to Java!");
}
如果像这个例子一样,循环体内只有一条语句,则可以省略花括号。
提示:控制变量必须在循环控制结构体内或循环前说明。如果循环控制变量只在循环内使用而不在其他地方使用,那么在 for 循环的初始动作中声明它是一个很好的编程习慣。如果在循环控制结构体内声明变量,那么在循环外不能引用它。例如,不能在前面代码的for 循环外引用变量 i, 因为它是在 for 循环内声明的。
注意:for 循环中的初始动作可以是 0 个或是多个以逗号隔开的变量声明语句或赋值表达式。例如:
for(int i=0,j=0;i+j<10;i++, j++){
// Do something
}
for 循环中每次迭代后的动作可以是 0 个或多个以逗号隔开的语句。例如:
for(int i=1;i<100; System.out.println(i), i++);
这个例子是正确的,但是它不是一个好例子,因为它增加了程序的阅读难度。通常,将声明和初始化一个变量作为初始动作,将增加或减少控制变量作为每次迭代后的操作。
注意:如果省略 for 循环中的循环继续条件,则隐含地认为循环继续条件为 true。因此,下面图 a 中给出的语句和图 b 中给出的语句一样,它们都是无限循环。但是,为了避免混淆,最好还是使用图 c 中的等价循环:
5.4 采用哪种循环
可以根据哪个更加方便,来使用 for 循环、while 循环,或者 do - while 循环。while 循环和 for 循环都称为前测循环( pretest丨oop), 因为继续条件是在循环体执行之前检测的,do - while 循环称为后测循环( posttest loop), 因为循环条件是在循环体执行之后检测的。三种形式的循环语句:while、do - while 和 for, 在表达上是等价的。也就是说,可以使用这三种形式之一来编写一个循环。例如,下面图 a 中 while 循环总能转化为图 b中的 for 循环:
除了某些特殊情况外,下面图 a 中的 for 循环通常都能转化为图 b 中的 while 循环:
建议使用自己觉得最自然、最舒服的一种循环语句。通常,如果已经提前知道重复次数,那就采用 for 循环,例如,需要打印一条信息 100 次时,如果无法确定重复次数,就采用 while 循环,就像读入一些数值直到读入 0 为止的这种情况。如果在检验继续条件前需要执行循环体,就用 do - while 循环替代 while 循环。
在 for 子句的末尾和循环体之间多写分号是一个常见的错误,如下面的图 a 中所示。图 a 中分号过早地表明循环的结束。循环体实际上都是为空的,如图 b 所示。图 a 和图 b 是等价的,都是不正确的。类似地,图 c 中的循环也是错的,图 c 与图 d 等价,都是不正确的。
通常在使用次行块格式时容易发生这些错误。使用行尾块风格可以避免这种类型的错误。在 do-while 循环中,需要分号来结束这个循环。
5.5 嵌套循环
一个循环可以嵌套在另外一个循环中。嵌套循环是由一个外层循环和一个或多个内层循环组成的。每当重复执行一次外层循环时将再次进人内部循环,然后重新开始。
注意:需要注意的是,嵌套循环将运行较长时间。考虑下面嵌套三层的循环:
for (int i = 0;i < 10000;i++){
for (int j = 0;j < 10000;j++){
for (int k = 0; k < 10000; k++){
Perform an action;
}
}
}
动作将被执行万亿次。如果需要丨微秒来执行一次动作,整个循环花费的事件将大于 2 7 7 小时。注意,1 微秒等于 1 秒的百万分之一。
5.6 最小化数值错误
在循环继续条件中使用浮点数将导致数值错误。
涉及浮点数的数值误差是不可避免的,因为浮点数在计算机中本身就是近似表示的。本节将通过实例讨论如何最小化这种误差。 用下列给出的例子计算从 0.01到 1.0 的数列之和,该数列中的数值以 0.01递增,如下所示:0.01+0.02+0.03 + ...。
TestSum.java
public class TestSum {
public static void main (String[] args) {
// Initialize sum
float sum = 0;
// Add 0.01,0.02, … ,0.99, 1 to sum
for (float i - 0.0lf ; i <= l.0f ; i = i + 0.01f) {
sum +- i ;
}
// Display result
System.out. println("The sum is " + sum);
}
}
显示
The sum is 50.499985
for 循环(第 7 ~8 行)重复地将控制变量 i 加到 sum 中。变量 i 从 0.01 开始,每次迭代增加 0.01。当 i 超过 1.0 时循环终止。
for 循环初始动作可以是任何语句,但是,它经常用来初始化控制变量。从本例中可以看到,控制变量可以是 float 型。事实上,它可以为任意数据类型。sum 的精确结果应该是 SO.50, 但是答案是 S0.49998S。这个结果是不精确的,因为计算机使用固定位数表示浮点数,因此,它就不能精确表示某些浮点数。如果如下所示,将程序中的 float 型改成 double 型,应该可以看到精度有一些小小的改善,因为 double 型变量占64 位而 float 型变量只占 32 位。
5.7 关键字 break 和 continue
关键字 break 和 continue 都可以在循环语句中使用,为循环提供额外的控制。在某些情况下,使用 break 和 continue 可以简化程序设计。但是,过度使用或者不正确地使用它们会使得程序难以读懂也难以调试。
你已经在 switch 语句中使用过关键字 break,你也可以在一个循环中使用 break 立即终止该循环。下列程序清单给出的程序演示了在循环中使用 break 的效果。
TestBreak.java
public class TestBreak {
public static void main(String[] args){
int sum = 0;
int number = 0;
while(number < 20){
number++;
sum += number;
if(sum >= 100){
break;
}
}
System.out.println("The number is" + number);
System.out.println("The sum is" + sum);
}
}
显示
The number is 14;
The sum is 105;
上述程序清单中的程序将从1到 20 的整数依次加到 sum 中,直到 sum 大于或等于 100。如果没有 if 语句(第 9行),该程序计算从1到 20 之间整数的和。但是,有了 if语句,那么当总和大于或等于 100 时,这个循环就会终止。没有 if 语句,程序输出结果将会是:
The number is 20;
The sum is 210;
也可以在循环中使用关键字 continue。当程序遇到 continue 时,它会结束当前的迭代。程序控制转向该循环体的末尾。换句话说,continue 只是跳出了一次迭代,而关键字 break是跳出了整个循环。下列程序清单给出的程序演示了在循环中使用 continue 的效果。
TestContinue.java
public class TestContinue {
public static void main(tring[] args){
int sum = 0;
int number = 0;
while(number < 20){
number++;
if(number ==10 || number == 11){
continue;
}
sum += number;
}
System.out.println("The sum is" + sum);
}
}
显示
The sum is 189
上述程序清单中的程序,将1到 20 中除去10 和 11外的整数都加到 sum 中。程序中有了 if 语句(第 8 行),当 number 为 10 或 11时就会执行 continue 语句。continue 语句结束了当前迭代,就不再执行循环体中的其他语句,因此,当 number 为 10 或 11时,它就没有被加到 sum 中。若程序中没有 if 语句,程序的输出就会如下所示:
The sum is 210
在这种情况下,即使当 number 为 10 或 11时,也要将所有的数都加到 sum 中。因此,结果为 210, 这个值比有 if 语句的情况获取的值大了 21。
注意:continue 语句总是在一个循环内。在 while 和 do - while 循环中,continue 语句之后会马上计算循环继续条件;而在 for 循环中,continue 语句之后会立即先执行每次迭代后的动作,再计算循环继续条件。
总是可以编写在循环中不使用 break 和 contimje 的程序。通常,只有在能够简化代码并使程序更容易阅读的情况下,才适合使用 break 和 continue。假设你需要编写一个程序,找到整数 n 的除1外的最小因子(假设 n>=2)。可以使用 break 语句编写简单直观的代码,如下所示:
int factor = 2;
while (factor <= n){
if (n % factor == 0){
break;
}
factor++;
}
System.out.println("The smallest factor other than 1 for " + n + " is " + factor);
你也可以不使用 break 语句重写该代码,如下所示:
boolean found = false;
int factor = 2;
while (factor <= n && !found){
if (n % factor = 0){
found = true;
}else{
factor++;
}
}
System.out.println("The smallest factor other than 1 for " + n + " is "+ factor);
显然,使用 break 语句可以使程序更简单和更易读。但是,应该谨慎使用 break 和 continue。过多使用 break 和 continue 会使循环有很多退出点,使程序很难阅读。
注意:很多程序设计语言都有 goto 语句。goto 语句可以随意地将控制转移到程序中的任意一条语句上,然后执行它。这使程序很容易出错。Java 中的 break 语句和 continue 语句是不同于 goto 语句的。它们只能运行在循环中或者 switch 语句中。break 语句跳出整个循环,而 continue 语句跳出循环的当前迭代。
注意:编程是一个富于创造性的工作。有许多不同的方式来编写代码。事实上,你可以通过更加闻单的代码来找到最小因子,如下所示:
int factor = 2;
while (factor <= n && n % factor != 0){
factor++;
}
编译运算练习
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.