为什么java里面3*0.1=0.30000000000000004,而4*0.1=0.4? - Go语言中文社区

为什么java里面3*0.1=0.30000000000000004,而4*0.1=0.4?


作者:蓝色
链接:https://www.zhihu.com/question/56545018/answer/149620518
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 

这个问题不简单,既跟浮点数的表示有关,也跟Java的设计机制有关。

首先参看https://en.wikipedia.org/wiki/IEEE_754-1985了解浮点数是如何表示的。

对于0.1来说,其本质是 1 / 10,那么若你用二进制表示它们,然后除的话,是这样的:1 / 1010,然而这一个是除不尽的,是无穷循环。

sqrt[1010]{1.00000000} ===> 0.0 00110011001100110011001100110011... 其中0011循环

而根据标准用科学表示法表示的话,就是1.10011001100 ... x 2^{-4}

 

根据这幅图和计算公式 (-1)^{s} * 2^{(e - 1023)} * m, 我们得到了exponent 是 -4,所以,我们的 e 取值为 1019 (1019 - 1023 = -4),1019的11位二进制表示为0111 1111 011 然后 m,即multipler,为2^{-4},即1/16。而我们0.1是正数,所以s取值为0。

 

那么根据图就应该这样摆:
0 01111111011 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 (52位)

然后在fraction,部分则为1 / 2^{n}, 那么就应该是 1 / 2 + 1/16 + 1/32 + 1/256 + 1/512 + 1/4096+ 1/8192 + 1/65536 + 1/131072 + 1/1048576 + 1/2097152 + 1/8388608 + ....,约等于0.6,然后再根据标准加上先前的隐式1,大约1.6,然后我们再乘以multipler 1 /16,值大约是0.10000000000000000555111512312578270211815834045410156。那么,若你在C++当中打印这个值我们也能得到这个结果:

#include <stdio.h>
int main(int argc, char *argv[])
{
  printf("%.53fn", 0.1);
  return 0;
}


然后若你打印17位的话,就会做round四舍五入,于是会是0.10000000000000001,而这个17位一般是我们计算机浮点数中能表示的区分其它浮点数字的位数,这是最坏的情况,一般来说我们可以更少位数就区分开相邻的浮点数。那么,是不是Java就直接取17位呢?看3 * 0.1似乎是这样,但是4 * 0.1却得到了0.4,若是取17位,应该是0.40000000000000002这样的结果,但是Java是0.4. 这种情况其实也让我疑惑了一下。然后,我去查看了Java println double的源代码,跟踪下去的时候发现println double的时候,是调用这句话

  public static String toString(double d) {
        return FloatingDecimal.toJavaFormatString(d);
 }

然后我去查询java doc: Double (Java Platform SE 7 )

Returns a string representation of the double argument. All characters mentioned below are ASCII characters.
  • If the argument is NaN, the result is the string "NaN".
  • Otherwise, the result is a string that represents the sign and magnitude (absolute value) of the argument. If the sign is negative, the first character of the result is '-' ('u002D'); if the sign is positive, no sign character appears in the result. As for the magnitude m:
    • If m is infinity, it is represented by the characters "Infinity"; thus, positive infinity produces the result "Infinity" and negative infinity produces the result "-Infinity".
    • If m is zero, it is represented by the characters "0.0"; thus, negative zero produces the result "-0.0" and positive zero produces the result "0.0".
    • If m is greater than or equal to 10
      -3
      but less than 10
      7
      , then it is represented as the integer part of m, in decimal form with no leading zeroes, followed by '.' ('u002E'), followed by one or more decimal digits representing the fractional part of m.
    • If m is less than 10
      -3
      or greater than or equal to 10
      7
      , then it is represented in so-called "computerized scientific notation." Let n be the unique integer such that 10
      n
      m < 10
      n+1
      ; then let a be the mathematically exact quotient of m and 10
      n
      so that 1 ≤ a < 10. The magnitude is then represented as the integer part of a, as a single decimal digit, followed by '.' ('u002E'), followed by decimal digits representing the fractional part of a, followed by the letter 'E' ('u0045'), followed by a representation of n as a decimal integer, as produced by the method Integer.toString(int).
How many digits must be printed for the fractional part of m or a? There must be at least one digit to represent the fractional part, and beyond that as many, but only as many, more digits as are needed to uniquely distinguish the argument value from adjacent values of type double. That is, suppose that x is the exact mathematical value represented by the decimal representation produced by this method for a finite nonzero argumentd. Then d must be the double value nearest to x; or if two double values are equally close to x, then d must be one of them and the least significant bit of the significand of d must be 0.


因崔斯汀,然后我就去查了一下资料,为什么Java会这样设计,然后我就发现有人也注意到了这个有趣的问题,接下来就看他是如何阐述这个问题吧(必读,很精彩):Java Doesn’t Print The Shortest Strings That Round-Trip,而且例子也是使用的0.1,所以也很切合题主的问题。

 

 

作者:罗智勇
链接:https://www.zhihu.com/question/56545018/answer/149944907
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 

首先预备知识:
什么是round-trip?
Any double-precision floating-point number can be identified with at most 17 significant decimal digits. This means that if you convert a floating-point number to a decimal string, round it (to nearest) to 17 digits, and then convert that back to floating-point, you will recover the original floating-point number. In other words, the conversion will round-trip.

解释:任何浮点数可以最多被17位十进制数字表示,这意味着如果你转换一个浮点数为十进制字符串需要保留17位数字,这样可以通过这17位数字转换恢复原来的浮点数,这种转换就是round-trip

我们知道了0.1,0.2...0.9,1在计算机保存中的真实值是


但是我们使用Java打印出来的0.1,0.2...0.9,1却是

咋一看,除了0.3打印的很奇怪,精确到小数点后17位,其他好像都是精确到小数点后16位,这就说明 Integer.toString(int)这个方法不是简单的四舍五入,而是所有round-trip字符串中最短的字符串~

为什么0.3不能和真实值round-trip,因为Java不一样,Java在程序中计算出的0.1,0.2...0.9,1是这样的


(ps:可以在Decimal to Floating-Point Converter中验证)
这并不是0.1,0.2...0.9,1的真正转化,这种计算的目的是保留到小数点后一位时仍然可以round-trip,比如0.3可以和0.299999999999999988897769753748434595763683319091796875相互转化,只有0.30000000000000004才能和真实值round-trip。

 

结论:
The floating-point numbers represented by the long strings are printed that way because no shorter strings (e.g., 0.3, 0.8, 0.9, and 1.0) will round-trip.

翻译:
被打印成长字符串的浮点数比如(0.3,0.8,0.9,1.0)是因为打印出的字符串
(如:0.30000000000000004)是满足round-trip字符串中最短的一个


欢迎批评指正!

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_44018338/article/details/91420963
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-02-13 14:56:53
  • 阅读 ( 1498 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢