不是三婶儿偏执,非要吐槽。家人们,咱就是说,按照基操逻辑谁会把严格金额计算相关的数据使用double类型呢…
“我以为吕布已经够勇猛了,这是谁的部下?”
前几天,一同事让帮忙写段代码。内容比较常规,就是按照自定义规则自动计算出一些金额数据。楼主想着暂时也不忙,就帮着写写呗。好家伙!不写不知道,当看到他用Double类型定义存储金额时,内心瞬间沸腾了。嗯,真是个小(大)可(傻)爱(逼)…
咱就是说,金额相关计算第一考虑肯定是确保精准,优选BigDecimal类型呀,Double类型很容易丢失精度的。尤其是金额,一定要严谨!
你看chatgpt都知道优先使用BigDecimal。
果不其然,这…真是暴风雨的前奏。我发现有好几个业务模块都使用了这些金额数据做运算,多次加减乘除之类的…
好家伙,“海燕啊,你可长点心吧!”。
那下面楼主就来详细分析一下,为什么更加建议使用BigDecimal。开整!
文章目录
- 一、BigDecimal类型数据和Double类型
- 1.1、BigDecimal
- 1.2、Double和double的区别
- 1.2.1、double
- 1.2.2、Double
- 1.2.3、double和Double之间的关系
- 二、两种类型数据的适用场景、优缺点
- 2.1、Double类型
- 2.2、BigDecimal类型
- 三、为什么不建议用Double类型计算金额
- 3.1、Double类型计算精度易丢失
- 3.2、数值过大会变为科学记数法形式
- 四、BigDecimal常用方法
- 4.1、BigDecimal的初始化
- 4.2、BigDecimal加法
- 4.3、BigDecimal减法
- 4.4、BigDecimal乘法
- 4.5、BigDecimal除法
- 4.6、BigDecimal比较大小
- 4.7、BigDecimal工具类
一、BigDecimal类型数据和Double类型
首先,先来了解一下什么是BigDecimal、什么是Double、什么是double。以及Double和double之间有什么关系。
1.1、BigDecimal
对于什么是BigDecimal,百度百科中这样描述:
Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数。在实际应用中,需要对更大或者更小的数进行运算和处理。float和double只能用来做科学计算或者是工程计算,在商业计算中要用java.math.BigDecimal。BigDecimal所创建的是对象,我们不能使用传统的+、-、*、/等算术运算符直接对其对象进行数学运算,而必须调用其相对应的方法。方法中的参数也必须是BigDecimal的对象。构造器是类的特殊方法,专门用来创建对象,特别是带有参数的对象。
主要是: 可对超过16位有效位的数进行精确的运算。
1.2、Double和double的区别
1.2.1、double
八大基本数据类型之一,双精度浮点数。
double(双精度浮点型)是计算机使用的一种资料型别。比起单精度浮点数(float),double(双精度浮点数)使用 64 位(8字节) 来储存一个浮点数。 它可以表示十进制的15或16位有效数字,负值取值范围为 -1.7976E+308 到 -4.94065645841246544E-324,正值取值范围为 4.94065645841246544E-324 到 1.797693E+308
1.2.2、Double
Double是基于基本数据类型double的一个封装类,就是我们常用的java.lang.Double。
1.2.3、double和Double之间的关系
嗯,是有关系,但也不是情侣关系…
从jdk1.5开始,引入了“自动装箱”、“自动拆箱”的概念,简化了基本数据类型和包装类之间的转化,提高了使用效率。
关于什么是装箱、拆箱。举个栗子:比如我们常用的List就是一个自动装箱、拆箱的体现。
如图所示,楼主定义了一个Integer类型的list。当往list中放入数据1和2时,会把int类型自动转换为Integer类型,这个过程就是自动装箱。
反过来说,当从list中取出数据时,会把Integer类型自动转换为int类型,这个过程就是自动拆箱。
自动装箱最大的优点就是:可以直接使用包装类中所有的方法。
我们直接调用即可,方法内部都已经帮我们处理好了。感兴趣的可以看下源码深入了解哈,这里不再过多介绍。
二、两种类型数据的适用场景、优缺点
2.1、Double类型
适用场景:双精度、非重要性、或“相对模糊”的数据存储。比如:百分比计算。
缺点:数值容易丢失精度。
优点:执行效率高。
2.2、BigDecimal类型
适用场景:较为严谨的数值计算。比如:交易金额相关计算。
优点:数值计算较为精准。
缺点:较Double类型执行效率弱一些。
三、为什么不建议用Double类型计算金额
3.1、Double类型计算精度易丢失
当我们进行数值加减乘除运算时,Double类型很大程度上会产生误差。
如下图示例,楼主定义了2个Double类型数据的加减乘除,运算结果有3个产生了误差。有误差,但是误差不大。
那么为什么会存在这个问题呢?
可想而知,double类型在运算时,会先将数值转换成二进制然后再做运算。但是在转换过程中,可能会发生存储小数部分的位数不够的现象(无限循环小数),所以很大程度上可能会有非常小的误差产生。
因此,不要直接使用入参double类型数值直接进行运算。可考虑使用string类型参数进行处理。
3.2、数值过大会变为科学记数法形式
当数值过大时,会变为这种科学记数法形式。
解决方案:可考虑使用BigDecimal类型进行转换。
四、BigDecimal常用方法
4.1、BigDecimal的初始化
楼主分别定义了2个不同类型的入参。
眼尖的小伙伴估计注意到了,实例化a对象时new BigDecimal(“0.12”)传入的是字符串,实例化b对象时new BigDecimal(0.12)传入的是double数值。大家觉得运行结果会一样吗?
可能不一样吧…嗯,不是可能,是一定!!!
字符串类型的输出了“0.12”,而double数值类型的却输出了“0.11999999999999999555910790149937383830547332763671875”。
这又回归到了上面说所的二进制转换存储小数部分的位数不够,造成的误差。使用double类型初始化BigDecimal对象,进行运算时可能会出现精度不准确的问题。
所以一定注意:不要使用double类型作为入参,直接去new一个BigDecimal对象!可能会有精度误差!
那么,为什么string就可以呢?
string是不可变的,定义为string之后再转换为数值肯定是固定的啊。
底层很多实现也是这个原理。比如BigDecimal.valueOf()方法,如果输入的是double类型,实质上源码中还是先转化为了字符串。
对于整型或保留小数位的,也有相应的处理方法。
方法有很多,大家想了解的可自行研究下,楼主就不再一一示例啦。
4.2、BigDecimal加法
BigDecimal add(BigDecimal augend)
两个BigDecimal类型数据相加,方法调用、及运行效果。
4.3、BigDecimal减法
BigDecimal subtract(BigDecimal subtrahend)
两个BigDecimal类型数据相减,方法调用、及运行效果。
4.4、BigDecimal乘法
BigDecimal multiply(BigDecimal multiplicand)
两个BigDecimal类型数据相乘,方法调用、及运行效果。
4.5、BigDecimal除法
BigDecimal divide(BigDecimal divisor)
两个BigDecimal类型数据相除,方法调用、及运行效果。
哎呦,惊喜不惊喜,意外不意外?相除的时候出现了无限循环小数。
因此 相除的场景下尽可能设置保留小数位,可避免运算当中抛出异常。
4.6、BigDecimal比较大小
楼主分别定义了2个值为“0.12”的数据a、b,以及值为“0.120”的数据c进行比较。
可以发现:相同值“0.12”通过双等号对比返回的结果是false,equals对比返回的是true。而“0.12”和“0.120”实质上数值大小是一样的,但通过双等号或equals进行对比,返回均为false。
那么,对于BigDecimal类型如何比较大小呢?(叫我靓妹我就告诉你…)
方法就是:使用compareTo方法进行比较
a、b两值进行比较:a.compareTo(b) 。 结果为0表示a、b值相等。结果为-1表示a小于b。结果为1表示a大于b
4.7、BigDecimal工具类
为大家附上常用的操作工具类,直接调用即可。
package com.wss.demo.cas;
import java.math.BigDecimal;
public class ArithUtil {
private static final int DEF_DIV_SCALE = 2; // 小数点后的保留位数
/**
* Double精确的加法运算
*
* @param d1 被加数
* @param d2 加数
* @return 两个参数的和
*/
public static BigDecimal add(double d1, double d2) {
BigDecimal value1 = new BigDecimal(Double.toString(d1));
BigDecimal value2 = new BigDecimal(Double.toString(d2));
return value1.add(value2);
}
/**
* Double精确的减法运算
*
* @param d1 被减数
* @param d2 减数
* @return 两个参数的差
*/
public static BigDecimal sub(double d1, double d2) {
BigDecimal value1 = new BigDecimal(Double.toString(d1));
BigDecimal value2 = new BigDecimal(Double.toString(d2));
return value1.subtract(value2);
}
/**
* Double精确的乘法运算
*
* @param d1 被乘数
* @param d2 乘数
* @return 两个参数的积
*/
public static BigDecimal mul(double d1, double d2) {
BigDecimal value1 = new BigDecimal(Double.toString(d1));
BigDecimal value2 = new BigDecimal(Double.toString(d2));
return value1.multiply(value2);
}
/**
* Double精确的除法运算, 当出现除不尽的情况时, 精确到小数点以后n位, 以后的数字四舍五入
*
* @param d1 被除数
* @param d2 除数
* @return 两个参数的商
*/
public static BigDecimal div(double d1, double d2) {
return div(d1, d2, DEF_DIV_SCALE);
}
/**
* Double精确的除法运算, 当出现除不尽的情况时, 精确到小数点以后n位, 以后的数字四舍五入
*
* @param d1 被除数
* @param d2 除数
* @param scale 表示需要精确到小数点的后几位
* @return 两个参数的商
*/
public static BigDecimal div(double d1, double d2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("参数[scale]必须是正整数或者零");
}
BigDecimal value1 = new BigDecimal(Double.toString(d1));
BigDecimal value2 = new BigDecimal(Double.toString(d2));
return value1.divide(value2, scale, BigDecimal.ROUND_HALF_UP);
}
/**
* 比较大小
*/
public static boolean equalTo(BigDecimal b1, BigDecimal b2) {
if(b1 == null || b2 == null) {
return false;
}
return 0 == b1.compareTo(b2);
}
}
嗯,今天的总结就先到这吧。散会了,别忘记给三婶儿点个赞哈~