Java 浮點數精度丟失


Java 浮點數精度丟失

問題引入

昨天幫室友寫一個模擬發紅包搶紅包的程序時,對金額統一使用的 double 來建模,結果發現在實際運行時程序的結果在數值上總是有細微的誤差,程序運行的截圖:

輸入依次為:紅包個數,搶紅包的人數,選擇固定金額紅包還是隨機金額紅包,每個紅包的金額(此例只有一個紅包)。

注意到程序最后的結果是有問題的,我們只有一個金額為 10 的紅包,一個人去搶,所以正確結果應該為這個人搶到了 10 RMB。

為了使問題更加明顯,我們測試一個更加簡單的例子:

public class SimpleTest {
    public static void main(String[] args) {
    	System.out.println(1.2 - 1);
    }
}
////
output:
0.19999999999999996			(不是 0.2)

為什么發生了我們預期之外的問題?


為什么?

\(\frac{1}{3}\)\(0.333...\)

我們先拋開 Java 不管,來看一下這個問題,如何用十進制小數表示 \(\frac{1}{3}\)

首先我們討論如何借用數軸表示 十進位小數 。如果我們將一個數軸分為 10 等份,100 等份(相當於 10 等份的每個單位區間再分 10 份),1000 等份,等等個相等的線段,則其中每個點對應着 一個十進制小數。

因此一個十進制小數可以表示為不同精度單位的加權(即乘以系數)和。

比如 \(0.12 = \frac{1}{10} + \frac{2}{100}\) ,它對應的點位於區間長為 \(10^{-1}\) 的第二個區間內(\(\frac{1}{10}\) 后),是長為 \(10^{-2}\) 的第二個子子區間的右端點。

一個十進制小數,其后有 n 個數碼,它的加權式可以寫成$$f = z + a_1{10^{-1}} + a_2{10^{-2}} + a_3{10^{-3}} + ... + a_n{10^{-n}};$$ 這里 z 是一整數,而 a 是十分之一,百分之一 等等的數碼,其中 \(a\in [0,1,2,...,9]\) 。另外,因為 \(10^{-n} = \frac{1}{10^n}\) ,所以十進位小數都可以寫成一個分數的形式。比如 $$f = 1.134 = 1 + \frac{1}{10} + \frac{3}{100} + \frac{4}{1000} = \frac{1134}{1000}$$ 。如果分子和分母有公因子,那么分數還可以進行約分。另一方面,如果分母不是 10 的某次冪的因子(即無法通過將分母乘以一個數得到 10 的某次冪(10,100,1000)來通分),那么這個分數不能表示為十進制小數。

十進制小數舉例:$$\frac{1}{5} = \frac{2}{10} = 0.2; \frac{1}{250} = \frac{4}{1000} = 0.004$$ 而非十進制小數如:$ \frac{1}{3}$,它不能寫成 n 位十進制小數的形式,因為 $$ \frac{1}{3} = \frac{b}{10^n}$$ 意味着 $$3b = 10^n$$ ,而這個結果是不成立的。我們根據之前得到的十進制小數的表達形式可以得知,非十進制小數將不會坐落在十進制數軸的任何端點上(如果在端點上那么我們就可以通過加權式來表達它,他也就是十進制小數了)。但是,雖然無法精確的在十進制數軸上表達他,我們卻可以采用無窮逼近的思想,去無限的接近它。

首先將 \(\frac{1}{3}\) 通分,$$ \frac{1}{3} = \frac{10}{30}$$ ;然后我們在數軸上尋找,最接近它的左端點為 $$\frac{9}{30} = \frac{3}{10}$$ ,右端點為 $$\frac{12}{30} = \frac{4}{10}$$ ,\(\frac{1}{3}\) 位於這個區間中的某一點,再進一步,做差 \(\frac{10}{30} - \frac{9}{30} = \frac{1}{30}\) ,這時我們取左端點為 \(\frac{3}{10}\) 后距離 \(\frac{1}{3}\) 在數軸上剩余的距離,再次進行通分,\(\frac{1}{30} = \frac{10}{300}\),現在最接近他的左端點為 \(\frac{9}{300} = \frac{3}{100}\) ,以此類推,我們取到的左端點將是 \(\frac{3}{10},\frac{3}{100},\frac{3}{1000},...\) ,而最后,\(\frac{1}{3}\) 就等於這些小的間距的加和(從原點到 \(\frac{1}{3}\) 所在點的距離):$$ \frac{1}{3} = \frac{3}{10} + \frac{3}{100} + \frac{3}{1000} + ...$$ 用十進制小數表示也即:$$\frac{1}{3} = 0.3 + 0.03 + 0.003 + ... = 0.333...$$ 無限接近但是永遠不等於 \(\frac{1}{3}\) 。在某種精度下,我們可以得到這樣的一個近似值 \(0.333333333334\)

二進制小數

之前我們討論的是在十進制的情況下,但計算機比較擅長使用二進制來表示數,所以我們接下來考慮如果二進制情況下,是否會出現之前在十進制中出現的無法精確表示的情況。(答案是肯定的,這也是我們的主題所在)

和十進制下相同,對於二進制,式:$$f = z + a_1{2^{-1}} + a_2{2^{-2}} + a_3{2^{-3}} + ... + a_n{2^{-n}};$$ 仍成立,不過基數變為了 2。(\(z\) 此時是二進制整數)

十進制下,我們對數軸每次進行 10 等划分,現在我們對數軸的單位長度每次進行 2 等划分。即:

在這種情況下,我們舉一個簡單的存在精度丟失的例子:十進制 0.1 的二進制表達

由於 $$0.1 = \frac{1}{10^1};$$ 在這里我們要求分母必須為 2 的某次冪(\(2,4,8,16,...)\)的因子,而 0.1 的 分母為 10 ,顯然不符合要求。所以無法用二進制小數精確表示,只能近似。近似的過程如下:

與 0.1 最為接近的左端點是 $$\frac{1}{16} = 0.0625$$ ,右端點為 \(\frac{1}{8} = 0.125\) ,0.1 在這個區間中的某一點上。做差:$$0.1 - 0.0625 = 0.0375$$ ;最接近的左端點為:$$\frac{1}{32} = 0.03125$$ ,如此往復,最終我們得到這樣一個式子:$$0.1 = \frac{1}{16} + \frac{1}{32} + \frac{1}{256} + ...$$ 。該式子只會無限接近 0.1 但是不會等於他。

我們當然不會忘記計算機組成原理課上學的計算十進制小數轉換二進制的方法,這個方法可以讓我們快速的求出二進制小數部分:

最后的結果是:$$0.1_{10} = 0.000110011001100_2 ...$$ 這里的小數位的每一位就是 式:$$f = z + a_1{2^{-1}} + a_2{2^{-2}} + a_3{2^{-3}} + ... + a_n{2^{-n}};$$ 中的權值 \(a\)

當我們精確位數為小數點后四位時,在計算機中的 0.1 的表示理論上就是 \(1 * \frac{1}{16} = 0.625\) ,當精確位數為十位時,即表示為:\(0.0001100110\) ,此時 \(0.1\) 的近似值為:\(0.09999847412109375\) 。如果精確位數為 27 此時值為:\(0.0999999940395355224609375\) 精確位數越高,值越接近實際值。

所以對於我們在開始時舉的例子 \(0.2\) ,情況是相似的,我們可以輕松的理解它。


在 Java 中怎么解決這個問題?

前文主要從數學的角度討論了不同進制間數的表示存在的精度問題,現在回歸到最初的問題,Java 中浮點數的精度丟失。我們知道,編程語言對數學中的數集建模是采用二進制數方式,而 計算機內存是有限 的,所以計算必然是在有限精度內進行,這種情況下精度丟失不可避免。對於 Java 中的原始類型 float、double 一般性的建議是使用他們進行科學數值計算,而對於像金額這一類生活中常見的數值類別,推薦使用 java.math.BigDecimal 類來建模(當然有精力也可自己造一個輪子)。該類可以避免之前討論的精度問題。

需要注意的是,在使用 BigDecimal 時,一定要將需要計算的數值作為 字符串 傳遞給構造函數,不然仍會發生精度丟失問題。

在使用了 BigDecimal 修改我的程序后,它工作的正常多了:)。

設置多個人並發搶紅包:

搶紅包過程(截取部分):

結果:

最后

寫這篇文章主要是因為最近在看科普書 《什么是數學》,恰好看到 十進制小數和無窮小數 部分,又遇到了 Java 精度丟失問題,然后驚喜的發現二者之間緊密的聯系,於是寫了這篇文章。

數學真奇妙。




作者:Skipper
出處:http://www.cnblogs.com/backwords/p/9826773.html
本博客中未標明轉載的文章歸作者 Skipper 和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM