为啥 Java 会出现“无法访问的语句”编译器错误?

Posted

技术标签:

【中文标题】为啥 Java 会出现“无法访问的语句”编译器错误?【英文标题】:Why does Java have an "unreachable statement" compiler error?为什么 Java 会出现“无法访问的语句”编译器错误? 【发布时间】:2011-04-17 06:31:31 【问题描述】:

在调试程序时,我经常发现在代码块中插入 return 语句很方便(尽管可以说是不好的做法)。我可能会在 Java 中尝试这样的事情 ....

class Test 
        public static void main(String args[]) 
                System.out.println("hello world");
                return;
                System.out.println("i think this line might cause a problem");
        

当然,这会产生编译器错误。

Test.java:7: 无法访问的语句

我可以理解为什么警告可能是正当的,因为有未使用的代码是不好的做法。但我不明白为什么这需要产生错误。

这只是 Java 试图成为 Nanny,还是有充分的理由让它成为编译器错误?

【问题讨论】:

Java 对此也不完全一致。对于一些导致死代码的控制流,但 Java 没有抱怨。对其他人来说确实如此。确定哪些代码死了是一个无法计算的问题。我不知道为什么 Java 决定开始一些它无法完成的事情。 【参考方案1】:

因为无法访问的代码对编译器来说毫无意义。虽然使代码对人们有意义比使其对编译器有意义更重要也更难,但编译器是代码的基本消费者。 Java 的设计者认为对编译器没有意义的代码是错误的。他们的立场是,如果你有一些无法访问的代码,你就犯了一个需要修复的错误。

这里有一个类似的问题:Unreachable code: error or warning?,其中作者说“我个人强烈认为这应该是一个错误:如果程序员编写一段代码,它应该总是有实际运行它的意图在某些情况下。”显然 Java 的语言设计者同意。

无法访问的代码是否应该阻止编译是一个永远不会达成共识的问题。但这就是 Java 设计者这样做的原因。


cmets 中的许多人指出,Java 不会阻止编译有许多无法访问的代码类。如果我正确理解了 Gödel 的后果,那么任何编译器都不可能捕获所有无法访问的代码类。

单元测试无法捕捉到每一个错误。我们不会将此作为反对其价值的论据。同样,编译器无法捕获所有有问题的代码,但它仍然可以在可能的情况下防止错误代码的编译。

Java 语言设计者将无法访问的代码视为错误。所以在可能的情况下阻止它编译是合理的。


(在你投反对票之前:问题不在于 Java 是否应该有一个无法访问的语句编译器错误。问题是 为什么Java 有一个无法访问的语句编译器错误。不要仅仅因为你认为 Java 做出了错误的设计决定。)

【讨论】:

Java 的语言设计者显然同意 “Java 的语言设计者”是人类,也容易出错。就个人而言,我觉得你提到的讨论中的另一方有更强有力的论据。 @Gabe:因为它不是无害的——这几乎可以肯定是一个错误。要么你把你的代码放在了错误的地方,要么你误解了你以这样一种方式编写了你的​​语句,以至于某些语句无法访问。将此设为错误可防止编写不正确的代码,如果您希望代码中某个无法访问的位置供其他开发人员阅读(无法访问代码的唯一受众),请改用注释。 SamStephens:看到未调用的方法和未使用的变量本质上都是无法访问的代码形式。为什么允许某些表格而不允许其他表格?特别是,为什么不允许这种有用的调试机制? 不是cmets也无法访问吗?在我看来,无法访问的代码实际上是 cmets 的一种形式(这是我之前尝试做的,等等)。但是考虑到“真正的” cmets 也不是“可达的”......也许他们也应该提出这个错误;) 问题是他们(java语言设计者)与此不一致。如果有实际的流程分析,很容易意识到return; System.out.println();if(true) return; System.out.println(); 都保证具有相同的行为,但一个是编译错误,另一个不是。所以,当我在调试时,我使用这种方法临时“注释掉”代码,我就是这样做的。注释掉代码的问题是你必须找到合适的地方来停止你的注释,这可能比扔return;真正快。【参考方案2】:

没有明确的理由为什么不允许不可到达的语句;其他语言允许他们没有问题。对于您的特定需求,这是通常的技巧:

if (true) return;

看起来很荒谬,任何阅读代码的人都会猜测它一定是故意的,而不是让其余语句无法访问的粗心错误。

Java 对“条件编译”有一点支持

http://java.sun.com/docs/books/jls/third_edition/html/statements.html#14.21

if (false)  x=3; 

不会导致编译时 错误。优化编译器可能 意识到语句 x=3;将要 永远不会被执行,并且可以选择 省略该语句的代码 生成的类文件,但是 声明 x=3;不被视为 技术意义上的“无法到达” 此处指定。

这种不同的理由 治疗是让程序员 定义“标志变量”,例如:

static final boolean DEBUG = false;

然后写代码如:

if (DEBUG)  x=3; 

这个想法应该是可能的 将 DEBUG 的值从 假到真或从真到假 然后正确编译代码 没有对程序进行其他更改 文本。

【讨论】:

仍然不明白为什么你会为你展示的把戏而烦恼。无法访问的代码对编译器来说毫无意义。所以它的唯一受众是开发人员。使用评论。虽然我猜如果你是临时添加一个 return in,使用你的解决方法可能比仅仅输入一个 return 并注释掉以下代码要快一些。 @SamStephens 但是每次你想切换时,你都必须记住你必须注释掉代码的所有地方,以及你必须做相反的事情的地方。您将不得不通读整个文件,更改源代码,可能会时不时地犯一些细微的错误,而不是在某个地方将“true”替换为“false”。我认为最好将切换逻辑编码一次,除非必要,否则不要触摸它。 "读过代码的人都会猜到一定是故意的"。这正是我认为的问题,人们不应该猜测,他们应该理解。记住代码是与人的交流,而不仅仅是对计算机的指令。如果它不是非常临时的,我认为你应该使用配置。看看你上面显示的内容,if (true) return; 并没有说明你为什么要跳过逻辑,而if (BEHAVIOURDISABLED) return; 传达的是意图。 这个技巧对于调试非常有用。我经常添加一个 if(true) 返回来消除试图找出失败的方法的“其余部分”。还有其他方法,但这既干净又快捷——但这不是你应该检查的东西。【参考方案3】:

是保姆。 我觉得 .Net 做对了——它会针对无法访问的代码发出警告,但不会引发错误。收到警告是件好事,但我认为没有理由阻止编译(尤其是在调试会话期间,可以很好地返回以绕过某些代码)。

【讨论】:

java 设计得比较早,当时每个字节都很重要,软盘还是高科技。 对于debug early return,需要另外五秒注释掉你early return之后的行。通过使无法访问的代码成为错误来防止错误的小代价。 编译器也很容易排除无法访问的代码。没有理由强迫开发者这样做。 @SamStephens 如果您已经有 cmets,则不要这样做,因为 cmets 不会堆叠,因此使用 cmets 解决此问题是不必要的痛苦。 @SamStephens 注释掉的代码不能很好地重构。【参考方案4】:

我只是注意到这个问题,并想将我的 $.02 添加到此。

对于 Java,这实际上不是一个选项。 “无法访问的代码”错误并非来自 JVM 开发人员认为保护开发人员免受任何事情或格外警惕的事实,而是来自 JVM 规范的要求。

Java 编译器和 JVM 都使用所谓的“堆栈映射” - 有关堆栈上所有项目的明确信息,如为当前方法分配的那样。必须知道堆栈的每个槽的类型,以便 JVM 指令不会将一种类型的项目误认为另一种类型。这对于防止将数值用作指针最为重要。可以使用 Java 程序集尝试推送/存储一个数字,然后弹出/加载一个对象引用。但是,JVM 将在类验证期间拒绝此代码,即在创建堆栈映射并测试其一致性时。

要验证堆栈映射,VM 必须遍历方法中存在的所有代码路径,并确保无论执行哪个代码路径,每条指令的堆栈数据都与之前的任何指令一致代码已推送/存储在堆栈中。所以,在简单的情况下:

Object a;
if (something)  a = new Object();  else  a = new String(); 
System.out.println(a);

在第 3 行,JVM 将检查 'if' 的两个分支是否仅存储到一个(这只是本地 var#0)中与 Object 兼容的东西(因为这就是第 3 行及以后的代码将如何对待本地变量#0)。

当编译器遇到无法访问的代码时,它并不完全知道堆栈此时可能处于什么状态,因此无法验证其状态。此时它无法完全编译代码,因为它也无法跟踪局部变量,因此不会将这种歧义留在类文件中,而是会产生致命错误。

当然,像if (1<2) 这样的简单条件会愚弄它,但这并不是真正的愚弄——它给了它一个可能导致代码的潜在分支,并且至少编译器和 VM 都可以确定堆栈的方式从那里可以使用物品。

附:我不知道 .NET 在这种情况下做了什么,但我相信它也会编译失败。对于任何机器代码编译器(C、C++、Obj-C 等)来说,这通常不会成为问题

【讨论】:

Java 的死代码警告系统有多激进?是否可以表示为语法的一部分(例如 [flowingstatement;]* =>flowingstatement [flowingstatement;]* stoppingstatement; =>stoppingstatementreturn=>stoppingstatementif (condition) stoppingstatement; else stoppingstatement=>stoppingstatement; 等? 我不擅长语法,但我相信是的。然而,“停止”可以是本地的,例如循环内的“中断”语句。此外,如果方法为 void,则方法的结尾是方法级别的隐式停止语句。 'throw' 也将是一个明确的停止语句。 Java 标准对 void blah() try return; catch (RuntimeError ex) DoSomethingElse(); 之类的东西会说什么 Java 会抱怨 try 块实际上不可能通过异常退出,或者它会认为任何类型的未经检查的异常可能发生在任何try 块内? 找出答案的最好方法是尝试编译它。但是编译器甚至允许空的 try/catch 语句。但是,理论上任何 JVM 指令都可以抛出异常,因此 try 块可能会退出。但我不确定这与这个问题有什么关系。 基本上,问题是唯一被禁止的“不可达”语句是否是那些可以在解析的基础上被识别为不可达的语句,而不必实际评估任何表达式,或者标准是否可以禁止不可达像if (constantThatEqualsFive < 3) doSomething;; 这样的语句。【参考方案5】:

编译器的目标之一是排除错误类别。一些无法访问的代码是偶然出现的,javac 在编译时排除了该类错误,这很好。

对于每条捕获错误代码的规则,都会有人希望编译器接受它,因为他们知道自己在做什么。这是编译器检查的惩罚,而获得正确的平衡是语言设计的技巧之一。即使经过最严格的检查,仍然可以编写无限数量的程序,所以事情不会那么糟糕。

【讨论】:

【参考方案6】:

虽然我认为这个编译器错误是一件好事,但有一种方法可以解决它。 使用你知道会成立的条件:

public void myMethod()

    someCodeHere();

    if(1 < 2) return; // compiler isn't smart enough to complain about this

    moreCodeHere();


编译器不够聪明,无法抱怨。

【讨论】:

这里的问题是为什么?无法访问的代码对编译器来说毫无意义。所以它的唯一受众是开发人员。使用评论。 所以我被否决了,因为我同意 java 人设计编译的方式?哎呀... 您因未回答实际问题而遭到反对。请参阅 SamStephens 的评论。 javac 绝对可以推断出1&lt;2 为真,其余方法不必包含在字节码中。 “不可达语句”的规则必须清晰、固定,并且独立于编译器的智能。 @Sam 以这种态度,您将不得不对本网站 50% 的最佳答案投反对票(我并不是说我的答案就是其中之一)。与任何 IT 咨询情况一样,回答有关 SO 的问题的很大一部分是从给定问题中找出真正的问题(或相关的附带问题)。【参考方案7】:

抱怨编译器越严格越好,这当然是一件好事,只要它允许你做你需要的事情。 通常要付出的小代价是把代码注释掉,好处是当你编译你的代码时可以工作。一个普遍的例子是 Haskell,人们尖叫直到他们意识到他们的测试/调试只是主要测试和短测试。我个人在 Java 中几乎不进行调试,而(实际上是故意)不专心。

【讨论】:

【参考方案8】:

如果允许if (aBooleanVariable) return; someMoreCode; 的原因是允许标志,那么if (true) return; someMoreCode; 不生成编译时错误这一事实似乎与生成 CodeNotReachable 异常的策略不一致,因为编译器“知道”@987654323 @ 不是标志(不是变量)。

另外两种可能很有趣,但不适用于关闭部分方法代码的方法以及if (true) return

现在,您可能不想说if (true) return;,而是说assert false 并将-ea OR -ea package OR -ea className 添加到jvm 参数中。好处是这允许一些粒度,并且需要在 jvm 调用中添加一个额外的参数,因此不需要在代码中设置 DEBUG 标志,而是在运行时添加参数,这在目标不是开发者机器和重新编译和传输字节码需要时间。

还有System.exit(0)的方式,但这可能有点过头了,如果你把它放在Java中的JSP中,它就会终止服务器。

除了 Java 被设计为一种“保姆”语言之外,我更愿意使用 C/C++ 之类的原生语言来获得更多控制。

【讨论】:

将静态初始化块中设置的static final 变量更改为在其声明中设置的static final 变量不应是重大更改。完全有可能的是,当一个类第一次编写时,它有时可能无法做某事(直到运行时才知道它是否能够做到),但该类的更高版本可能已经能够总是做那个动作。将myClass.canFoo 更改为常量应该会使if (myClass.canFoo) myClass.Foo(); else doSomethingElse(); 更有效率——而不是破坏它。

以上是关于为啥 Java 会出现“无法访问的语句”编译器错误?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我在 Java 中出现无法访问的语句错误?

java 为啥使用continue会使程序错误

代码中显然有一个无法访问的语句,但可以编译 - 为啥? [复制]

编译错误 - 无法访问的语句

编译问题:无法访问的语句

java 为啥文件不加入Source中编译就出现source not found