浮點類型精度丟失和BigDecimal

精度丟失

在工作中經常會遇到數值精度問題,比如說使用float或者double的時候,可能會有精度丟失問題,下面來總結一下吧。

爲了引出問題,先看一個例子(Java代碼):

    public static void main(String[] args) {
        float f = 2.25f;
        double d = (double) f;
        System.out.println(d);

        f = 2.2f;
        d = (double) f;
        System.out.println(d);
    }

結果如下:

2.25
2.200000047683716

問題出現了,單精度類型的2.2在轉換爲雙精度類型後,2.2變成了2.200000047683716。

下面分析下原因。
對於浮點類型,Oracle的JVM規範(jdk8)是這樣定義的:

浮點類型是 float和double,它們在概念上與32位單精度和64位雙精度格式的IEEE 754值以及在IEEE二進制浮點算術標準(ANSI / IEEE Std。 754-1985,紐約)。

引文可參考:浮點類型,值集和值
那麼什麼是IEEE二進制浮點算術標準呢?

IEEE二進制浮點數算術標準(IEEE 754)是20世紀80年代以來最廣泛使用的浮點數運算標準,爲許多CPU與浮點運算器所採用。這個標準定義了表示浮點數的格式(包括負零-0)與反常值(denormal number)),一些特殊數值(無窮(Inf)與非數值(NaN)),以及這些數值的“浮點數運算符”;它也指明瞭四種數值舍入規則和五種例外狀況(包括例外發生的時機與處理方式)。

官方文檔:754-2008-浮點算法的IEEE標準

簡單來說,就是IEEE 754定義了一種浮點類型數值在計算機內部的存儲方式。
無論是單精度還是雙精度在存儲中都分爲三個部分:

  1. 符號位(sign) : 0表示正,1表示負。佔1bit;
  2. 指數位(biased exponent):首先exponent表示該域用於表示指數,也就是數值可表示數值範圍,而biased則表示它採用偏移的編碼方式。那麼什麼是採用偏移的編碼方式呢?也就是位模式中沒有設立sign-bit,而是通過設置一箇中間值作爲0,小於該中間值則爲負數,大於改中間值則爲正數。IEEE 754中規定bias = 2^e-1 - 1,e爲Biased-exponent所佔位數;
  3. 尾數部分(trailing significand field):尾部有效位字段,也就是數值可表示的精度。

如下圖所示:
在這裏插入圖片描述
對於單精度類型(32bit)和雙精度類型(64bit),其不同之處在於指數位,單精度爲8位指數位,而雙精度爲11位。其實裏面有個公式,可以對不同精度的數值算出不同的指數位,這裏不展開了。
如圖所示:
在這裏插入圖片描述
再回到上面那個問題,2.25的單精度和雙精度分別是:

單精度:0 1000 0001 001 0000 0000 0000 0000 0000
雙精度:0 100 0000 0001 0010 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

這樣2.25在進行強制轉換的時候,數值是不會變的,而我們再看看2.2呢。
2.2的小數部分在轉換爲二進制的時候,是乘不盡的,得到的二進制是一個無限循環的排列 00110011001100110011…,如果是單精度的,那麼就是:

單精度:0 1000 0001 001 1001 1001 1001 1001 1001

那麼以這樣的存儲方式,當它再轉換爲十進制的時候,就不再是2.2了。
雙精度也是如此。
這就是精度的丟失。

BigDecimal

BigDecimal爲什麼就能避免精度的丟失問題呢?
首先,BigDecimal的存儲方式並不是像浮點類型一樣在計算機中直接存儲的,而是Java通過封裝了一系列的基本類型來實現的,它會把一個浮點類型的數值拆分進行分別存儲。
BigDecimal裏面有下面四個主要的字段:

   private final BigInteger intVal;
   private final int scale;
   private transient int precision;
   private final transient long intCompact;
  • intVal:有效數字(去掉前綴0和小數點)的數值,比如-092233720368.54775807,intVal就是-9223372036854775807。
  • scale:比例尺度,也就是小數位數。比如說2.567,那麼scale就是3;如果沒有小數位,那麼scale就是0.
  • precision:精度,也就是有效數字的位數。比如12.567,那麼precision就是5,即使輸入“-0012.567”,precision仍然是5。
  • intCompact:由於intVal的類型是BigInteger,需要佔用更多的內存空間,所以增加了long類型的intCompact字段。如果此BigDecimal的有效數字的絕對值小於或等於Long.MAX_VALUE,該值可以存儲在此字段中,並用於計算。

這樣的存儲組合方式,就可以在計算的時候直接使用intVal或者intCompact進行計算了,而整數直接的計算是不會產生精度丟失問題的。

如果有小數的話,只需要對比scale,然後補全scale較小的那個值,進行小數點對齊之後再計算就可以了。

比如說9.53和2.1進行相加的時候,實際上就是scale=2的條件下的953+210=1163,因爲scale=2,所以表示的真實數值就是11.63.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章