1/0是一个合法的Java表达式吗?

43

以下代码在我的Eclipse中可以顺利编译:

final int j = 1/0;
// compiles fine!!!
// throws ArithmeticException: / by zero at run-time

Java的优点之一是它可以在代码编译时就防止许多“愚蠢的代码”(例如,"Five" instanceof Number不能编译!)。因此,这个事实甚至没有产生警告就更加令人惊讶了。当你考虑到常量表达式允许在编译时进行优化时,这种好奇心变得更加深了:

public class Div0 {
    public static void main(String[] args) {
        final int i = 2+3;
        final int j = 1/0;
        final int k = 9/2;
    }
}

使用Eclipse编译,上面的代码片段生成以下字节码(javap -c Div0)

Compiled from "Div0.java"
public class Div0 extends java.lang.Object{
public Div0();
  Code:
   0:   aload_0
   1:   invokespecial   #8; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[]);
  Code:
   0:   iconst_5
   1:   istore_1      // "i = 5;"
   2:   iconst_1
   3:   iconst_0
   4:   idiv
   5:   istore_2      // "j = 1/0;"
   6:   iconst_4
   7:   istore_3      // "k = 4;"
   8:   return

}

正如你所看到的,ik 的赋值被优化为编译时常量,但除以0的操作(本应在编译时可检测到)仍然按原样编译。

javac 1.6.0_17 行为更加奇怪,会静默地进行编译,但完全将 ik 的赋值从字节码中删除(可能是因为它确定了它们在任何地方都没有被使用),但保留了 1/0 (因为删除它将导致完全不同的程序语义)。

因此问题是:

  • 1/0 是实际上可以在任何时间任何地方编译的合法Java表达式吗?
    • JLS 对此有什么说法?
  • 如果这是合法的,是否有好的理由?
    • 这可能有什么好处?

5
给出负评的人:能否解释一下这个问题为什么不清晰或者无用?因为我将来还会有许多问题,所以我想知道需要在哪些方面进行改进。 - polygenelubricants
8
@polygenelubricants:老实说,我不知道这些细节是否会让你成为一个更好的程序员。我已经从事编程工作超过25年了,除了“语言律师”视角和“Java Puzzlers”视角(虽然Java Puzzlers比这个好),我认为这可能只是纠缠不清和浪费时间的非零概率事件(我只是说实话:在这里回答的大多数专业开发人员可能根本没有头绪,而且可能在他们的职业生涯中不知道这个Java细节并没有伤害他们)。至少我不想看到SO上充斥着像这样的问题 :( - SyntaxT3rr0r
3
另一种表达方式是:对我来说,把时间浪费在像这样的问题上(显然你未来还会有更多类似的问题)细枝末节地挑剔/回答/评论,不如花点时间学习 Lisp 这样的编程语言……那才是真正让你成为更好的程序员的方法。 - SyntaxT3rr0r
6
另一位编程工作者的观点:很久以前,我非常尊重的人告诉我,如果我不知道至少三种破坏一个工具的方法,那么我就没有真正学会使用它。当我开始学习编程时,我将这个比喻扩展到了应用于边缘条件、奇怪的行为,甚至是一些挑剔的细节。编程就是关注细节。 - CPerkins
9
@Webinator:我更多地是一名计算机科学家,而不是软件工程师,因此像这样的语言问题深深地吸引着我。我不仅关心如何编写更好的程序,也关心如何设计更好的编程语言。 - polygenelubricants
显示剩余2条评论
8个回答

34
是的。
《JLS》对此没有具体规定...除了说除零会导致运行时异常。然而,《JLS》在以下定义中承认了运行时异常的可能性:
"编译时常量表达式是指一个不突然终止并且仅使用以下内容组成的原始类型或字符串值的表达式:..."
(强调添加)。因此,以下内容将无法编译:
switch(i) {
    case 1:
    case 1 + 1: 
    case 1 / 0:  // compilation error.
}

如果这是合法的,是否有一个很好的理由呢?
好问题。我想这是一种抛出ArithmeticException的方式,尽管这几乎不是一个可信的理由。指定Java的这种方式更可能是为了避免JLS和编译器处理一个很少会影响人们的边缘情况而带来的不必要的复杂性。
但这都不是重点。事实是,1/0是有效的Java代码,任何Java编译器都不应该将其标记为编译错误。
(对于Java编译器发出警告是合理的,前提是还有一种方法可以关闭警告。但与此相反,通过常量零进行除法是非常罕见的,因此实现这一点的价值是值得怀疑的。)

没有警告是合理的,因为除以编译时常量并评估为零不是足以浪费开发时间检测错误的常见原因。 - Pete Kirkham
2
@Pete - 我可以接受这一点,尽管这种警告有先例。例如,Eclipse Java编译器会在确定语句将始终引发NPE时发出警告。 - Stephen C
3
考虑到我遇到空指针异常和算术异常的次数,这是相当合理的。 - Pete Kirkham
1
@Pete:编译器无论如何都必须检查表达式是否会抛出异常,因为在这种情况下它不被视为编译时常量。因此,在这种情况下发出警告并不会增加太多开销。 - Paŭlo Ebermann
2
字节码不包含switch语句,因此需要在编译时进行评估。除零操作无法计算,因此会出现编译错误(编译器实际上正在运行它以进行评估)。 - CrackerJack9

20
我在Bug数据库中进行了一些调查,并发现了一些有趣的信息。

Bug ID 4178182: JLS未指定1/0作为常量表达式的行为

以下代码是非法的:

class X { static final int i = 1 / 0; }
这个编译时常量的值是未定义的,因此这必须是一个编译时错误。Guy Steele在大约18个月前确认了这确实是预期的行为。
编译时常量必须在静态上下文中可用(这就是它成为编译时常量的原因);例如,由包含除以零的常量确定其值的其他常量的值是未定义的。这会影响到switch语句的语义、明确赋值和未赋值等。
Bug ID 4089107:javac将整数除以(常量)零视为错误。
public class zero {
   public static void main(String[] args) {
      System.out.println(1/0);
   }
}

运行上述代码将产生以下结果:
zero.java:3: Arithmetic exception.
     System.out.println(1/0);
                         ^
1 error

Bug ID 4154563: javac接受在case表达式中除以零的常量表达式。

尝试编译下一个测试时,Java编译器崩溃。这个测试也会导致所有1.2beta4版本的编译器崩溃,但在12.beta3中不存在此错误。以下是一个示例和编译器诊断:

public class B {
   public static void main(String argv[]) {
      switch(0){
         case 0/0:
      }
  }
}

评估:编译器以前会将所有试图除以常数零的操作报告为编译时错误。这个问题在beta3版本中得到修复,使得能够生成除以常数零的代码。不幸的是,这个错误也被引入了。编译器应该能够优雅地处理在case表达式中出现的除零操作。
结论:
关于是否应该编译1/0这个问题引起了激烈的讨论,一些人引用了Guy Steele的观点,认为这应该是一个编译时错误,而其他人则认为不应该。最终似乎决定既不是编译时错误,也不是编译时常量。

3

如果你查看Double类,你会看到以下内容:

/**
 * A constant holding the positive infinity of type
 * <code>double</code>. It is equal to the value returned by
 * <code>Double.longBitsToDouble(0x7ff0000000000000L)</code>.
 */
public static final double POSITIVE_INFINITY = 1.0 / 0.0;

在Float类中进行相同的计算,只不过使用浮点数而不是双精度浮点数。基本上,除以0会返回一个非常非常大的数字,比Double.MAX_VALUE还要大。

以下代码:

public static void main(String[] args) {
    System.out.println(Double.POSITIVE_INFINITY);
    System.out.println(Double.POSITIVE_INFINITY > Double.MAX_VALUE);
}

输出:

Infinity
true

请注意在打印Double.POSITIVE_INFINITY时的特殊情况。尽管它被视为双精度浮点型,但它打印出一个字符串。
回答这个问题,是的,在Java中这样做是合法的,但1/0会解析为“无穷大”并且与标准的双精度浮点数(或浮点数等等)有所不同。
我应该指出,我完全不知道为什么要以这种方式实现。当我看到上面的输出时,所有的东西对我来说都像黑魔法一样。

9
浮点数类型(doublefloat)与整数类型不同。浮点数类型具有表示无穷大的方式,而整数类型则没有。原问题是关于整数的,因此有关浮点数类型的讨论并不相关。 - Jesper
1
问题是在询问除零是否“应该随时随地编译?”。提供的示例确实使用整数,但并没有明确询问整数是否可以保存被零除的值,只是询问在语言中是否合法。因此,答案是肯定的。 - Corey
4
问题明确询问的是表达式1/0,而不是表达式1.0/0.0,因此我仍然同意Jesper的评论。在我看来,这很明显是一次讨论,讨论是否以及为什么1/0应该作为编译时错误而不是运行时异常,而这两种情况都不适用于浮点类型。 - Alderath

2
Java明确要求整数除以零会触发一个ArithmeticException异常。不能省略对j的赋值,因为那将违反规范。

2
如果编译通过,那么肯定不能省略;这一点是已知的并且被认可的。问题是它是否应该在第一时间就编译通过。 - polygenelubricants

0

这是合法的,因为编译器在编译时没有规定必须折叠常量表达式。

“智能”的编译器可能会编译:

a = 1 + 2

如下:

a = 3

但是并没有规定编译器必须这样做。除此之外,1/0就像其他合法的表达式一样:

int a;
int b;

a = a/b;

是一个合法的表达式。

在运行时它会抛出异常,但这是有原因的运行时错误。


4
int a; int b; a = a/b; 会在javac编译时抛出一个异常。 - Bart Kiers
@Bart K. - 没有所谓的“在编译时抛出的异常”。编译器确实会给你一个错误。异常只会在运行时抛出。 - Jesper
叹气 表达式是合法的,编译器错误是由于未初始化的变量引起的。我很惊讶自己没有因为第一个表达式缺少分号而被投票否决。 - Will Hartung
1
@Will,我没有说表达式是(不)合法的。你说“在运行时它会抛出异常”,这是不正确的:int a; int b; a = a/b;不会到达运行时。 - Bart Kiers
@Bart,但你过于追求细节了,期望那些仅仅是为了推广故事情节而编写的代码应该是合法和完美的代码。尽管a和b没有被初始化,但代码的意图是清晰的,即使它是不完美的Java。我打了它们来确保没有人会说“如果b是一个字符串”或其他无聊的话。 - Will Hartung
@Will,不,我并不是故意挑剔(至少,这不是我的意图)。由于你明确表示它会抛出运行时异常,我只是指出这不是真的。显然,你的意图对我来说并不清楚,因为我认为你的意思是“未初始化”错误将在运行时抛出,但根据你的评论,这并不是你的意图。 - Bart Kiers

0

从编译的角度来看,这是合法的,但如果执行会抛出异常!

原因是...编程必须允许灵活性,因此您键入的所有表达式和每个代码都是编译器的变量,因此在数学表达式X/Y中,编译器不关心Y变量值是(Y==0)还是任何其他数字,对于编译器来说,这是一个变量...如果编译器也必须查看值,那将被视为运行时,不是吗。


我认为检查除零操作并不会显著增加编译时间。 - helpermethod
如果“零”是一个变量,那么Java将不得不执行在除法语句之前涉及该变量的每个计算,以确定此时它是否为零,在许多情况下这是不可能的(例如从外部源加载的变量)。如果它不是一个变量,那就意味着你实际上编写了: a = b/0;而这将是一个相当业余的错误,只有在受影响时才会犯下 :P - Daniel
从未说过它会增加编译时间。我想要表达的是,在编译时编译器不会评估表达式,但它会确定除法中变量(int/float/...)的类型及其值,并将类型和值保存在类文件中作为字节码,而不进行表达式的评估。因为评估所有可能的表达式将是无尽且毫无意义的。 - TacB0sS
3
@OliverWeiler - 编译时性能成本并不重要,关键是编译器是否被允许实现该检查并把它称为编译错误。除非语言规范允许,否则编译器不能像那样做聪明的事情。在这种情况下,JLS 不允许这样做。那些做了 JLS 不支持的有用事情的智能编译器是不好的,因为它们会导致源代码的可移植性问题。 - Stephen C

0

既然你最终需要一个运行时变量,为什么要在编译时捕获它呢?

例如,如果你从文本文件中加载和解析“0”,然后尝试除以它,Java 在编译时不知道你在做什么,因为它不知道外部文件的内容。

此外,如果你将任何变量设置为 0 并除以该变量,Java 必须跟踪脚本中每个变量的每个可能值,以便在编译时捕获除以 0 的错误。

最好保持一致,并将其作为仅在运行时发生的异常。


4
为什么在编译时要捕获 String s = (String) Integer.valueOf(0); // doesn't compile!,如果它最终会在运行时抛出 ClassCastException 呢? - polygenelubricants
因为Integer.valueOf(0)是一个类型问题(期望字符串参数而传入整型参数),而类型问题在Java的严格类型检查下会在编译时被检测到。除以零是一个计算问题,而Java不会在编译时执行计算,因为在许多情况下这是不可能的(例如对从数据库/外部文件加载的变量进行计算)。因此,所有计算问题都会在运行时被捕获。 - Daniel

0

既然别人已经回答了1/0的合法性问题,那我们来看第二个问题:

  • 如果这是合法的,是否有很好的理由呢?
    • 它可能有什么好处?

一个答案可以是:

用来逗弄同事。 ;o)

当同事离开房间时,他的电脑没有锁定,你可以溜进去,在应用程序早期使用的某个类的静态初始化器中深埋1/0。这样,他很快就会在(甚至在)部署应用程序时遇到异常的ArithmeticException,并且可能要花一些时间思考。使用这种快速失败的方式,您可以确保这是一个相对无害的玩笑。

P.S.:它奏效了。 ;o)


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