一. 浮点类型的问题
首先举一个float.double遇到的问题:
float a = 1;
float b = 0.9f;
System.out.println("a-b的结果是 = "+ (a-b));
使用Float,Double等浮点类型进行计算时,有可能得到的是一个近似值,而不是精确的值
当a-b后结果是0.100000024,之所以产生这样的结果,是因为0.1的二进制表示无限循环的,由于计算机的资源是无限的,所以没办法用二进制精确的表示0.1,只能用[近似值]表示.就是在有限精度情况下,最大化接近0.1的二进制数,于是会造成精度确实的情况.
这时候就应该考虑使用BigDecimal了,但是使用BigDecimal就一定能解决上面的浮点问题吗? 答案是不能的,
BigDecimal a1 = new BigDecimal(0.01);
BigDecimal b1 = BigDecimal.valueOf(0.01);
System.out.println("a1 = " + a1);
System.out.println("b1 = "+ b1);
运行的结果分别是:
a1 = 0.01000000000000000020816681711721685132943093776702880859375
b1 = 0.01
总结:即使使用BigDecimal,结果依旧会出现精度问题,这就涉及到创建BigDecimal对象时,如果有初始值,是采用 new BigDecimal的形式,还是通过BigDecimal的valueOf了之所以会出现上述情况,是因为 new BigDecimal时,传入的0.1已经是浮点类型了,鉴于上面说的这个值只是近似值,在使用new BigDecimal时就把这个近似值完整的保留下来了,而BigDecimal的valueOf方法则不同,它的实现源码如下:
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));
/**
* 在valueOf内部,使用Double的toString方法,将浮点类型的值转换为了字符串,
* 因此就不存在精度丢失的问题.
*/
}
拓展 :
构造方法 | 作用 |
---|---|
BigDecimal(int) | 创建一个具有参数所指定数值的对象 |
BigDecimal(double) | 创建一个具有参数所指定双精度值的对象 |
BigDecimal(long) | 创建一个具有参数所指定长整数值的对象 |
BigDecimal(String) | 创建一个具有参数所指定以字符串表示的数值的对象 |
其中涉及到参数类型为double的构造方法,会出现上述的问题,使用时留意
二. 浮点精度的问题
假设: 如果比较两个BigDecimal的值是否相等,是使用equals方法还是compareTo方法?
BigDecimal a = new BigDecimal("0.01");
BigDecimal b = new BigDecimal("0.010");
System.out.println("equals方法比较的结果 : " + a.equals(b)); //equals方法比较的结果 : false
System.out.println("compareTo方法比较的结果 : " + a.compareTo(b)); //compareTo方法比较的结果 : 0
PS: equals方法是基于BigDecimal实现的equals方法来进行比较的,直观印象就是比较两个对象是否相同,equals源码如下:
@Override
public boolean equals(Object x) {
if (!(x instanceof BigDecimal))
return false;
BigDecimal xDec = (BigDecimal) x;
if (x == this)
return true;
if (scale != xDec.scale)
return false;
long s = this.intCompact;
long xs = xDec.intCompact;
if (s != INFLATED) {
if (xs == INFLATED)
xs = compactValFor(xDec.intVal);
return xs == s;
} else if (xs != INFLATED)
return xs == compactValFor(this.intVal);
return this.inflated().equals(xDec.inflated());
}
通过阅读源码可以看出,equals方法不仅比较了值是否相等,还比较了精度是否相同,上述示例中,由于两者的精度不同,所以equals方法的判断结果为false,而compareTo方法实现了Comparable接口,真正比较的是值得大小,返回的值为-1(小于),0(等于),1(大于)
基本结论:
1.通常情况,如果比较两个BigDecimal值的大小,才用其实现的compareTo方法,如果严格限制精度的比较,那么可以考虑使用equals方法.
2.另外,这种场景在比较0值的时候比较常见,比如比较BigDecimal("0")/BigDecimal("0.0")/BigDecimal("0.00"),此时一定要使用compareTo方法 进行比较.
三. 设置精度问题
在开发中看到好多人通过BigDecimal进行计算时不设置计算结果的精度和舍入模式,虽然大多数情况下不会出现问题,但是以下情况就不一定了
案例一:
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("3.0");
a.divide(b);
执行代码的结果是ArithmeticException异常!!!
Exception in thread “main” java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
at java.math.BigDecimal.divide(BigDecimal.java:1690)
这个异常的发生在官方文档中也有声明:
If the quotient has a nonterminating decimal expansion and the operation is specified to return an exact result,
an ArithmeticException is thrown. Otherwise, the exact result of the division is returned, as done for other operations.
总结一下就是:如果在除法(divide)运算的过程中,如果商是一个无限小数(0.3333…),而操作的结果预期是一个精确的数字,那么会抛出一个ArithmeticException异常
此时只需要在使用divide方法指定结果的精度即可:
案例二 :
BigDecimal c = new BigDecimal("0.1");
BigDecimal d = new BigDecimal("0.3");
BigDecimal e = c.divide(d, 2, RoundingMode.HALF_UP);
System.out.println("经过处理后e的结果是: " + e); //经过处理后e的结果是: 0.33
基本结论: 在使用BigDecimal进行所有运算时,一定要明确指定精度和舍入模式
拓展 :
属性 | 介绍 |
---|---|
RoundingMode.UP | 舍入远离零的舍入模式。在丢弃非零部分之前始终增加数字(始终对非零舍弃部分前面的数字加1)。注意,此舍入模式始终不会减少计算值的大小。 |
RoundingMode.DOWN | 接近零的舍入模式。在丢弃某部分之前始终不增加数字(从不对舍弃部分前面的数字加1,即截短)。注意,此舍入模式始终不会增加计算值的大小。 |
RoundingMode.CEILING | 接近正无穷大的舍入模式。如果 BigDecimal 为正,则舍入行为与 ROUNDUP 相同;如果为负,则舍入行为与 ROUNDDOWN 相同。注意,此舍入模式始终不会减少计算值。 |
RoundingMode.FLOOR | 接近负无穷大的舍入模式。如果 BigDecimal 为正,则舍入行为与 ROUNDDOWN 相同;如果为负,则舍入行为与 ROUNDUP 相同。注意,此舍入模式始终不会增加计算值。 |
RoundingMode.HALF_UP | 向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则为向上舍入的舍入模式。如果舍弃部分 >= 0.5,则舍入行为与 ROUND_UP 相同;否则舍入行为与 ROUND_DOWN 相同。注意,这是我们在小学时学过的舍入模式(四舍五入)。 |
RoundingMode.HALF_DOWN | 向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则为上舍入的舍入模式。如果舍弃部分 > 0.5,则舍入行为与 ROUND_UP 相同;否则舍入行为与 ROUND_DOWN 相同(五舍六入)。 |
RoundingMode.HALF_EVEN | RoundingMode.HALF_EVEN:向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则向相邻的偶数舍入。如果舍弃部分左边的数字为奇数,则舍入行为与 ROUNDHALFUP 相同;如果为偶数,则舍入行为与 ROUNDHALF_DOWN 相同。注意,在重复进行一系列计算时,此舍入模式可以将累加错误减到最小。此舍入模式也称为“银行家舍入法”,主要在美国使用。四舍六入,五分两种情况。如果前一位为奇数,则入位,否则舍去。以下例子为保留小数点1位,那么这种舍入方式下的结果。1.15 ==> 1.2 ,1.25 ==> 1.2 |
RoundingMode.UNNECESSARY | 断言请求的操作具有精确的结果,因此不需要舍入。如果对获得精确结果的操作指定此舍入模式,则抛出ArithmeticException。 |
四. 三种字符串输入的问题
BigDecimal a = BigDecimal.valueOf(35634535255456719.22345634534124578902);
System.out.println(a.toString());
执行结果是: 3.563453525545672E+16,是上述对应的值吗? 并不是
.
也就是说,本来想打印字符串的,结果打印出来的是科学计数法的值。
这里我们需要了解BigDecimal转换字符串的三个方法:
1. toPlainString(): 不适用任何科学计数法
2. toString(): 在必要的时候使用科学计数法
3. toEngineeringString(): 在必要的的时候使用工程计数法,类似于科学计数法,只不过只是的幂都是3的倍数,这样方便工程上的应用,因为在很多单位转换的时候都是10^3
三种方法展示结果示例如下:
不使用指数 | 科学计数法 | 工程计数法 |
---|---|---|
2700 | 2.7 x 10³ | 2.7 x 10³ |
27000 | 2.7 x10⁴ | 27 x10³ |
270000 | 2.7 x 10⁵ | 270 x 10³ |
2700000 | 2.7 x 10⁶ | 2.7 x 10⁶ |
基本结论: 根据数据结果展示格式不同,采用不同的字符串输出方法,通常使用比较多的方法是toPlainString().
另外,NumberFormat类的format()方法可以使用BigDecimal对超出16位有效数字的货币值,百分值,以及一般数值进行格式化控制。
案例:
NumberFormat c = NumberFormat.getCurrencyInstance();//建立货币格式化引用
NumberFormat d = NumberFormat.getPercentInstance(); //建立百分比格式化引用
d.setMaximumFractionDigits(3); //百分比小数点最多三位
BigDecimal loanAmount = new BigDecimal("15000.48");
BigDecimal interestRate = new BigDecimal("0.008");
BigDecimal interest = loanAmount.multiply(interestRate);
System.out.println("金额: " + c.format(loanAmount));
System.out.println("利率: " + d.format(interestRate));
System.out.println("利息: " + c.format(interest));
执行结果如下 :