五、控制流(2)

本章概要

  • return
  • break 和 continue
  • 臭名昭著的 goto
  • switch
  • switch 字符串

return

在 Java 中有几个关键字代表无条件分支,这意味无需任何测试即可发生。这些关键字包括 returnbreakcontinue 和跳转到带标签语句的方法,类似于其他语言中的 goto

return 关键字有两方面的作用:1.指定一个方法返回值 (在方法返回类型非 void 的情况下);2.退出当前方法,并返回作用 1 中值。我们可以利用 return 的这些特点来改写上例 IfElse.java 文件中的 test() 方法。代码示例:

// control/TestWithReturn.java
public class TestWithReturn {
    static int test(int testval, int target) {
        if (testval > target) {
          return +1;
        }
        if (testval < target) {
          return -1;
        }
        return 0; // Match
    }

    public static void main(String[] args) {
        System.out.println(test(10, 5));
        System.out.println(test(5, 10));
        System.out.println(test(5, 5));
    }
}

输出结果:

1
-1
0

这里不需要 else,因为该方法执行到 return 就结束了。

如果在方法签名中定义了返回值类型为 void,那么在代码执行结束时会有一个隐式的 return。 也就是说我们不用在总是在方法中显式地包含 return 语句。 注意:如果你的方法声明的返回值类型为非 void 类型,那么则必须确保每个代码路径都返回一个值。

break 和 continue

在任何迭代语句的主体内,都可以使用 breakcontinue 来控制循环的流程。 其中,break 表示跳出当前循环体。而 continue 表示停止本次循环,开始下一次循环。

下例向大家展示 breakcontinueforwhile 循环中的使用。代码示例:

Range.java

public class Range {
    // Produce sequence [start..end) incrementing by step
    public static int[] range(int start, int end, int step) {
        if (step == 0) {
            throw new IllegalArgumentException("Step cannot be zero");
        }
        int sz = Math.max(0, step >= 0 ? (end + step - 1 - start) / step : (end + step + 1 - start) / step);
        int[] result = new int[sz];
        for (int i = 0; i < sz; i++) {
            result[i] = start + (i * step);
        }
        return result;
    }  // Produce a sequence [start..end)

    public static int[] range(int start, int end) {
        return range(start, end, 1);
    }

    // Produce a sequence [0..n)
    public static int[] range(int n) {
        return range(0, n);
    }
}

BreakAndContinue.java

import static BASE0002.Range.range;
// control/BreakAndContinue.java
// Break 和 continue 关键字
public class BreakAndContinue {
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) { // [1]
            if (i == 74) {
                break; // 跳出循环
            }
            if (i % 9 != 0) {
                continue; // 下一次循环
            }
            System.out.print(i + " ");
        }
        System.out.println();
        // 使用 for-in 循环:
        for (int i : range(100)) { // [2]
            if (i == 74) {
                break; // 跳出循环
            }
            if (i % 9 != 0) {
                continue; // 下一次循环
            }
            System.out.print(i + " ");
        }
        System.out.println();
        int i = 0;
        //  "无限循环":
        while (true) { // [3]
            i++;
            int j = i * 27;
            if (j == 1269) {
                break; // 跳出循环
            }
            if (i % 10 != 0) {
                continue; // 循环顶部
            }
            System.out.print(i + " ");
        }
    }
}

输出结果:

0 9 18 27 36 45 54 63 72
0 9 18 27 36 45 54 63 72
10 20 30 40

[1] 在这个 for 循环中,i 的值永远不会达到 100,因为一旦 i 等于 74,break 语句就会中断循环。通常,只有在不知道中断条件何时满足时,才需要 break。因为 i 不能被 9 整除,continue 语句就会使循环从头开始。这使 i 递增)。如果能够整除,则将值显示出来。

[2] 使用 for-in 语法,结果相同。

[3] 无限 while 循环。循环内的 break 语句可中止循环。注意,continue 语句可将控制权移回循环的顶部,而不会执行 continue 之后的任何操作。 因此,只有当 i 的值可被 10 整除时才会输出。在输出中,显示值 0,因为 0%9 产生 0。还有一种无限循环的形式: for(;;)。 在编译器看来,它与 while(true) 无异,使用哪种完全取决于你的编程品味。

臭名昭著的 goto

goto 关键字 很早就在程序设计语言中出现。事实上,goto 起源于汇编(assembly language)语言中的程序控制:“若条件 A 成立,则跳到这里;否则跳到那里”。如果你读过由编译器编译后的代码,你会发现在其程序控制中充斥了大量的跳转。较之汇编产生的代码直接运行在硬件 CPU 中,Java 也会产生自己的“汇编代码”(字节码),只不过它是运行在 Java 虚拟机里的(Java Virtual Machine)。

一个源码级别跳转的 goto,为何招致名誉扫地呢?若程序总是从一处跳转到另一处,还有什么办法能识别代码的控制流程呢?随着 _Edsger Dijkstra_发表著名的 “Goto 有害” 论(Goto considered harmful)以后,goto 便从此失宠。甚至有人建议将它从关键字中剔除。

正如上述提及的经典情况,我们不应走向两个极端。问题不在 goto,而在于过度使用 goto。在极少数情况下,goto 实际上是控制流程的最佳方式。

尽管 goto 仍是 Java 的一个保留字,但其并未被正式启用。可以说, Java 中并不支持 goto。然而,在 breakcontinue 这两个关键字的身上,我们仍能看出一些 goto 的影子。它们并不属于一次跳转,而是中断循环语句的一种方法。之所以把它们纳入 goto 问题中一起讨论,是由于它们使用了相同的机制:标签。

“标签”是后面跟一个冒号的标识符。代码示例:

label1:

对 Java 来说,唯一用到标签的地方是在循环语句之前。进一步说,它实际需要紧靠在循环语句的前方 —— 在标签和循环之间置入任何语句都是不明智的。而在循环之前设置标签的唯一理由是:我们希望在其中嵌套另一个循环或者一个开关。这是由于 breakcontinue 关键字通常只中断当前循环,但若搭配标签一起使用,它们就会中断并跳转到标签所在的地方开始执行。代码示例:

label1:
outer-iteration { 
  inner-iteration {
  // ...
  break; // [1] 
  // ...
  continue; // [2] 
  // ...
  continue label1; // [3] 
  // ...
  break label1; // [4] 
  } 
}

[1] break 中断内部循环,并在外部循环结束。

[2] continue 移回内部循环的起始处。但在条件 3 中,continue label1 却同时中断内部循环以及外部循环,并移至 label1 处。

[3] 随后,它实际是继续循环,但却从外部循环开始。

[4] break label1 也会中断所有循环,并回到 label1 处,但并不重新进入循环。也就是说,它实际是完全中止了两个循环。

下面是 for 循环的一个例子:

// control/LabeledFor.java
// 搭配“标签 break”的 for 循环中使用 break 和 continue
public class LabeledFor {
    public static void main(String[] args) {
        int i = 0;
        outer:
        // 此处不允许存在执行语句
        for (; true; ) { // 无限循环
            inner:
            // 此处不允许存在执行语句
            for (; i < 10; i++) {
                System.out.println("i = " + i);
                if (i == 2) {
                    System.out.println("continue");
                    continue;
                }
                if (i == 3) {
                    System.out.println("break");
                    i++; // 否则 i 永远无法获得自增
                    // 获得自增
                    break;
                }
                if (i == 7) {
                    System.out.println("continue outer");
                    i++;  // 否则 i 永远无法获得自增
                    // 获得自增
                    continue outer;
                }
                if (i == 8) {
                    System.out.println("break outer");
                    break outer;
                }
                for (int k = 0; k < 5; k++) {
                    if (k == 3) {
                        System.out.println("continue inner");
                        continue inner;
                    }
                }
            }
        }
        // 在此处无法 break 或 continue 标签
    }
}

输出结果:

在这里插入图片描述

注意 break 会中断 for 循环,而且在抵达 for 循环的末尾之前,递增表达式不会执行。由于 break 跳过了递增表达式,所以递增会在 i==3 的情况下直接执行。在 i==7 的情况下,continue outer 语句也会到达循环顶部,而且也会跳过递增,所以它也是直接递增的。

如果没有 break outer 语句,就没有办法在一个内部循环里找到出外部循环的路径。这是由于 break 本身只能中断最内层的循环(对于 continue 同样如此)。 当然,若想在中断循环的同时退出方法,简单地用一个 return 即可。

下面这个例子向大家展示了带标签的 break 以及 continue 语句在 while 循环中的用法:

// control/LabeledWhile.java
// 带标签的 break 和 conitue 在 while 循环中的使用
public class LabeledWhile {
    public static void main(String[] args) {
        int i = 0;
        outer:
        while (true) {
            System.out.println("Outer while loop");
            while (true) {
                i++;
                System.out.println("i = " + i);
                if (i == 1) {
                    System.out.println("continue");
                    continue;
                }
                if (i == 3) {
                    System.out.println("continue outer");
                    continue outer;
                }
                if (i == 5) {
                    System.out.println("break");
                    break;
                }
                if (i == 7) {
                    System.out.println("break outer");
                    break outer;
                }
            }
        }
    }
}

输出结果:

在这里插入图片描述

同样的规则亦适用于 while

  1. 简单的一个 continue 会退回最内层循环的开头(顶部),并继续执行。
  2. 带有标签的 continue 会到达标签的位置,并重新进入紧接在那个标签后面的循环。
  3. break 会中断当前循环,并移离当前标签的末尾。
  4. 带标签的 break 会中断当前循环,并移离由那个标签指示的循环的末尾。

大家要记住的重点是:在 Java 里需要使用标签的唯一理由就是因为有循环嵌套存在,而且想从多层嵌套中 breakcontinue

breakcontinue 标签在编码中的使用频率相对较低 (此前的语言中很少使用或没有先例),所以我们很少在代码里看到它们。

Dijkstra“Goto 有害” 论文中,他最反对的就是标签,而非 goto。他观察到 BUG 的数量似乎随着程序中标签的数量而增加。标签和 goto 使得程序难以分析。但是,Java 标签不会造成这方面的问题,因为它们的应用场景受到限制,无法用于以临时方式传输控制。由此也引出了一个有趣的情形:对语言能力的限制,反而使它这项特性更加有价值。

switch

switch 有时也被划归为一种选择语句。根据整数表达式的值,switch 语句可以从一系列代码中选出一段去执行。它的格式如下:

switch(integral-selector) {
	case integral-value1 : statement; break;
	case integral-value2 : statement;	break;
	case integral-value3 : statement;	break;
	case integral-value4 : statement;	break;
	case integral-value5 : statement;	break;
	// ...
	default: statement;
}

其中,integral-selector (整数选择因子)是一个能够产生整数值的表达式,switch 能够将这个表达式的结果与每个 integral-value (整数值)相比较。若发现相符的,就执行对应的语句(简单或复合语句,其中并不需要括号)。若没有发现相符的,就执行 default 语句。

在上面的定义中,大家会注意到每个 case 均以一个 break 结尾。这样可使执行流程跳转至 switch 主体的末尾。这是构建 switch 语句的一种传统方式,但 break 是可选的。若省略 break, 会继续执行后面的 case 语句的代码,直到遇到一个 break 为止。通常我们不想出现这种情况,但对有经验的程序员来说,也许能够善加利用。注意最后的 default 语句没有 break,因为执行流程已到了 break 的跳转目的地。当然,如果考虑到编程风格方面的原因,完全可以在 default 语句的末尾放置一个 break,尽管它并没有任何实际的作用。

switch 语句是一种实现多路选择的干净利落的一种方式(比如从一系列执行路径中挑选一个)。但它要求使用一个选择因子,并且必须是 intchar 那样的整数值。例如,假若将一个字串或者浮点数作为选择因子使用,那么它们在 switch 语句里是不会工作的。对于非整数类型(Java 7 以上版本中的 String 型除外),则必须使用一系列 if 语句。 在后面中,我们将会了解到枚举类型被用来搭配 switch 工作,并优雅地解决了这种限制。

下面这个例子可随机生成字母,并判断它们是元音还是辅音字母:

import java.util.*;
// control/VowelsAndConsonants.java
// switch 执行语句的演示
public class VowelsAndConsonants {
    public static void main(String[] args) {
        Random rand = new Random(47);
        for (int i = 0; i < 100; i++) {
            int c = rand.nextInt(26) + 'a';
            System.out.print((char) c + ", " + c + ": ");
            switch (c) {
                case 'a':
                case 'e':
                case 'i':
                case 'o':
                case 'u':
                    System.out.println("vowel");
                    break;
                case 'y':
                case 'w':
                    System.out.println("Sometimes vowel");
                    break;
                default:
                    System.out.println("consonant");
            }
        }
    }
}

输出结果:

y, 121: Sometimes vowel
n, 110: consonant
z, 122: consonant
b, 98: consonant
r, 114: consonant
n, 110: consonant
y, 121: Sometimes vowel
g, 103: consonant
c, 99: consonant
f, 102: consonant
o, 111: vowel
w, 119: Sometimes vowel
z, 122: consonant
  ...

由于 Random.nextInt(26) 会产生 0 到 25 之间的一个值,所以在其上加上一个偏移量 a,即可产生小写字母。在 case 语句中,使用单引号引起的字符也会产生用于比较的整数值。

请注意 case 语句能够堆叠在一起,为一段代码形成多重匹配,即只要符合多种条件中的一种,就执行那段特别的代码。这时也应该注意将 break 语句置于特定 case 的末尾,否则控制流程会继续往下执行,处理后面的 case。在下面的语句中:

int c = rand.nextInt(26) + 'a';

此处 Random.nextInt() 将产生 0~25 之间的一个随机 int 值,它将被加到 a 上。这表示 a 将自动被转换为 int 以执行加法。为了把 c 当作字符打印,必须将其转型为 char;否则,将会输出整数。

switch 字符串

Java 7 增加了在字符串上 switch 的用法。 下例展示了从一组 String 中选择可能值的传统方法,以及新式方法:

// control/StringSwitch.java
public class StringSwitch {
    public static void main(String[] args) {
        String color = "red";
        // 老的方式: 使用 if-then 判断
        if ("red".equals(color)) {
            System.out.println("RED");
        } else if ("green".equals(color)) {
            System.out.println("GREEN");
        } else if ("blue".equals(color)) {
            System.out.println("BLUE");
        } else if ("yellow".equals(color)) {
            System.out.println("YELLOW");
        } else {
            System.out.println("Unknown");
        }
        // 新的方法: 字符串搭配 switch
        switch (color) {
            case "red":
                System.out.println("RED");
                break;
            case "green":
                System.out.println("GREEN");
                break;
            case "blue":
                System.out.println("BLUE");
                break;
            case "yellow":
                System.out.println("YELLOW");
                break;
            default:
                System.out.println("Unknown");
                break;
        }
    }
}

输出结果:

RED
RED

一旦理解了 switch,你会明白这其实就是一个逻辑扩展的语法糖。新的编码方式能使得结果更清晰,更易于理解和维护。

作为 switch 字符串的第二个例子,我们重新访问 Math.random()。 它是否产生从 0 到 1 的值,包括还是不包括值 1 呢?在数学术语中,它属于 (0,1)、[0,1)、(0,1]、[0,1] 中的哪种呢?(方括号表示“包括”,而括号表示“不包括”)

下面是一个可能提供答案的测试程序。 所有命令行参数都作为 String 对象传递,因此我们可以 switch 参数来决定要做什么。 那么问题来了:如果用户不提供参数 ,索引到 args 的数组就会导致程序失败。 解决这个问题,我们需要预先检查数组的长度,若长度为 0,则使用空字符串 "" 替代;否则,选择 args 数组中的第一个元素:

TimedAbort.java

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class TimedAbort {
    private volatile boolean restart = true;

    public TimedAbort(double t, String msg) {
        CompletableFuture.runAsync(() -> {
            try {
                while (restart) {
                    restart = false;
                    TimeUnit.MILLISECONDS
                            .sleep((int) (1000 * t));
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(msg);
            System.exit(0);
        });
    }

    public TimedAbort(double t) {
        this(t, "TimedAbort " + t);
    }

    public void restart() {
        restart = true;
    }
}
// control/RandomBounds.java
// Math.random() 会产生 0.0 和 1.0 吗?
// {java RandomBounds lower}
public class RandomBounds {
    public static void main(String[] args) {
        new TimedAbort(3);
        switch (args.length == 0 ? "" : args[0]) {
            case "lower":
                while (Math.random() != 0.0) {
                    ; // 保持重试
                }
                System.out.println("Produced 0.0!");
                break;
            case "upper":
                while (Math.random() != 1.0) {
                    ; // 保持重试
                }
                System.out.println("Produced 1.0!");
                break;
            default:
                System.out.println("Usage:");
                System.out.println("\tRandomBounds lower");
                System.out.println("\tRandomBounds upper");
                System.exit(1);
        }
    }
}

要运行该程序,请键入以下任一命令:

java RandomBounds lower 
// 或者
java RandomBounds upper

TimedAbort 类可使程序在三秒后中止。从结果来看,似乎 Math.random() 产生的随机值里不包含 0.0 或 1.0。 这就是该测试容易混淆的地方:若要考虑 0 至 1 之间所有不同 double 数值的可能性,那么这个测试的耗费的时间可能超出一个人的寿命了。 这里我们直接给出正确的结果:Math.random() 的结果集范围包含 0.0 ,不包含 1.0。 在数学术语中,可用 [0,1) 来表示。由此可知,我们必须小心分析实验并了解它们的局限性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只小熊猫呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值