Java下Eclipse中的不可访问代码错误与死代码警告有何区别?

54

有没有人知道为什么:

public void foo()
{
    System.out.println("Hello");
    return;
    System.out.println("World!");
}

在Eclipse下将报告为"不可到达的错误(unreachable error)",但是

public void foo()
{
    System.out.println("Hello");
    if(true) return;
    System.out.println("World!");
}
只会触发“死代码”警告吗?
我能想到的唯一解释是Java编译器仅标记第一个,而Eclipse中的一些额外分析则找出了第二个。然而,如果是这种情况,为什么Java编译器不能在编译时找到这种情况呢?
Java编译器难道不会在编译时找出if(true)没有效果,从而生成基本相同的字节码吗?在什么时候应用可达代码分析?
我想更一般地问这个问题:“何时应用可达代码分析”?在将第二个Java代码片段转换为最终字节码时,我确定“if(true)”的运行时等效会被删除,并且两个程序的表示变得相同。难道Java编译器不会再次应用其可达代码分析吗?
8个回答

34

第一个代码段无法编译(会报错),而第二个代码段可以编译(只是收到了警告)。这就是区别所在。

至于为什么Eclipse可以检测到死代码,那只是一个具有内置编译器并且可以进行更精细调整以检测此类代码的集成开发工具的便利之处,相较于JDK。

更新:JDK实际上可以消除死代码。

public class Test {
    public void foo() {
        System.out.println("foo");
        if(true)return;
        System.out.println("foo");
    }
    public void bar() {
        System.out.println("bar");
        if(false)return;
        System.out.println("bar");
    }
}

javap -c说:

public class Test extends java.lang.Object{
public Test();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."":()V
   4:   return

public void foo();
  Code:
   0:   getstatic       #2; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   ldc             #3; //String foo
   5:   invokevirtual   #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   8:   return

public void bar();
  Code:
   0:   getstatic       #2; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   ldc             #5; //String bar
   5:   invokevirtual   #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   8:   getstatic       #2; //Field java/lang/System.out:Ljava/io/PrintStream;
   11:  ldc             #5; //String bar
   13:  invokevirtual   #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   16:  return
}

至于为什么(Sun)不对此发出警告,我不知道 :) 至少JDK编译器实际上具有内置的DCE(死代码消除)。


2
嘿Uri,你可以用“boolean x=true; if(x)”替换“if(true)”。这不是编译器的工作,因为你可以在执行期间设置断点并更改x的值。 - Yoni
3
Java编译器不会为未执行的代码块生成字节码。如果一个表达式在运行时求值为false,仍将生成代码。唯一不生成字节码的情况是表达式在编译时就被判断为false。至少这是我对它的工作原理的理解。 - ChadNC
我会给你点赞,但第一句话只是“半对”的。Java编译器不允许编译第一个片段,而Eclipse可以决定不允许第二个片段,甚至完全忽略它。这更像是一个lint样式检查,第一个是语言强制要求的。有关详细信息,请参见我的答案。 - Paul Wagland
@BalusC:我同意,但我认为重要的是要指出为什么你不能忽略第一个错误。这是一项由语言规定的检查,第二个错误只是Eclipse开发人员认为有用的东西。 - Paul Wagland
5
Java 语言规范专门允许将求值为 truefalse 的常量 if 表达式作为一种开启或关闭代码的方式,例如调试或诊断代码,只需“翻转开关”。表达式 if(true)if(DEBUGS) 属于这个类别。因此,无论是 Eclipse 还是任何其他 Java 编译器都不允许将它们视为错误。更重要的是,它明确要求如果这样的常量保护代码求值为 false,则应将其删除。 - Lawrence Dol
显示剩余12条评论

26

根据Java语言规范,无法到达的代码是一个错误。

引用自JLS:

其核心思想是从构造函数、方法、实例初始化程序、静态初始化程序的开头到包含该语句的本身之间必须存在一些可能的执行路径。分析考虑了语句的结构。除了while、do和for语句的条件表达式具有常量值true的特殊处理外,不考虑表达式的值在流程分析中。

这意味着if块不被考虑在内,因为如果您通过if语句的某个路径,则可以到达最终的打印语句。如果您更改代码如下:

public void foo() {
    System.out.println("Hello");
    if (true)
        return;
    else
        return;
    System.out.println("World!");
}

突然间它无法编译了,因为if语句中没有任何路径可以到达最后一行。

也就是说,符合Java规范的编译器不允许编译您的第一个代码片段。继续引用JLS:

例如,以下语句导致编译时错误:

while (false) { x=3; }

因为语句 x=3; 是无法到达的,但表面上类似的情况是:

if (false) { x=3; }

在编译时不会出现错误。优化编译器可能会意识到语句 x=3; 永远不会被执行,并选择省略生成的类文件中的该语句的代码,但是从技术上规定的意义上讲,语句 x=3; 不被视为“无法访问”。

Eclipse 给出的第二个警告与死代码有关,这是编译器生成的警告,根据 JLS 的规定并非“无法访问”,但在实际应用中却是如此。 这是Eclipse提供的额外lint风格检查。 这完全是可选的,通过使用Eclipse配置可以将其禁用,或将其转换为编译器错误而不是警告。

这个第二个块是一种“代码异味”,if (false)块通常用于禁用调试目的的代码,留下它通常是意外的,因此会发出警告。

事实上,Eclipse还进行了更高级的测试,以确定 if 语句的可能值,以确定是否可能采取两条路径。例如,Eclipse 还会抱怨以下方法中的死代码:

public void foo() {
    System.out.println("Hello");
    boolean bool = Random.nextBoolean();
    if (bool)
        return;
    if (bool || Random.nextBoolean())
      System.out.println("World!");
}

第二个 if 语句将会产生无法访问的代码,因为它可以推断出在此时代码中 bool 变量只能是 false。在这样一个短的代码片段中,两个 if 语句测试的内容显然相同,但如果代码中有10-15行的中间代码,那么这个差异可能就不再那么明显了。

因此,总结一下,两者之间的区别:一个被 JLS 禁止,而另一个则没有,但 Eclipse 将其检测作为向程序员提供的服务。


有趣的是,他们在“特殊处理”的控制流语句列表中没有包括“if”。我想知道为什么? - meriton
2
哦,我明白了:特殊处理就是为了检测明显的无限循环。'If' 不是一个循环语句。(我今天有点迟钝…) - meriton
@meriton:我添加了JLS中适当的部分,以展示他们试图解决的问题。 - Paul Wagland
1
第二个 if 可能为 true,因为 Random.nextBoolean 可能返回 true。也许你的意思是 && - Mordechai

14

这是为了允许一种条件编译
对于if,它不是一个错误,但编译器会标记whiledo-whilefor的错误。
这样做没问题:

if (true) return;    // or false
System.out.println("doing something");

这些是错误

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

while (false) {
    System.out.println("unreachable");
}

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

for(;;) {
}
System.out.println("unreachable");

这在JLS 14.21:不可到达的语句的结尾进行了解释:

The rationale for this differing treatment is to allow programmers to define "flag variables" such as:

 static final boolean DEBUG = false;

and then write code such as:

   if (DEBUG) { x=3; }

The idea is that it should be possible to change the value of DEBUG from false to true or from true to false and then compile the code correctly with no other changes to the program text.


2
+1 是针对行为差异背后的原因。 - Thorkil Holm-Jacobsen

2

我认为总结一下,不可达代码很可能是个错误,JLS试图保护你免受这些错误的影响。

如果你真的想故意编写这样的代码,允许if (true) return;是一个好的解决办法。如果JLS停止这样做,那就会妨碍你的工作。此外,它还应该阻止以下情况:

 public static boolean DEBUG = true; //In some global class somewhere else


 ...

 if (DEBUG) return; //in a completely unrelated class.

 ...

因为DEBUG常量完全是内联的,并且在该if条件中与只键入true函数上等效。从JLS的角度来看,这两种情况非常相似。


2
< p > if (true) 比 "不可达" 更微妙一些;因为那个硬编码的 return 总是会使后面的代码无法到达,但是改变 if 中的条件可能会使后面的语句可达。< /p > < p > 在那里有一个条件语句意味着条件可能会改变。有时括号中会出现比 true 更复杂的东西,对于人类读者来说,以下代码被“消除”并不明显,但编译器会注意到,所以它能够警告你。< /p > < p > 这里提到了 Eclipse,并且这让用户感觉更加复杂;但实际上,在 Eclipse 下面只是一个(非常复杂的)Java 编译器,恰好具有许多用于打开和关闭警告等的开关。换句话说,您无法从直接的 javac 编译中获得各种不同的警告/错误,也没有方便的方法来全部打开或关闭它们。但它是相同的交易,只是带有更多的花哨功能。< /p >

我不确定我理解这里的微妙之处。条件怎么会改变呢?由于我使用的是文字,所以在运行时无法更改它。 - Uri
Yoni在BalusC的答案下的示例是我在第二段中试图表达的一个很好的实际例子。 - Carl Smotricz

0

运行时和编译时语义的区别在于代码是否可达。在你的第二个例子中,该代码会被编译成一个 if-else 分支的字节码,并且 Eclipse 只是聪明地告诉你,在运行时 else 部分永远不会被执行。Eclipse 只是警告你,因为它仍然是合法的代码。

在你的第一个例子中,这是一个错误,因为该代码在 Java 的定义下是非法的。编译器不允许你创建包含无法到达的语句的字节码。


0

如果您希望忽略Java下Eclipse中的“死代码警告”,请在Eclipse内执行以下操作:

  1. 点击“Window-Preferences-Java-Compiler-Errors/Warnings”
  2. 点击“Potential Programming Problems”
  3. 选择“忽略”“Dead Code eg if(false)”
  4. 点击“应用”
  5. 点击“确定”

保存并关闭您的Eclipse IDE。当您重新打开Eclipse时,这些特定的警告将不再列出。

*对于此示例解决方案,我使用的是Eclipse IDE for Java Developers - Version: Mars.2 Release (4.5.2)


0

我在Eclipse上进行了一些尝试,认为JDK有三种死代码处理方式: 1)无警告,2)警告和3)错误。

对于典型的"IF"条件编译代码,JDK会检测到并不报告其为死代码。 对于由常量布尔标志引起的死代码,JDK在警告级别上报告它。 对于由程序控制流引起的死代码,JDK将其检测为错误。

以下是我的尝试:

    public class Setting {
        public static final boolean FianlDebugFlag = false;
    }


    class B {
    .....

    // no warn, it is typical "IF" conditional compilataion code
    if(Setting.FianlDebugFlag) 
        System.out.println("am i dead?");   
    if(false) 
        System.out.println("am i dead?");   


    // warn, as the dead code is caused by a constant boolean flag
    if(ret!=null && Setting.FianlDebugFlag) 
        System.out.println("am i dead?");   

    if(Setting.FinalDebug)                  
        return null;                                                            
    System.out.println("am i dea?");        

    // error, as the dead code is due to the program's control flow
    return null;
    System.out.println("am i dead");        
    }

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