
3.7 循环结构
循环结构是程序中一种很重要的编程构造,特点是在给定条件成立时,反复执行某程序段,直到条件不成立为止。给定的条件称为循环条件,反复执行的程序段称为循环体。Java语言提供了多种循环语句,可以组成各种不同形式的循环结构。
3.7.1 while语句
while语句的一般形式为:
while(表达式) 语句
其中,表达式是循环条件,语句为循环体。如果循环体包含一个以上的语句,则必须用{}括起来,组成复合语句。
while语句的执行流程是:计算表达式的值,当值为真时,执行循环体语句。其执行过程如图3.14所示。

图3.14 while语句的执行流程框图
例3.5 扩充例3.3分数统计程序的功能,使该程序可以计算并显示平均分数。
分析:如果还像例3.3一样为类添加字段,每输入一个分数就累加到总分数字段并且分数个数也增加1,可以实现平均分数的计算功能。但是,如果统计的指标比较多,例如还要计算标准差等,则类中的字段变得繁多,程序也显得不够简练。事实上可以采用这样的思路:用户输入的所有合法分数都存放在文本区域jTextAreaNums中,可以通过该组件的getText()方法将所有分数拼合成的字符串取出来,然后从该字符串中解析出各个分数,从而实现平均分数的计算功能。此分数字符串的格式是“89\n65\n46\n100\n73\n92”,其中的字符’\n’是转义字符——换行符,使用String类的以下两个方法可以从中解析出单个分数。
1. substring(int beginIndex, int endIndex)
此方法返回一个新字符串,它是此字符串的一个子字符串。该子字符串是从指定的beginIndex处开始直到索引endIndex−1处的字符。例如,"89\n65\n46\n100\n73\n92".substring(0, 2)返回"89"。
2. indexOf(int ch, int fromIndex)
该方法返回在此字符串中第一次出现指定字符ch的索引,从指定的索引fromIndex处开始搜索。在此String对象表示的字符序列中,如果带有值ch的字符的索引不小于fromIndex,则返回第一次出现该值的索引。如果此字符串中fromIndex或之后的位置没有这样的字符出现,则返回−1。如果fromIndex的值为负或0,则搜索整个字符串;如果大于或等于此字符串的长度,则返回−1。被搜索的字符ch是Unicode字符,即可以是汉字或其他语言字符。例如,"89\n65\n46\n100\73\92". indexOf('\n', 0)返回2。
分数字符串可能包含多个分数,需要反复提取每一个分数,因此采用循环结构。
解:按照以下步骤操作。
(1)右击chap03项目的“源包”节点,在快捷菜单中选择“新建”|“Java包”菜单项,包名输入“book.loopdemos”,单击“完成”按钮。
(2)右击chap03项目的“源包”|ifdemos包下的IfElseIfDemo.java节点,在快捷菜单中选择“复制”菜单项。
(3)右击book.loopdemos包名节点,在快捷菜单中选择“粘贴”|“重构复制”菜单项,新名称输入“WhileDemo”,单击“重构”按钮。
(4)在WhileDemo窗体中将“清除”按钮移到文本区域左侧;在文本区域下方创建“平均分”按钮并将其变量名修改为jButtonAvg;在该按钮右边创建文本字段组件jTextFieldAvg,并设置其editable属性值为False、columns属性值为12、text属性值为空。完成后的界面如图3.15所示。

图3.15 例3.5分数统计程序界面设计视图
(5)单击“平均分”按钮,在属性窗口中单击“事件”标签,单击actionPerformed行右端的…按钮,在对话框中单击“添加”按钮,新建处理程序的名称输入“calcAvg”,单击“确定”按钮,再次单击“确定”按钮。
(6)自动切换到源代码视图后,在calcAvg方法体中输入以下代码段。

(7)在clearNums方法中添加语句“jTextFieldAvg.setText("");”。
完成上述操步骤作后运行程序,可以正确计算显示平均分数。
3.7.2 do-while语句
while循环首先计算循环条件,有可能第一次循环时循环条件就是false,则循环体一次都不执行。如例3.5中没有输入分数或只输入一个分数,则循环控制变量to的值为−1,循环条件为false,循环体不会执行。但是,有时希望循环体先执行一次,然后判断是否继续下次循环,此时就应该使用do-while循环。

此循环语句的执行流程是:它先执行循环中的语句,然后再判断表达式是否为真(true),如果为真则继续循环;如果为假(false),则终止循环。因此,do-while循环至少要执行一次循环语句。其执行过程可用图3.16表示。

图3.16 do-while语句的执行流程框图
分析例3.5分数统计程序中calcAvg方法的源代码,发现循环体中的关键语句之一“to = numStr.indexOf('\n', from);”在while循环语句之前就执行过一次,且to是循环控制变量。这就是说,无论循环何时结束,此语句必须执行一次。因此,在calcAvg方法中采用do-while循环更合适。修改后calcAvg方法体中的程序段如下。

可见,采用do-while循环结构后,程序似乎变得简洁了。
3.7.3 for语句
在Java程序设计中经常会遇到使一组语句迭代执行指定次数的问题,Java语言提供了for语句来完成“计数”循环。for语句使用也最为灵活,完全可以取代while语句。for语句的一般形式为:
for(表达式1; 表达式2; 表达式3) 语句
for语句的执行流程如图3.17所示。它的执行过程是:①先求解表达式1;②求解表达式2,若其值为true(真),则执行for语句循环体内的语句,然后转至③执行;若其值为false(假),则循环结束,转到for语句下面的语句执行;③求解表达式3;④转至②执行。

图3.17 for语句的执行流程框图
for语句最简单的应用形式是计数循环,也是最容易理解的形式,语句如下:
for(循环变量赋初值; 循环条件; 循环变量增量) 语句
循环变量赋初值是一个赋值语句,用来给循环控制变量赋初始值;循环条件是一个关系表达式,它决定什么时候退出循环;循环变量增量用来定义循环控制变量每循环一次后按什么方式变化。这三个部分之间用“;”分开。例如:
for(i=1; i<=100; i++) sum=sum+i;
先给i赋初值1,判断i是否小于或等于100,若是则执行循环体语句,之后值增加1。再重新判断,直到i>100,结束循环。显然,此for语句与下面的while等价:

事实上,for语句与while语句的这种等价关系具有一般性。
for循环中的“表达式1(循环变量赋初值)”“表达式2(循环条件)”和“表达式3(循环变量增量) ”都是选择项,即表达式可以省略,但“;”不能省略。由此,可以形成for语句的以下灵活运用方式。
(1)省略“表达式1(循环变量赋初值)”,表示不对循环控制变量赋初值。
(2)省略“表达式2(循环条件)”。但应特别注意:若在循环体内不做其他处理,则会形成死循环——循环一直进行。例如,“for(i=1; ; i++) sum = sum+1 ;”就是死循环。
(3)省略“表达式3(循环变量增量)”,则不对循环控制变量进行操作。这时应该在语句体中加入修改循环控制变量的语句,使循环控制变量向着使循环结束的方向靠近。例如:
for(i=1;i<=100;) { sum=sum+i; i++; // 变量i向着101靠近 }
(4)省略“表达式1(循环变量赋初值)”和“表达式3(循环变量增量)”。例如:
for(; i<=100; ) { sum = sum + 1; i++; }
(5)三个表达式都可以省略。例如,“for(; ; )语句”,此种结构应在“语句”部分创造循环结束的条件。
(6)表达式1可以是设置循环变量的初值的赋值表达式,也可以是其他表达式。例如,“for(sum=0;i<=100;i++) sum = sum + i;”。
(7)表达式1和表达式3既可以是一个简单表达式也可以是逗号表达式。例如,“for(sum=0,i=1;i<=100;i++)sum=sum+i;”;又如,“for(i=0,j=100;i<=100;i++,j--)k=i+j;”。
逗号运算符“,”可以放在多个表达式之间形成逗号表达式,使各个表达式依次求值,逗号表达式的值是其中最后一个表达式的值。逗号运算符“,”只用于for语句。
如果for语句的循环控制变量在该语句块之外不再有用,可以在for语句的第一个表达式之处直接声明其类型,例如:“for(int i=1;i<=100;i++) sum+=i;”。但是,语句“for(int i=1,sum=0;i<=100;i++) sum+=i;”,在之后不能访问变量sum,如接着执行语句“system.out.println("sum="+sum);”会发生找不到变量sum的错误。
for语句尽管有这么多灵活用法,但一般还是应该按照其正常形式使用,不应人为增加程序的复杂性。应该遵循一条不成文规矩:for语句的三个表达式应该对同一个计数变量进行初始化、检测和更新。
还应注意,如果for语句的表达式2检测浮点数,尽量不要检测相等,而是检测其误差在一定范围内。例如,“for(double x=0; x!=10; x+=0.1) …”则可能形成死循环——由于误差,变量x永远不会等于10。
Java 10可以使用保留字var在for语句的第一个表达式中定义循环控制变量,该变量的类型由Java 10根据给其所赋值的类型推断。例如语句“for(var i='a';i<='z';i++)system.out.println(i);”则会分行输出26个英文字母。但此处var一次只能定义一个推断类型变量,语句“for(var i=1,sum=0;i<=100;i++) ……”是不允许的。
例3.6 设计一个阶乘计算程序,使程序能够计算任意自然数的阶乘。
分析:从阶乘的计算公式n! = 1×2×…×(n−1)×n来看,是一个迭代相乘的操作,且迭代次数是n次,可以采用计数循环完成:
long fact = 1; for(int i=1; i<=n; i++) fact *= i;
但是,运行程序发现,当n取比较大的值时,计算结果超出long类型的范围,显然并不能满足计算任意自然数阶乘的要求。在3.1.2节介绍过BigInteger类可以表示和处理不可变的任意精度的整数,当然可以处理大整数运算。查阅该类的API文档,找到以下两个可以用于本例的方法。
valueOf(long val):返回其值等于指定long类型值的BigInteger对象,且可以使用类名直接调用。
multiply(BigInteger val):返回当前BigInteger对象与val对象乘积的BigInteger,也就是执行大整数相乘运算。
显然,使用这两个方法,采用上述for循环就可以实现任意自然数阶乘的计算。
解:按以下步骤操作。
(1)右击book.loopdemos包名节点,在快捷菜单中选择“新建”|“JFrame窗体”菜单项,类名输入“ForDemo1”,单击“完成”按钮。
(2)在窗体中设计如图3.18所示GUI界面。其中,文本字段组件jTextFieldFact存放和显示阶乘值,columns属性值设置为20,editable属性值为false。

图3.18 阶乘计算程序设计界面
(3)单击“=”按钮,在属性窗口中单击“事件”标签,单击actionPerformed行右端的…按钮,在对话框中单击“添加”按钮,新建处理程序的名称输入“calcFact”,单击“确定”按钮,再次单击“确定”按钮。
(4)自动切换到源代码视图后,在calcFact方法体中输入以下代码段。

(5)使用与步骤(3)相同的操作,为“清除”按钮添加actionPerformed事件处理方法clearAll,并在该方法体中输入以下程序代码。
jTextFieldNum.setText(""); jTextFieldFact.setText("");
(6)使用与步骤(3)相同的操作,为“退出”按钮添加actionPerformed事件处理方法calcExit,并在该方法体中输入语句“System.exit(0);”。
完成上述步骤后,运行程序,输入45,单击“=”按钮,显示阶乘值为119622220865480194561963161495657715064383733760000000000,显然远远超出long类型范围,程序能够按照题意运行。
循环语句的使用还有一种常见的情况是在一个循环语句内包含另外一个循环语句,分别称为外循环和内循环语句,这种用法称为循环的嵌套。甚至在内循环语句中还可能包含一条循环语句,从而构成多重嵌套循环结构。本书后面章节将介绍嵌套循环的例子。
3.7.4 循环中的跳转
循环中的跳转是指在循环条件仍然成立时强制改变循环流程的操作,主要包括强制结束循环和强制开始下一次循环两种情况。
1. break语句
Java语言中break语句除了用于switch语句之外,还可以用于do-while、for、while循环语句中使程序终止循环而执行循环后面的语句。通常break语句总是与if语句连在一起,使循环在满足特定条件时跳出循环。在循环语句中使用break语句的一般形式如下。

其执行流程如图3.19所示。
对于以下常见的计算自然数阶乘的程序段,利用break语句可以在阶乘值超出long类型范围时直接结束计算阶乘的for循环。


图3.19 while循环体内使用break语句的执行流程框图
Java语句还提供了一个带标号的break语句。Java程序中的标号是一个后面加冒号“:”的标识符,一般可以在循环语句、if语句或任何块语句前面加标号。当执行“break标号;”语句时,执行流程直接跳转到标号语句块之后。例如:

2. continue语句
continue语句的作用是跳过循环体中剩余的语句而强行执行下一次循环。continue语句只用在for、while、do-while等循环体中,常与if条件语句一起使用以加速循环。在循环语句中使用break语句的一般形式如下。

其执行过程如图3.20所示。

图3.20 while循环体内使用continue语句的执行流程框图
continue语句也可以带有标号,使流程跳转到标号标记的语句块首部执行。
例3.7 设计一个Java GUI程序,提供文本框让用户输入一个正整数n,并将2~n的全部素数显示在文本区域。
分析:如果一个整数不能被除了1和它自身之外的任何整数整除,这个整数就是素数。显然,所有的偶数都不是素数;最小的素数是2;对一个奇数m,如果除以3~m/2的任一整数的余数为0即不是素数,反之m即为素数。
可以采用for循环对2~n的奇数逐一测试。for循环内部嵌套一个循环,测试当前整数是否为素数。
解:按照以下步骤操作。
(1)右击chap03项目的“源包”|ifdemos包下的IfMaxDemo.java节点,在快捷菜单中选择“复制”菜单项。
(2)右击book.loopdemos包名节点,在快捷菜单中选择“粘贴”|“重构复制”菜单项,新名称输入“LoopsDemo”,单击“重构”按钮。修改窗体LoopsDemo的title属性值为“素数查找程序”。
(3)在设计视图下删除LoopsDemo窗体中的“最大整数”标签组件及文本字段jTextFieldMax组件;将“确定”和“清除”按钮移到靠近窗体底边框位置;修改jLabel1组件的text属性值为“2——”、jLabel2组件的text属性值为“所有素数:”。修改后的程序GUI如图3.21所示。

图3.21 查找素数程序GUI设计视图
(4)切换到“源”视图,删除LoopsDemo类中的“int maxNum=Integer.MIN_VALUE;”语句,删除addNumber方法中的所有语句但保留方法首尾行,在该方法体中输入以下语句。

(5)删除clearNums方法中的后两行语句。
jTextFieldMax.setText(""); maxNum = Integer.MIN_VALUE;
完成上述步骤后运行程序,可以找出指定范围内的所有素数(见图3.22)。程序中对于2~n的偶数,使用continue语句直接跳过(见程序代码中的注①);对于某个数m,如果能被3~m/2的任意数整除,则使用break语句直接退出内层循环体(见程序代码中的注②)。

图3.22 例3.7素数查找程序运行结果
3.7.5 递归方法
对于阶乘计算问题,除了例3.6给出的迭代相乘方法外,还有递归求解方法。即n!可以分解为n×(n−1)!,即变为求解(n−1)!问题;(n−1)! = (n−1)×(n−2)!,从而变为求解(n−2)!的问题;以此类推,当最终变为求解1!的问题时,可以直接得到结果1(见图3.23)。然后逆序得到各步结果,即用1替换1!代入2×1!得到2!结果2;用2替换2!代入3×2!得到3!结果6;用6替换3!代入4×3!得到4!结果24;以此类推,直到用(n−1)!结果计算出n!(见图3.24)。
递归是一种非常有用的解题方法,其基本思路是:首先寻找一种方法将原问题分解为规模较小的同类问题,而这种分解方法又可以应用于前面分解得到的较小问题上,从而将问题逐步降解为同类较小的问题。当问题降解为最小规模或足够小的规模时,可以直接得到结果。第二阶段,利用足够小的规模问题的结果获得较大规模问题的结果,进一步得到更大规模问题的结果,直至得到原始问题的结果。

图3.23 求解5!的递归过程

图3.24 求解5!的逆推过程中间截图
恰当地利用递归方法可以降低某些问题的解决难度,同时也使解题思路更加清晰,程序逻辑更为简单。例如,在JShell中可以输入如图3.25所示的Java程序片段实现图3.23和图3.24的递归算法。

图3.25 JShell中n!计算程序片段
可以采用递归方法解决的问题必须符合以下三个条件。
(1)问题可以转化为一个新问题,而新问题解决方法与原问题相同,且新问题为原问题的有规律递增或递减。
(2)可以通过转化过程使问题得到解决。
(3)必须有一个明确的结束递归的条件,使问题在有限步内得到解决。
事实上,某些问题只有使用递归方法才能设计计算机程序来解决。此外,Java语言利用递归方法解决的问题递归层数不能太多,否则会造成JVM出错。