老大說:誰要再用double定義商品金額,就自己收拾東西走
先看現象
涉及諸如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
)處理問題有非常多隱晦的坑在等着咱們!
怪不得技術總監發狠話:誰要是敢在處理諸如 商品金額、訂單交易、以及貨幣計算時用浮點型數據(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
這兩種浮點數在內存中的存儲結構如下所示:


1、符號部分(S)
0
-正 1
-負
2、階碼部分(E)(指數部分):
- 對於
float
型浮點數,指數部分8
位,考慮可正可負,因此可以表示的指數范圍為-127 ~ 128
- 對於
double
型浮點數,指數部分11
位,考慮可正可負,因此可以表示的指數范圍為-1023 ~ 1024
3、尾數部分(M):
浮點數的精度是由尾數的位數來決定的:
- 對於
float
型浮點數,尾數部分23
位,換算成十進制就是2^23=8388608
,所以十進制精度只有6 ~ 7
位; - 對於
double
型浮點數,尾數部分52
位,換算成十進制就是2^52 = 4503599627370496
,所以十進制精度只有15 ~ 16
位
所以對於上面的數值0.99999999f
,很明顯已經超過了float
型浮點數據的精度范圍,出問題也是在所難免的。
精度問題如何解決
所以如果涉及商品金額、交易值、貨幣計算等這種對精度要求很高的場景該怎么辦呢?
方法一:用字符串或者數組解決多位數問題
校招刷過算法題的小伙伴們應該都知道,用字符串或者數組表示大數是一個典型的解題思路。
比如經典面試題:編寫兩個任意位數大數的加法、減法、乘法等運算。
這時候我們我們可以用字符串或者數組來表示這種大數,然后按照四則運算的規則來手動模擬出具體計算過程,中間還需要考慮各種諸如:進位、借位、符號等等問題的處理,確實十分復雜,本文不做贅述。
方法二:Java的大數類是個好東西
JDK早已為我們考慮到了浮點數的計算精度問題,因此提供了專用於高精度數值計算的大數類來方便我們使用。
在前文《不瞞你說,我最近跟Java源碼杠上了》中說過,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
這種大數類的運算效率肯定是不如原生類型效率高,代價還是比較昂貴的,是否選用需要根據實際場景來評估。