2017年6月17日 星期六

使用 BigDecimal 注意事項

※錯誤的用法

System.out.println(2-1.1);//應該要0.9
System.out.println(0.2*0.7);//應該要1.4
    
BigDecimal b1 = new BigDecimal(2);
BigDecimal b2 = new BigDecimal(1.1);
System.out.println(b1.subtract(b2));
    
BigDecimal b3 = new BigDecimal(0.2);
BigDecimal b4 = new BigDecimal(0.7);
System.out.println(b3.multiply(b4));

※一般都會覺得 b1~b4 就對了,其實還是有問題

※使用 valueOf 方法沒問題,因為源碼已經 toString() 了,所以推薦使用 valueOf 取代建構子的方式

※double d = 0.1f;
這行本身精度就不準了,所以再用 toString() 也是不正確的

※使用小數點做四則運算時,有些時候會出現不可預期的行為,所以 java 提供了 BigDecimal 類別
P.S. 這個問題都是小數點才會有,因為電腦懂得是二進位,所以會將小數轉換成二進位,最後再轉成10進位,但某些數字類似10/3這種除不進的情況,所有結果只是接近我們期待的數字,例如
一、0.5 轉二進制
0.5 * 2 = 1 (取整後必須做到餘 0),所以轉成二進制的結果為 0.1
   驗算 0.1=>   2 的-1次方為 1/2
                       0.5 為 5/10為 1/2,所以正確
二、0.75 轉二進制
0.75 *2 = 1.5 取 1,剩 0.5
       0.5 *2 = 1 取 1,餘0,轉成二進制的結果為 0.11
    驗算 0.11 => 2 的 -1次方 + 2 的 -2 次方,為 1/2 + 1/4 = 3/4
                         0.75 為 75 /100 = 3/4
所以在 BigDecimal 的建構子給 0.5、0.75 不會有問題,但是看例三

三、0.2 轉二進制
0.2 * 2 取 0,剩 0.4
0.4 * 2 取 0,剩 0.8
0.8 * 2 取 1,剩 0.6
0.6 * 2 取 1,剩 0.2
此時又是 0.2,所以結果為 0.0011 0011 0011…只是比較接近而已,所以建構子裡寫 0.2 會有誤差

※0.1~0.9 只有 0.5 沒問題;0.15~0.95,只有 0.25 和 0.75 沒問題 (加或減 0.5 的一半)

※BigDecimal 的原理就是先變成整數,運算完再除即可,如:
0.2 * 0.07 會放大,2 * 7=14 ,小數點後共 3 位數,
所以 14 / 1000 變成 0.014

※toString、toPlainString、toEngineeringString 的差別、判斷正負數、判斷兩個 BigDecimal 大小

BigDecimal d1 = new BigDecimal("10");
BigDecimal d2 = new BigDecimal("0.2");
System.out.println(d1.divide(d2)); // 5E+1
System.out.println(d1.signum()); // 判斷正負0,返回 1、-1、0
System.out.println(d1.compareTo(BigDecimal.TEN)); // 判斷兩個 BigDecimal 大小,返回 1、-1、0
    
    
    
BigDecimal a = BigDecimal.valueOf(1_0000);
BigDecimal b = a.divide(BigDecimal.valueOf(0.2));
System.out.println(b.toPlainString()); // 50000
System.out.println(b.toString()); // 5.000E+4
System.out.println(b.toEngineeringString()); // 50.00E+3
    
BigDecimal c = BigDecimal.valueOf(1000);
BigDecimal d = c.divide(BigDecimal.valueOf(0.2));
System.out.println(d.toPlainString()); // 5000
System.out.println(d.toString()); // 5.00E+3
System.out.println(d.toEngineeringString()); // 5.00E+3
    
BigDecimal bg = new BigDecimal("1E4");
System.out.println(bg.toPlainString()); // 10000
System.out.println(bg.toString()); // 1E+4
System.out.println(bg.toEngineeringString()); // 10E+3

※使用字串包起來才不會有不可預期的情形發生或者用 valueOf 方法

※注意 toString() 、toPlainString() 、toEngineeringString() 的差別,E+1 表示 10 的 1 次方,且只有在運算有小數點時才會發生不一樣的情形,toEngineeringString() 的指數一定是 3 的倍數



※比較 equals 和 compareTo

BigDecimal myZero = new BigDecimal("0.0");
System.out.println(myZero.equals(new BigDecimal("0.00"))); // false
System.out.println(myZero.hashCode() == (new BigDecimal("0.00")).hashCode()); // false
System.out.println(myZero.compareTo(new BigDecimal("0.00"))); // 0
// 所以一般都會使用 compareTo,但其實還有 stripTrailingZeros() 方法可用

// 使用  stripTrailingZeros 可將最後的 0 去除
System.out.println(myZero.stripTrailingZeros().equals(new BigDecimal("0.00").stripTrailingZeros())); // true
System.out.println(myZero.stripTrailingZeros().hashCode() == (new BigDecimal("0.00")).stripTrailingZeros().hashCode()); // true


※除法問題

BigDecimal d1 = new BigDecimal("10");
BigDecimal d2 = new BigDecimal("3");
    
// System.out.println(d1.divide(d2));//Non-terminating decimal expansion; no exact representable decimal result.
System.out.println(d1.divide(d2, BigDecimal.ROUND_UP));//4
System.out.println(d1.divide(d2, 2, RoundingMode.UP));//3.34

※如果有除不盡時就必須要進位,但預設進位是 ROUND_UNNECESSARY,此種方式是除不盡時就直接拋例外,要參考以下介紹的另外七種方式

※BigDecimal.ROUNT_UP 和 RoundingMode.UP 是一樣的,總共有八種進位方式



※八種進位方式

BigDecimal bd = new BigDecimal("42.46");
// System.out.println(bd.setScale(0, BigDecimal.ROUND_UNNECESSARY));//java.lang.ArithmeticException: Rounding necessary
System.out.println(bd.setScale(2, BigDecimal.ROUND_UNNECESSARY));//42.46

※ROUND_UNNECESSARY 是預設值

※只能精確的回傳,如註解的第一個參數是 0,但 42.46 有兩位小數,如果第一個參數不是 2,就會出例外



※UP、DOWN、CEILING、FLOOR

BigDecimal bd = new BigDecimal("32.543");
System.out.println(bd.setScale(0, RoundingMode.UP));//33
System.out.println(bd.setScale(1, RoundingMode.UP));//32.6
System.out.println(bd.setScale(0, RoundingMode.CEILING));//33
System.out.println(bd.setScale(1, RoundingMode.CEILING));//32.6
    
System.out.println(bd.setScale(0, RoundingMode.DOWN));//32
System.out.println(bd.setScale(1, RoundingMode.DOWN));//32.5
System.out.println(bd.setScale(0, RoundingMode.FLOOR));//32
System.out.println(bd.setScale(1, RoundingMode.FLOOR));//32.5

※UP、CEILING 都是無條件進位; DOWN、FLOOR 都是無條件捨去

※CEILING 和 FLOOR 是天花板與地板的意思,數字越大就代表天花板越高,所以CEILING 會往正的方向、FLOOR 會往負的方向

※UP、DOWN 沒有數字觀念,正負數時,數字都一樣 (只要將正的結果乘-1即可),如下:


BigDecimal bd = new BigDecimal("-32.543");
System.out.println(bd.setScale(0, RoundingMode.UP));//-33
System.out.println(bd.setScale(1, RoundingMode.UP));//-32.6
System.out.println(bd.setScale(0, RoundingMode.CEILING));//-32
System.out.println(bd.setScale(1, RoundingMode.CEILING));//-32.5
    
System.out.println(bd.setScale(0, RoundingMode.DOWN));//-32
System.out.println(bd.setScale(1, RoundingMode.DOWN));//-32.5
System.out.println(bd.setScale(0, RoundingMode.FLOOR));//-33
System.out.println(bd.setScale(1, RoundingMode.FLOOR));//-32.6





※四捨五入、五捨六入

final int scale = 0;
System.out.println(new BigDecimal("46.5").setScale(scale, BigDecimal.ROUND_HALF_DOWN));//46
System.out.println(new BigDecimal("46.50").setScale(scale, BigDecimal.ROUND_HALF_DOWN));//46
System.out.println(new BigDecimal("46.501").setScale(scale, BigDecimal.ROUND_HALF_DOWN));//47
System.out.println(new BigDecimal("46.512").setScale(scale, BigDecimal.ROUND_HALF_DOWN));//47
System.out.println(new BigDecimal("46.523").setScale(scale, BigDecimal.ROUND_HALF_DOWN));//47
System.out.println(new BigDecimal("46.534").setScale(scale, BigDecimal.ROUND_HALF_DOWN));//47
System.out.println(new BigDecimal("46.545").setScale(scale, BigDecimal.ROUND_HALF_DOWN));//47
System.out.println(new BigDecimal("46.556").setScale(scale, BigDecimal.ROUND_HALF_DOWN));//47
System.out.println(new BigDecimal("46.567").setScale(scale, BigDecimal.ROUND_HALF_DOWN));//47
System.out.println(new BigDecimal("46.578").setScale(scale, BigDecimal.ROUND_HALF_DOWN));//47
System.out.println(new BigDecimal("46.589").setScale(scale, BigDecimal.ROUND_HALF_DOWN));//47
System.out.println(new BigDecimal("46.590").setScale(scale, BigDecimal.ROUND_HALF_DOWN));//47
    
System.out.println(new BigDecimal("46.55").setScale(1, BigDecimal.ROUND_HALF_DOWN));//46.5
System.out.println(new BigDecimal("46.551").setScale(1, BigDecimal.ROUND_HALF_DOWN));//46.6

※ROUND_HALF_UP 為四捨五入; ROUND_HALF_DOWN 為五捨六入

※注意:
五捨六入和四捨五入的規則不一樣,注意要進位如果是 5 會不一樣
xxx.550 視同 xxx.55,最後的 0 會自動刪除


.假設要取整,那就要看小數第一位如果是 0~4、6~9 和四捨五入一樣
如果是 5,還要再看第二位之後有沒有數字,有就進位;沒有則不進位

.假設要取小數第一位,那就要看小數第二位如果是 5,再看第二位之後有沒有數字,有就進位;沒有則不進位


※ROUND_HALF_EVEN (四捨六入奇進偶不進)

final int scale = 0;
int method = BigDecimal.ROUND_HALF_EVEN;
System.out.println(new BigDecimal("45.5").setScale(scale, method));//46
System.out.println(new BigDecimal("46.5").setScale(scale, method));//46
    
System.out.println(new BigDecimal("45.55").setScale(1, method));//45.6
System.out.println(new BigDecimal("45.65").setScale(1, method));//45.6

奇數時是四捨五入;偶數時是五捨六入
.假設要取整,那就看整數位是奇數還偶數

※個設要取小數第一位,如果小數第二位是 5,再看小數第一位是偶數還奇數,奇進偶不進



※四種方法差異

System.out.println(new BigDecimal("45.55").setScale(1, BigDecimal.ROUND_HALF_UP));//45.6
System.out.println(new BigDecimal("45.55").setScale(1, BigDecimal.ROUND_HALF_DOWN));//45.5
System.out.println(new BigDecimal("45.55").setScale(1, BigDecimal.ROUND_HALF_EVEN));//45.6
    
System.out.println(new BigDecimal("45.65").setScale(1, BigDecimal.ROUND_HALF_UP));//45.7
System.out.println(new BigDecimal("45.65").setScale(1, BigDecimal.ROUND_HALF_DOWN));//45.6
System.out.println(new BigDecimal("45.65").setScale(1, BigDecimal.ROUND_HALF_EVEN));//45.6
    
System.out.println(new BigDecimal("-2.5").setScale(0, BigDecimal.ROUND_HALF_UP));// -3
System.out.println(new BigDecimal("-2.5").setScale(0, BigDecimal.ROUND_HALF_DOWN));// -2
System.out.println(new BigDecimal("-2.5").setScale(0, BigDecimal.ROUND_HALF_EVEN));// -2
System.out.println(Math.round(-2.5)); // -2
System.out.println("-----------------------------------------");
System.out.println(new BigDecimal("-3.5").setScale(0, BigDecimal.ROUND_HALF_UP));// -4
System.out.println(new BigDecimal("-3.5").setScale(0, BigDecimal.ROUND_HALF_DOWN));// -3
System.out.println(new BigDecimal("-3.5").setScale(0, BigDecimal.ROUND_HALF_EVEN));// -4
System.out.println(Math.round(-3.5)); // -3


※負數時,UP、DOWN、EVEN 都和正數一樣,只是是負的而已

※java9 的 BigDecimal.XXX 過時了,要使用 RoundingMode.XXX 取代

※Math.round 正數和 BigDecimal.ROUND_HALF_UP 一樣;但負數和 BigDecimal.ROUND_HALF_DOWN 一樣




MathContext

BigDecimal one1 = new BigDecimal("123.45565");
BigDecimal one2 = new BigDecimal("123.45565", MathContext.UNLIMITED);
BigDecimal one3a = new BigDecimal("123.45565", MathContext.DECIMAL32);
BigDecimal one4a = new BigDecimal("123.45565", new MathContext(7, RoundingMode.HALF_UP));
BigDecimal one3b = new BigDecimal("123.45575", MathContext.DECIMAL32);
BigDecimal one4b = new BigDecimal("123.45575", new MathContext(7, RoundingMode.HALF_UP));
System.out.println(one1); // 123.45565
System.out.println(one2); // 123.45565
System.out.println(one3a); // 123.4556
System.out.println(one4a); // 123.4557
System.out.println(one3b); // 123.4558
System.out.println(one4b); // 123.4558
※one1 和 one2 是一樣的,one1 底層也是用 MathContext.UNLIMITED,是無限制的意思

※MathContext.DECIMAL32 底層是 new MathContext(7, RoundingMode.HALF_EVEN)
7 表示整數加小數位不能超過 7,不包括小數點,但是整數是 0 時不包括,如 0.1234567 是可以顯示到最後的 7 的

※看 one3a 和 one4a 的比較,half_even 是四捨六入,奇數進位;偶數不進位,第 7 位是 6 不進位,所以兩個值不相同

※看 one3b 和 one4b 的比較,四捨六入時,最後是 7 要進位,所以兩個值相同

※MathContext 有個傳 String 的建構子,使用方法 new MathContext("precision=7 roundingMode=HALF_EVEN"),大小寫不能錯

※本來 BigDecimal 建構子,除了0.5外,會有誤差,用 MathContext 就不會了,但 MathContext.UNLIMITED 還是有誤差,但還是不能在建構子裡面做運算,如下:

System.out.println(new BigDecimal(0.8 * 0.4, MathContext.DECIMAL64)); // 0.3200000000000001
System.out.println(new BigDecimal(0.32 / 0.4, MathContext.DECIMAL64)); // 0.7999999999999999

沒有留言:

張貼留言