【問題】
在之前的一篇文章中,提到過float和double不能用於金額計算,原因是浮點型數據計算中會產生誤差,造成結果不准確。這一篇我們仔細分析這種誤差的產生來源。
先看一段代碼:
public static void main(String[] args) { float a = 34.12f; float b = 34; float c = 0.12f; System.out.println(a - b); System.out.println(c); }
//0.11999893
//0.12
問題來了:為什么計算出來的0.12不能准確地展示,但是浮點型的0.12可以完整展示出來呢?如果將三個變量換為double類型,第一個結果會變成:0.11999999999999744,這又是為什么?
————————————————————————————————————————————————
【拆解】
首先,我們來了解,float和double這兩種浮點類型的數據,在計算中是怎么存儲的。比如22.45這個浮點數,在計算機的0和1這個體系里是怎么存儲呢?
整數部分22,直接轉換成二進制:10110
小數部分0.45,計算機的處理方式是乘以2,並取整數:
0.45*2=0.90---0
0.90*2=1.80---1
0.80*2=1.60---1
0.60*2=1.20---1
0.20*2=0.40---0
0.40*2=0.80---0
0.80*2=1.60---1
......
此時,我們已經知道,這種方式去存儲小數的話,一定是一個需要無限空間的,計算機不可能也沒必要為了這么一個浮點數,進行不限制空間的精確存儲,肯定是有舍棄的,這也就是32位和64位的精度這個說法的來源。那么,此時還有一個問題,這個小數點該如何處理呢?
到這里,我們就要了解計算機存儲浮點數的規則。這個規則定義了如何處理符號,小數點和精度。比如22.45這個數,我們剛剛轉換的結果為:10110.01110011001100......,那么計算機要怎么存儲這個數字呢?
首先,計算機使用科學計數法,將10110.01110011001100...表示為1.0110...*2^4,這里涉及到了原始的數據、指數、符號三個關鍵因素。也就是說,只要確定了這三個問題,那么浮點數就可以完全按照0和1的方式存儲下來。
————————————————————————————————————————————————
【背景】
其實在20世紀80年代之前,業界還沒有一個統一的浮點數表示標准。很多計算機制造商根據自己的需要來設計自己的浮點數表示規則,以及浮點數的執行運算細節,這樣就給代碼的可移植性造成了重大障礙。
直到 1976 年,Intel 公司打算為其 8086 微處理器引進一種浮點數協處理器時,意識到作為芯片設計者的電子工程師和固體物理學家也許並不能通過數值分析來選擇最合理的浮點數二進制格式。於是,他們邀請加州大學伯克利分校的 William Kahan 教授(當時最優秀的數值分析家)來為 8087 浮點處理器(FPU)設計浮點數格式。而這時,William Kahan 教授又找來兩個專家協助他,於是就有了 KCS 組合(Kahn、Coonan和Stone),並共同完成了 Intel 公司的浮點數格式設計。
由於 Intel 公司的 KCS 浮點數格式完成得如此出色,以致 IEEE(Institute of Electrical and Electronics Engineers,電子電氣工程師協會)決定采用一個非常接近 KCS 的方案作為 IEEE 的標准浮點格式。於是,IEEE 於 1985 年制訂了二進制浮點運算標准 IEEE 754(IEEE Standard for Binary Floating-Point Arithmetic,ANSI/IEEE Std 754-1985),該標准限定指數的底為 2,並於同年被美國引用為 ANSI 標准。目前,幾乎所有的計算機都支持 IEEE 754 標准,它大大地改善了科學應用程序的可移植性。
考慮到 IBM System/370 的影響,IEEE 於 1987 年推出了與底數無關的二進制浮點運算標准 IEEE 854,並於同年被美國引用為 ANSI 標准。1989 年,國際標准組織 IEC 批准 IEEE 754/854 為國際標准 IEC 559:1989。后來經修訂后,標准號改為 IEC 60559。現在,幾乎所有的浮點處理器完全或基本支持 IEC 60559。同時,C99 的浮點運算也支持 IEC 60559。
IEEE 浮點數標准是從邏輯上用三元組{S,E,M}來表示一個數 V 的,即 V=(-1)S×M×2E,如圖所示。
————————————————————————————————————————————————
【規則】
我們來詳細了解double與float的組成規則。
如圖所示,double類型中,sign用1bit表示正負,其中0表示正,1表示負;exponent用11bits表示科學計數法中的指數數據;剩余的52bits表示尾數,稱為R64.53標准。float類型中,sign用1bit表示正負,其中0表示正,1表示負;exponent用8bits表示科學計數法中的指數數據;剩余的23bits表示尾數,稱為R32.24標准。
至此,我們應該明白,絕大部分浮點數,在存儲時都會損失部分數據,即存儲之后,再將存儲數據轉換為原始數據時會產生誤差。而double和float只是保留尾數長度不同,所以精度不同。
回到文初的問題,整個過程應該是這樣的:
計算機先存儲a,此時會造成精度丟失;存儲b時,精度不會丟失;二進制的a減去二進制的b,過程(求階差、対階、尾數相減、規格化)在此不詳細描述,但也會再次造成精度的丟失;最終計算機再將這個二進制數按照IEEE規則轉回去,輸出;
第二步的c,計算機轉化為二進制存儲,再按照IEEE規則轉回去。
雖然第二步的精度也有損失,但是精度的損失保持在很小的范圍,所以從二進制轉為字符串展示時,能夠保持字面一致;但是第一步在計算過程中損失了更多的精度,字面的一致已經無法保證。說到底,這是一個精度損失了多少的問題,如果是在一個極小范圍內的精度損失,即便兩個浮點數值略微不同,但由於最終轉換為的二進制保持了一致,所以再轉回字符串時可以保持字面相同,我們再看一個示例:
System.out.println(Long.toBinaryString(Float.floatToIntBits(22.45999999999999999999f))+"----"+22.45999999999999999999f); System.out.println(Long.toBinaryString(Float.floatToIntBits(22.46000000000000000001f))+"----"+22.46000000000000000001f); System.out.println(Long.toBinaryString(Float.floatToIntBits(22.45999999999999999998f))+"----"+22.45999999999999999998f); System.out.println(Long.toBinaryString(Float.floatToIntBits(22.45999999999999999997f))+"----"+22.45999999999999999997f); System.out.println(Long.toBinaryString(Float.floatToIntBits(22.45999799999999999999f))+"----"+22.45999799999999999999f);
結果為:
1000001101100111010111000010100----22.46
1000001101100111010111000010100----22.46
1000001101100111010111000010100----22.46
1000001101100111010111000010100----22.46
1000001101100111010111000010011----22.459997
可以看到,前四個數據,雖然有所不同,但是極為接近,導致按照R32.24標准轉化的二進制相同。所以最終轉化的字面值也是相同的。但是最后一個數據按照標准轉為二進制已經發生了變化,所以再轉回字面值也發生了變化。
————————————————————————————————————————————————
【拓展】
此時我們已經了解了float和double的誤差來源。那么BigDecimal為什么可以保證計算精度呢?
此處直接給出原因:BigDecimal並沒有按照浮點數那樣,依照IEEE754標准進行轉換,而是直接將浮點數放大一定的倍數,使得小數剛好轉換為整數,再進行整數轉換二進制,也就不會出現精度損失。也就是說,在BigDecimal處理數據的過程中,不會出現無限循環的情況。
但如果在BigDecimal的除法運算中,沒有指定scale,造成了循環的除法運算,會拋出異常:
Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
at java.math.BigDecimal.divide(BigDecimal.java:1690)