如果分数不能以二进制精确表示,Double.toString() 如何工作?

Posted

技术标签:

【中文标题】如果分数不能以二进制精确表示,Double.toString() 如何工作?【英文标题】:How does Double.toString() work if a fraction number cannot be precisely represented in binary? 【发布时间】:2019-07-05 14:50:02 【问题描述】:

我无法理解 Double.toString() 在 Java/JVM 中的工作原理。 我的理解是,通常小数不能用浮点类型(例如 Double 和 Float)精确表示。例如,206.64 的二进制表示将是 206.6399999999999863575794734060764312744140625。那么 (206.64).toString() 怎么会返回“206.64”而不是“206.6399999999999863575794734060764312744140625”?

在 Kotlin 中测试代码。

@Test
fun testBigDecimalToString() 
    val value = 206.64
    val expected = "206.64"

    val bigDecimal = BigDecimal(value)

    assertEquals(expected, value.toString()) // success
    assertEquals(expected, bigDecimal.toString()) // failed. Actual: 206.6399999999999863575794734060764312744140625

【问题讨论】:

您是否尝试调试Double#toString() 1000*value(最好使用 var)可能会显示小的近似误差。有一种尝试表示双重“四舍五入”。 Double 具有几乎 16 位十进制数字的精度,并且仍在 9 范围内。一种小小的谎言。 【参考方案1】:

打印floatdouble 时看到的位数是Java 将floatdouble 默认转换为十进制的规则的结果。

Java 对浮点数的默认格式使用所需的最少有效十进制数字来区分数字与附近的可表示数字。1

在您的示例中,源文本中的 206.64 转换为 double 值 206.6399999999999863575794734060764312744140625,因为在 double 类型中可表示的所有值中,该值最接近 206.64。下一个较低值和下一个较高值是 206.639999999999957935870043002068996429443359375 和 206.640000000000014779288903810083866119384765625。

当打印这个值时,Java 只需要打印“206.64”,因为这已经足够我们可以从它的邻居 206.639999999999957939999999957059385702934025 和 206.6399999999999863575794734060764312744140625 和 206.640000000000014779288903810083866119384765625。请注意,从 206.63999... 中的 9 末尾开始,第一个值与 206.64 相差 0.1364...,而第三个值 206.64000... 相差 0.1477...。所以,当爪哇打印“206.64”,它意味着的double正在打印是最接近的可表示的值的值,那就是206.6399999999999863575794734060764312744140625值,而不是更远206.640000000000014779288903810083866119384765625值。 P>

脚注

1Java SE 10 的规则可以在 toString(float d) 部分的 java.lang.float 文档中找到。 double 文档类似。这段话,最相关的部分用粗体表示,是:

返回float argument 的字符串表示形式。下面提到的所有字符都是ASCII字符。

如果参数为 NaN,则结果为字符串“NaN”。

否则,结果是一个字符串,表示参数的符号和大小(绝对值)。如果符号为负,则结果的第一个字符是'-'('\u002D');如果符号为正,则结果中不会出现符号字符。至于幅度m

如果m为无穷大,则用字符“Infinity”表示;因此,正无穷大产生结果“Infinity”,负无穷大产生结果“-Infinity”。

如果m为零,则用字符“0.0”表示;因此,负零产生结果“-0.0”,正零产生结果“0.0”。

如果m大于等于10-3但小于107,则表示为整数部分m,十进制形式,不带前导零,后跟“.”('\u002E'),后跟代表m小数部分的一位或多位十进制数字.

如果m小于10-3或者大于等于107,那么用所谓的“计算机科学记数法。”令 n 为唯一整数,满足 10nm n+1;然后让 am 和 10n 的数学精确商,因此 1 ≤ a a 的整数部分,作为单个十进制数字,后跟“.”('\u002E'),然后是表示a 的小数部分,后跟字母 'E' ('\u0045'),后跟 n 表示为十进制整数,由该方法生成Integer.toString(int).

ma 的小数部分必须打印多少位? 必须至少有一位数字来表示小数部分,除此之外,必须有尽可能多的数字,但仅能将参数值与float 类型的相邻值唯一区分开来。 也就是说,假设 x 是由该方法为有限非零参数 f 生成的十进制表示所表示的精确数学值。那么f必须是最接近xfloat值;或者,如果两个float 值同样接近x,则f 必须是其中之一,并且是f 的有效位的最低有效位em> 必须为 0。

【讨论】:

【参考方案2】:

我有点新手,所以希望有更多经验的人能回答得更彻底,但这是我推测的原因......

格式化

虽然这是针对 .NET 框架而不是专门针对 Java,但我想它们的工作方式类似:toString 方法使用 optional formatter input,并且 Java 很可能使用类似的东西,将双精度格式格式化为 toString 中的近似值方法。 考虑到 Oracle 明确指出 toString should be concise and easy-to-read,很可能为 Double.toString() 实现了这种方法。

只区分必要的数字...

This is about as much documentation 我可以在 Double.toString() 方法的细节上找到——注意最后一段:

m 或 a 的小数部分必须打印多少位?必须至少有一个数字来表示小数部分,除此之外,但只有尽可能多的数字才能唯一地将参数值与相邻的 double 类型值区分开来。也就是说,假设 x 是由该方法为有限非零参数 d 生成的十进制表示所表示的精确数学值。那么 d 必须是最接近 x 的 double 值;或者如果两个双精度值同样接近 x,则 d 必须是其中之一,并且 d 的有效位的最低有效位必须为 0。

我很好奇“double 类型的相邻值”(其他变量?)是什么意思,但它似乎也与上​​述一致——toString 和其他方法可能只使用尽可能少的数字来唯一标识double,当数字任意接近时四舍五入,例如 23.675999999999 与 23.676 “足够接近”。 或者我可能会严重误解文档。

【讨论】:

“相邻”表示最接近的可表示值。任何使用有限位数的类型都可以表示有限数量的值。如果这些值是有序的,那么两个可表示的值 aba b 是相邻的,如果有没有可表示的值 x 使得 a x b。换句话说,在可表示值的有序列表中,ab 将是相邻的。 (根据具体情况,可以稍微改变这个定义以适应相邻的 -0 和 +0,尽管可以通过相应地定义顺序来适应它们。)

以上是关于如果分数不能以二进制精确表示,Double.toString() 如何工作?的主要内容,如果未能解决你的问题,请参考以下文章

java中的浮点数

浮点数怎么表示精确度?

matlab ssolve函数计算出来的数怎么是分数表示的

csharp 分数类的基础。可以用来精确地表示有理数。

[JAVA面试题]为什么会出现4.0-3.6=0.3999999999这种现象?

如何在 C/C++ 中获得大数的精确二进制表示?