Java中的无限循环


82

请看下面的whileJava无限循环。它导致其下面的语句的编译时错误。

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即使return语句在try块本身中遇到之前也总是执行该块。


46
谁在乎?显然,这只是编译器的功能。不要担心这种事情。
CJ7

17
另一个线程如何更改局部非静态变量?
CJ7 '12

4
对象的内部状态可能会通过反射同时更改。这就是为什么JLS要求仅检查最终(常量)表达式的原因。
lsoliveira 2011年

5
我讨厌这些愚蠢的错误。无法访问的代码应该是警告而不是错误。
user606723 2011年

2
@ CJ7:我不会将其称为“功能”,并且实现合规的Java编译器非常无聊(无缘无故)。享受您按设计的供应商锁定。
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

Answers:


105

编译器可以轻松而明确地证明第一个表达式始终会导致无限循环,但第二个表达式则不那么容易。在您的玩具示例中,这很简单,但是如果:

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

编译器显然不会检查您的简单情况,因为它完全放弃了这条路。为什么?因为它更难被禁止规范。参见第14.21节

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


25
-1-与编译器的功能无关。支票变得更容易或更难也没什么。这是关于Java语言规范允许编译器执行的操作。根据JLS,该代码的第二个版本是有效的Java,因此编译错误将是错误的。
斯蒂芬·C

10
@StephenC-感谢您提供该信息。愉快地进行了更新以反映出更多信息。
韦恩

仍为-1;您的“假设假设”均不适用于此处。就其本身而言,编译器确实可以解决问题-用静态分析术语来说,这称为常量传播,并且已在许多其他情况下成功地广泛使用。JLS是唯一的原因,解决问题的难度并不重要。当然,首先以这种方式编写JLS的原因可能与该问题的难度有关,尽管我个人怀疑这实际上是因为很难在不同的编译器之间实施标准化的常量传播解决方案。
橡树

2
@Oak-我在回答中承认“在[OP的玩具示例中,这很简单”,并且根据Stephen的评论,JLS是限制因素。我很确定我们同意。
韦恩

55

根据规范,以下是关于while语句的内容。

如果满足以下至少一项条件,则while语句可以正常完成:

  • while语句是可到达的,并且条件表达式不是值为true的常量表达式。
  • 有一个可达的break语句退出while语句。\

因此,如果while条件是具有真值的常量,或者while内有break语句,则编译器只会说在while语句之后的代码不可访问。在第二种情况下,由于b的值不是常数,因此它不认为其后的代码不可访问。该链接后面有很多信息,可让您详细了解什么是不可达的内容。


3
+1表示不只是编译器编写者可以考虑或不能考虑的内容-JLS告诉他们他们可以和不能考虑的东西。
yshavit 2011年

14

因为true是常数,所以b可以在循环中更改。


1
正确但无关紧要,因为b不会在循环中更改。您可以同样认为使用true的循环可能包含break语句。
deworde 2011年

@deworde-只要有可能被其他线程更改(例如,另一个线程),编译器就无法将其称为错误。您不能仅通过查看循环本身来判断b在循环运行时是否将被更改。
Peter Recore 2011年

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代码。而且,如果您使用的是不符合标准的浏览器,虽然它可能具有一些不错的功能,但您现在依赖的是繁忙的编译器设计者关于“明智”规则的理论。这是一个真正可怕的场景:“为什么任何人都想在同一条件下使用两个条件?我永远都不会”)
deworde 2011年

这个示例使用if与稍有不同while,因为编译器甚至可以编译序列if(true) return; System.out.println("Hello");而不会抱怨。IIRC,这是JLS中的特殊例外。和if一样简单,编译器将能够检测到无法访问的代码while
Christian Semrau 2011年

2
@emory-当您通过第三方注释处理器提供Java程序时,它实际上是……不再是Java。相反,它是Java覆盖的注释处理器实现的任何语言扩展和修改。但这不是这里发生的事情。OP的问题是关于香草Java的,而JLS规则控制什么是有效的香草Java程序,什么不是有效的Java程序。
斯蒂芬·C

6

后者并非无法实现。布尔值b仍然有可能在循环内某个地方被更改为false,从而导致结束条件。


正确但无关紧要,因为b不会在循环中更改。您可能同样会争辩说,使用true的循环可能包含break语句,这将给出结束条件。
deworde 2011年

4

我的猜测是变量“ b”有可能更改其值,因此编译器认为 System.out.println("while terminated"); 可以实现。


4

编译器并不完美-也不应该如此

编译器的责任是确认语法-而不是确认执行。编译器最终可以用强类型的语言捕获并防止许多运行时问题-但是它们不能捕获所有此类错误。

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

强大的输入和面向对象:提高编译器的效率

有些错误本质上是语法上的-在Java中,强类型使很多运行时异常都可捕获。但是,通过使用更好的类型,您可以帮助编译器执行更好的逻辑。

如果希望编译器更有效地执行逻辑,则在Java中,解决方案是构建可以实施这种逻辑的强大必需对象,并使用这些对象来构建应用程序,而不是基元。

一个典型的例子是迭代器模式的使用,结合Java的foreach循环,此构造比简单的while循环更不容易受到您所说明的错误类型的影响。


请注意,OO不是帮助静态强类型化机制发现错误的唯一方法。实际上,Haskell的参数化类型和类型类(与OO语言中所谓的类有些不同)实际上可以说是更好的选择。
左右5点37分左右

3

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


4
这与编译器的“复杂性”无关。JLS竭尽全力指定用于检测不可达语句的规则。根据这些规则,第一个示例不是有效的Java,第二个示例是有效的Java。而已。编译器编写者必须执行指定的规则...否则他的编译器将不合格。
斯蒂芬·C

3

我很惊讶您的编译器拒绝编译第一种情况。我觉得这很奇怪。

但是第二种情况并未针对第一种情况进行优化,因为(a)另一个线程可能会更新b(b)的值;(b)被调用的函数可能会修改b副作用的值。


2
如果您感到惊讶,那么您对Java语言规范的了解还不够:-)
Stephen C

1
哈哈:)很高兴知道我的立场。谢谢!
sarnold 2011年

3

实际上,我认为没有人能做到完全正确(至少从原始提问者的角度而言)。OQ不断提到:

正确,但无关紧要,因为循环中未更改b

但这并不重要,因为最后一行是可以到达的。如果您使用该代码,将其编译为类文件,然后将该类文件交给其他人(例如作为库),他们可以将编译后的类与通过反射修改“ b”的代码链接,退出循环并导致最后一个行执行。

对于不是常量的任何变量(或在使用它的位置编译为常量的final变量)都是如此,如果您用final而不是引用它的类重新编译该类,有时会导致奇怪的错误。类仍将保留旧值,而不会出现任何错误)

我已经使用了反射功能来修改另一个类的非最终私有变量,以在购买的库中猴子类上打补丁—修复了一个错误,以便我们可以在等待供应商的正式补丁时继续开发。

顺便说一句,这几天实际上可能无法正常工作-尽管我之前已经做过,但这样的小循环很有可能会被缓存在CPU缓存中,并且由于变量未标记为volatile,所以缓存的代码可能永远不会获得新的价值。我从未见过这样的动作,但我相信这在理论上是正确的。


好点子。我假设这b是一个方法变量。您真的可以使用反射修改方法变量吗?无论如何,总的来说,我们不应该假设最后一行是无法到达的。
emory

uch,您是对的,我假设b是成员。如果b是一个方法变量,那么我相信编译器“可以”知道它不会改变,但是不会改变(这使得所有其他答案毕竟很正确)
Bill K

3

这只是因为编译器虽然有可能,但不会过多地照顾婴儿。

所示示例对于编译器检测无限循环而言是简单合理的。但是,如果我们插入1000行代码而不与变量有任何关系b呢?这些陈述全都b = true;如何?编译器肯定可以评估结果,并告诉您最终在while循环中为真,但是编译一个实际的项目有多慢?

PS,皮棉工具绝对应该为您做。


2

从编译器的角度来看,binwhile(b)可能会在某处更改为false。编译器只是不打扰检查。

有趣的尝试while(1 < 2)for(int i = 0; i < 1; i--)等等。


2

表达式是在运行时求值的,因此,当将标量值“ true”替换为布尔值变量时,会将标量值更改为布尔值表达式,因此,编译器无法在编译时知道它。


2

如果编译器可以最终确定布尔值将true在运行时求值,则它将引发该错误。编译器假定您声明的变量可以更改(尽管在这里我们是人类,但不会)。

为了强调这一事实,如果final在Java中声明了变量,则大多数编译器将抛出与替换值相同的错误。这是因为变量是在编译时定义的(并且不能在运行时更改),因此编译器可以最终确定表达式true在运行时的计算结果。


2

第一条语句始终会导致无限循环,因为我们在while循环的条件下指定了一个常量,在第二种情况下,编译器假定,循环内b的值可能会发生变化。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.