编程语言从一开始就有 goto 关键字。甚至可以说,goto 是汇编语言里程序控制的起源:“若条件 A 成立,则跳到这里;否则跳到那里”。如果阅读编译器最终生成的汇编代码,你就会发现程序控制流中包含了许多跳转。(Java 编译器会生成自己的 “汇编代码”,但这个代码运行在 Java 虚拟机上,而不是直接运行在 CPU 硬件上。)
goto 是在源码级别上进行的跳转,这给它带来了坏名声。如果一个程序总是从一个地方跳到另一个地方,难道不应该有其他更好的方式来重组代码,让它的控制流不这么不可控吗?随着 Edsger Dykstra 的著名论文 “Go To Statement Considered Harmful” 的发表,goto 开始失宠了。自那以后,抨击 goto 变成了时尚,提倡废弃这个关键字的人则急着找证据。
正如很多相似情况下的典型做法,遵守中庸之道是最富有成效的。真正的问题并不在于 goto 本身,而在于滥用 goto。在极少数场景下,goto 实际上是组织控制流最好的方式。
尽管 goto 是 Java 中的一个保留字,但 Java 中并没有使用它——Java 没有 goto。不过 Java 也有一些类似于跳转的操作,这些操作与 break 和 continue 关键字有关。它们不是跳转,而只是中断循环的一种方式。之所以和goto一起讨论,是因为它们使用了相同的机制:标签。
标签是以冒号结尾的标识符:label1:
在 Java 中,放置标签的唯一地方是正好在迭代语句之前。“正好”的意思就是,不要在标签和迭代之间插入任何语句。在迭代之前使用标签的唯一原因是,你要在这个迭代里再嵌套一个迭代或一个 switch(很快就会学到它)。这是因为 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 处,然后它实际上会重新进入外部迭代开始继续执行。
[4] 这里的 break label1 也会中断所有迭代,跳回到 label1 处,不过它并不会重新进入外部迭代。它实际是完全跳出了两个迭代。
带标签的 break 和带标签的 continue 也可以用于 for 循环:
// control/LabeledFor.java
// 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;
}
}
}
}
// 此处不能有标签
}
}
/* 输出:
i = 0
continue inner
i = 1
continue inner
i = 2
continue
i = 3
break
i = 4
continue inner
i = 5
continue inner
i = 6
continue inner
i = 7
continue outer
i = 8
break outer
注意 break 中断了 for 循环,而 for 循环在执行到末尾之前,它的递增表达式不会执行。因为 break 导致递增表达式被跳过,所以我们在 i == 3 的分支下直接执行递增运算。i == 7 的分支也是这样,continue outer 语句会跳到外部循环顶部,并且跳过内部循环的递增表达式执行,因此我们在这里也进行了直接递增。
如果没有 break outer 语句,我们就没有办法从内部循环直接跳出外部循环。这是因为 break 本身只能中断最内层的循环(continue 也是一样)。
如果要在中断循环的同时退出方法,直接用 return 就可以了。
下面这个例子展示了 while 循环里的带标签的 break 和带标签的 continue:
// control/LabeledWhile.java
// while循环里带标签的break和带标签的continue
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;
}
}
}
}
}
/* 输出:
Outer while loop
i = 1
continue
i = 2
i = 3
continue outer
Outer while loop
i = 4
i = 5
break
Outer while loop
i = 6
i = 7
break outer
*/
同样的规则也适用于 while。
- 普通的 continue 会跳到最内层循环的起始处,并继续执行。
- 带标签的 continue 会跳到对应标签的位置,并重新进入这个标签后面的循环。
- 普通的 break 会 “跳出循环的底部”,也就是跳出当前循环。
- 带标签的 break 会跳出标签所指的循环。
一定要记住,在 Java 里使用标签的唯一理由就是你用到了嵌套循环,而且你需要使用 break 或 continue 来跳出多层的嵌套。
带标签的 break 和 continue 是较少使用的试验性功能,在此前的编程语言中几乎没有先例。
Dijkstra 在他的论文 “Go To Statement Considered Harmful” 中,特别反对使用的是标签,而非 goto。他观察到,在一个程序里随着标签的增多,错误的数量也跟着上升2,并且标签和 goto 也使得程序难以分析。注意 Java 的标签不会有这些问题,因为它被限定了应用场景,不能通过点对点跳转的方式改变程序的控制流程。通过限制一个语言特性的使用,我们反而使其更加有用。
2请注意,这似乎是一个很难证明的断言,并且很可能是属于“相关-因果关系”认知谬误的一个例子。