Java 中的无限循环

看看下面这个 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;
}
}

这是因为即使 return语句在 try块本身之前遇到,也始终执行 finally块。

14104 次浏览

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

因为分析变量状态是很困难的,所以编译器基本上已经放弃了,让你做你想做的事情。此外,Java 语言规范对 如何允许编译器检测无法访问的代码有明确的规则。

欺骗编译器的方法有很多——另一个常见的例子是

public void test()
{
return;
System.out.println("Hello");
}

这是行不通的,因为编译器会意识到这个区域是不可访问的

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

这将工作,因为编译器无法实现表达式将永远不会是假。

后者并非遥不可及。 布尔值 b 仍然有可能在循环内部的某个地方被修改为 false,从而导致结束条件。

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

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

编译器可以轻松明确地证明第一个表达式 一直都是导致一个无限循环,但是对于第二个表达式就不那么容易了。在你的玩具例子中,这很简单,但是如果:

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

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

(顺便说一下,当变量声明为 final时,我的编译器 是的会报警。)

我的猜测是变量“ b”有可能改变它的值,所以编译器认为 System.out.println("while terminated"); 可以达到。

因为 true 是常量 b 可以在循环中改变。

从编译器的角度来看,while(b)中的 b可能在某个地方变成 false。

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

如果编译器最终确定布尔值在运行时将计算为 true,它将抛出该错误。编译器假设您声明的 可以变量被更改(尽管我们在这里知道作为人类它不会更改)。

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

编译器不是完美的,也不应该是强大的

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

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

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

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

如果您希望编译器更有效地强制执行逻辑,在 Java 中,解决方案是构建可以强制执行此类逻辑的健壮的必需对象,并使用这些对象构建应用程序,而不是使用原语。

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

根据 说明书,下面是关于 while 的语句。

一个 while 语句可以正常地完成 以下是正确的:

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

因此,编译器只会说,如果 while 条件是一个值为真的常量,或者 while 中有一个 break 语句,则 while 语句后面的代码是不可达的。在第二种情况下,因为 b 的值不是常量,所以它不认为它后面的代码是不可达的。在这个链接后面还有更多的信息可以告诉你更多的细节,比如什么是不可到达的,什么不是不可到达的。

表达式在运行时进行计算,因此,当用布尔变量之类的东西替换标量值“ true”时,您将标量值更改为布尔表达式,因此,编译器在编译时无法知道它。

这仅仅是因为编译器没有做太多的保姆工作,虽然这是可能的。

显示的示例对于编译器检测无限循环是简单和合理的。但是,如果我们插入1000行代码,而与变量 b没有任何关系呢?这些陈述都是 b = true;怎么样?编译器肯定可以计算结果,并告诉你它最终在 while循环中是正确的,但是编译一个真正的项目会有多慢呢?

另外,棉绒工具绝对可以为你做到这一点。

实际上,我认为没有人说得很对(至少在最初提问者的意义上不是这样)。OQ 不断提到:

正确,但是无关紧要,因为 b 在循环中没有被更改

但是没关系,因为最后一行是可到达的。如果你把这段代码编译成一个类文件,然后把这个类文件交给别人(比如说作为一个库) ,他们就可以把编译好的类和通过反射修改“ b”的代码连接起来,退出循环并执行最后一行。

任何不是常量的变量都是如此(或者 final 在使用它的位置编译成常量——如果你用 final 而不是引用它的类重新编译类,有时会导致奇怪的错误,引用类仍然保存旧值,没有任何错误)

我已经使用了反射的能力来修改另一个类的非最终私有变量,从而在购买的库中修补一个类——修复一个 bug,这样我们就可以在等待供应商的正式补丁的同时继续开发。

顺便说一句,这种方法现在可能不起作用了——虽然我以前也这么做过,但是这样一个小循环有可能会被缓存在 CPU 缓存中,而且由于变量没有标记为易失性,所以缓存的代码可能永远不会接收到新的值。我从未见过这种行为,但我相信理论上是正确的。

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