精度丟失的問題是在其餘計算機語言中也都會出現,float和double類型的數據在執行二進制浮點運算的時候,並無提供徹底精確的結果。產生偏差不在於數的大小,而是由於數的精度。html
關於浮點數存儲精度丟失的問題,話題過於龐大,感興趣的同窗能夠自行搜索一下:【解惑】剖析float型的內存存儲和精度丟失問題java
這裏簡單討論一下十進制數轉二進制爲何會出現精度丟失的現象,十進制數分爲整數部分和小數部分,咱們分開來看看就知道緣由爲什麼:算法
將被除數每次都除以2,只要除到商爲0就能夠中止這個過程。app
5 / 2 = 2 餘 1 2 / 2 = 1 餘 0 1 / 2 = 0 餘 1 // 結果爲 101
這個算法永遠都不會無限循環,整數永遠均可以使用二進制數精確表示,但小數呢?ide
每次將小數部分乘2,取出整數部分,若是小數部分爲0,就能夠中止這個過程。函數
0.1 * 2 = 0.2 取整數部分0 0.2 * 2 = 0.4 取整數部分0 0.4 * 2 = 0.8 取整數部分0 0.8 * 2 = 1.6 取整數部分1 0.6 * 2 = 1.2 取整數部分1 0.2 * 2 = 0.4 取整數部分0 //... 我想寫到這就沒必要再寫了,你應該也已經發現,上面的過程已經開始循環,小數部分永遠不能爲0
這個算法有必定機率會存在無限循環,即沒法用有限長度的二進制數表示十進制的小數,這就是精度丟失問題產生的緣由。工具
咱們已經明白爲何精度會存在丟失現象,那麼咱們就應該知道,當某個業務場景對double數據的精度要求很是高時,就必須採起某種手段來處理這個問題,這也是BigDecimal爲何會被普遍應用於金額支付場景中的緣由啦。學習
BigDecimal類位於
java.math
包下,用於對超過16位有效位的數進行精確的運算。通常來講,double類型的變量能夠處理16位有效數,但實際應用中,若是超過16位,就須要BigDecimal類來操做。this
既然這樣,那用BigDecimal就可以很好解決這個問題咯?code
public static void main(String[] args) { // 方法1 BigDecimal a = new BigDecimal(0.1); System.out.println("a --> " + a); // 方法2 BigDecimal b = new BigDecimal("0.1"); System.out.println("b --> " + b); // 方法3 BigDecimal c = BigDecimal.valueOf(0.1); System.out.println("c --> " + c); }
你能夠思考一下,控制檯輸出會是啥。
a --> 0.1000000000000000055511151231257827021181583404541015625 b --> 0.1 c --> 0.1
能夠看到,使用方法一的構造函數仍然出現了精度丟失的問題,而方法二和方法三符合咱們的預期,爲何會這樣呢?
這三個方法其實對應着三種不一樣的構造函數:
// 傳入double public BigDecimal(double val) { this(val,MathContext.UNLIMITED); } // 傳入string public BigDecimal(String val) { this(val.toCharArray(), 0, val.length()); } public static BigDecimal valueOf(double val) { // Reminder: a zero double returns '0.0', so we cannot fastpath // to use the constant ZERO. This might be important enough to // justify a factory approach, a cache, or a few private // constants, later. // 能夠看到實際上就是第二種 return new BigDecimal(Double.toString(val)); }
關於這三個構造函數,JDK已經給出瞭解釋,並用Notes標註:
爲了防止之後圖片可能會存在顯示問題,這裏再記錄一下:
該方法是不可預測的,以0.1爲例,你覺得你傳了一個double類型的0.1,最後會返回一個值爲0.1的BigDecimal嗎?不會的,緣由在於,0.1沒法用有限長度的二進制數表示,沒法精確地表示爲雙精度數,最後的結果會是0.100000xxx。
該方法是徹底可預測的,也就是說你傳入一個字符串"0.1",他就會給你返回一個值徹底爲0,1的BigDecimal,官方也表示,能用這個構造函數就用這個構造函數叭。
第二種構造方式已經足夠優秀,可你仍是想傳入一個double值,怎麼辦呢?官方其實提供給你思路而且實現了它,可使用Double.toString(double val)
先將double值轉爲String,再調用第二種構造方式,你能夠直接使用靜態方法:valueOf(double val)
。
BigDecimal所建立的是對象,故咱們不能使用傳統的+、-、*、/
等算術運算符直接對其對象進行數學運算,而必須調用其相對應的方法。方法中的參數也必須是BigDecimal的對象。網上有不少這樣的工具類,這邊直接貼一下,邏輯不難,主要爲了簡化項目中頻繁互相轉化的問題。
/** * 用於高精確處理經常使用的數學運算 */ public class ArithmeticUtils { //默認除法運算精度 private static final int DEF_DIV_SCALE = 10; /** * 提供精確的加法運算 * * @param v1 被加數 * @param v2 加數 * @return 兩個參數的和 */ public static double add(double v1, double v2) { BigDecimal b1 = new BigDecimal(Double.toString(v1)); BigDecimal b2 = new BigDecimal(Double.toString(v2)); return b1.add(b2).doubleValue(); } /** * 提供精確的加法運算 * * @param v1 被加數 * @param v2 加數 * @return 兩個參數的和 */ public static BigDecimal add(String v1, String v2) { BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v2); return b1.add(b2); } /** * 提供精確的加法運算 * * @param v1 被加數 * @param v2 加數 * @param scale 保留scale 位小數 * @return 兩個參數的和 */ public static String add(String v1, String v2, int scale) { if (scale < 0) { throw new IllegalArgumentException( "The scale must be a positive integer or zero"); } BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v2); return b1.add(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString(); } /** * 提供精確的減法運算 * * @param v1 被減數 * @param v2 減數 * @return 兩個參數的差 */ public static double sub(double v1, double v2) { BigDecimal b1 = new BigDecimal(Double.toString(v1)); BigDecimal b2 = new BigDecimal(Double.toString(v2)); return b1.subtract(b2).doubleValue(); } /** * 提供精確的減法運算。 * * @param v1 被減數 * @param v2 減數 * @return 兩個參數的差 */ public static BigDecimal sub(String v1, String v2) { BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v2); return b1.subtract(b2); } /** * 提供精確的減法運算 * * @param v1 被減數 * @param v2 減數 * @param scale 保留scale 位小數 * @return 兩個參數的差 */ public static String sub(String v1, String v2, int scale) { if (scale < 0) { throw new IllegalArgumentException( "The scale must be a positive integer or zero"); } BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v2); return b1.subtract(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString(); } /** * 提供精確的乘法運算 * * @param v1 被乘數 * @param v2 乘數 * @return 兩個參數的積 */ public static double mul(double v1, double v2) { BigDecimal b1 = new BigDecimal(Double.toString(v1)); BigDecimal b2 = new BigDecimal(Double.toString(v2)); return b1.multiply(b2).doubleValue(); } /** * 提供精確的乘法運算 * * @param v1 被乘數 * @param v2 乘數 * @return 兩個參數的積 */ public static BigDecimal mul(String v1, String v2) { BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v2); return b1.multiply(b2); } /** * 提供精確的乘法運算 * * @param v1 被乘數 * @param v2 乘數 * @param scale 保留scale 位小數 * @return 兩個參數的積 */ public static double mul(double v1, double v2, int scale) { BigDecimal b1 = new BigDecimal(Double.toString(v1)); BigDecimal b2 = new BigDecimal(Double.toString(v2)); return round(b1.multiply(b2).doubleValue(), scale); } /** * 提供精確的乘法運算 * * @param v1 被乘數 * @param v2 乘數 * @param scale 保留scale 位小數 * @return 兩個參數的積 */ public static String mul(String v1, String v2, int scale) { if (scale < 0) { throw new IllegalArgumentException( "The scale must be a positive integer or zero"); } BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v2); return b1.multiply(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString(); } /** * 提供(相對)精確的除法運算,當發生除不盡的狀況時,精確到 * 小數點之後10位,之後的數字四捨五入 * * @param v1 被除數 * @param v2 除數 * @return 兩個參數的商 */ public static double div(double v1, double v2) { return div(v1, v2, DEF_DIV_SCALE); } /** * 提供(相對)精確的除法運算。當發生除不盡的狀況時,由scale參數指 * 定精度,之後的數字四捨五入 * * @param v1 被除數 * @param v2 除數 * @param scale 表示表示須要精確到小數點之後幾位。 * @return 兩個參數的商 */ public static double div(double v1, double v2, int scale) { if (scale < 0) { throw new IllegalArgumentException("The scale must be a positive integer or zero"); } BigDecimal b1 = new BigDecimal(Double.toString(v1)); BigDecimal b2 = new BigDecimal(Double.toString(v2)); return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue(); } /** * 提供(相對)精確的除法運算。當發生除不盡的狀況時,由scale參數指 * 定精度,之後的數字四捨五入 * * @param v1 被除數 * @param v2 除數 * @param scale 表示須要精確到小數點之後幾位 * @return 兩個參數的商 */ public static String div(String v1, String v2, int scale) { if (scale < 0) { throw new IllegalArgumentException("The scale must be a positive integer or zero"); } BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v1); return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).toString(); } /** * 提供精確的小數位四捨五入處理 * * @param v 須要四捨五入的數字 * @param scale 小數點後保留幾位 * @return 四捨五入後的結果 */ public static double round(double v, int scale) { if (scale < 0) { throw new IllegalArgumentException("The scale must be a positive integer or zero"); } BigDecimal b = new BigDecimal(Double.toString(v)); return b.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue(); } /** * 提供精確的小數位四捨五入處理 * * @param v 須要四捨五入的數字 * @param scale 小數點後保留幾位 * @return 四捨五入後的結果 */ public static String round(String v, int scale) { if (scale < 0) { throw new IllegalArgumentException( "The scale must be a positive integer or zero"); } BigDecimal b = new BigDecimal(v); return b.setScale(scale, BigDecimal.ROUND_HALF_UP).toString(); } /** * 取餘數 * * @param v1 被除數 * @param v2 除數 * @param scale 小數點後保留幾位 * @return 餘數 */ public static String remainder(String v1, String v2, int scale) { if (scale < 0) { throw new IllegalArgumentException( "The scale must be a positive integer or zero"); } BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v2); return b1.remainder(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString(); } /** * 取餘數 BigDecimal * * @param v1 被除數 * @param v2 除數 * @param scale 小數點後保留幾位 * @return 餘數 */ public static BigDecimal remainder(BigDecimal v1, BigDecimal v2, int scale) { if (scale < 0) { throw new IllegalArgumentException( "The scale must be a positive integer or zero"); } return v1.remainder(v2).setScale(scale, BigDecimal.ROUND_HALF_UP); } /** * 比較大小 * 阿里巴巴開發規範明確:比較BigDecimal的等值須要使用compareTo,不可用equals * equals會比較值和精度,compareTo會忽略精度 * @param v1 被比較數 * @param v2 比較數 * @return 若是v1 大於v2 則 返回true 不然false */ public static boolean compare(String v1, String v2) { BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v2); int bj = b1.compareTo(b2); boolean res; if (bj > 0) res = true; else res = false; return res; } }
【強制】如上所示BigDecimal的等值比較應使用compareTo()方法,而不是equals()方法。
說明:equals()方法會比較值和精度(1.0和1.00返回結果爲false),而compareTo()則會忽略精度。
關於這一點,咱們來看一個例子就明白了:
public static void main(String[] args) { BigDecimal a = new BigDecimal("1"); BigDecimal b = new BigDecimal("1.0"); System.out.println(a.equals(b)); // false System.out.println(a.compareTo(b)); //0 表示相等 }
JDK中對這兩個方法的解釋是這樣的:
x.compareTo(y) <op> 0
來表示(<, == , > , >= , != , <=)
中的其中一個關係,