为什么将float转换为double会改变数值?

24

我一直在试图找出原因,但是我没能找到。 有人可以帮助我吗?

看下面的例子。

float f = 125.32f;
System.out.println("value of f = " + f);
double d = (double) 125.32f; 
System.out.println("value of d = " + d);

这是输出结果:

value of f = 125.32
value of d = 125.31999969482422

1
你能提供你所看到的这种行为的具体示例吗?最好展示一个SSCCE:http://sscce.org/ - Shafik Yaghmour
1
一个词:精度。从技术上讲...这些值并没有“改变” ;) - Brian Roach
3
据我所知,那是不可能的。双精度数据类型在精度和范围方面都比单精度更高,因此在将数据类型转换为单精度时并不会失去任何精度和范围。但输出的显示可能会有所不同。 - harold
10个回答

17

当将一个float转换为double时,其值不会改变。由于需要区分double值和其相邻值,因此显示数字存在差异,这是Java文档所要求的。这是toString的文档,该文档通过多个链接与println的文档相关联。

125.32f的精确值为125.31999969482421875。两个相邻的float值分别为125.3199920654296875和125.32000732421875。注意125.32更接近于125.31999969482421875而不是其任何一个相邻值。因此,通过显示“125.32”,Java已经显示了足够的数字,以便将十进制数转换回float可以再现传递给printlnfloat的值。

125.3199996948242的两个相邻的double值分别为125.3199996948242045391452847979962825775146484375和125.3199996948242329608547152020037174224853515625
观察到125.32更接近后面的那个邻居而不是原始值(125.31999969482421875)。因此,打印“125.32”并不能包含足够的数字来区分原始值。Java必须打印更多的数字以确保从显示的数字转换回double会产生与传递给printlndouble值相同的值。


2
@Amro:额外的数字不会被舍弃或忽略。对浮点对象执行的算术运算的行为就像它们恰好具有完整的值一样,包括我展示的值。这是因为它们确实具有那些完整的值;IEEE 754规范规定它们恰好具有那些值而没有其他值。它们不是作为带有几位数字的十进制近似值由计算机使用。计算机在二进制中进行计算,并且它们确切地具有指定的值。 - Eric Postpischil
@EricPostpischil:IEEE标准规定,两个浮点数的和应该是最能代表它们名义上的数字和的值,其他操作也是如此,但这并不意味着浮点数“代表”精确数字。我认为2000000.1f表示的不是“确切数量2000000.125”,而是“可能在2000000.0625到2000000.1875范围内的某个数字数量”。如果它真的代表前者,那么为什么... - supercat
@supercat:IEEE 754-2008标准确实表示浮点数值代表一个具体的数字,而不是一个区间或某种近似。请参见第3.3条款。有些人将浮点格式用作近似数字的方法,这是Stack Overflow上许多常见误解的根源。(从错误的假设开始得出错误的结论。)要正确地推断浮点数,必须认识到每个浮点数值代表一个特定的数字,并从中派生结果。 - Eric Postpischil
@EricPostpischil:也许混淆是因为“表示”的问题。假设我说Resistance = scaleFactor/log(v1/v2);。更有帮助的是说该值代表执行所有适当指定舍入的操作所得到的位组合,还是代表测量电阻?如果要定义应在一堆位上执行哪些操作,则前一种定义更有用,但如果要使用数字,则后者更有帮助。 - supercat
1
@supercat:我不会在这里评论任何特定人是否应该以任何特定方式使用浮点数。我只是简单地陈述IEEE 754标准非常明确地指定浮点值(除NaN外)表示一个确切的值,并且标准精确地指定了该值。无论您认为这是好还是坏,都与标准规定的事实无关,并且浮点运算的结果是根据这些值定义的。由于IEEE 754的实现遵循这些规则,因此对其行为的分析必须遵循这些规则。 - Eric Postpischil
显示剩余15条评论

12
  1. 当你将一个 float 转换为 double 时,不会有信息的丢失。每个 float 都可以准确地表示为一个 double
  2. 另一方面,System.out.println 输出的十进制表示并不是该数字的精确值。精确的十进制表示可能需要多达大约760个十进制数字。相反,System.out.println 输出的是刚好足够解析回原始 floatdouble 的小数位数。由于有更多的 double,因此在打印一个 double 时,System.out.println 需要打印更多的数字才能使其表示变得明确无误。

实际上,没有任何“double”等效于最佳的“float”表示形式3.5E + 38。将这样的值与任何其他大于3.5E + 38的值的最佳浮点表示进行比较将表明这些值是无法区分的 - 不一定非常有信息性,但是正确的。另一方面,将该值转换为“double”将导致它错误地比较大于所有低于1.798E + 308的值的最佳“double”表示形式 - 错误的数量级高达数百个订单。 - supercat
1
@supercat 这个答案是关于将 float 转换为 double 的。 “3.5E+38 的最佳浮点表示” 是 +inf,而浮点数 +inf 转换为双精度浮点数 +inf 时不会失去精度(它们是相同的 inf!)。如何解释这个无穷大是你的问题,而不是转换的问题。浮点值(这里是 +inf)仅代表一个值(这里是无穷大)。您可以使用围绕 1.0f 和双精度浮点数 1.0 的 1-ulp 间隔进行相同的论证,但该论证同样不相关。被转换为 double 的是一个 float,即单个值。 - Pascal Cuoq
@supercat 请参阅 http://lipforge.ens-lyon.fr/www/crlibm/documents/cern.pdf 中的“一些常见误解(2)”。De Dinerchin 在那里只谈到了有限浮点值,但同样适用于 inf。浮点值 inf 不是“包括 3.5E+38 的一系列值”,它是一个单独的值,即无穷大。在转换为 double 之前,将 3.5E38 转换为 +inf 的近似已经发生,并且不会阻止从 double 转换为精确值。 - Pascal Cuoq
一个浮点数值有效地封装了两个概念:关于产生它的计算可以说什么,以及将来应该输入什么进行计算。给定这样的表达式 float2=float1/0.625f,如果 float1 是 62.5f,那么 float2 的值表示最后一次操作的算术结果在 13421772.5/134217728 和 13421773.5/134217728 之间,并且将来使用 float2 的精确值为 13421773/134217728。如果 float1 是 3.4E38,则 float2 的值将指示... - supercat
一个算术结果超过了3.4028E38的未知数量,并且将来使用float2将视为无穷大。如果想知道产生float2的计算的算术结果是否可以明确地被认为比0.11或1.7E+308大,将每个比较的第二个操作数转换为float会正确地报告它们不能。将float2转换为double会暗示它们可能会。 - supercat
显示剩余9条评论

4
float转换为double是一种扩展转换,正如 JLS所规定的。扩展转换被定义为一个小集合到其超集的单射映射。因此,在从float转换为double后,所表示的数字不会改变

有关您更新问题的更多信息

在您的更新中,您添加了一个例子,旨在证明数字已经改变。然而,它只显示了由于转换为double而获得的额外精度导致数字的字符串表示已经改变。请注意,您的第一个输出只是第二个输出的四舍五入。正如Double.toString所指定的那样,

必须至少有一个数字来表示小数部分,并且除此之外还需要尽可能多的数字,但只有这么多数字才能唯一地区分类型double的相邻值与参数值。

由于类型double中相邻的值比float更接近,因此需要更多的数字来符合该规定。


请阅读原始问题,我的原始答案是完全适当的回应,请点击此处。自那以后,该问题已经进行了大幅更新。 - Marko Topolnik

3
距离125.32最近的32位IEEE-754浮点数实际上是125.31999969482421875。非常接近,但不完全相同(这是因为0.32在二进制中重复出现)。将其转换为double时,值125.31999969482421875将成为double(此时125.32无处可寻,其应该以.32结尾的信息完全丢失)。当您打印该double时,打印程序认为它具有比实际更多的有效数字(但它当然无法知道),因此将其打印为125.31999969482422,这是四舍五入到该确切double的最短十进制数(而所有长度为该长度的小数中,它最接近)。

那么,我犯了一个错误?是什么错误? - harold

1
其他答案解释了为什么数字不同的背景,这很好。然而,如果你需要一种实用的方法来在浮点数和双精度数之间进行转换,而不会突然得到不同的表示,你可以使用字符串表示作为转换的中间步骤,即你可以这样做:
float f = 125.32f;
System.out.println("value of f = " + f);
double d = Double.valueOf(String.valueOf(125.32f)); 
System.out.println("value of d = " + d);


这将打印
value of f = 125.32
value of d = 125.32

注意:从性能和计算角度来看,这并不推荐使用,但如果您需要在用户界面层面上进行来回转换,并且不想让用户对意外的值变化感到惊讶,这可能会有所帮助。例如,如果他们输入了一个浮点数,而您希望确保在另一个地方将其作为双精度数进行处理-那么双精度值将基于用户输入的内容,而不是基于其输入值的浮点解释。在这种情况下,这样做可能是有意义的,但是您应该确保不要混淆不同的解释,并假设它们是“相同的实际值”。

1
如前所述,所有浮点数都可以用双精度表示。你的问题是因为 System.out.println 在显示 floatdouble 值时会进行一些四舍五入,但两种情况下的四舍五入方法不同。
若要查看浮点数的确切值,可以使用 BigDecimal
float f = 125.32f;
System.out.println("value of f = " + new BigDecimal(f));
double d = (double) 125.32f;
System.out.println("value of d = " + new BigDecimal(d));

它的输出如下:

value of f = 125.31999969482421875
value of d = 125.31999969482421875

1

浮点数精度问题与编程语言无关,因此我将在解释中使用MATLAB。

之所以会出现差异是因为某些数字无法用固定位数精确表示。例如,取0.1

>> format hex

>> double(0.1)
ans =
   3fb999999999999a

>> double(single(0.1))
ans =
   3fb99999a0000000

当你将单精度的近似值 0.1 转换为双精度浮点数时,误差会变大。如果直接使用双精度开始计算,结果将与其近似值不同。请注意保留 HTML 标签。
>> double(single(0.1)) - double(0.1)
ans =
     1.490116113833651e-09

这些近似值可能以意想不到的方式潜在影响您。例如,0.1 * 3 == 0.3 的结果为 false。如果您需要更高的精度,请使用任意精度库。 - Amro
非常清晰的解释。谢谢。 - Curt
@Curt:你可以在这里找到更好的解释:http://www.mathworks.com/company/newsletters/news_notes/pdf/Fall96Cleve.pdf(由MATLAB发明者Cleve Moler提供)。此页面还有一些不错的例子。 - Amro
这个答案没有解释为什么打印float会显示一个与打印从相同值转换而来的double不同的数字(事实上它们具有相同的值,因为从floatdouble的转换并不改变值)。 - Eric Postpischil
@EricPostpischil:同意,那种措辞很容易引起误解。感谢澄清。 - Amro
显示剩余3条评论

0

在Java中它不会工作,因为Java默认将实数作为双精度浮点数处理,如果我们声明一个没有float表示的浮点数值,例如123.45f,默认情况下它将被视为双精度浮点数,这将导致精度损失错误。


0

由于将数值转换为String的方法的约定,数值的表示方式会发生变化,分别是java.lang.Float#toString(float)java.lang.Double#toString(double),而实际值保持不变。在这两个方法的Javadoc中有一个共同的部分,详细说明了数值的String表示要求:

至少需要一个数字来表示小数部分,并且除此之外还需要足够多的数字,但只能是足够多的数字,以便能够唯一区分参数值与相邻值

为了说明这两种类型的值的重要部分的相似性,可以运行以下代码片段:

package com.my.sandbox.numbers;

public class FloatToDoubleConversion {

    public static void main(String[] args) {
        float f = 125.32f;
        floatToBits(f);
        double d = (double) f;
        doubleToBits(d);
    }

    private static void floatToBits(float floatValue) {
        System.out.println();
        System.out.println("Float.");
        System.out.println("String representation of float: " + floatValue);
        int bits = Float.floatToIntBits(floatValue);
        int sign = bits >>> 31;
        int exponent = (bits >>> 23 & ((1 << 8) - 1)) - ((1 << 7) - 1);
        int mantissa = bits & ((1 << 23) - 1);
        System.out.println("Bytes: " + Long.toBinaryString(Float.floatToIntBits(floatValue)));
        System.out.println("Sign: " + Long.toBinaryString(sign));
        System.out.println("Exponent: " + Long.toBinaryString(exponent));
        System.out.println("Mantissa: " + Long.toBinaryString(mantissa));
        System.out.println("Back from parts: " + Float.intBitsToFloat((sign << 31) | (exponent + ((1 << 7) - 1)) << 23 | mantissa));
        System.out.println(10D);
    }

    private static void doubleToBits(double doubleValue) {
        System.out.println();
        System.out.println("Double.");
        System.out.println("String representation of double: " + doubleValue);
        long bits = Double.doubleToLongBits(doubleValue);
        long sign = bits >>> 63;
        long exponent = (bits >>> 52 & ((1 << 11) - 1)) - ((1 << 10) - 1);
        long mantissa = bits & ((1L << 52) - 1);
        System.out.println("Bytes: " + Long.toBinaryString(Double.doubleToLongBits(doubleValue)));
        System.out.println("Sign: " + Long.toBinaryString(sign));
        System.out.println("Exponent: " + Long.toBinaryString(exponent));
        System.out.println("Mantissa: " + Long.toBinaryString(mantissa));
        System.out.println("Back from parts: " + Double.longBitsToDouble((sign << 63) | (exponent + ((1 << 10) - 1)) << 52 | mantissa));
    }
}

在我的环境中,输出为:
Float.
String representation of float: 125.32
Bytes: 1000010111110101010001111010111
Sign: 0
Exponent: 110
Mantissa: 11110101010001111010111
Back from parts: 125.32

Double.
String representation of double: 125.31999969482422
Bytes: 100000001011111010101000111101011100000000000000000000000000000
Sign: 0
Exponent: 110
Mantissa: 1111010101000111101011100000000000000000000000000000
Back from parts: 125.31999969482422

通过这种方式,您可以看到值的符号、指数相同,而它的尾数被扩展并保留了其重要部分(11110101010001111010111)完全相同。

浮点数部分的提取逻辑使用了12


-1

两者都是微软所称的“近似数字数据类型”。

有一个原因。float的精度为7位数字,double为15位。但我已经看到许多次8.0 - 1.0 - 6.999999999。这是因为它们不能保证完全表示小数。

如果您需要绝对不变的精度,请选择decimal或整数类型。


1
“近似”这个词用来描述IEEE浮点数是相当笨拙的,因为它确切地表示了一个非常精确定义的数字集合。 - Marko Topolnik
1
这完全没有回答问题。 - Zong
“近似数数据类型”正是微软所称的浮点数和实数数据类型:http://msdn.microsoft.com/en-us/library/ms173773.aspx - Curt
@Curt 如果有人开始称浮点类型为“实数”,那么第一件需要提到的事情就是它是近似的:作为存储实数的数据类型,它肯定是近似的。我认为微软关于SQL中浮点类型的文档不应该被视为讨论浮点数在Java中的参考资料。 - Pascal Cuoq
2
@MarkoTopolnik:从执行低级计算的代码角度来看,IEEE浮点数是精确定义的类型。然而,从消费者代码的角度来看,如果程序读取两个float值(比如x=1.0和y=10.0),并计算float z=x/y;,那么程序员更可能认为z保存了输入分数的不完美表示,而不是13421773/134217728分数的精确表示。 - supercat
@supercat 是的,我同意所有的观点。 - Marko Topolnik

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