一昆婿、問題呈現(xiàn)
非常經(jīng)典問題生宛,其實不僅僅是 Java 語言灼芭,還是 JS 等語言的通病粉楚,即:
當(dāng)我們在計算 0.1+0.2 時馁痴,驚訝的發(fā)現(xiàn)黔漂,結(jié)果竟然不是 0.3订雾,而是:0.30000000000000004晴玖。
二晶默、問題分析
問題很簡單谨娜,是由于我們輸入的十進(jìn)制的 double 類型的數(shù)據(jù)在進(jìn)行計算的時候,計算機會先將其轉(zhuǎn)換為二進(jìn)制數(shù)據(jù)磺陡,然后再進(jìn)行相關(guān)的運算趴梢。
然而在十進(jìn)制轉(zhuǎn)二進(jìn)制的過程中,有些十進(jìn)制數(shù)是無法使用一個有限的二進(jìn)制數(shù)來表達(dá)的币他,換言之就是轉(zhuǎn)換的時候出現(xiàn)了精度的丟失問題坞靶,所以導(dǎo)致最后在運算的過程中,自然就出現(xiàn)了我們看到的一幕蝴悉。
三彰阴、問題解決
Java 語言中最經(jīng)典的便是使用 BigDecimal 來解決。
整體思路是先將 double 類型的數(shù)據(jù)轉(zhuǎn)換成 BigDecimal 來進(jìn)行運算拍冠,最后再轉(zhuǎn)換成 double 類型的數(shù)據(jù)尿这。
但是此處有一個坑簇抵,即在將 double 轉(zhuǎn) BigDecimal 的時候,是可以有三種方式去實現(xiàn)的射众,其中兩種構(gòu)造方式碟摆,還有一種靜態(tài)方法方式:
// 方式一
public BigDecimal(double val) {
this(val,MathContext.UNLIMITED);
}
// 方式二
public BigDecimal(String val) {
this(val.toCharArray(), 0, val.length());
}
// 方式三(其實底層就是方式二)
public static BigDecimal valueOf(double val) {
// Reminder: a zero double returns '0.0', so we cannot fastpath
// to use the constant ZERO. This might be important enough to
// justify a factory approach, a cache, or a few private
// constants, later.
return new BigDecimal(Double.toString(val));
}
從上面我們可以清晰地看出,其實 BigDecimal 是希望我們使用傳入 string 類型的數(shù)據(jù)的構(gòu)造方法责球。
所以解決起來就順?biāo)浦哿私孤模慈缦?demo:
double d1 = 0.1, d2 = 0.2;
System.out.println(d1 + d2);
System.out.println(new BigDecimal(d1).add(new BigDecimal(d2)).doubleValue());
System.out.println(BigDecimal.valueOf(d1).add(BigDecimal.valueOf(d2)).doubleValue());
System.out.println(new BigDecimal(Double.toString(d1)).add(new BigDecimal(Double.toString(d2))).doubleValue());
貼出運行的結(jié)果:
0.30000000000000004
0.30000000000000004
0.3
0.3
小 Tip:
很多人會說 public BigDecimal(double val) 這個構(gòu)造方法是 Java 的一個 bug,其實我并不認(rèn)同雏逾,我覺得是傳遞的 double 類型的參數(shù)的問題嘉裤,這個數(shù)據(jù)本身就存在精度的問題,所以導(dǎo)致了最終的計算問題栖博。
換言之屑宠,其實使用計算機的二進(jìn)制來表達(dá)十進(jìn)制的小數(shù),本身就是個偽命題仇让。
四典奉、double 轉(zhuǎn)二進(jìn)制
那么,為什么使用二進(jìn)制無法精確表達(dá)一個 double 類型的數(shù)據(jù)呢丧叽?下面來手動畫圖婆剖析下?lián)Q算的方法:
舉個栗子:15.75 -> 1111.11
- step1:拆分
將整數(shù)和小數(shù)部分拆分得:15 和 0.75
- step2:計算整數(shù)部分
整數(shù)部分是 15卫玖,計算得 1111,見下圖:
- step3:計算小數(shù)部分
小數(shù)部分是 0.75踊淳,計算得 0.11假瞬,見下圖:
- step4:合并
將整數(shù)部分和小數(shù)部分拼接得到最終的結(jié)果:1111.11
再舉個經(jīng)典的栗子:0.1 -> 0.000110011001100110011001100………..
還是四步走:
- step1:拆分
將整數(shù)部分和小數(shù)部分拆分得: 0 和 0.1
- step2:計算整數(shù)部分
整數(shù)部分是 0 ,計算得: 0
- step3:計算小數(shù)部分
小數(shù)部分是 0.1迂尝,計算得:0.0001100110011001100………….脱茉,計算過程見下圖:
- step4:合并
將整數(shù)部分和小數(shù)部分合并得到最終的結(jié)果:0.000110011001100110011001100………..
五、二進(jìn)制轉(zhuǎn) double
同樣舉個栗子:1111.11 -> 15.75
還是分四步走:
- step1:拆分
將整數(shù)和小數(shù)部分拆分得:1111 和 0.11
- step2:計算整數(shù)部分
整數(shù)部分 1111 計算得 15垄开,詳細(xì)計算過程見下圖:
- step3:計算小數(shù)部分
小數(shù)部分 0.11 計算得 0.75琴许,詳細(xì)計算過程見下圖:
- step4:合并
整數(shù)部分和小數(shù)部分合并得最終的結(jié)果:15.75