☕【Java實戰系列】「技術盲區」Double與Float的坑與解決辦法以及BigDecimal的取而代之!

探究背景

涉及諸如float或者double這兩種浮點型數據的處理時,偶爾總會有一些怪怪的現象,不知道大家注意過沒,舉幾個常見的栗子:

條件判斷超預期

System.out.println( 1f == 0.9999999f );   // 打印:false
System.out.println( 1f == 0.99999999f );  // 打印:true

數據轉換超預期

float f = 1.1f;
double d = (double) f;
System.out.println(f);  // 打印:1.1
System.out.println(d);  // 打印:1.100000023841858 

基本運算超預期

System.out.println( 0.2 + 0.7 );
// 打印:0.8999999999999999   納尼?

數據自增超預期

float f1 = 8455263f;
for (int i = 0; i < 10; i++) {
    System.out.println(f1);
    f1++;
}
// 打印:8455263.0
// 打印:8455264.0
// 打印:8455265.0
// 打印:8455266.0
// 打印:8455267.0
// 打印:8455268.0
// 打印:8455269.0
// 打印:8455270.0
// 打印:8455271.0
// 打印:8455272.0

float f2 = 84552631f;
for (int i = 0; i < 10; i++) {
    System.out.println(f2);
    f2++;
}
//    打印:8.4552632E7   納尼?不是 +1了嗎?
//    打印:8.4552632E7   納尼?不是 +1了嗎?
//    打印:8.4552632E7   納尼?不是 +1了嗎?
//    打印:8.4552632E7   納尼?不是 +1了嗎?
//    打印:8.4552632E7   納尼?不是 +1了嗎?
//    打印:8.4552632E7   納尼?不是 +1了嗎?
//    打印:8.4552632E7   納尼?不是 +1了嗎?
//    打印:8.4552632E7   納尼?不是 +1了嗎?
//    打印:8.4552632E7   納尼?不是 +1了嗎?
//    打印:8.4552632E7   納尼?不是 +1了嗎?

所以說用浮點數(包括double和float)處理問題有非常多隱晦的坑在等着咱們!

分析原因出處

我們就以第一個典型現象爲例來分析一下:

System.out.println( 1f == 0.99999999f );

直接用代碼去比較1和0.99999999,居然打印出true!這說明了什麼?這說明了計算機壓根區分不出來這兩個數。這是爲什麼呢?

深入分析

輸入的這兩個浮點數只是我們人類肉眼所看到的具體數值,是我們通常所理解的十進制數,但是計算機底層在計算時可不是按照十進制來計算的,學過計算機組成原理的人都知道,計算機底層最終都是基於像010100100100110011011這種0、1二進制來完成的。

將這兩個十進制浮點數轉化到二進制,直接給出結果(把它轉換到IEEE 754 Single precision 32-bit,也就float類型對應的精度)

1.0(十進制)
    ↓
00111111 10000000 00000000 00000000(二進制)
    ↓
0x3F800000(十六進制)
0.99999999(十進制)
    ↓
00111111 10000000 00000000 00000000(二進制)
    ↓
0x3F800000(十六進制)

這兩個十進制浮點數的底層二進制表示是一樣的,怪不得==的判斷結果返回true!

但是1f == 0.9999999f返回的結果是符合預期的,打印false,我們也把它們轉換到二進制模式下看看情況:

1.0(十進制)
    ↓
00111111 10000000 00000000 00000000(二進制)
    ↓
0x3F800000(十六進制)

0.9999999(十進制)
    ↓
00111111 01111111 11111111 11111110(二進制)
    ↓
0x3F7FFFFE(十六進制)

它倆的二進制數字表示確實不一樣,這是理所應當的結果。

那麼爲什麼0.99999999的底層二進制表示竟然是:00111111 10000000 00000000 00000000呢?

這不明明是浮點數1.0的二進制表示嗎?主要要分一下浮點數的精度問題了。

浮點數的精度問題!
學過 《計算機組成原理》 這門課的小夥伴應該都知道,浮點數在計算機中的存儲方式遵循IEEE 754 浮點數計數標準,可以用科學計數法表示爲:

只要給出:符號(S)、階碼部分(E)、尾數部分(M) 這三個維度的信息,一個浮點數的表示就完全確定下來了,所以float和double這兩種浮點數在內存中的存儲結構如下所示:

符號部分(S)

0-正 1-負

階碼部分(E)(指數部分):

對於float型浮點數,指數部分8位,考慮可正可負,因此可以表示的指數範圍爲-127 ~ 128
對於double型浮點數,指數部分11位,考慮可正可負,因此可以表示的指數範圍爲-1023 ~ 1024

尾數部分(M):

浮點數的精度是由尾數的位數來決定的:

  • 對於float型浮點數,尾數部分23位,換算成十進制就是 2^23=8388608,所以十進制精度只有6 ~ 7位;
  • 對於double型浮點數,尾數部分52位,換算成十進制就是 2^52 = 4503599627370496,所以十進制精度只有15 ~ 16位

對於上面的數值0.99999999f,很明顯已經超過了float型浮點數據的精度範圍,出問題也是在所難免的。

精度問題如何解決

涉及商品金額、交易值、貨幣計算等這種對精度要求很高的場景該怎麼辦呢?

方法一:用字符串或者數組解決多位數問題

方法二:Java的大數類是個好東西

JDK早已爲我們考慮到了浮點數的計算精度問題,因此提供了專用於高精度數值計算的大數類來方便我們使用。Java的大數類位於java.math包下:可以看到,常用的BigInteger 和 BigDecimal就是處理高精度數值計算的利器。

BigDecimal num3 = new BigDecimal( Double.toString( 1.0f ) );
BigDecimal num4 = new BigDecimal( Double.toString( 0.99999999f ) );
System.out.println( num3 == num4 );  // 打印 false
BigDecimal num1 = new BigDecimal( Double.toString( 0.2 ) );
BigDecimal num2 = new BigDecimal( Double.toString( 0.7 ) );
// 加
System.out.println( num1.add( num2 ) );  // 打印:0.9
// 減
System.out.println( num2.subtract( num1 ) );  // 打印:0.5
// 乘
System.out.println( num1.multiply( num2 ) );  // 打印:0.14
// 除
System.out.println( num2.divide( num1 ) );  // 打印:3.5

當然了,像BigInteger 和 BigDecimal這種大數類的運算效率肯定是不如原生類型效率高,代價還是比較昂貴的,是否選用需要根據實際場景來評估。

實際案例場景

使用Double計算問題

如果需要記錄一個16位整數且保留兩位小數點的金額數值,於是使用Double類型來接收金額,但在最後進行金額總和統計後,得出的金額數值小數點後面多出了小數位,且多出的小數位不爲0,簡直要瘋了,每一筆的金額都是兩位小數點,但最後統計的總金額數值卻是多位小數點的。

double和float類型主要用於科學計算與工程計算而設計的,用於二進制浮點計算,但我們在程序中寫的時候往往都是寫的10進制,而這個10進制的小數,對於計算機內部而言,是無法用二進制的小數來精確表達出來的,只能表示出一個“不精確性”或者說“近似性”的結果,而用這個近似性的結果進行計算得出的數據,也往往與我們心中想要的數據不一樣,所以如果是想進行金額或其他類似的浮點型數值計算,不要使用double或float,推薦大家使用BigDecimal來進行運算。

BigDecimal的工具使用

BigDecimal是Java在java.math包中提供的API類,它可以用來對超過16位有效位的數進行精確的運算和處理。

BigDecimal創建對象

BigDecimal提高了四個構造方法來創建對象:

  • 創建整數類型的對象:new BigDecimal(int);
  • 創建雙精度數值類型的對象:new BigDecimal(double);
  • 創建長整數類型的對象:new BigDecimal(long);
  • 創建以字符串表示的數值的字符串類型對象:new BigDecimal(String);

四個構造方法就是四種創建對象的方式,但推薦使用第1、3、4種方式,而不推薦使用第2種方式,因爲前面說了double無法精確的表示10進制的小數,只能近似性的表示,這就具有一定的不可預知性了,如需創建浮點類型的BigDecimal對象,可以使用new BigDecimal(String)來創建。

BigDecimal的運算

BigDecimal對於數值的運算,提供了專用的方法:

  • BigDecimal.add(BigDecimal)  BigDecimal對象的相加方法,返回BigDecimal對象
  • BigDecimal.subtract(BigDecimal)  BigDecimal對象的相減方法,返回BigDecimal對象
  • BigDecimal.multiply(BigDecimal)  BigDecimal對象的相乘方法,返回BigDecimal對象
  • BigDecimal.divide(BigDecimal)  BigDecimal對象的相除方法,返回BigDecimal對象

注意:BigDecimal的對象都是不可變的,它的每一次四則運算,都會產生並返回新的對象,所以在做加減乘除運算時要用新的對象來保存操作後的值。

BigDecimal比較大小

BigDecimal提供了compareTo(BigDecimal)來進行數值的大小比較,compareTo返回值爲int類型:-1,0,1;

例如:bigdemical_1.compareTo(bigdemical_2)

  • 返回-1:表示bigdemical_1小於bigdemical_2;
  • 返回0,表示bigdemical_1等於bigdemical_2;
  • 返回1,表示bigdemical_1大於bigdemical_2;

BigDecimal還有其他一些東西,例如,BigDecimal的格式化、BigDecimal的輸出類型轉換、BigDecimal的異常情況處理及注意事項等等。

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