我所在的公司近期要做一个打赏的功能,比如说发一张照片其他人可以对这张照片进行打赏,给些小钱。我的工作是负责给客户端下发打赏消息。工作完工之后客户端同学说有个问题,我下发的打赏金额是string类型的,他们觉得double才对。于是我就去找老大问这个能不能改成double类型,老大说这个应该是string才对的,我说金额不是数字么,然后老大笑着说你回去好好想想。。。。。。

  (二逼版开头:天下没有白费的努力,世间没有无用的奋斗,我想公司应该是想向社会传达这种美丽的鸡汤才决定开启打赏这个项目的……在我看来,终于可以名正言顺的给暗恋的女神打钱了~亲,你的照片真美,这是赏你的……我不知道有没有限额,因为我的工作只是负责给客户端下发打赏消息……不过或许另一个哥们估计试了下限额,这小子难道已经做好倾家荡产的准备了……他说限额类型不对,是string类型的,他觉得应该是double类型的……瞬间,我就被说服了……当然我不能怀疑老大,我只是需要装作什么都不懂的样子去问老大就可以了……快点改过来,女神还等着我线上撒钱呢……老大的微笑貌似将我整个人看透了一样,那嘴角的弧度让我想起了长者……图样图森破,老大说,就是string类型,回去自己好好想想,不然这个月工资给你扣了,看你怎么给女的打赏……)

  浮点数会有精度损失这个在上学的时候就知道,但是至今完全没有体会到,老师讲的时候也是一笔带过的,自己也没有自己琢磨。终于在工作的时候碰到了,于是google了一番。

  首先上代码:



Java中双精度赋值_浮点数



package sort;

public class NumTest {
    public static void main(String[] args) {
        double a =1;
        double b =0.99;
        
        System.out.println(a-b);
    }
}



Java中双精度赋值_浮点数



  这段代码运行结果很简单不是0.01么?!

  在这一天之前如果问我结果是什么我也会毫不犹豫的答道0.01,然而真实的结果是:0.010 00000 00000 00009。

  虽然运算结果不太对,但是这个结果和0.01相差不大会产生影响么?那看下面这一段代码:



Java中双精度赋值_浮点数



package sort;

public class NumTest {
    public static void main(String[] args) {
        double a =1;
        double b =0.99;
        
        System.out.println(a-b);
        if((a-b) == 0.01){
            System.out.println("1 - 0.99 == 0.01");
        }else{
            System.out.println("1 - 0.99 != 0.01");
        }
    }
}



Java中双精度赋值_浮点数



  输出的结果也在意料之中:1 - 0.99 != 0.01

  如果在程序中直接使用double会造成精度损失,极有可能对造成一些莫名奇妙的bug。

  但是所有的浮点数都会有精度损失么?



Java中双精度赋值_浮点数



package sort;

public class NumTest {
    public static void main(String[] args) {
        double a = 0.5;
        double b = 0.25;
        
        System.out.println(a-b);
        if((a-b) == 0.25){
            System.out.println("no problem");
        }else{
            System.out.println("has problem");
        }
        
    }
}



Java中双精度赋值_浮点数



输出的结果是:no problem,也就是说double类型的0.5和0.25在运算的时候没有出现精度损失。

  关于精度损失的原理可以很简单的讲,首先一个正整数在计算机中表示使用01010形式表示的,浮点数也不例外。

    比如11,11除以2等于5余1

         5除以2等于2余1

         2除以2等于1余0

         1除以2等于0余1

  所以11二进制表示为:1011.

  double类型占8个字节,64位,第1位为符号位,后面11位是指数部分,剩余部分是有效数字。

  正整数除以2肯定会有个尽头的,之后二进制还原成十进制只需要乘以2即可。

  举个例子:0.99用的有效数字部分,

        0.99 * 2 = 1+0.98 --> 1

        0.98 * 2 = 1+ 0.96 --> 1

        0.96 * 2 = 1+0.92 -- >1

        0.92 * 2 = 1+0.84 -- >1

          ...............

  这样周而复始是没法有尽头的,而double有效数字有限,所以必定会有损失,所以二进制无法准确表示0.99,就像十进制无法准确表示1/3一样。

2.解决

  一般遇到这种需要用到浮点数运算的地方都可以使用java.math.BigDecimal。

  首先需要注意的是,直接使用字符串来构造BigDecimal是绝对没有精度损失的,如果用double或者把double转化成string来构造BigDecimal依然会有精度损失,所以我觉得这种解决方法就是在数据库中就把浮点数用string来表示存放,涉及到运算直接用string构造double,否则肯定会有精度损失。

 



Java中双精度赋值_浮点数



package sort;

import java.math.BigDecimal;

public class NumTest {
    public static void main(String[] args) {
        String a = "301353.0499999999883584678173065185546875";
        double c = 301353.0499999999883584678173065185546875d;
        BigDecimal sa = new BigDecimal(a);
        BigDecimal sc = new BigDecimal(String.valueOf(c));
        BigDecimal dc = new BigDecimal(Double.toString(c));
        
        System.out.println("sa : "+ sa);
        System.out.println("sc : "+ sc);
        System.out.println("dc : "+ dc);
        
    }
}



Java中双精度赋值_浮点数



上述代码的输出结果是:

  sa : 301353.0499999999883584678173065185546875

  sc : 301353.05

  dc : 301353.05

所以最好的方法是完全抛弃double,用string和java.math.BigDecimal。

关于java.math.BigDecimal的原理有待继续探究。