在我们日常工作中数值计算是不可避免的,特别是电商类系统中,这个问题一般情况下我们都是特别注意的,但是一不注意就会出大问题,跟钱有关的事情没小事。这不新来的大兄弟就一个不注意,在这个小阴沟里翻车了,闹笑话了。
为了我们以后可以避免在这个问题上犯错,我今天特地写了这一篇来总结一下。
公众号:Java架构师联盟,每日更新技术好文
避免用Double来进行运算
使用Double来计算,我们以为的算术运算和计算机计算的并不完全一直,这是因为计算机是以二进制存储数值的,我们输入的十进制数值都会转换成二进制进行计算,十进制转二进制再转换成十进制就不是原来那个十进制了,再也不是曾经那个少年了。举个例子:十进制的0.1转换成二进制是0.0 0011 0011 0011…(无数个0011),再转换成十进制就是0.1000000000000000055511151231,看到了吧,没有骗你的。
计算机无法精确地表达浮点数,这是不可避免的,这是为什么浮点数计算后精度损失的原因。
为此,我特地做了一点展示
public class test {
public static void main(String[] args) {
System.out.println(0.1+0.2);
System.out.println(1.0-0.8);
System.out.println(4.015*100);
System.out.println(123.3/100);
}
}
通过简单的例子,我们发现精度损失并不是很大,但是这并不代表我们可以使用,特别是电商类系统中,每天少说几百万的单量,每笔订单哪怕少计算一分钱,算下来也是一笔不小的金额,所以说,这不是个小事情,然后很多人就说,金额计算啊,你用BigDecimal啊,对的,这个没毛病,但是用了BigDecimal就完事大吉了吗?当问出这句话的时候,就说明这其中必有蹊跷。
BigDecimal你遇见过哪些坑?
还是通过一个简单的例子,计算上边例子中的运算,来看一下结果:
public class test {
public static void main(String[] args) {
System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.2)));
System.out.println(new BigDecimal(1.0).subtract(new BigDecimal(0.8)));
System.out.println(new BigDecimal(4.015).multiply(new BigDecimal(100)));
System.out.println(new BigDecimal(123.3).divide(new BigDecimal(100)));
}
}
我们来看结果有多少偏差
G:\JDK1.8\bin\java.exe "-javaagent:G:\idea\IntelliJ IDEA 2019.3.3\lib\idea_rt.jar=14053:G:\idea\IntelliJ IDEA 2019.3.3\bin" -Dfile.encoding=UTF-8 -classpath G:\JDK1.8\jre\lib\charsets.jar;G:\JDK1.8\jre\lib\deploy.jar;G:\JDK1.8\jre\lib\ext\access-bridge-64.jar;G:\JDK1.8\jre\lib\ext\cldrdata.jar;G:\JDK1.8\jre\lib\ext\dnsns.jar;G:\JDK1.8\jre\lib\ext\jaccess.jar;G:\JDK1.8\jre\lib\ext\jfxrt.jar;G:\JDK1.8\jre\lib\ext\localedata.jar;G:\JDK1.8\jre\lib\ext\nashorn.jar;G:\JDK1.8\jre\lib\ext\sunec.jar;G:\JDK1.8\jre\lib\ext\sunjce_provider.jar;G:\JDK1.8\jre\lib\ext\sunmscapi.jar;G:\JDK1.8\jre\lib\ext\sunpkcs11.jar;G:\JDK1.8\jre\lib\ext\zipfs.jar;G:\JDK1.8\jre\lib\javaws.jar;G:\JDK1.8\jre\lib\jce.jar;G:\JDK1.8\jre\lib\jfr.jar;G:\JDK1.8\jre\lib\jfxswt.jar;G:\JDK1.8\jre\lib\jsse.jar;G:\JDK1.8\jre\lib\management-agent.jar;G:\JDK1.8\jre\lib\plugin.jar;G:\JDK1.8\jre\lib\resources.jar;G:\JDK1.8\jre\lib\rt.jar;E:\Java\kxtest\target\classes;D:\MAVEN\mvn_repo\org\apache\rocketmq\rocketmq-client\4.6.1\rocketmq-client-4.6.1.jar;D:\MAVEN\mvn_repo\org\apache\commons\commons-lang3\3.4\commons-lang3-3.4.jar;D:\MAVEN\mvn_repo\org\apache\rocketmq\rocketmq-common\4.6.1\rocketmq-common-4.6.1.jar;D:\MAVEN\mvn_repo\org\apache\rocketmq\rocketmq-remoting\4.6.1\rocketmq-remoting-4.6.1.jar;D:\MAVEN\mvn_repo\com\alibaba\fastjson\1.2.61\fastjson-1.2.61.jar;D:\MAVEN\mvn_repo\io\netty\netty-all\4.0.42.Final\netty-all-4.0.42.Final.jar;D:\MAVEN\mvn_repo\org\apache\rocketmq\rocketmq-logging\4.6.1\rocketmq-logging-4.6.1.jar;D:\MAVEN\mvn_repo\io\netty\netty-tcnative-boringssl-static\1.1.33.Fork26\netty-tcnative-boringssl-static-1.1.33.Fork26.jar;D:\MAVEN\mvn_repo\commons-validator\commons-validator\1.6\commons-validator-1.6.jar;D:\MAVEN\mvn_repo\commons-beanutils\commons-beanutils\1.9.2\commons-beanutils-1.9.2.jar;D:\MAVEN\mvn_repo\commons-digester\commons-digester\1.8.1\commons-digester-1.8.1.jar;D:\MAVEN\mvn_repo\commons-logging\commons-logging\1.2\commons-logging-1.2.jar;D:\MAVEN\mvn_repo\commons-collections\commons-collections\3.2.2\commons-collections-3.2.2.jar com.biws.test
0.3000000000000000166533453693773481063544750213623046875
0.1999999999999999555910790149937383830547332763671875
401.49999999999996802557689079549163579940795898437500
1.232999999999999971578290569595992565155029296875
Process finished with exit code 0
我们发现使用了BigDecimal之后计算结果还是不精确,这里就要记住BigDecimal的第一个坑了:
BigDecimal来表示和计算浮点数的时候,要使用String的构造方法来初始化BigDecimal。
小的改进一下再来看看结果:
public class test {
public static void main(String[] args) {
System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));
System.out.println(new BigDecimal("1.0").subtract(new BigDecimal("0.8")));
System.out.println(new BigDecimal("4.015").multiply(new BigDecimal("100")));
System.out.println(new BigDecimal("123.3").divide(new BigDecimal("100")));
}
}
那么接下来一个问题,使用了BigDecimal就万事大吉了吗?并不是的!
接下来我们来看一下BigDecimal的源码,这里面有一个地方需要注意,先看图:
private final int scale; // Note: this may have any value, so
// calculations must be done in longs
/**
* The number of decimal digits in this BigDecimal, or 0 if the
* number of digits are not known (lookaside information). If
* nonzero, the value is guaranteed correct. Use the precision()
* method to obtain and set the value if it might be 0. This
* field is mutable until set nonzero.
*
* @since 1.5
*/
private transient int precision;
注意看这两个属性,scale表示小数点右边几位,precision表示精度,就是我们常说的有效长度。
前边我们已经知道,BigDecimal必须传入字符串类型数值,那么如果我们现在是一个Double类型数值,该如何操作呢?通过一个简单的测试我们来看一下:
public class test2 {
private static void testScale() {
BigDecimal bigDecimal1 = new BigDecimal(Double.toString(100));
BigDecimal bigDecimal2 = new BigDecimal(String.valueOf(100d));
BigDecimal bigDecimal3 = BigDecimal.valueOf(100d);
BigDecimal bigDecimal4 = new BigDecimal("100");
BigDecimal bigDecimal5 = new BigDecimal(String.valueOf(100));
print(bigDecimal1);
print(bigDecimal2);
print(bigDecimal3);
print(bigDecimal4);
print(bigDecimal5);
}
private static void print(BigDecimal bigDecimal) {
System.out.println(String.format("scale %s precision %s result %s", bigDecimal.scale(), bigDecimal.precision(), bigDecimal.multiply(new BigDecimal("1.001"))));
}
public static void main(String[] args) {
testScale();
}
}
run一下我们发现,以上前三种方式是将double转换成BigDecimal之后,得到的BigDecimal的scale都是1,precision都是4,后两种方式的toString方法得到的scale都是0,precision都是3,与1.001进行乘运算后,我们发现,scale是两个数的scale相加的结果。
我们在处理浮点数的字符串的时候,应该显式的方式通过格式化表达式或者格式化工具来明确小数位数和舍入方式。
浮点数的舍入和格式化该如何选择?
我们首先来看看使用String.format的格式化舍入,会有什么结果,我们知道浮点数有double和float两种,下边我们就用这两种来举例子:
public class test3 {
public static void main(String[] args) {
double num1 = 3.35;
float num2 = 3.35f;
System.out.println(String.format("%.1f", num1));
System.out.println(String.format("%.1f", num2));
}
}
得到的结果似乎与我们的预期有出入,其实这个问题也很好解释,double和float的精度是不同的,double的3.35相当于3.350000000000000088817841970012523233890533447265625,而float的3.35相当于3.349999904632568359375,String.format才有的又是四舍五入的方式舍入,所以精度问题和舍入方式就导致了运算结果与我们预期不同。
Formatter类中默认使用的是HALF_UP的舍入方式,如果我们需要使用其他的舍入方式来格式化,可以手动设置。
到这里我们就知道通过String.format的方式来格式化这条路坑有点多,所以,浮点数的字符串格式化还得要使用BigDecimal来进行。
来,上代码,测试一下究竟是不是那么回事:
//改变格式
BigDecimal num3 = new BigDecimal("3.35");
//小数点后1位,向下舍入
BigDecimal num4 = num3.setScale(1, BigDecimal.ROUND_DOWN);
System.out.println(num2);
//小数点后1位,四舍五入
BigDecimal num5= num3.setScale(1, BigDecimal.ROUND_HALF_UP);
System.out.println(num3);
这次得到的结果与我们预期一致。
BigDecimal不能使用equals方法比较?
我们都知道,包装类的比较要使用equals,而不能使用==,那么这一条在Bigdecimal中也适用吗?数据说话,简单的一个测试来说明:
System.out.println(new BigDecimal("1").equals(new BigDecimal("1.0")))
结果:false
复制代码
按照我们的理解1和1.0是相等的,也应该是相等的,但是Bigdecimal的equals在比较中不只是比较了value,还比较了scale,我们前边说了scale是小数点后的位数,明显两个值的小数点后位数不一样,所以结果位false。
实际的使用中,我们常常是只希望比较两个BigDecimal的value,这里就要注意,要使用compareTo方法:
System.out.println(new BigDecimal("1").compareTo(new BigDecimal("1.0")))
结果:true
最后
再总结一下今天的文章:
- 避免使用Double来进行运算
- BigDecimal的初始化要使用String入参或者BigDecimal.valueOf()
- 浮点数的格式化建议使用BigDecimal
- 比较两个BigDecimal的value要使用compareTo