老大說:誰要再用double定義商品金額,就本身收拾東西走

image


先看現象

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

典型現象(一):條件判斷超預期面試

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   納尼?

典型現象(四):數據自增超預期ide

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了嗎?

看到沒,這些簡單場景下的使用狀況都很難知足咱們的需求,因此說用浮點數(包括doublefloat)處理問題有很是多隱晦的坑在等着我們!spa

怪不得技術總監發狠話:誰要是敢在處理諸如 商品金額訂單交易、以及貨幣計算時用浮點型數據(double/float),直接讓咱們走人!code

image


緣由出在哪裏?

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

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

直接用代碼去比較10.99999999,竟然打印出trueip

image

這說明了什麼?這說明了計算機壓根區分不出來這兩個數。這是爲何呢?內存

咱們不妨來簡單思考一下:

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

因此爲了搞懂實際狀況,咱們應該將這兩個十進制浮點數轉化到二進制空間來看一看。

十進制浮點數轉二進制 怎麼轉、怎麼計算,我想這應該屬於基礎計算機進制轉換常識,在 《計算機組成原理》 相似的課上確定學過了,咱就不在此贅述了,直接給出結果(把它轉換到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 浮點數計數標準,能夠用科學計數法表示爲:

image

只要給出:符號(S)階碼部分(E)尾數部分(M) 這三個維度的信息,一個浮點數的表示就徹底肯定下來了,因此floatdouble這兩種浮點數在內存中的存儲結構以下所示:

image

image

一、符號部分(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的大數類位於java.math包下:

image

能夠看到,經常使用的BigIntegerBigDecimal就是處理高精度數值計算的利器。

BigDecimal num3 = new BigDecimal( Double.toString( 0.1f ) );
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

固然了,像BigIntegerBigDecimal這種大數類的運算效率確定是不如原生類型效率高,代價仍是比較昂貴的,是否選用須要根據實際場景來評估。

相關文章
相關標籤/搜索