Java中的无限循环

83

看下面这个Java中的无限while循环。它会导致紧接着它下面的语句在编译时出现错误。

while(true) {
    System.out.println("inside while");
}

System.out.println("while terminated"); //Unreachable statement - compiler-error.
下面这个相同的无限 while 循环却可以正常工作,而且不会发出任何错误,其中我只是用一个布尔变量替换了条件。
boolean b=true;

while(b) {
    System.out.println("inside while");
}

System.out.println("while terminated"); //No error here.
在第二种情况下,循环后的语句显然是无法到达的,因为布尔变量b仍为true,但编译器却没有任何警告。为什么? 编辑: 下面这个版本的while陷入了一个明显的无限循环,但即使循环内的if条件始终为false,编译器也不会对其下方的语句发出任何编译器错误,因此循环永远不能返回并且可以在编译期间由编译器确定。
while(true) {

    if(false) {
        break;
    }

    System.out.println("inside while");
}

System.out.println("while terminated"); //No error here.

while(true) {

    if(false)  { //if true then also
        return;  //Replacing return with break fixes the following error.
    }

    System.out.println("inside while");
}

System.out.println("while terminated"); //Compiler-error - unreachable statement.

while(true) {

    if(true) {
        System.out.println("inside if");
        return;
    }

    System.out.println("inside while"); //No error here.
}

System.out.println("while terminated"); //Compiler-error - unreachable statement.

编辑: 对于ifwhile同样适用。

if(false) {
    System.out.println("inside if"); //No error here.
}

while(false) {
    System.out.println("inside while");
    // Compiler's complain - unreachable statement.
}

while(true) {

    if(true) {
        System.out.println("inside if");
        break;
    }

    System.out.println("inside while"); //No error here.
}      
以下版本的while也会陷入无限循环。
while(true) {

    try {
        System.out.println("inside while");
        return;   //Replacing return with break makes no difference here.
    } finally {
        continue;
    }
}

这是因为finally块总是会被执行,即使在try块中的return语句之前就已经遇到了。


46
谁在意呢?显然这只是编译器的一个特性。不要关注这种东西。 - CJ7
17
另一个线程如何改变局部非静态变量? - CJ7
4
对象的内部状态可能通过反射并发地被改变。这就是为什么Java语言规范要求只检查final(常量)表达式的原因。 - lsoliveira
6
我讨厌这些愚蠢的错误。无法到达的代码应该是一个警告而不是一个错误。 - user606723
只是顺便提一下...你想要在这里检测的最好适用于形式验证工具(例如模型检查器),而不是编译器。此外,通常情况下停机问题是不可判定的。 - Riccardo T.
2
@CJ7:我不会称之为“特性”,这使得实现符合Java编译器变得非常繁琐(没有理由)。享受你的设计上的供应商锁定吧。 - L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
15个回答

106

编译器可以轻松而明确地证明第一个表达式始终导致无限循环,但对于第二个表达式来说并不容易。在你的玩具示例中很简单,但如果:

  • 变量内容是从文件中读取的?
  • 变量不是本地的,可以被另一个线程修改?
  • 变量依赖于一些用户输入?

编译器显然没有检查您更简单的情况,因为它完全放弃了那条路。为什么?因为规范禁止这样做,请参见第14.21节

(顺便说一句,当变量被声明为final时,我的编译器会抱怨。)

26
这句话的重点不在于编译器能做什么,也不在于检查难易程度。而是关于编译器被允许做什么,根据Java语言规范,第二个代码版本是符合JLS的有效Java代码,因此编译错误是错误的。 - Stephen C
10
@StephenC - 谢谢您提供的信息。我已经欣然更新以反映这一点。 - Wayne
仍然是-1;你所提出的“如果”都不适用于这里。实际上,编译器确实可以解决这个问题——在静态分析术语中,这被称为常量传播,并且在许多其他情况下已经被广泛成功地使用。JLS在这里是唯一的原因,而解决这个问题的难度是无关紧要的。当然,JLS之所以首先以那种方式编写可能与解决该问题的困难程度有关,尽管我个人认为实际上是因为在不同的编译器中强制执行标准化的常量传播解决方案很困难。 - Oak
2
@Oak - 我在我的回答中承认了“在[OP的]玩具示例中很简单”,并且基于Stephen的评论,JLS是限制因素。我相信我们达成了一致。 - Wayne

55
根据规范,while语句有以下情况之一成立时方能正常完成:
  • while语句可以被执行,并且条件表达式不是一个值为true的常量表达式。
  • 存在一个可执行的break语句退出while语句。
因此,如果while条件是一个值为true的常量表达式,或者while语句内存在break语句,编译器会认为while语句后的代码是无法到达的。在第二种情况下,因为变量b的值并不是一个常量,编译器不会认为它后面的代码是无法到达的。详细信息请点击上面的链接。

3
+1 表示不仅仅是编译器编写者可以或不能想到的——JLS 告诉他们可以和不可达到哪里。 - yshavit

14

因为 true 是一个常量,而 b 可以在循环中被改变。


1
正确,但无关紧要,因为在循环中b没有被改变。同样可以认为使用true的循环可能包括一个break语句。 - deworde
只要存在另一个线程可能会更改它,编译器就不能将其称为错误。仅仅通过查看循环本身,你无法确定在循环运行时b是否会被更改。 - Peter Recore

10
因为分析变量状态很困难,所以编译器基本上已经放弃了并让你随意操作。此外,Java语言规范有明确的规则关于编译器如何允许检测到无法访问的代码
欺骗编译器的方法有很多——另一个常见的例子是:
public void test()
{
    return;
    System.out.println("Hello");
}

这种方法不会起作用,因为编译器会意识到该区域是无法访问的。相反,您可以尝试以下方法:

public void test()
{
    if (2 > 1) return;
    System.out.println("Hello");
}

这将起作用,因为编译器无法意识到表达式永远不会为假。


6
正如其他答案所述,这与编译器很难或很容易检测到无法访问的代码没有直接关系。JLS非常详细地规定了检测无法访问语句的规则。根据这些规则,第一个例子不是有效的Java代码,而第二个例子是有效的Java代码。这就是全部内容。编译器只是按规范实现这些规则。 - Stephen C
我不理解JLS的论点。编译器是否真的有义务将所有合法的Java代码转换为字节码?即使可以证明它是荒谬的。 - emory
@emory 是的,这就是符合标准的浏览器的定义,它遵守标准并编译合法的Java代码。如果您使用不符合标准的浏览器,尽管它可能具有一些不错的功能,但现在您正在依赖于繁忙的编译器设计师关于什么是“明智”规则的理论。这是一个真正可怕的情况:“为什么任何人都想在同一个if中使用两个条件语句?我从来没有这样做过”。 - deworde
这个使用 if 的示例与 while 有些不同,因为即使序列 if(true) return; System.out.println("Hello"); 编译器也会编译而不会抱怨。如我所记,这是JLS中的一个特殊例外。编译器可以像处理 while 一样轻松地检测到 if 之后的无法访问的代码。 - Christian Semrau
关于Oracle编译器-javac,@deworde提到:某人可以编写一个注解处理器,它会在编译某些(合法的)无限循环时抛出错误。到底是谁违法了?是提供注解处理编译器的Oracle,还是使用它的开发人员? - emory
2
@emory - 当你通过第三方注解处理器输入Java程序时,它在非常真实的意义上已经不再是Java了。相反,它是Java与注解处理器实现的任何语言扩展和修改所叠加的结果。但这并不是这里正在发生的事情。OP的问题涉及纯Java,并且JLS规则决定了什么是有效的纯Java程序,什么不是。 - Stephen C

6
后者并非不可达。 布尔变量b仍然有可能在循环中被改为false,从而引发结束条件。

正确,但无关紧要,因为在循环中b没有被改变。同样可以认为使用true的循环可以包括一个break语句,这将给出一个结束条件。 - deworde

4
我猜变量 "b" 可能会改变其值,所以编译器认为可以到达 System.out.println("while terminated");

4

编译器并不完美,也不应该是

编译器的职责是确认语法,而不是确认执行。在强类型语言中,编译器最终可以捕获和防止许多运行时问题,但它们无法捕获所有此类错误。

实际的解决方案是使用单元测试来补充你的编译器检查,或者使用面向对象的组件来实现已知为健壮的逻辑,而不是依赖于原始变量和停止条件。

强类型和面向对象:提高编译器的效力

一些错误是语法性质的,在Java中,强类型使得许多运行时异常可被捕获。但是,通过使用更好的类型,您可以帮助编译器强制实施更好的逻辑。

如果您想让编译器更有效地强制执行逻辑,在Java中,解决方案是构建健壮、必需的对象来强制执行这种逻辑,并使用这些对象来构建您的应用程序,而不是使用原始数据类型。

迭代器模式的一个经典例子,结合Java的foreach循环,这种构造比简单的while循环更不容易出现您所示例的错误类型。


请注意,面向对象编程不是唯一的方法来帮助静态强类型机制发现错误。Haskell 的参数化类型和类型类(与 OO 语言中所谓的类有所不同)实际上可能更擅长这个领域。 - leftaroundabout

3
编译器不够复杂,无法遍历可能包含的b值(尽管只分配了一次)。第一个示例对编译器来说很容易看出它将是一个无限循环,因为条件不是变量。

4
这与编译器的“复杂性”无关。《Java 语言规范》详细说明了检测无法到达语句的规则。根据这些规则,第一个示例不是有效的 Java 代码,而第二个示例是有效的 Java 代码。编译器的作者必须按照规定实现这些规则……否则他的编译器就不符合标准。 - Stephen C

3

我很惊讶你的编译器拒绝编译第一个案例。这对我来说似乎很奇怪。

但第二种情况并没有优化到第一种情况,因为(a)另一个线程可能会更新b的值 (b) 调用的函数可能会通过副作用修改b的值。


2
如果你感到惊讶,那么你还没有读足够的Java语言规范 :-) - Stephen C
1
哈哈 :) 知道自己的位置感觉很好。谢谢! - sarnold

3

这是因为编译器不想过多地处理琐碎的任务,但这是有可能的。

所示例子是简单且合理的,编译器容易检测到无限循环。但如果我们插入了1000行与变量b没有任何关系的代码呢?而且那些语句都是b = true;?编译器肯定可以在while循环中最终评估结果并告诉你它是真的,但它编译一个实际项目会变得多么缓慢呢?

PS,lint工具一定会为您做好这个。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接