(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。。。
(2)减法运算。
double d1 = 2.11;
double d2 = 2.10;
System.out.println( d1 - d2);
口算结果是0.01。可是用程序执行出来的结果却出乎意料,执行结果为0.009999999999999787。
(3)乘法运算。
public static void main(String[] args) {
double a = 152.70;
System.out.println(a);
System.out.println(a * 100);//15280.000000000002
System.out.println(Math.round(a * 100));//还凑合
double b = 152.70;
System.out.println(b);
System.out.println(b * 100);//15269.999999999998
System.out.println((int)(b * 100));//15269 错误!!!不是想要的
}
然后把15280.000000000002 - 15280 = 1.8189894035458565E-12这个结果返回给了前端,
前端拿去和0比较, 结果就炸了
原因:就是double的精度问题, 单个double数可能就有损失精度,,多个double数运算(减法和乘法都输入加法)可能导致损失的精度更多。。。
我们知道,计算机发展了如此长的一段时间,但它始终只能识别0和1(即二进制)。无论我们使用哪种编程语言,在哪种编译环境下工作,都要先把源代码翻译成二进制的机器码才能被计算机所识别。
举个简单的例子,在源程序里面2.4,是十进制的,但计算机不能直接识别,要先编译成二进制。
那么问题来了,2.4的二进制并非是精确的2.4,反而是最为接近的二进制表示是2.39999999996。
原因在于浮点数由两部分组成:指数和尾数。如果知道怎么进行浮点数的二进制和十进制转换,应该不难理解。
如果在这个转换过程中,浮点数参与了计算,那么在转换的过程中就会变得不可预知,并且变得不可逆。我们有理由相信,就是在这个过程中,发生了精度的丢失。
而至于为什么有些浮点计算会得到准确的结果,应该也是碰巧那个计算二进制和十进制之间能够准确转换。
而当输出单个浮点型数据时,可以正确输出,如:
Double num3 = 2.4;
System.out.println(num3);
输出的结果是2.4,而不是2.39999999996。也就是说,不进行浮点计算的时候,在十进制里浮点数能正确显示。
这正印证了上面的说法,即如果浮点数参与了计算,那么浮点数二进制与十进制的转换过程就会变得不可预知,并且变得不可逆。
结论就是,在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,它 的小数点移动到了一个新的位置(即浮动了)。
可见,浮点数本来就是用于科学计算的,用来进行精确计算实在太不合适了。
我们已经明白为什么精度会存在丢失现象,那么我们就应该知道,当某个业务场景对double数据的精度要求非常高时,就必须采取某种手段来处理这个问题,
这也是BigDecimal为什么会被广泛应用于金额支付场景中的原因啦。
BigDecimal类位于java.math包下,用于对超过16位有效位的数进行精确的运算。
一般来说,double类型的变量可以处理16位有效数,
但实际应用中,如果超过16位,就需要BigDecimal类来操作。
new BigDecimal(double val)
该方法是不可预测的,以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)构造函数的参数,这样才能避免出现精度问题。
以下是提供的double精确运算工具类(如果计算的数值总和很大很大,超过50 0000,请使用带有ToStr的方法,不然double数字会转成科学计数法显示)
package com.phone.common_library;
import java.math.BigDecimal;
public class BigDecimalManager {
/**
* double类型的加法运算(不需要舍入)
* @param m1
* @param m2
* @return 不加doubleValue()则, 返回BigDecimal对象
*/
public static double additionDouble(double m1, double m2) {
BigDecimal p1 = new BigDecimal(Double.toString(m1));
BigDecimal p2 = new BigDecimal(Double.toString(m2));
return p1.add(p2).doubleValue();
}
/**
* double类型的加法运算(需要舍入,保留三位小数)
* @param m1
* @param m2
* @return 不加doubleValue()则, 返回BigDecimal对象
*/
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();
}
/**
* double类型的超大数值加法运算(需要舍入,保留三位小数)
* @param m1
* @param m2
* @return 不加doubleValue()则, 返回BigDecimal对象
*/
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();
}
/**
* double类型的减法运算
* @param m1
* @param m2
* @return 不加doubleValue()则, 返回BigDecimal对象
*/
public static double subtractionDouble(double m1, double m2) {
BigDecimal p1 = new BigDecimal(Double.toString(m1));
BigDecimal p2 = new BigDecimal(Double.toString(m2));
return p1.subtract(p2).doubleValue();
}
/**
* double类型的减法运算(需要舍入,保留三位小数)
* @param m1
* @param m2
* @return 不加doubleValue()则, 返回BigDecimal对象
*/
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();
}
/**
* double类型的超大数值减法运算(需要舍入,保留三位小数)
* @param m1
* @param m2
* @return 不加doubleValue()则, 返回BigDecimal对象
*/
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();
}
/**
* double类型的乘法运算
* @param m1
* @param m2
* @return 不加doubleValue()则, 返回BigDecimal对象
*/
public static double multiplicationDouble(double m1, double m2) {
BigDecimal p1 = new BigDecimal(Double.toString(m1));
BigDecimal p2 = new BigDecimal(Double.toString(m2));
return p1.multiply(p2).doubleValue();
}
/**
* double类型的乘法运算
* @param m1
* @param m2
* @return 不加doubleValue()则, 返回BigDecimal对象
*/
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();
}
/**
* double类型的超大数值的乘法运算
* @param m1
* @param m2
* @return 不加doubleValue()则, 返回BigDecimal对象
*/
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();
}
/**
* double类型的除法运算
* @param m1
* @param m2
* @param scale
* @return 不加doubleValue()则, 返回BigDecimal对象
*/
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();
}
/**
* double类型的超大数值的除法运算
* @param m1
* @param m2
* @param scale
* @return 不加doubleValue()则, 返回BigDecimal对象
*/
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 void main(String[] args) {
double result1 = additionDouble(1, 20.2);
double result2 = additionDouble(result1, 300.03);
System.out.println("使用BigDecimal运算结果:" + result2);
}
结果如下:
使用BigDecimal运算结果:321.23
完美解决了double类型数据加减操作时精度丢失的问题。
如对此有疑问,请联系qq1164688204。
推荐Android开源项目
项目功能介绍:RxJava2和Retrofit2项目,添加自动管理token功能,添加RxJava2生命周期管理,使用App架构设计是MVP模式和MVVM模式,同时使用组件化,部分代码使用Kotlin,此项目持续维护中。
项目地址:https://gitee.com/urasaki/RxJava2AndRetrofit2