购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

bt2-L 1.7 控制流

在以下几小节中,你将学习如何实现分支和循环。Java语言的这部分语法与其他常用语言(特别是C/C++和JavaScript)非常相似。

1.7.1 分支

if语句在圆括号内有一个分支条件,后面会有一个语句或一组括在花括号中的语句:

if (count > 0){
     double average = sum / count; 
     System.out.println(average);
}

你也可以添加一个else分支以在条件不满足时运行该分支:

if (count > 0) {
     double average = sum / count;
     System.out.println(average); 
} else {
     System.out.println(0);
}

else分支的语句中也可以再添加另外一个if语句:

if (count > 0) {
     double average = sum / count; 
     System.out.println(average); 
} else if (count == 0) {
     System.out.println(0);
} else {
     System.out.println ("Huh?");
}

1.7.2 switch语句

switch表达式的功能是将一个操作数与多个选项进行比较,并为每个具体情况生成一个值:

String seasonName = switch (seasonCode) { // switch expression
     case 0 -> "Spring";
     case 1 -> "Summer";
     case 2 -> "Fall";
     case 3 -> "Winter";
     default -> {
          System.out.println("???");
          yield "";
     }
};

需要注意的是,switch在这里是一个 表达式 (expression),并且有一个值,即5个字符串"Spring" "Summer""Fall""Winter"""中的一个。这个switch表达式的值被赋值给seasonName变量。

其实最常见的情况是,一个case后面跟着一个表达式。你也可以在一个花括号括起来的语句块中做一些其他的额外工作,就像前面示例中的default部分一样。然后,你需要在语句块中使用yield语句来生成一个值。

switch还有一种语句形式,如下所示:

switch (seasonCode) { // switch statement
     case 0 -> seasonName = "Spring";
     case 1 -> seasonName = "Summer";
     case 2 -> seasonName = "Fall";
     case 3 -> seasonName = "Winter";
     default -> {
          System.out.println("???");
          seasonName = "";
     }
}

在前面的示例中,case标签是整数。你可以使用以下任意类型的值:

char类型、byte类型、short类型或int类型的常量表达式(或与其相对应的封装类Character、Byte、Short和Integer,将在1.8.3小节中介绍);

字符串字面量;

枚举的值(参见第4章)。

每个case都可以有多个标签,并用逗号分隔:

int numLetters = switch (seasonName) {
     case "Spring", "Summer", "Winter" -> 6;
     case "Fall" -> 4;
     default -> throw new IllegalArgumentException();
}; 

注意: 整数或String上的switch表达式总是有一个default部分。无论操作数值是什么,switch表达式都必须生成一个值。此外,如前一个示例所示,大小写的区别可能会引发异常。异常将在第5章中具体介绍。

警告: 如果switch的操作数值为null,那么一个NullPointerException异常将会被抛出。当操作数类型为String或枚举时,会发生这种情况。

在前面的示例中,switch表达式和语句中,对于给定的操作数值只有一个case分支被执行。当然有时也可能会发生一些例外,这种情况通常被称作直通(fall-through,也称贯通)。即其从匹配的case分支开始执行,然后继续执行下一个case,除非被yield或break语句打断。switch的直通式变体同样也具有表达式和语句形式。在下面的示例中,当seasonName为"Spring"时会发生这种直通。

int numLetters = switch (seasonName) { // switch expression with fall-through
    case "Spring":
        System.out.println("spring time!");
    case "Summer", "Winter":
        yield 6;
    case "Fall":
        yield 4;
    default:
        throw new IllegalArgumentException();
};
 
switch (seasonName) { // switch statement with fall-through
    case "Spring":
        System.out.println("spring time!");
    case "Summer", "Winter":
        numLetters = 6;
        break;
     case "Fall":
        numLetters = 4;
        break;
    default:
        throw new IllegalArgumentException();
}

需要注意的是,在直通式变体中,每个case后面都跟一个冒号,而不是一个 ->。这样可以在冒号后跟任意数量的语句,并且不需要花括号。此外,在带有直通的switch表达式中,必须使用yield来生成一个值。

警告: 在直通式变体中,忘记yield或break是一个常见的错误。除非真的需要直通行为,否则请避免使用这种变体。

1.7.3 循环

while循环会依据具体的条件,反复执行其循环体的语句。例如,假定有一个对数值求和的任务,直到数值的总和达到目标值。我们将使用随机数生成器作为数值的来源,其由java.util包中的Random类提供:

var generator = new Random();

下面的调用将会生成0~9的一个随机整数:

int next = generator.nextInt(10);

以下是用于求和的循环:

while(sum < target) {
     int next = generator.nextInt(10);
     sum += next;
     count++;
}

这是while循环的典型用法。当总和小于目标值时,循环会持续执行。

有时你需要先执行循环体,然后才能评估循环条件。假设你想知道达到特定值所需的具体时间,那么在测试循环条件之前,需要先进入循环并获取到那个测试值。在这种情况下,要使用do/while循环:

int next;
do {
     next = generator.nextInt(10);
     count++;
} while (next != target);

这样就可以先进入循环体,再设定next的值,然后再评估是否满足循环条件。只要满足循环条件,循环体就会重复执行。

在前面的示例中,循环迭代的次数都是未知的。然而,在实践中的许多循环中,循环迭代的次数都是固定的。在这些情况下,最好使用for循环。

例如,下面示例中的循环计算固定数量的随机值之和:

for (int i = 1; i <= 20; i++){
     int next = generator.nextInt(10);
     sum += next;
}

这个循环将会执行20次,每次循环迭代中,i分别会被设置为1、2、……、20。

可以将任何一个for循环重写为while循环。上面的循环等效于:

int i = 1;
while (i <= 20){
     int next = generator.nextInt(10);
     sum += next;
     i++;
}

在while循环中,变量i的初始化、测试和更新分散在循环体的不同位置。而使用for循环,变量i的初始化、测试和更新可以很紧凑地聚集在一起。此外,for循环中变量的初始化、测试和更新可以采用任意形式。例如,当一个值小于目标值时,可以将其加倍:

for (int i = 1; i < target; i *= 2) {
     System.out.println(i);
}

也可以不在for循环的头部声明变量,而是初始化现有变量:

for (i = 1; i <= target; i++) // Uses existing variable i

或者可以声明或初始化多个变量并提供多个变量的更新,用逗号分隔。例如:

for (int i = 0, j = n - 1; i < j; i++, j--)

如果不需要初始化或更新,那么也可以将其留空。如果忽略该条件,则认为该条件总是为true:

for (;;) // An infinite loop

你将在下一小节中看到如何退出这种无限循环。

1.7.4 break和continue

如果想从循环迭代的过程中退出,可以使用break语句。例如,假设你想处理用户输入的单词,直到用户输入字母Q为止。下面是一个使用boolean变量来控制循环的解决方案:

boolean done = false; 
while (!done) {
     String input = in.next();
     if ("Q".equals(input)) {
          done = true;
     } else {
          Process input
     }
}

下面的循环使用break语句执行相同的任务:

while(true) {
     String input = in.next();
     if ("Q".equals(input)) break; // Exits loop 
     Process input
}
// break jumps here

当到达break语句时,循环将立即退出。

continue语句类似于break,但它不会跳到循环的终点,而是跳到当前循环迭代的终点。可以使用它来略过不需要的输入,例如:

while (in.hasNextInt()) {
     int input = in.nextInt();
     if (input < 0) continue; // Jumps to test of in.hasNextInt()
     Process input
}

在for循环中,continue语句将会跳转到下一个更新语句处:

for (int i = 1; i <= target; i++) {
     int input = in.nextInt();
     if (n < 0) continue; // Jumps to i++
     Process input
}

break语句仅从紧邻着的封闭循环或switch中跳转出来。如果要跳转到另一个封闭语句的末尾,请使用带标签的 break语句。在需要退出的语句处打上标签,例如:

outer:
while(...){
     ...
     while (...) {
          ...
          if (...) break outer:
          ...
      }
      ...
}
// Labeled break jumps here

标签可以是任何名称。

警告: 虽然你在语句的顶部打上了标签,但break语句将跳转到 末尾

常规break语句只能用于退出循环或switch,但带标签的break语句可以将控制转移到任何语句的末尾,甚至是块语句:

exit: {
     ...
     if(...)break exit;
     ...
}
// Labeled break jumps here

还有一个带标签的continue语句,它跳转到标签处开始下一次迭代。

提示: 许多编程人员发现break语句和continue语句令人困惑。需要知道的是,这些语句完全是可选的,没有它们也是可以表达相同的逻辑的。本书不会使用break语句或continue语句。

1.7.5 局部变量的作用域

现在,你已经看到了使用了嵌套形式的语句块的示例。这是一个很好的开始,下面我们即将开始学习变量作用域的一些基本规则。 局部变量 (local variable)就是在方法中声明的任何变量,甚至包括方法的参数变量。变量的 作用域 (scope)就是可以在程序中访问该变量的范围。局部变量的作用域是从变量声明处开始,一直延伸到当前的封闭块的末尾:

while (...) {
     System.out.println(...);
     String input = in.next(); // Scope of input starts here
     ...
     // Scope of input ends here
}

换言之,每个循环在迭代时,都会创建一个新的input变量的副本,并且该变量在循环之外并不存在。

参数变量的作用域是整个方法:

public static void main(String[] args) { // Scope of args starts here
     ...
     // Scope of args ends here
}

这里还有一种需要理解作用域规则的情况。以下的循环计算了获取特定随机数字需要尝试的次数:

int count = 0;
int next;
do {
     next = generator.nextInt(10);
     count++;
} while (next != target);

这里的next变量必须在循环外部声明,以便在循环中实现条件判断。如果在循环内部声明,那么它的作用域将只延伸到循环体的结尾。

当你在for循环中声明变量时,它的作用域将延伸到循环的结尾,包括测试和更新语句:

for (int i = 0; i < n; i++) { // i is in scope for the test and update
     ...
}
// i not defined here

如果需要循环后的i值,就请在外部声明变量:

int i;
for (i = 0; !found && i < n; i++) {
     ...
}
// i still available

在Java中,不能在重叠的作用域内有名称相同的局部变量:

int i = 0; 
while (...) {
     String i = in.next(); // Error to declare another variable i
     ...
}

但是,如果作用域不重叠,则变量名可以相同: GQ+tt9MCFyIEQfqVBw/J6l70Pge4IM+0hQltH+tKzDbWjs8tpC0lrJZa9RWBBn+N

for (int i = 0; i < n / 2; i++) { ... }
for (int i = n / 2; i < n; i++) { ... } // OK to redefine i
点击中间区域
呼出菜单
上一章
目录
下一章
×