用AI赚第一桶💰低成本搭建一套AI赚钱工具,源码可二开。 广告
## [臭名昭著的 goto](https://lingcoder.gitee.io/onjava8/#/book/05-Control-Flow?id=%e8%87%ad%e5%90%8d%e6%98%ad%e8%91%97%e7%9a%84-goto) [**goto**关键字](https://en.wikipedia.org/wiki/Goto)很早就在程序设计语言中出现。事实上,**goto**起源于[汇编](https://en.wikipedia.org/wiki/Assembly_language)(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 标签 } } ~~~ 输出结果: ~~~ 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**即可。 下面这个例子向大家展示了带标签的**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; } } } } } ~~~ 输出结果: ~~~ 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**: 1. 简单的一个**continue**会退回最内层循环的开头(顶部),并继续执行。 2. 带有标签的**continue**会到达标签的位置,并重新进入紧接在那个标签后面的循环。 3. **break**会中断当前循环,并移离当前标签的末尾。 4. 带标签的**break**会中断当前循环,并移离由那个标签指示的循环的末尾。 大家要记住的重点是:在 Java 里需要使用标签的唯一理由就是因为有循环嵌套存在,而且想从多层嵌套中**break**或**continue**。 **break**和**continue**标签在编码中的使用频率相对较低 (此前的语言中很少使用或没有先例),所以我们很少在代码里看到它们。 在*Dijkstra*的**“Goto 有害”**论文中,他最反对的就是标签,而非**goto**。他观察到 BUG 的数量似乎随着程序中标签的数量而增加\[^2\]。标签和**goto**使得程序难以分析。但是,Java 标签不会造成这方面的问题,因为它们的应用场景受到限制,无法用于以临时方式传输控制。由此也引出了一个有趣的情形:对语言能力的限制,反而使它这项特性更加有价值。