【轉載】Double為什麼會丟失精度
前言:在工作中,談到有小數點的加減乘除都會想到用BigDecimal來解決,但是有很多人對於double或者float為啥會丟失精度一臉茫然。還有BigDecimal是怎麼解決的?話不多說,我們開始。
1.浮點數是啥?
浮點數是計算機用來表示小數的一種資料型別,採用科學計數法。
在java中,double是雙精度,64位,浮點數,預設是0.0d。float是單精度,32位,浮點數,預設是0.0f;
在記憶體中儲存
float 符號位(1bit) 指數(8 bit) 尾數(23 bit)
double 符號位(1bit) 指數(11 bit) 尾數(52 bit)
float在記憶體中佔8位,由於階碼實際儲存的是指數的移碼,假設指數的真值是e,階碼為E,則有E=e+(2^n-1 -1)。其中 2^n-1 -1是IEEE754標準規定的指數偏移量,根據這個公式我們可以得到 2^8 -1=127。於是,float的指數範圍為-128 +127,而double的指數範圍為-1024 +1023。其中負指數決定了浮點數所能表達的絕對值最小的非零數;而正指數決定了浮點數所能表達的絕對值最大的數,也即決定了浮點數的取值範圍。
loat的範圍為-2^128 ~ +2^127,也即-3.40E+38 ~ +3.40E+38;
double的範圍為-2^1024 ~ +2^1023,也即-1.79E+308 ~ +1.79E+308
2.走進失真之科學計數法
我們先說說科學計數法,科學計數法是一種簡化計數的方法,用來近似表示一個極大或極小且位數較多的數,對於位數較小的數值,科學計數法沒有什麼優勢,但對於位數較多的數值其計數方法的優勢就非常明顯了。例如:光的速速是300000000米/秒,全世界人口數大約是6100000000。類似光的速度和世界人口數這樣大數值的數,讀、寫都很不方便,所以光的速度可以寫成3*10^8,全世界人口數可以寫成6.1*10^9。所以計算器用科學計數法表示光速是3E8,世界人口數大約是6.1E9。
我們小時候玩計算器喜歡瘋狂的累加或者累減,到最後計算器就會顯示下圖。這個就是科學計數法顯示的結果
那圖中真實的值是 -4.86*10^11=-486000000000。十進位制科學計數法要求有效數字的整數部分必須在【1,9】區間內。
3.走進失真之精度
計算機在處理資料都涉及到資料的轉換和各種複雜運算,比如,不同單位換算,不同進位制(如二進位制十進位制)換算等,很多除法運算不能除盡,比如10÷3=3.3333.....無窮無盡,而精度是有限的,3.3333333x3並不等於10,經過複雜的處理後得到的十進位制資料並不精確,精度越高越精確。float和double的精度是由尾數的位數來決定的,其整數部分始終是一個隱含著的“1”,由於它是不變的,故不能對精度造成影響。float:2^23 = 8388608,一共七位,由於最左為1的一位省略了,這意味著最多能表示8位數: 28388608 = 16777216 。有8位有效數字,但絕對能保證的為7位,也即float的精度為7~8位有效數字;double:2^52 = 4503599627370496,一共16位,同理,double的精度為16~17位
當到達一定值自動開始使用科學計數法,並保留相關精度的有效數字,所以結果是個近似數,並且指數為整數。在十進位制中小數有些是無法完整用二進位制表示的。所以只能用有限位來表示,從而在儲存時可能就會有誤差。對於十進位制的小數轉換成二進位制採用乘2取整法進行計算,取掉整數部分後,剩下的小數繼續乘以2,直到小數部分全為0。
如遇到
輸出是 0.19999999999999998 double型別 0.3-0.1的情況。需要將0.3轉成二進位制在運算 0.3 * 2 = 0.6 => .0 (.6)取0剩0.6 0.6 * 2 = 1.2 => .01 (.2)取1剩0.2 0.2 * 2 = 0.4 => .010 (.4)取0剩0.4 0.4 * 2 = 0.8 => .0100 (.8) 取0剩0.8 0.8 * 2 = 1.6 => .01001 (.6)取1剩0.6 .............
3.總結
從上面看,很清楚為什麼浮點數有精度問題。簡單地說,float和double型別主要是為科學計算和工程計算而設計的。它們執行二進位制浮點運算,這些運算經過精心設計,能夠在廣泛的數值範圍內提供更精確的快速近似和計算而精心設計的。但是,它們不能提供完全準確的結果,因此不能用於需要計算精確結果的場景中。當浮點數達到一定的大數時自動使用科學計數法。這樣的表示只是近似真實數而不等於真實數。當十進位制小數轉換為二進位制時,也會出現無限迴圈或超出浮點數尾部的長度。
4.那我們怎麼用BigDecimal來解決?
大家看下面的兩個輸出
輸出結果:
0.299999999999999988897769753748434595763683319091796875
0.3
圖上阿里的程式碼約束外掛在圖表上已經標記了警告,所以讓我使用String字串引數的構造方法建立BigDecimal。由於double不能精確表示為0.3(任何有限長度的二進位制),因此用double建構函式傳遞的值不完全等於0.3。使用bigdecimal時,必須使用String字串引數構造方法來建立它。在這一點上,有沒有好奇的疑問。BigDecimal原理是什麼?為什麼它就沒事?原理很簡單。BigDecimal是不可變的,可以用來表示任意精度的帶符號十進位制數。double的問題是從小數點轉換到二進位制丟失精度,二進位制丟失精度。BigDecimal在處理的時候把十進位制小數擴大N倍讓它在整數上進行計算,並保留相應的精度資訊。至於BigDecimal是怎麼儲存的可以翻閱一下原始碼。
5.總結
(1)商業計算使用BigDecimal。 (2)儘量使用引數型別為String的建構函式。 (3) BigDecimal都是不可變的(immutable)的,在進行每一步運算時,都會產生一個新的物件,所以在做加減乘除運算時千萬要儲存操作後的值。 (4)我們往往容易忽略JDK底層的一些實現細節,導致出現錯誤,需要多加註意。
轉載至:https://blog.csdn.net/u011277123/article/details/95774544