为什么在Java中将Double.NaN强制转换为int不会抛出异常?

24

我知道IEEE 754为非实数指定了一些特殊的浮点值。在Java中,将这些值强制转换为原始类型 int 不会像我预期的那样抛出异常。相反,我们有以下情况:

int n;
n = (int)Double.NaN; // n == 0
n = (int)Double.POSITIVE_INFINITY; // n == Integer.MAX_VALUE
n = (int)Double.NEGATIVE_INFINITY; // n == Integer.MIN_VALUE

为什么在这些情况下抛出异常?这是IEEE标准吗,还是Java设计者的选择?如果在这些类型转换中抛出异常是否会有坏的后果我没有意识到的?

5个回答

13
在这些情况下不抛出异常的理由是什么?
我想原因包括:
- 这些边缘情况在执行此类操作的应用程序中很少发生。 - 行为并非“完全出乎意料”。 - 当应用程序从double转换为int时,会有重要信息丢失。 应用程序要么会忽略这种可能性,要么转换将在检查其是否存在风险的前提下进行...也可以检查这些情况。 - 没有其他双精度/浮点运算导致异常,并且(在我看来)在这种情况下这样做可能有点分裂。 - 可能会在某些硬件平台上(当前或未来)产生性能损失。
一位评论者说:
“我怀疑不让转换引发异常的决定是出于强烈的希望避免因任何原因引发异常的想法,因为担心迫使代码将其添加到throws子句中。”
我认为这不是一个可信的解释:
- Java语言设计者没有避免“任何原因”引发异常的思维方式。 Java API中有许多例子证明了这一点。 - 通过将异常设置为未经检查来处理 throws 子句问题。实际上,许多相关异常,如 ArithmeticException 或 ClassCastException ,出于这个原因被声明为未经检查的异常。
这是IEEE标准还是Java设计者的选择?
我认为是后者。
如果此类转换可能引发异常是否存在我不知道的不良后果?
除了明显的后果外,没有其他不良后果。

(但这并不是很相关。JLS和JVM规范说了什么,改变它们可能会破坏现有的代码。而且我们现在不仅谈论的是Java代码...)


我做了一些调查。许多可以用于将双精度转换为整数的x86指令似乎会生成硬件中断...除非屏蔽。对于我来说,规定的Java行为是否比OP建议的替代方案更容易实现并不清楚。


1- 我不否认一些Java程序员确实会这样想。但他们不是Java设计师,而且这个问题特别询问的是Java设计理念。


2
我怀疑决定不抛出异常的转换是出于强烈的愿望,即避免因为任何原因而抛出异常,因为这会迫使代码将其添加到“throws”子句中。然而,从实际角度来看,如果代码尝试将浮点数转换为整数并且它无法适应,则代码不太可能正常工作,因此抛出异常可能比继续使用虚假数据更好。 - supercat
(假设的)异常不需要是checked异常。事实上,那样做是错误的。但无论如何,这种情况不会发生。 - Stephen C

10
“在这些情况下不抛出异常的理由是什么?这是IEEE标准,还是Java设计者的选择?”
IEEE 754-1985标准在第20页和21页的2.2.1 NANs和2.2.2 Infinity章节中清楚地解释了为什么需要标准中的NAN和Infinity值。因此这不是Java的问题。”
Java虚拟机规范在第3.8.1浮点算术和IEEE 754节中指出,在进行到整数类型的转换时,JVM会向零舍入,这解释了您所看到的结果。”
“该标准确实提到了一个名为“陷阱处理程序”的功能,可以用于确定溢出或NAN何时发生,但Java虚拟机规范明确指出,这对于Java来说没有被实现。它在第3.8.1节中说:”
“Java虚拟机的浮点运算不会抛出异常、陷阱或其他信号,也不会发出IEEE 754的无效操作、除以零、溢出、下溢或不精确的异常条件。Java虚拟机没有信号NaN值。”
“因此,无论后果如何,行为都不是未指定的。”

如果使用这种类型转换会发生异常,那么我是否有所不知的坏后果?

理解标准中给出的原因就足以回答这个问题。标准用详尽的例子解释了你在这里问的后果。我可以把它们发布出来,但这会是太多信息,而且这些例子可能无法适当地格式化在此版本工具中。

编辑

我正在阅读最新的Java虚拟机规范维护审核,这是由JCP最近作为JSR 924的一部分发布的,其中第2.11.14节命名为类型转换指令包含了一些更多的信息,这可能有助于您寻找答案,虽然还不是您要寻找的,但我相信它有一点帮助。它说:

在将浮点值转换为整数类型T的缩小数值转换中,其中T为int或long时,浮点值的转换如下:
- 如果浮点值为NaN,则转换的结果为int或long 0。 - 否则,如果浮点值不是无穷大,则使用IEEE 754向零舍入模式将浮点值四舍五入为整数值V。
有两种情况:
- 如果T为long且此整数值可以表示为long,则结果为long值V。 - 如果T为int类型且此整数值可以表示为int,则结果为int值V。
否则:
- 值可能太小(具有较大幅度的负值或负无穷大),结果是类型int或long的最小可表示值。 - 或者值可能太大(具有较大幅度的正值或正无穷大),结果是类型int或long的最大可表示值。
从double到float的缩小数值转换遵循IEEE 754。使用IEEE 754舍入到最近模式正确舍入结果。无法表示为float的值将转换为float类型的正零或负零;无法表示为float的值将转换为正无穷大或负无穷大。双精度NaN始终转换为浮点NaN。
尽管可能会发生溢出、下溢或精度损失,但数值类型之间的缩小转换从不导致Java虚拟机抛出运行时异常(不要与IEEE 754浮点异常混淆)。

我知道这只是重申了您已经知道的内容,但它有一个线索,似乎IEEE标准要求四舍五入到最近的数字。也许在那里您可以找到这种行为的原因。

编辑

有关IEEE标准的2.3.2节“舍入模式”规定:

默认情况下,“舍入”意味着四舍五入至最近的数字。标准要求提供另外三种舍入模式;即向零舍入、向+无穷大舍入和向-无穷大舍入。

当与转换为整数操作一起使用时,向-无穷大舍入会导致转换成为floor函数,而向+无穷大舍入则是ceiling。

舍入模式会影响溢出,因为当向0舍入或向-无穷大舍入生效时,正数溢出会导致默认结果成为可表示的最大数字,而不是+无穷大。

类似地,当向+无穷大或向0舍入生效时,负数溢出将产生最大负数。

然后他们举了一个例子,说明为什么这在区间算术中很有用。不确定是否这是您要找的答案,但它可以丰富您的搜索。


2
看起来你在引用IEEE标准来证明NaN和无穷大的存在是合理的。我绝对不会质疑这些值应该成为IEEE标准或Java的一部分,所以我不确定你为什么要提到这一点。此外,“向零舍入”并不能自动解释这个决定;从数学上讲,这种舍入并没有明确定义。鉴于要进行舍入的决定,这些选择是合理的,但并不明显这个决定必须被做出。 - Michael McGowan
@Michael McGowan 我想我误解了你的问题。我原以为它与Java中浮点运算为什么不会抛出异常有关。现在我意识到你的意思是为什么将浮点特殊值向下转换不会抛出算术异常。我想你的问题同样适用于“为什么整数数据类型在溢出或下溢时不会抛出异常?”因为事实上,Java整数数据类型只有在面对除零操作时才会抛出异常。我也不知道这些设计选择的原因。对于误解造成的困扰,我感到非常抱歉。 - Edwin Dalorzo
@Michael McGowan 尽管您已经接受了一个答案,但我出于好奇心决定再深入研究一下这个主题,并找到了一些其他的参考资料,您可能会觉得有用。我已经编辑了我的答案,将它们包含在内。也许这可以帮助您在寻找更多细节方面迈进一步。然而,我确定这不是您正在寻找的答案。 - Edwin Dalorzo
规范说它将NaN转换为0很好,但他们告诉我们为什么吗?是因为最初以某种方式按位运算更“容易”吗?我猜最初JVM编写起来更容易,如果“本地”操作(如强制转换)从不引发任何异常。所以我想这只是一个实现细节。 - rogerdpack

4

有一份来自1998年的ACM演示仍然让人感到惊讶并且仍然很有价值:https://people.eecs.berkeley.edu/~wkahan/JAVAhurt.pdf

更具体地说,关于NaN和无穷数强制转换时缺乏异常的问题:请参见第3页,第3点:“在没有IEEE标准754/854规定的浮点陷阱和标志的保护下释放的无穷数和NaN与Java的健壮性声称相矛盾。”

这份演示文稿并没有真正回答“为什么”的问题,但解释了Java语言实现浮点数时所做出的有问题的设计决策的后果,并将它们放在IEEE标准甚至其他实现的背景下。


1

1

实际上,我认为在某些类型转换过程中进行了位运算(可能是出于性能问题?),因此您可能会遇到一些意外的行为。请看当您使用 >> 和 << 运算符时会发生什么。

例如:

public static void main(String[] args) {
    short test1 = (short)Integer.MAX_VALUE;
    System.out.println(test1);
    short test2 = (short)Integer.MAX_VALUE-1;
    System.out.println(test2);
    short test3 = (short)Integer.MAX_VALUE-2;
    System.out.println(test3);
    short test4 = (short)Double.MAX_VALUE-3;
    System.out.println(test4);
}

将输出:

-1
-2
-3
-4

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