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

时间:2019-07-05 14:50:02

标签: java floating-point bigdecimal

我无法理解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
}

2 个答案:

答案 0 :(得分:7)

在打印floatdouble时看到的位数是Java的规则,即默认将floatdouble转换为十进制的规则。 / p>

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

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

在打印此值时,Java只需要打印“ 206.64”,因为这足以使我们可以从其邻居206.63999999999999999579358700430430020689962443359375和中选择double值206.6399999999999863575794734060764312744140625 206.640000000000014779288903810083866119384765625。请注意,从206.63999…的9s末尾开始,第一个值与206.64的差值为1.1364…,而第三个值206.64000…的差值则为.1477…。因此,当Java打印“ 206.64”时,表示正在打印的double的值是最接近的可表示值,即206.6399999999999863575794734060764312744140625的值,而不是更远的206.640000000000014779288903810083866119384765625的值。

脚注

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

  

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

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

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

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

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

    •   
    • 如果 m 大于或等于10 -3 但小于10 7 ,则表示为 m 的整数部分,无十进制形式,无前导零,后跟“ .”('\u002E'),后跟一个或多个十进制数字,表示< em> m 。

    •   
    • 如果 m 小于10 -3 或大于或等于10 7 ,则表示为所谓的“计算机科学计数法”。设 n 为唯一整数,以使10 n m <10 n < / em> +1 ;然后让 a m 和10 n 的数学精确商,这样1≤ a < / em> <10。然后将幅度表示为 a 的整数部分,用一个十进制数字表示,后跟'.'('\u002E'),后跟代表 a 的小数部分的十进制数字,后跟字母'E'('\u0045'),然后以 n 的形式表示为十进制整数,由方法Integer.toString(int)产生。

    •   
  •   
     

m a 的小数部分必须打印多少个数字? 必须至少有一位数字来表示小数部分,并且除此以外,还必须要有多少个数字才能唯一地将参数值与类型为float的相邻值区分开来。< / strong>也就是说,假设 x 是此方法为有限的非零参数 f 生成的十进制表示形式所表示的精确数学值。然后 f 必须是最接近 x float值;或者,如果两个float值均与 x 相等,则 f 必须是其中之一,并且是 f的有效位的最低有效位必须为0。

答案 1 :(得分:0)

我是新手,所以我希望有更多经验的人可以做更彻底的回答,但这就是我的理论依据...

格式化

尽管这是.NET框架而不是Java专用的,但我想它们的工作原理类似:toString方法使用optional formatter input,并且很可能Java使用类似的东西,将double格式化为近似于toString方法。 考虑到Oracle特别声明了toString should be concise and easy-to-read,可能为Double.toString()实现了这种方法。

只有必要的数字才能区分...

我可以在Double.toString()方法的细节中找到

This is about as much documentation -请注意最后一段:

  

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

我很好奇“ double类型的相邻值”(其他变量?)的含义,但似乎也与上​​述观点一致-toString和其他方法可能仅使用尽可能少的数字来唯一地标识double,当数字任意足够接近时(如23.675999999999被“足够接近”到23.676的情况)四舍五入。 否则我可能会误解文档。