探究背景
涉及諸如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的異常情況處理及注意事項等等。