从 float 到 int 的精确转换

Exact conversion from float to int

我想将 float 值转换为 int 值,或者如果此转换不准确则抛出异常。

我发现了以下建议:使用 Math.round 进行转换,然后使用 == 检查这些值是否相等。如果它们相等,则转换是准确的,否则不是。

但我发现了一个不起作用的例子。下面是演示此示例的代码:

        String s = "2147483648";

        float f = Float.parseFloat(s);
        System.out.printf("f=%f\n", f);

        int i = Math.round(f);
        System.out.printf("i=%d\n", i);

        System.out.printf("(f == i)=%s\n", (f == i));

它输出:

f=2147483648.000000
i=2147483647
(f == i)=true

我知道 2147483648 不在整数范围内,但我很惊讶 == returns 对于这些值是正确的。有没有更好的方法来比较 float 和 int?我想可以将这两个值都转换为字符串,但是对于这样一个原始函数来说那会非常慢。

浮点数是相当不精确的概念。它们也大多毫无意义,除非你 运行 在这一点上使用相当旧的硬件,或者专门与系统交互 and/or 在浮动中工作的协议或在其规范中硬编码 'use a float' 。这可能是真的,但如果不是,停止使用浮点数并开始使用 double - 除非你有相当大的 float[]zero 内存和性能差异,浮点数不太准确。

你的算法在使用 intdouble 时不会失败 - 所有 整数都可以完美地表示为 double.

让我们先解释一下您的代码片段

这里的潜在错误是 'silent casting' 的概念以及 java 如何在那里采取一些有意的自由。

在一般的计算机系统中,你只能比较同类。如果 a 和 b 的类型完全相同,则很容易用位和机器代码的精确术语来确定 a == b 是真还是假。根本不清楚 a 和 b 何时是不同的东西。同样的事情几乎适用于任何运营商; a + b,如果两者都是一个int,是一个清晰易懂的操作。但是,如果 acharbdouble,那根本就不清楚。

因此,在java中,所有个涉及不同类型的二元运算符都是非法的。在基础上,没有字节码可以直接比较 float 和 double,例如,将 string 添加到 int。

但是,有语法糖:当你写 a == b 其中 a 和 b 是不同的类型时, and java 确定两种类型之一是另一个的'a subset',那么java会简单地将'smaller'类型转换为'larger'类型,这样操作就可以成功了。例如:

int x = 5;
long y = 5;
System.out.println(x == y);

这行得通 - 因为 java 意识到将 int 值转换为 long 值永远不会失败,所以它不会打扰您明确指定您打算用代码来做到这一点。在 JLS 术语中,这称为 扩大转换。相反,任何将 'larger' 类型转换为 'smaller' 类型的尝试都是不合法的,您必须显式转换:

long x = 5;
int y = x; // does not compile
int y = (int) x; // but this does.

要点很简单:当你写上面的反向(int x = 5; long y = x;)时,代码是相同的,只是编译器默默地注入了(long) 为您投,本着不亏本的原则。同样的事情发生在这里:

int x = 5;
long y = 10;
long z = x + y;

编译 因为 javac 为您添加了一些语法糖,具体来说,编译就像说:long z = ((long) x) + y;。表达式 x + y 的 'type' 有 long.

这是关键技巧:Java 考虑将 int 转换为 float,以及将 int 或 long 转换为 double - 扩大转换。

如,javac 将假设它可以安全地做到这一点而不会造成任何损失,因此不会强制程序员通过手动添加强制转换来明确承认。但是,int->float 以及 long->double 实际上并不完全安全

浮点数可以表示 -2^23 到 +2^23 之间的每个整数值,双精度数可以表示 -2^52 到 +2^52 (source) 之间的每个整数值。但是int可以表示-2^31+2^31-1之间的每一个整数值,longs -2^63+2^63-1。这意味着在边缘(非常大的 negative/positive 数字),存在整数值,它们可以用整数表示但不能用浮点数表示,或者可以用长整数表示但不能用双精度表示(幸运的是,所有整数都可以用双精度表示;int -> double转换是完全安全的)。但是 java 不会 'acknowledge' 这个,这意味着静默扩大转换仍然可以静默地抛出数据(引入舍入)。

这就是这里发生的事情:(f == i) 被语法加糖到 (f == ((float) i)) 中,从 int 到 float 的转换引入了舍入。

解决方案

大多数情况下,当您使用双精度数和浮点数并且仍然希望得到准确的数字时,您已经搞砸了。这些概念根本上就是不精确的,并且不能通过尝试考虑误差带来侧载这种精确性,因为无法跟踪由于 float 和 double 的舍入行为而引入的错误(无论如何都不容易)。因此,您不应该使用 float/double。要么找到一个原子单位并用 int/long 表示这些单位,要么使用 BigDecimal。 (示例:要编写簿记软件,不要将财务金额存储为 double 将其存储为 'cents' (或 satoshis 或日元或便士或该货币中的任何原子单位)long,或者,如果你真的知道自己在做什么,请使用 BigDecimal

无论如何我想要一个答案

如果您绝对肯定在这里使用浮点数(甚至双精度数)是可以接受的并且您仍然想要精确性,我们有一些解决方案。

选项 1 是利用 BigDecimal 的强大功能:

new BigDecimal(someDouble).intValueExact()

这有效,100% 可靠(除非浮点数到双精度的转换可以以某种方式将非精确值转换为精确值,我认为这不可能发生),然后抛出。也很慢。

另一种方法是利用我们对 IEEE 浮点标准工作原理的了解。

一个真正简单的答案就是 运行 您编写的算法,但要添加额外的检查:如果您的 int 获得的值低于 -2^23 或更高+2^23 那么 可能 不正确。然而,仍然有一些低于 -2^23 和 +2^23 的数字可以用 float 和 int 完美表示,只是,不再是那个时候的每个数字。如果您想要一种算法也能接受这些确切的数字,那么它会变得更加复杂。我的建议是不要深入研究那个污水池:如果你有一个过程,你最终得到一个接近这种极端的 float,并且你想把它们变成 int 但前提是可能没有损失,你遇到了一个疯狂的问题,你需要重新连接你得到的部分!

如果你真的需要它,而不是尝试对 float 进行编号 运行,我建议你使用 BigDecimal().intValueExact() 技巧,如果你确实需要这个。