6. 流程控制
6. 流程控制
不论哪一种编程语言,都会提供两种基本的流程控制结构:分支结构和循环结构。其中分支结构用于实现根据条件来选择性地执行某段代码,循环结构则用于实现根据循环条件重复执行某段代码。
顺序结构
任何编程语言中最常见的程序结构就是顺序结构。顺序结构就是程序从上到下逐行地执行,中间没有任何判断和跳转。
如果 main 方法的多行代码之间没有任何流程控制,则程序总是从上向下依次执行,排在前面的代码先执行,排在后面的代码后执行。这意味着:如果没有流程控制,Java 方法里的语句是一个顺序执行流,从上向下依次执行每条语句。
分支结构
if 条件语句
在任何情况下只能有一个分支执行,不可能存在 2 个或者多个分支执行,if 语句中只要有 1 个分支执行,整个 if 语句就结束了。
if 语句使用布尔表达式或布尔值作为分支条件来进行分支控制。if 语句有如下三种形式。
第一种:
if (logic expression) {
statement...
}
第二种:
if (logic expression) {
statement...
} else {
statement...
}
第三种:
if (logic expression) {
statement...
} else if (logic expression) {
statement...
}
... // 可以有零个或者多个else if语句
else // 最后的else语句也可以省略
{
statement...
}
在上面 if 语句的三种形式中,放在 if 之后括号里的只能是一个逻辑表达式,即这个表达式的返回值只能是 true 或 false。
在上面的条件语句中,if(logic expression)、else if(logic expression)和 else 后花括号{}
括起来的多行代码被称为代码块,一个代码块通常被当成一个整体来执行(除非运行过程中遇到 return
、break
、continue
等关键字,或者遇到了异常),因此这个代码块也被称为条件执行体。
public class IfTest {
public static void main(String[] args) {
int age = 30;
//只有当age > 20时,下面花括号括起来的代码块才会执行
//花括号括起来的语句是一个整体,要么一起执行,要么一起不执行
if (age > 20) {
System.out.println("年龄已经大于20岁了");
System.out.println("20岁以上的人应该学会承担责任...");
}
}
}
如果 if(logic expression)、else if(logic expression)和 else 后的代码块只有一行语句时,则可以省略花括号,因为单行语句本身就是一个整体,无须用花括号来把它们定义成一个整体。
//定义变量a,并为其赋值
int a = 5;
if (a > 4)
//如果a>4,则执行下面的执行体,只有一行代码作为代码块
System.out.println("a大于4");
else
//否则,执行下面的执行体,只有一行代码作为代码块
System.out.println("a不大于4");
通常,我们建议不要省略 if、else、else if 后执行体的花括号,即使条件执行体只有一行代码,也保留花括号会有更好的可读性,而且保留花括号会减少发生错误的可能。
//定义变量b,并为其赋值
int b = 5;
if(b > 4)
//如果b>4,则执行下面的执行体,只有一行代码作为代码块
System.out.println("b大于4");
else
//否则,执行下面的执行体,只有一行代码作为代码块
b--;
//对于下面代码而言,它已经不再是条件执行体的一部分,因此总会执行
System.out.println("b不大于4");
System.out.println("b不大于4");
,总会执行,因为这行代码并不属于 else 后的条件执行体,else 后的条件执行体就是b--;
这行代码。
//定义变量c,并为其赋值
int c = 5;
if (c > 4)
//如果c>4,则执行下面的执行体,将只有c--;一行代码为执行体
c--;
//下面是一行普通代码,不属于执行体
System.out.println("c大于4");
//此处的else将没有if语句,因此编译出错
else
//否则,执行下面的执行体,只有一行代码作为代码块
System.out.println("c不大于4");
对于 if 语句,还有一个很容易出现的逻辑错误,这个逻辑错误并不属于语法问题,但引起错误的可能性更大。
public class IfErrorTest {
public static void main(String[] args) {
int age = 45;
if (age > 20) {
System.out.println("青年人");
} else if (age > 40) {
System.out.println("中年人");
} else if (age > 60) {
System.out.println("老年人");
}
}
}
表面上看起来,上面的程序没有任何问题:人的年龄大于 20 岁是青年人,年龄大于 40 岁是中年人,年龄大于 60 岁是老年人。但运行上面程序,发现打印结果是:青年人,而实际上我们希望 45 岁应判断为中年人——这显然出现了一个问题。
对于任何的 if else 语句,表面上看起来 else 后没有任何条件,或者 else if 后只有一个条件——但这不是真相:因为 else 的含义是“否则”——else 本身就是一个条件!
else 的隐含条件是对前面条件取反。
public class IfErrorTest2 {
public static void main(String[] args) {
int age = 45;
if (age > 20) {
System.out.println("青年人");
}
//在原本的if条件中增加了else的隐含条件
else if (age > 40 && !(age > 20)) {
System.out.println("中年人");
}
//在原本的if条件中增加了else的隐含条件
else if (age > 60 && !(age > 20) && !(age > 40 && !(age > 20) {
System.out.println("老年人");
}
}
}
对于 age > 40 && !(age > 20)
这个条件,又可改写成 age > 40 && age <=20
,这样永远也不会发生了。对于 age > 60 && !(age > 20) && !(age > 40 && !(age> 20))
这个条件,则更不可能发生了。因此,程序永远都不会判断中年人和老年人的情形。
为了达到正确的目的,我们把程序改写成如下形式。
public class IfCorrectTest {
public static void main(String[] args) {
int age = 45;
if (age > 60) {
System.out.println("老年人");
} else if (age > 40) {
System.out.println("中年人");
} else if (age > 20) {
System.out.println("青年人");
}
}
}
上面程序等同于下面代码。
public class TestIfCorrect2 {
public static void main(String[] args) {
int age = 45;
if (age > 60) {
System.out.println("老年人");
}
//在原本的if条件中增加了else的隐含条件
else if (age > 40 && (age > 60)) {
System.out.println("中年人");
}
//在原本的if条件中增加了else的隐含条件
else if (age > 20 && !(age > 60) && !(age > 40 && !(age > 60))) {
System.out.println("青年人");
}
}
}
switch 语句
switch 语句执行原理:
switch 后面的小括号当中的“数据”和 case 后面的“数据”进行一一匹配,匹配成功分支执行。按照顺序结构依次匹配,匹配成功的分支执行,分支当中最后有“break”语句的话,整个 switch 语句中止。
如果没有“break”语句,就会直接进入下一个分支执行,不进行匹配(case 穿透)当所有分支都匹配失败,此时如果有 default,则执行 default 语句。
switch 和 case 后面只能是 int
或者 String
类型的数据,不能是变量,可以是枚举类型 byte
、short
、char
写进去会自动转换成 int
。
一个比较完整的 switch 语句编写:
switch (expression) {
case condition1: {
statement(s)
break;
}
case condition2: {
statement(s)
break;
}
case conditionN: {
statement(s)
break;
}
default: {
statement(s)
}
}
这种分支语句的执行是先对 expression 求值,然后依次匹配 condition1、condition2、…、conditionN 等值,遇到匹配的值即执行对应的执行体;如果所有 case 标签后的值都不与 expression 表达式的值相等,则执行 default 标签后的代码块。
public class SwitchTest {
public static void main(String[] args) {
//声明变量score,并为其赋值为'C'
char score = 'C';
//执行switch分支语句
switch (score) {
case 'A':
System.out.println("优秀");
break;
case 'B':
System.out.println("良好");
break;
case 'C':
System.out.println("中");
break;
case 'D':
System.out.println("及格");
break;
case 'F':
System.out.println("不及格");
break;
default:
System.out.println("成绩输入错误");
}
}
}
在 case 标签后的每个代码块后都有一条 break;
语句,这个 break;
语句有极其重要的意义,Java 的 switch 语句允许 case 后代码块没有 break;
语句,但这种做法可能引入一个陷阱。如果把上面程序中的 break;
语句都注释掉,将看到如下运行结果:
中
及格
不及格
成绩输入错误
switch 语句会先求出 expression 表达式的值,然后拿这个表达式和 case 标签后的值进行比较,一旦遇到相等的值,程序就开始执行这个 case 标签后的代码,不再判断与后面 case、default 标签的条件是否匹配,除非遇到 break;
才会结束;该现象称为:case 穿透。
循环结构
循环语句可以在满足循环条件的情况下,反复执行某一段代码,这段被重复执行的代码被称为循环体。当反复执行这个循环体时,需要在合适的时候把循环条件改为假,从而结束循环,否则循环将一直执行下去,形成死循环。循环语句可能包含如下 4 个部分。
- 初始化语句(init_statement):一条或多条语句,这些语句用于完成一些初始化工作,初始化语句在循环开始之前执行。
- 循环条件(test_expression):这是一个 boolean 表达式,这个表达式能决定是否执行循环体。
- 循环体(body_statement):这个部分是循环的主体,如果循环条件允许,这个代码块将被重复执行。如果这个代码块只有一行语句,则这个代码块的花括号是可以省略的。
- 迭代语句(iteration_statement):这个部分在一次循环体执行结束后,对循环条件求值之前执行,通常用于控制循环条件中的变量,使得循环在合适的时候结束。
上面 4 个部分只是一般分类,并不是每个循环中都非常清晰地分出了这 4 个成分。
while 循环
while 循环的语法格式如下:
[init_statement]
while (test_expression) {
statement;
[iteration_statement]
}
while 循环每次执行循环体之前,先对 test_ expression 循环条件求值,如果循环条件为 true,则运行循环体部分。从上面的语法格式来看,迭代语句 iteration_statement 总是位于循环体的最后,因此只有当循环体能成功执行完成时,while 循环才会执行 iteration_statement 迭代语句。
while 循环也可被当成条件语句——如果 test_expression 条件一开始就为 false,则循环体部分将永远不会获得执行。
public class WhileTest {
public static void main(String[] args) {
//循环的初始化条件
int count = 0;
//当count小于10时,执行循环体
while (count < 10) {
System.out.println(count);
//迭代语句
count++;
}
System.out.println("循环结束!");
}
}
使用 while 循环时,一定要保证循环条件有变成 false 的时候,否则这个循环将成为一个死循环,永远无法结束这个循环。
//下面是一个死循环
int count = 0;
while (count < 10) {
System.out.println("不停执行的死循环" + count);
count--;
}
System.out.println("永远无法跳出的循环体");
在上面代码中,count 的值越来越小,这将导致 count 值永远小于 10,count < 10 循环条件一直为 true,从而导致这个循环永远无法结束。
除此之外,对于许多初学者而言,使用 while 循环时还有一个陷阱:while 循环的循环条件后紧跟一个分号。
int count = 0;
//while后紧跟一个分号,表明循环体是一个分号(空语句)
while (count < 10);
//下面的代码块与while循环已经没有任何关系
{
System.out.println("------" + count);
count++;
}
一个单独的分号表示一个空语句,不做任何事情的空语句,这意味着这个 while 循环的循环体是空语句。当 Java 反复执行这个循环体时,循环条件的返回值没有任何改变,这就成了一个死循环。分号后面的代码块则与 while 循环没有任何关系。
do while
do while 循环与 while 循环的区别在于:while 循环是先判断循环条件,如果条件为真则执行循环体;而 do while 循环则先执行循环体,然后才判断循环条件,如果循环条件为真,则执行下一次循环,否则中止循环。do while 循环的语法格式如下:
[init_statement]
do {
statement;
[iteration_statement]
} while (test_expression);
与 while 循环不同的是,do while 循环的循环条件后必须有一个分号,这个分号表明循环结束。
public class DoWhileTest {
public static void main(String[] args) {
//定义变量count
int count = 1;
//执行do while循环
do {
System.out.println(count);
//循环迭代语句
count++;
//循环条件紧跟while关键字
} while (count < 10);
System.out.println("循环结束!");
}
}
即使 test_expression 循环条件的值开始就是假,do while 循环也会执行循环体。因此,do while 循环的循环体至少执行一次。
//定义变量count2
int count2 = 20;
//执行do while循环
do
//这行代码把循环体和迭代部分合并成了一行代码
System.out.println(count2++);
while (count2 < 10);
System.out.println("循环结束!");
for xun'h
for 循环是更加简洁的循环语句,大部分情况下,for 循环可以代替 while 循环、do while 循环。
for 循环的基本语法格式如下:
for ([init_statement]; [test_expression]; [iteration_statement]) {
statement;
}
程序执行 for 循环时,先执行循环的初始化语句 init_statement,初始化语句只在循环开始前执行一次。
每次执行循环体之前,先计算 test_expression 循环条件的值,如果循环条件返回 true,则执行循环体部分,循环体执行结束后执行循环迭代语句。
因此,对于 for 循环而言,循环条件总比循环体要多执行一次,因为最后一次执行循环条件返回 false,将不再执行循环体。
for 循环的循环迭代语句并没有与循环体放在一起,因此即使在执行循环体时遇到 continue 语句结束本次循环,循环迭代语句也一样会得到执行。
提示
for 循环和 while、do while 循环不一样:由于 while、do while 循环的循环迭代语句紧跟着循环体,因此如果循环体不能完全执行,如使用 continue 语句来结束本次循环,则循环迭代语句不会被执行。
但 for 循环的循环迭代语句并没有与循环体放在一起,因此不管是否使用 continue 语句来结束本次循环,循环迭代语句一样会获得执行。
与前面循环类似的是,如果循环体只有一行语句,那么循环体的花括号可以省略。
下面使用 for 循环代替前面的 while 循环,代码如下。
public class ForTest {
public static void main(String[] args) {
//循环的初始化条件、循环条件、循环迭代语句都在下面一行
for (int count = 0; count < 10; count++) {
System.out.println(count);
}
System.out.println("循环结束!");
}
}
在上面的循环语句中,for 循环的初始化语句只有一个,循环条件也只是一个简单的 boolean 表达式。实际上,for 循环允许同时指定多个初始化语句,循环条件也可以是一个包含逻辑运算符的表达式。
public class ForTest2 {
public static void main(String[] args) {
//同时定义了三个初始化变量,使用&&来组合多个boolean表达式
for (int b = 0, s = 0, p = 0; b < 10 && s < 4 && p < 10; p++) {
System.out.println(b++);
System.out.println(++s + p);
}
}
}
上面代码中初始化变量有三个,但是只能有一个声明语句,因此如果需要在初始化表达式中声明多个变量,那么这些变量应该具有相同的数据类型。
初学者使用 for 循环时也容易犯一个错误,以为只要在 for 后的括号内控制了循环迭代语句就万无一失,但实际情况则不是这样的。例如下面的程序:
public class ForErrorTest {
public static void main(String[] args) {
//循环的初始化条件、循环条件、循环迭代语句都在下面一行
for (int count = 0; count < 10; count++) {
System.out.println(count);
//再次修改了循环变量
count *= 0.1;
}
System.out.println("循环结束!");
}
}
在循环体内修改了 count 变量的值,并且把这个变量的值乘以了 0.1,这也会导致 count 的值永远都不能超过 10,因此上面程序也是一个死循环。
for 循环圆括号中只有两个分号是必需的,初始化语句、循环条件、迭代语句部分都是可以省略的,如果省略了循环条件,则这个循环条件默认为 true,将会产生一个死循环。例如下面程序:
public class DeadForTest{
public static void main(String[] args){
//省略了for循环三个部分,循环条件将一直为true
for(;;){
System.out.println("=============");
}
}
}
使用 for 循环时,还可以把初始化条件定义在循环体之外,把循环迭代语句放在循环体内,这种做法就非常类似于前面的 while 循环了。
public class ForInsteadWhile{
public static void main(String[] args){
//把for循环的初始化条件提出来独立定义
int count = 0;
//for循环里只放循环条件
for(;count < 10;){
System.out.println(count);
//把循环迭代部分放在循环体之后定义
count++;
}
System.out.println("循环结束!");
//此处将还可以访问count变量
}
}
把 for 循环的初始化语句放在循环之前定义还有一个作用:可以扩大初始化语句中所定义变量的作用域。在 for 循环里定义的变量,其作用域仅在该循环内有效,for 循环终止以后,这些变量将不可被访问。如果需要在 for 循环以外的地方使用这些变量的值,就可以采用上面的做法。
除此之外,还有一种做法也可以满足这种要求:额外定义一个变量来保存这个循环变量的值。
int tmp = 0;
// 循环的初始化条件、循环条件、循环迭代语句都在下面一行
for (int i = 0; i < 10; i++) {
System.out.println(count);
// 使用tmp来保存循环变量i的值
tmp = i;
}
System.out.println("循环结束!");
// 此处还可通过tmp变量来访问i变量的值
使用一个变量 tmp 来保存循环变量 i 的值,使得程序更加清晰,变量 i 和变量 tmp 的责任更加清晰。
嵌套循环
如果把一个循环放在另一个循环体内,那么就可以形成嵌套循环,嵌套循环既可以是 for 循环嵌套 while 循环,也可以是 while 循环嵌套 do while 循环……即各种类型的循环都可以作为外层循环,各种类型的循环也可以作为内层循环。
当程序遇到嵌套循环时,如果外层循环的循环条件允许,则开始执行外层循环的循环体,而内层循环将被外层循环的循环体来执行——只是内层循环需要反复执行自己的循环体而已。当内层循环执行结束,且外层循环的循环体执行结束时,则再次计算外层循环的循环条件,决定是否再次开始执行外层循环的循环体。
根据上面分析,假设外层循环的循环次数为 n 次,内层循环的循环次数为 m 次,那么内层循环的循环体实际上需要执行 n×m 次。
嵌套循环就是把内层循环当成外层循环的循环体。当只有内层循环的循环条件为 false 时,才会完全跳出内层循环,才可以结束外层循环的当次循环,开始下一次循环。
public class NestedLoopTest {
public static void main(String[] args) {
// 外层循环
for (int i = 0; i < 5; i++) {
// 内层循环
for (int j = 0; j < 3; j++) {
System.out.println("i的值为:" + i + "j的值为:" + j);
}
}
}
}
进入嵌套循环时,循环变量 i 开始为 0,这时即进入了外层循环。进入外层循环后,内层循环把 i 当成一个普通变量,其值为 0。在外层循环的当次循环里,内层循环就是一个普通循环。
实际上,嵌套循环不仅可以是两层嵌套,而且可以是三层嵌套、四层嵌套……不论循环如何嵌套,我们都可以把内层循环当成外层循环的循环体来对待,区别只是这个循环体里包含了需要反复执行的代码。
控制循环结构
Java 提供了 continue
和 break
来控制循环结构。除此之外,return
可以结束整个方法,当然也就结束了一次循环。
break
在某些时候,我们需要在某种条件出现时强行终止循环,而不是等到循环条件为 false 时才退出循环,此时,可以使用 break 来完成这个功能。
break 用于完全结束一个循环,跳出循环体。不管是哪种循环,一旦在循环体中遇到 break,系统将完全结束该循环,开始执行循环之后的代码。
public class BreakTest {
public static void main(String[] args) {
// 一个简单的for循环
for (int i = 0; i < 10; i++) {
System.out.println("i的值是" + i);
if (i == 2) {
// 执行该语句时将结束循环
break;
}
}
}
}
运行上面程序,将看到 i 循环到 2 时即结束,当 i 等于 2 时,循环体内遇到 break 语句,程序跳出该循环。
break 语句不仅可以结束其所在的循环,还可以直接结束其外层循环。此时需要在 break 后紧跟一个标签,这个标签用于标识一个外层循环。
Java 中的标签就是一个紧跟着英文冒号 :
的标识符,标签只有放在循环语句之前才有作用。
public class BreakTest2 {
public static void main(String[] args) {
// 外层循环,outer作为标识符
outer:
for (int i = 0; i < 5; i++) {
// 内层循环
for (int j = 0; j < 3; j++) {
System.out.println("i的值为:" + i + " j的值为:" + j);
if (j == 1) {
// 跳出outer标签所标识的循环
break outer;
}
}
}
}
}
程序从外层循环进入内层循环后,当 j 等于 1 时,程序遇到一个 break outer;
语句,这行代码将会导致结束 outer 标签指定的循环,不是结束 break 所在的循环,而是结束 break 循环的外层循环。
break 后的标签必须是一个有效的标签,即这个标签必须在 break 语句所在的循环之前定义,或者在其所在循环的外层循环之前定义。
如果把这个标签放在 break 语句所在的循环之前定义,也就失去了标签的意义,因为 break 默认就是结束其所在的循环。
continue
continue 的功能和 break 有点类似,区别是 continue 只是终止本次循环,接着开始下一次循环;而 break 则是完全终止循环本身。可以理解为 continue 的作用是跳过当次循环中剩下的语句,重新开始一次新的循环。
public class ContinueTest {
public static void main(String[] args) {
// 一个简单的for循环
for (int i = 0; i < 3; i++) {
System.out.println("i的值是" + i);
if (i == 1) {
// 略过本次循环的剩下语句
continue;
}
System.out.println("continue后的输出语句");
}
}
}
当 i 等于 1 时,程序没有输出“continue 后的输出语句”字符串,因为程序执行到 continue 时,忽略了当次循环中 continue 语句后的代码。
从这个意义上来看,如果把一个 continue 语句放在单次循环的最后一行,这个 continue 语句是没有任何意义的——因为它仅仅忽略了一片空白,没有忽略任何程序语句。
与 break 类似的是,continue 后也可以紧跟一个标签,用于直接跳过标签所标识循环的当次循环的剩下语句,重新开始下一次循环。
public class ContinueTest2 {
public static void main(String[] args) {
// 外层循环
outer:
for (int i = 0; i < 5; i++) {
// 内层循环
for (int j = 0; j < 3; j++) {
System.out.println("i的值为:" + i + " j的值为:" + j);
if (j == 1) {
// 跳出outer标签所指定的循环
continue outer;
}
}
}
}
}
运行上面程序可以看到,循环变量 j 的值将无法超过 1,因为每当 j 等于 1 时,continue outer 语句就结束了外层循环的当次循环,直接开始下一次循环,内层循环没有机会执行完成。
与 break 类似的是,continue 后的标签也必须是一个有效标签,即这个标签通常应该放在 continue 所在循环的外层循环之前定义。
return
return 关键字并不是专门用于结束循环的,return 的功能是结束一个方法。当一个方法执行到一个 return 语句时,这个方法将被结束。
一旦在循环体内执行到一个 return 语句,return 语句就会结束该方法,循环自然也随之结束。
public class ReturnTest {
public static void main(String[] args) {
//一个简单的for循环
for (int i = 0; i < 3; i++) {
System.out.println("i的值是" + i);
if (i == 1) {
return;
}
System.out.println("continue后的输出语句");
}
}
}
运行上面程序,循环只能执行到 i 等于 1 时,当 i 等于 1 时程序将完全结束(当 main 方法结束时,也就是 Java 程序结束时)。
从这个运行结果来看,虽然 return 并不是专门用于循环结构控制的关键字,但通过 return 语句确实可以结束一个循环。
与 continue 和 break 不同的是,return 直接结束整个方法,不管这个 return 处于多少层循环之内。