6. 流程控制

空~2022年7月6日
  • java
大约 21 分钟

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 后花括号{}括起来的多行代码被称为代码块,一个代码块通常被当成一个整体来执行(除非运行过程中遇到 returnbreakcontinue 等关键字,或者遇到了异常),因此这个代码块也被称为条件执行体。

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 类型的数据,不能是变量,可以是枚举类型 byteshortchar 写进去会自动转换成 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 个部分。

  1. 初始化语句(init_statement):一条或多条语句,这些语句用于完成一些初始化工作,初始化语句在循环开始之前执行。
  2. 循环条件(test_expression):这是一个 boolean 表达式,这个表达式能决定是否执行循环体。
  3. 循环体(body_statement):这个部分是循环的主体,如果循环条件允许,这个代码块将被重复执行。如果这个代码块只有一行语句,则这个代码块的花括号是可以省略的。
  4. 迭代语句(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 次。

img

嵌套循环就是把内层循环当成外层循环的循环体。当只有内层循环的循环条件为 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 提供了 continuebreak 来控制循环结构。除此之外,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 处于多少层循环之内。