本章概要
- return
- break 和 continue
- 臭名昭著的 goto
- switch
- switch 字符串
return
在 Java 中有几个关键字代表无条件分支,这意味无需任何测试即可发生。这些关键字包括 return,break,continue 和跳转到带标签语句的方法,类似于其他语言中的 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
在任何迭代语句的主体内,都可以使用 break 和 continue 来控制循环的流程。 其中,break 表示跳出当前循环体。而 continue 表示停止本次循环,开始下一次循环。
下例向大家展示 break 和 continue 在 for、while 循环中的使用。代码示例:
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。然而,在 break 和 continue 这两个关键字的身上,我们仍能看出一些 goto 的影子。它们并不属于一次跳转,而是中断循环语句的一种方法。之所以把它们纳入 goto 问题中一起讨论,是由于它们使用了相同的机制:标签。
“标签”是后面跟一个冒号的标识符。代码示例:
label1:
对 Java 来说,唯一用到标签的地方是在循环语句之前。进一步说,它实际需要紧靠在循环语句的前方 —— 在标签和循环之间置入任何语句都是不明智的。而在循环之前设置标签的唯一理由是:我们希望在其中嵌套另一个循环或者一个开关。这是由于 break 和 continue 关键字通常只中断当前循环,但若搭配标签一起使用,它们就会中断并跳转到标签所在的地方开始执行。代码示例:
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:
- 简单的一个 continue 会退回最内层循环的开头(顶部),并继续执行。
- 带有标签的 continue 会到达标签的位置,并重新进入紧接在那个标签后面的循环。
- break 会中断当前循环,并移离当前标签的末尾。
- 带标签的 break 会中断当前循环,并移离由那个标签指示的循环的末尾。
大家要记住的重点是:在 Java 里需要使用标签的唯一理由就是因为有循环嵌套存在,而且想从多层嵌套中 break 或 continue。
break 和 continue 标签在编码中的使用频率相对较低 (此前的语言中很少使用或没有先例),所以我们很少在代码里看到它们。
在 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 语句是一种实现多路选择的干净利落的一种方式(比如从一系列执行路径中挑选一个)。但它要求使用一个选择因子,并且必须是 int 或 char 那样的整数值。例如,假若将一个字串或者浮点数作为选择因子使用,那么它们在 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) 来表示。由此可知,我们必须小心分析实验并了解它们的局限性。