1. 程式人生 > >java浮點數精度損失原理和解決

java浮點數精度損失原理和解決


我所在的公司近期要做一個打賞的功能,比如說發一張照片其他人可以對這張照片進行打賞,給些小錢。我的工作是負責給客戶端下發打賞訊息。工作完工之後客戶端同學說有個問題,我下發的打賞金額是string型別的,他們覺得double才對。於是我就去找老大問這個能不能改成double型別,老大說這個應該是string才對的,我說金額不是數字麼,然後老大笑著說你回去好好想想。。。。。。

  (二逼版開頭:天下沒有白費的努力,世間沒有無用的奮鬥,我想公司應該是想向社會傳達這種美麗的雞湯才決定開啟打賞這個專案的……在我看來,終於可以名正言順的給暗戀的女神打錢了~親,你的照片真美,這是賞你的……我不知道有沒有限額,因為我的工作只是負責給客戶端下發打賞訊息……不過或許另一個哥們估計試了下限額,這小子難道已經做好傾家蕩產的準備了……他說限額型別不對,是string型別的,他覺得應該是double型別的……瞬間,我就被說服了……當然我不能懷疑老大,我只是需要裝作什麼都不懂的樣子去問老大就可以了……快點改過來,女神還等著我線上撒錢呢……老大的微笑貌似將我整個人看透了一樣,那嘴角的弧度讓我想起了長者……圖樣圖森破,老大說,就是string型別,回去自己好好想想,不然這個月工資給你扣了,看你怎麼給女的打賞……)

  浮點數會有精度損失這個在上學的時候就知道,但是至今完全沒有體會到,老師講的時候也是一筆帶過的,自己也沒有自己琢磨。終於在工作的時候碰到了,於是google了一番。

  首先上程式碼:

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

這段程式碼執行結果很簡單不是0.01麼?!

  在這一天之前如果問我結果是什麼我也會毫不猶豫的答道0.01,然而真實的結果是:0.010 00000 00000 00009。

  雖然運算結果不太對,但是這個結果和0.01相差不大會產生影響麼?那看下面這一段程式碼:

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");
        }
    }
}

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

  如果在程式中直接使用double會造成精度損失,極有可能對造成一些莫名奇妙的bug。

  但是所有的浮點數都會有精度損失麼?

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");
        }
    }
}

輸出的結果是: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,否則肯定會有精度損失。

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);
    }
}

上述程式碼的輸出結果是:

  sa : 301353.0499999999883584678173065185546875

  sc : 301353.05

  dc : 301353.05

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

關於java.math.BigDecimal的原理有待繼續探究。

 


來源於:

https://www.cnblogs.com/kniught-ice/p/4755122.html