先看一下double类型精度丢失问题
(1)加法运算。
public static void main(String[] args) { double number1 = 1; double number2 = 20.2; double number3 = 300.03; double result = number1 + number2 + number3; System.out.println("使用double运算结果: "+result); }
打印结果如下:
使用double运算结果: 321.22999999999996。。。
为什么double类型会出现精度丢失问题?我们知道,计算机发展了如此长的一段时间,但它始终只能识别0和1(即二进制)。无论我们使用哪种编程语言,在哪种编译环境下工作,都要先把源代码翻译成二进制的机器码才能被计算机所识别。
举个简单的例子,在源程序里面2.4,是十进制的,但计算机不能直接识别,要先编译成二进制。
那么问题来了,2.4的二进制并非是精确的2.4,反而是最为接近的二进制表示是2.39999999999999999999。
原因在于浮点数由两部分组成:指数和尾数。如果知道怎么进行浮点数的二进制和十进制转换,应该不难理解。
如果在这个转换过程中,浮点数参与了计算,那么在转换的过程中就会变得不可预知,并且变得不可逆。我们有理由相信,就是在这个过程中,发生了精度的丢失。
而至于为什么有些浮点计算会得到准确的结果,应该也是碰巧那个计算二进制和十进制之间能够准确转换。
而当输出单个浮点型数据时,可以正确输出,如:
Double num3 = 2.4; System.out.println(num3);
输出的结果是2.4,而不是2.399999999999999999999999。也就是说,不进行浮点计算的时候,在十进制里浮点数能正确显示。
这正印证了上面的说法,即如果浮点数参与了计算,那么浮点数二进制与十进制的转换过程就会变得不可预知,并且变得不可逆。
结论就是,在Java中,浮点数不精确,因为计算机内部无法用二进制的小数来精确的表达。
经典问题:浮点数精度丢失
精度丢失的问题是在其他计算机语言中也都会出现,float和double类型的数据在执行二进制浮点运算的时候,并没有提供完全精确的结果。
产生误差不在于数的大小,而是因为数的精度。
关于浮点数存储精度丢失的问题,话题过于庞大,感兴趣的同学可以自行搜索一下:【解惑】剖析float型的内存存储和精度丢失问题
这里简单讨论一下十进制数转二进制为什么会出现精度丢失的现象,十进制数分为整数部分和小数部分,我们分开来看看就知道原因为何:
十进制整数如何转化为二进制整数?将被除数每次都除以2,只要除到商为0就可以停止这个过程。
5 / 2 = 2 余 1
2 / 2 = 1 余 0
1 / 2 = 0 余 1
// 结果为 101
这个算法永远都不会无限循环,整数永远都可以使用二进制数精确表示,但小数呢?
十进制小数如何转化为二进制数?每次将小数部分乘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
这个算法有一定概率会存在无限循环,即无法用有限长度的二进制数表示十进制的小数,这就是精度丢失问题产生的原因。
浮点数并不适合用于精确计算,而适合进行科学计算。
float和double型用来表示带有小数点的数,那为什么我们不称他们为小数或实数,要叫浮点数呢?是因为这些数都以科学计数法的形式存储。
当一个数如50.534,转换成科学计数法的形式为5.053e1,它 的小数点移动到了一个新的位置(即浮动了)。
可见,浮点数本来就是用于科学计算的,用来进行精确计算实在太不合适了。
如何用BigDecimal解决double精度问题?我们已经明白为什么精度会存在丢失现象,那么我们就应该知道,当某个业务场景对double数据的精度要求非常高时,就必须采取某种手段来处理这个问题,
这也是BigDecimal为什么会被广泛应用于金额支付场景中的原因啦。
new BigDecimal(double val)BigDecimal类位于java.math包下,用于对超过16位有效位的数进行精确的运算。
一般来说,double类型的变量可以处理16位有效数,
但实际应用中,如果超过16位,就需要BigDecimal类来操作。
该方法是不可预测的,以0.1为例,你以为你传了一个double类型的0.1,最后会返回一个值为0.1的BigDecimal吗?不会的,原因在于,0.1无法用有限长度的二进制数表示,无法精确地表示为双精度数,最后的结果会是0.100000xxx。
new BigDecimal(String val)该方法是完全可预测的,也就是说你传入一个字符串"0.1",他就会给你返回一个值完全为0,1的BigDecimal,官方也表示,能用这个构造函数就用这个构造函数叭。
BigDecimal.valueOf(double val)第二种构造方式已经足够优秀,可你还是想传入一个double值,怎么办呢?官方其实提供给你思路并且实现了它,可以使用Double.toString(double val)先将double值转为String,再调用第二种构造方式,你可以直接使用静态方法:valueOf(double val)。
小结:将double转为BigDecimal的时候,需要先把double转换为字符串,然后再作为BigDecimal(String val)构造函数的参数,这样才能避免出现精度问题。
以下代码在项目里,项目地址:https://gitee.com/urasaki/RxJava2AndRetrofit2
以下是提供的double精确运算工具类(5位后边的四舍五入,小数位可自定义,至多可以保留6位小数,如果计算的数值总和很大,超过50 0000,请使用带有DoubleToStr的方法,不然可能会有保留小数位出现问题,不然也可能double数字会转成科学计数法显示)
package com.phone.common_library.manager; import java.math.BigDecimal; import java.text.NumberFormat; public class BigDecimalManager { public static double additionDouble(double m1, double m2, int scale) { BigDecimal p1 = new BigDecimal(Double.toString(m1)); BigDecimal p2 = new BigDecimal(Double.toString(m2)); return p1.add(p2).setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue(); } public static String additionDoubleToStr(double m1, double m2, int scale) { BigDecimal p1 = new BigDecimal(Double.toString(m1)); BigDecimal p2 = new BigDecimal(Double.toString(m2)); return p1.add(p2).setScale(scale, BigDecimal.ROUND_HALF_UP).toPlainString(); } public static double subtractionDouble(double m1, double m2, int scale) { BigDecimal p1 = new BigDecimal(Double.toString(m1)); BigDecimal p2 = new BigDecimal(Double.toString(m2)); return p1.subtract(p2).setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue(); } public static String subtractionDoubleToStr(double m1, double m2, int scale) { BigDecimal p1 = new BigDecimal(Double.toString(m1)); BigDecimal p2 = new BigDecimal(Double.toString(m2)); return p1.subtract(p2).setScale(scale, BigDecimal.ROUND_HALF_UP).toPlainString(); } public static double multiplicationDouble(double m1, double m2, int scale) { BigDecimal p1 = new BigDecimal(Double.toString(m1)); BigDecimal p2 = new BigDecimal(Double.toString(m2)); return p1.multiply(p2).setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue(); } public static String multiplicationDoubleToStr(double m1, double m2, int scale) { BigDecimal p1 = new BigDecimal(Double.toString(m1)); BigDecimal p2 = new BigDecimal(Double.toString(m2)); return p1.multiply(p2).setScale(scale, BigDecimal.ROUND_HALF_UP).toPlainString(); } public static String multiplicationDoubleToStr(String m1, String m2, int scale) { BigDecimal p1 = new BigDecimal(m1); BigDecimal p2 = new BigDecimal(m2); return p1.multiply(p2).setScale(scale, BigDecimal.ROUND_HALF_UP).toPlainString(); } public static double divisionDouble(double m1, double m2, int scale) { if (scale < 0) { throw new IllegalArgumentException("Parameter error"); } BigDecimal p1 = new BigDecimal(Double.toString(m1)); BigDecimal p2 = new BigDecimal(Double.toString(m2)); return p1.divide(p2, scale, BigDecimal.ROUND_HALF_UP).doubleValue(); } public static String divisionDoubleToStr(double m1, double m2, int scale) { if (scale < 0) { throw new IllegalArgumentException("Parameter error"); } BigDecimal p1 = new BigDecimal(Double.toString(m1)); BigDecimal p2 = new BigDecimal(Double.toString(m2)); return p1.divide(p2, scale, BigDecimal.ROUND_HALF_UP).toPlainString(); } public static double getDoubleKeepDecimals(double m1, int scale) { BigDecimal p1 = new BigDecimal(Double.toString(m1)); double value = p1.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue(); return value; } public static String getDoubleKeepDecimalsToStr(double m1, int scale) { BigDecimal p1 = new BigDecimal(Double.toString(m1)); String value = p1.setScale(scale, BigDecimal.ROUND_HALF_UP).toPlainString(); return value; } }
测试、验证小数点5位四舍五入的问题是否解决:
double m1 = 986790.278576897; double m2 = 1887906.795768; double numAdd = BigDecimalManager.additionDouble(m1, m2, 5); LogManager.i(TAG, "numAdd*****" + numAdd); double n1 = 1870689.79557790; double n2 = 987900.27876876656; double numSub = BigDecimalManager.subtractionDouble(n1, n2, 5);; LogManager.i(TAG, "numSub*****" + numSub); double o1 = 9860.2785667; double o2 = 1000; //这个因为超过了50 0000,已经出现问题了 double numMul = BigDecimalManager.multiplicationDouble(o1, o2, 5); LogManager.i(TAG, "numMul*****" + numMul); String r1 = "98679007.27798867"; String r2 = "1000"; String numMulStr = BigDecimalManager.multiplicationDoubleToStr(r1, r2, 0); LogManager.i(TAG, "numMulStr*****" + numMulStr); String s1 = "98679007.27798867"; String s2 = "1000.55859767"; String numMulStr2 = BigDecimalManager.multiplicationDoubleToStr(s1, s2, 5); LogManager.i(TAG, "numMulStr2*****" + numMulStr2); double p1 = 9867900.278676575; double p2 = 18790689.795565; double numDiv = BigDecimalManager.divisionDouble(p1, p2, 5); LogManager.i(TAG, "numDiv*****" + numDiv); double q1 = 9867900.278590876; double numKeepDecimals = BigDecimalManager.getDoubleKeepDecimals(q1, 5); LogManager.i(TAG, "numKeepDecimals*****" + numKeepDecimals); double z1 = 98679689078000.278590876596890; String numKeepDecimalsStr = BigDecimalManager.getDoubleKeepDecimalsToStr(z1, 5); LogManager.i(TAG, "numKeepDecimalsStr*****" + numKeepDecimalsStr);
打印结果如下:
2022-08-06 15:27:46.365 5777-5777/com.phone.rxjava2andretrofit2 I/DecimalOperationActivity: numAdd*****2874697.07434 2022-08-06 15:27:46.365 5777-5777/com.phone.rxjava2andretrofit2 I/DecimalOperationActivity: numSub*****882789.51681 2022-08-06 15:27:46.365 5777-5777/com.phone.rxjava2andretrofit2 I/DecimalOperationActivity: numMul*****9860278.5667 2022-08-06 15:27:46.365 5777-5777/com.phone.rxjava2andretrofit2 I/DecimalOperationActivity: numMulStr*****98679007278 2022-08-06 15:27:46.366 5777-5777/com.phone.rxjava2andretrofit2 I/DecimalOperationActivity: numMulStr2*****98734129141.53207 2022-08-06 15:27:46.366 5777-5777/com.phone.rxjava2andretrofit2 I/DecimalOperationActivity: numDiv*****0.52515 2022-08-06 15:27:46.366 5777-5777/com.phone.rxjava2andretrofit2 I/DecimalOperationActivity: numKeepDecimals*****9867900.27859 2022-08-06 15:27:46.366 5777-5777/com.phone.rxjava2andretrofit2 I/DecimalOperationActivity: numKeepDecimalsStr*****98679689078000.28000新需求(知识拓展)
现在有一个需求是直接舍去小数点5位后边的数不要,就稍微有一点点繁琐了,文中带DoubleCompatible字段的方法都是直接舍去小数点5位后边的数不要 (只有在小数点后边有连续10个5以上的情况下会五入,其他情况下都会正常舍去),至多可以保留6位小数。
注意:如果计算的数值总和很大,*超过50 0000,请使用带有DoubleCompatibleToStr的方法,不然可能会有保留小数位出现问题,不然也可能double数字会转成科学计数法显示,*但是带有DoubleCompatibleToStr的方法计算之后的总和不要超过900 0000 0000,不然BigDecimal也无法提供舍去小数点5位后边的数不要的精确计算了(只是用上边的四舍五入进行计算了)。
package com.phone.common_library.manager; import java.math.BigDecimal; import java.text.NumberFormat; public class BigDecimalManager { public static double additionDoubleCompatible(double m1, double m2, int scale) { BigDecimal p1 = new BigDecimal(Double.toString(m1)); BigDecimal p2 = new BigDecimal(Double.toString(m2)); double value = p1.add(p2).setScale(10, BigDecimal.ROUND_HALF_UP).doubleValue(); return keepDecimalsCompatibleToDou(value, scale); } public static double additionDoubleCompatible(String m1, String m2, int scale) { BigDecimal p1 = new BigDecimal(m1); BigDecimal p2 = new BigDecimal(m2); double value = p1.add(p2).setScale(10, BigDecimal.ROUND_HALF_UP).doubleValue(); return keepDecimalsCompatibleToDou(value, scale); } public static String additionDoubleCompatibleToStr(String m1, String m2, int scale) { BigDecimal p1 = new BigDecimal(m1); BigDecimal p2 = new BigDecimal(m2); double value = p1.add(p2).setScale(10, BigDecimal.ROUND_HALF_UP).doubleValue(); return keepDecimalsCompatibleToStr(value, scale); } public static double subtractionDoubleCompatible(double m1, double m2, int scale) { BigDecimal p1 = new BigDecimal(Double.toString(m1)); BigDecimal p2 = new BigDecimal(Double.toString(m2)); double value = p1.subtract(p2).setScale(10, BigDecimal.ROUND_HALF_UP).doubleValue(); return keepDecimalsCompatibleToDou(value, scale); } public static double subtractionDoubleCompatible(String m1, String m2, int scale) { BigDecimal p1 = new BigDecimal(m1); BigDecimal p2 = new BigDecimal(m2); double value = p1.subtract(p2).setScale(10, BigDecimal.ROUND_HALF_UP).doubleValue(); return keepDecimalsCompatibleToDou(value, scale); } public static String subtractionDoubleCompatibleToStr(String m1, String m2, int scale) { BigDecimal p1 = new BigDecimal(m1); BigDecimal p2 = new BigDecimal(m2); double value = p1.subtract(p2).setScale(10, BigDecimal.ROUND_HALF_UP).doubleValue(); return keepDecimalsCompatibleToStr(value, scale); } public static double multiplicationDoubleCompatible(double m1, double m2, int scale) { BigDecimal p1 = new BigDecimal(Double.toString(m1)); BigDecimal p2 = new BigDecimal(Double.toString(m2)); double value = p1.multiply(p2).setScale(10, BigDecimal.ROUND_HALF_UP).doubleValue(); return keepDecimalsCompatibleToDou(value, scale); } public static String multiplicationDoubleCompatibleToStr(double m1, double m2, int scale) { BigDecimal p1 = new BigDecimal(Double.toString(m1)); BigDecimal p2 = new BigDecimal(Double.toString(m2)); double value = p1.multiply(p2).setScale(10, BigDecimal.ROUND_HALF_UP).doubleValue(); return keepDecimalsCompatibleToStr(value, scale); } public static String multiplicationDoubleCompatibleToStr(String m1, String m2, int scale) { BigDecimal p1 = new BigDecimal(m1); BigDecimal p2 = new BigDecimal(m2); double value = p1.multiply(p2).setScale(10, BigDecimal.ROUND_HALF_UP).doubleValue(); return keepDecimalsCompatibleToStr(value, scale); } public static double divisionDoubleCompatible(double m1, double m2, int scale) { BigDecimal p1 = new BigDecimal(Double.toString(m1)); BigDecimal p2 = new BigDecimal(Double.toString(m2)); double value = p1.divide(p2, 10, BigDecimal.ROUND_HALF_UP).doubleValue(); return keepDecimalsCompatibleToDou(value, scale); } public static String divisionDoubleCompatibleToStr(double m1, double m2, int scale) { BigDecimal p1 = new BigDecimal(Double.toString(m1)); BigDecimal p2 = new BigDecimal(Double.toString(m2)); double value = p1.divide(p2, 10, BigDecimal.ROUND_HALF_UP).doubleValue(); return keepDecimalsCompatibleToStr(value, scale); } public static double divisionDoubleCompatible(String m1, String m2, int scale) { BigDecimal p1 = new BigDecimal(m1); BigDecimal p2 = new BigDecimal(m2); double value = p1.divide(p2, 10, BigDecimal.ROUND_HALF_UP).doubleValue(); return keepDecimalsCompatibleToDou(value, scale); } public static double getDoubleKeepDecimalsCompatible(double m1, int scale) { BigDecimal p1 = new BigDecimal(Double.toString(m1)); double value = p1.setScale(10, BigDecimal.ROUND_HALF_UP).doubleValue(); return keepDecimalsCompatibleToDou(value, scale); } public static String getDoubleKeepDecimalsCompatibleToStr(double m1, int scale) { BigDecimal p1 = new BigDecimal(Double.toString(m1)); double value = p1.setScale(10, BigDecimal.ROUND_HALF_UP).doubleValue(); return keepDecimalsCompatibleToStr(value, scale); } private static double keepDecimalsCompatibleToDou(double value, int scale) { //将科学计数法转化成小数 NumberFormat numberFormat = NumberFormat.getInstance(); numberFormat.setGroupingUsed(false); // 设置数的小数部分所允许的最小位数 numberFormat.setMinimumFractionDigits(0); // 设置数的小数部分所允许的最大位数 numberFormat.setMaximumFractionDigits(10); String valueStr = numberFormat.format(value); //返回的double数据如果很大还是科学计数法的,如果需要显示,建议返回String类型的 if (valueStr.contains(".")){ String[] valueStrArr = valueStr.split("\."); if (valueStrArr.length > 1 && valueStrArr[1].length() > 0){ if (scale <= 0){ return Double.parseDouble(valueStrArr[0]); } else { if (valueStrArr[1].length() < scale){ return Double.parseDouble(valueStrArr[0] + "." + valueStrArr[1]); } else { return Double.parseDouble(valueStrArr[0] + "." + valueStrArr[1].substring(0, scale)); } } } else { return Double.parseDouble(valueStr); } } else { return Double.parseDouble(valueStr); } } private static String keepDecimalsCompatibleToStr(double value, int scale) { //将科学计数法转化成小数 NumberFormat numberFormat = NumberFormat.getInstance(); numberFormat.setGroupingUsed(false); // 设置数的小数部分所允许的最小位数 numberFormat.setMinimumFractionDigits(0); // 设置数的小数部分所允许的最大位数 numberFormat.setMaximumFractionDigits(10); String valueStr = numberFormat.format(value); if (valueStr.contains(".")){ String[] valueStrArr = valueStr.split("\."); if (valueStrArr.length > 1 && valueStrArr[1].length() > 0){ if (scale <= 0){ return valueStrArr[0]; } else { if (valueStrArr[1].length() < scale){ return valueStrArr[0] + "." + valueStrArr[1]; } else { return valueStrArr[0] + "." + valueStrArr[1].substring(0, scale); } } } else { return valueStr; } } else { return valueStr; } } }
测试、验证舍去小数点3位后边的数不要的问题是否解决:
double mCompatible1 = 986790.278576897; double mCompatible2 = 1887906.795768; double numAddCompatible = BigDecimalManager.additionDoubleCompatible(mCompatible1, mCompatible2, 5); LogManager.i(TAG, "numAddCompatible*****" + numAddCompatible); double nCompatible1 = 1870689.79557790; double nCompatible2 = 987900.27876876656; //这个因为超过了50 0000,已经出现问题了 double numSubCompatible = BigDecimalManager.subtractionDoubleCompatible(nCompatible1, nCompatible2, 5);; LogManager.i(TAG, "numSubCompatible*****" + numSubCompatible); double oCompatible1 = 9860.2785667; double oCompatible2 = 1000; //这个因为超过了50 0000,已经出现问题了 double numMulCompatible = BigDecimalManager.multiplicationDoubleCompatible(oCompatible1, oCompatible2, 5); LogManager.i(TAG, "numMulCompatible*****" + numMulCompatible); String rCompatible1 = "98679007.27798867"; String rCompatible2 = "1000"; String numMulCompatibleStr = BigDecimalManager.multiplicationDoubleCompatibleToStr(rCompatible1, rCompatible2, 0); LogManager.i(TAG, "numMulCompatibleStr*****" + numMulCompatibleStr); String sCompatible1 = "98679007.27798867"; String sCompatible2 = "1000.55859767"; String numMulCompatibleStr2 = BigDecimalManager.multiplicationDoubleCompatibleToStr(sCompatible1, sCompatible2, 5); LogManager.i(TAG, "numMulCompatibleStr2*****" + numMulCompatibleStr2); double pCompatible1 = 9867900.278676575; double pCompatible2 = 18790689.795565; double numDivCompatible = BigDecimalManager.divisionDoubleCompatible(pCompatible1, pCompatible2, 5); LogManager.i(TAG, "numDivCompatible*****" + numDivCompatible); double qCompatible1 = 9867900.278590876; double numKeepDecimalsCompatible = BigDecimalManager.getDoubleKeepDecimalsCompatible(qCompatible1, 5); LogManager.i(TAG, "numKeepDecimalsCompatible*****" + numKeepDecimalsCompatible); double z1 = 98679689078000.278590876596890; //这个因为超过了900 0000 0000,已经出现问题了 String numKeepDecimalsCompatibleStr = BigDecimalManager.getDoubleKeepDecimalsCompatibleToStr(z1, 5); LogManager.i(TAG, "numKeepDecimalsCompatibleStr*****" + numKeepDecimalsCompatibleStr);
打印结果如下:
2022-08-06 15:29:01.571 5777-5777/com.phone.rxjava2andretrofit2 I/DecimalOperationActivity: numAddCompatible*****2874697.07434 2022-08-06 15:29:01.573 5777-5777/com.phone.rxjava2andretrofit2 I/DecimalOperationActivity: numSubCompatible*****882789.5168 2022-08-06 15:29:01.575 5777-5777/com.phone.rxjava2andretrofit2 I/DecimalOperationActivity: numMulCompatible*****9860278.5667 2022-08-06 15:29:01.577 5777-5777/com.phone.rxjava2andretrofit2 I/DecimalOperationActivity: numMulCompatibleStr*****98679007277 2022-08-06 15:29:01.579 5777-5777/com.phone.rxjava2andretrofit2 I/DecimalOperationActivity: numMulCompatibleStr2*****98734129141.53207 2022-08-06 15:29:01.580 5777-5777/com.phone.rxjava2andretrofit2 I/DecimalOperationActivity: numDivCompatible*****0.52514 2022-08-06 15:29:01.581 5777-5777/com.phone.rxjava2andretrofit2 I/DecimalOperationActivity: numKeepDecimalsCompatible*****9867900.27859 2022-08-06 15:29:01.583 5777-5777/com.phone.rxjava2andretrofit2 I/DecimalOperationActivity: numKeepDecimalsCompatibleStr*****98679689078000.28
如对此有疑问,请联系qq1164688204。
推荐Android开源项目
项目功能介绍:RxJava2和Retrofit2项目,添加自动管理token功能,添加RxJava2生命周期管理,使用App架构设计是MVP模式和MVVM模式,同时使用组件化,部分代码使用Kotlin,此项目持续维护中。
项目地址:https://gitee.com/urasaki/RxJava2AndRetrofit2