目录
setScale(1,BigDecimal.ROUND_DOWN)
setScale(1,BigDecimal.ROUND_UP)
setScale(1,BigDecimal.ROUND_HALF_UP)
setScaler(1,BigDecimal.ROUND_HALF_DOWN)
setScaler(1,BigDecimal.ROUND_CEILING)
setScaler(1,BigDecimal.ROUND_FLOOR)
setScaler(1,BigDecimal.ROUND_HALF_EVEN)
一、前言
我们在做浮点数运算时,很多时间计算的结果并不是我们想要的,比如下面的代码:
double y = 1.0 - 0.9;
System.out.println(y);
正常来说,我们想要的结果为0.1,但结果确是:
0.09999999999999998
我们发现,计算出来的值和我们预期结果不一致。原因在于我们的计算机是二进制的。浮点数没有办法使用二进制进行精确表示。
计算机的 CPU 表示浮点数由两个部分组成:指数和尾数,这样的表示方法一般都会失去一定的精确度,有些浮点数运算也会产生一定的误差。
浮点运算很少是精确的,只要是超过精度能表示的范围就会产生误差。往往产生误差不是因为数的大小,而是因为数的精度。因此,产生的结果接近但不等于想要的结果。尤其在使用 float 和 double 作精确运算的时候要特别小心。
二、BigDecimal
因为浮点类型无法满足我们的精度要求,在做金融行业运算时,我们尽量使用BigDecimal类。
2.1、常用构造方法
构造方法 | 含义 |
BigDecimal(double val) | 创建一个具有参数所指定双精度值的对象。(不推荐使用,因为存在精度丢失问题) |
BigDecimal(String val) | 创建一个具有参数所指定以字符串表示的数值的对象。(推荐使用) |
2.2、常用方法
方法 | 含义 |
add(BigDecimal) | BigDecimal对象中的值相加,返回BigDecimal对象 |
subtract(BigDecimal) | BigDecimal对象中的值相减,返回BigDecimal对象 |
multiply(BigDecimal) | BigDecimal对象中的值相乘,返回BigDecimal对象 |
divide(BigDecimal) | BigDecimal对象中的值相除,返回BigDecimal对象 |
abs() | 将BigDecimal对象中的值转换成绝对值 |
doubleValue() | 将BigDecimal对象中的值转换成双精度数 |
floatValue() | 将BigDecimal对象中的值转换成单精度数 |
longValue() | 将BigDecimal对象中的值转换成长整数 |
intValue() | 将BigDecimal对象中的值转换成整数 |
compareTo(BigDecimal val) | 比较大小,返回int类型。0(相等) 1(大于) -1(小于) |
toString() | 有必要时使用科学计数法。 |
toPlainString() | 不使用任何指数(推荐使用) |
toEngineeringString() | 有必要时使用工程计数法。 工程记数法是一种工程计算中经常使用的记录数字的方法,与科学技术法类似,但要求10的幂必须是3的倍数 |
max(BigDecimal val) | 两值比较,返回最大值 |
negate() | 求相反数,正变负,负变正 |
pow(int n) | 求乘方,如BigDecimal.valueOf(2).pow(3)的值为8 |
2.3、示例代码
add(BigDecimal)方法
BigDecimal b1 = new BigDecimal("1.0");
BigDecimal b2 = new BigDecimal("0.9");
BigDecimal b3 = b1.add(b2);
double d1 = b3.doubleValue();
System.out.println(d1); //1.9
subtract(BigDecimal)方法
BigDecimal b1 = new BigDecimal("1.0");
BigDecimal b2 = new BigDecimal("0.9");
BigDecimal b3 = b1.subtract(b2);
double d1 = b3.doubleValue();
System.out.println(d1); //0.1
multiply(BigDecimal)方法
BigDecimal b1 = new BigDecimal("1.0");
BigDecimal b2 = new BigDecimal("0.9");
BigDecimal b3 = b1.multiply(b2);
double d1 = b3.doubleValue();
System.out.println(d1); //0.9
divide(BigDecimal)方法
BigDecimal b1 = new BigDecimal("2.0");
BigDecimal b2 = new BigDecimal("1.0");
BigDecimal b3 = b1.divide(b2);
double d1 = b3.doubleValue();
System.out.println(d1); //2.0
max(BigDecimal val)方法
BigDecimal b1 = new BigDecimal("2.0");
BigDecimal b2 = new BigDecimal("1.0");
BigDecimal b3 = b1.max(b2);
double d1 = b3.doubleValue();
System.out.println(d1); //2.0
2.4、BigDecimal的八种舍入模式
setScale(1)
表示保留一位小数,默认用四舍五入方式。
BigDecimal b1 = new BigDecimal("2.6789");
BigDecimal b2 = b1.setScale(1);
System.out.println(b2.doubleValue());
setScale方法默认使用的roundingMode是ROUND_UNNECESSARY,不需要使用舍入模式,设置精度2位,但是小数点后有4位肯定会抛异常。
setScale(1,BigDecimal.ROUND_DOWN)
直接删除多余的小数位,如2.35会变成2.3。
BigDecimal b1 = new BigDecimal("2.6789");
BigDecimal b2 = b1.setScale(1,BigDecimal.ROUND_DOWN);
System.out.println(b2.doubleValue()); //2.6
setScale(1,BigDecimal.ROUND_UP)
进位处理,2.35变成2.4。
BigDecimal b1 = new BigDecimal("2.6789");
BigDecimal b2 = b1.setScale(1,BigDecimal.ROUND_UP);
System.out.println(b2.doubleValue()); //2.7
setScale(1,BigDecimal.ROUND_HALF_UP)
四舍五入,2.35变成2.4。
BigDecimal b1 = new BigDecimal("2.6789");
BigDecimal b2 = b1.setScale(1,BigDecimal.ROUND_HALF_UP);
System.out.println(b2.doubleValue()); //2.7
setScaler(1,BigDecimal.ROUND_HALF_DOWN)
四舍五入,2.35变成2.3,如果是5则向下舍。
BigDecimal b1 = new BigDecimal("2.35");
BigDecimal b2 = b1.setScale(1,BigDecimal.ROUND_HALF_DOWN);
System.out.println(b2.doubleValue()); //2.3
setScaler(1,BigDecimal.ROUND_CEILING)
接近正无穷大的舍入。
BigDecimal b1 = new BigDecimal("2.35");
BigDecimal b2 = b1.setScale(1,BigDecimal.ROUND_CEILING);
System.out.println(b2.doubleValue()); //2.4
setScaler(1,BigDecimal.ROUND_FLOOR)
接近负无穷大的舍入,数字>0和ROUND_UP作用一样,数字<0和ROUND_DOWN作用一样。
setScaler(1,BigDecimal.ROUND_HALF_EVEN)
向最接近的数字舍入,如果与两个相邻数字的距离相等,则向相邻的偶数舍入。
2.5、注意事项
(1)创建 BigDecimal精度丢失的坑
在BigDecimal 中提供了多种创建方式,可以通过new 直接创建,也可以通过 BigDecimal.valueOf 创建。这两种方式使用不当,也会导致精度问题。如下:
public static void main(String[] args) throws Exception {
BigDecimal b1 = new BigDecimal(0.1);
System.out.println(b1);
BigDecimal b2 = BigDecimal.valueOf(0.1);
System.out.println(b2);
BigDecimal b3 = BigDecimal.valueOf(0.111111111111111111111111111234);
System.out.println(b3);
}
执行结果:
0.1000000000000000055511151231257827021181583404541015625
0.1
0.1111111111111111
上面示例中两个方法都传入了double类型的参数0.1但是 b1 还是出现了精度的问题。造成这种问题的原因是 0.1 这个数字计算机是无法精确表示的,送给 BigDecimal 的时候就已经丢精度了,而 BigDecimal.valueOf 的实现却完全不同。如下源码所示,BigDecimal.valueOf 中是把浮点数转换成了字符串来构造的BigDecimal,因此避免了问题。
public static BigDecimal valueOf(double val) {
return new BigDecimal(Double.toString(val));
}
结论:
- 在使用BigDecimal构造函数时,尽量传递字符串而非浮点类型。
- 如果无法满足第一条,则可采用BigDecimal.valueOf方法来构造初始化值(但是valueOf受double类型精度影响,当传入参数小数点后的位数超过double允许的16位精度还是可能会出现问题的)。
(2)等值比较的坑
一般在比较两个值是否相等时,都是用equals 方法,但是,在BigDecimal 中使用equals可能会导致结果错误,BigDecimal 中提供了 compareTo 方法,在很多时候需要使用compareTo 比较两个值。如下所示:
public static void main(String[] args){
BigDecimal b1 = new BigDecimal("1.0");
BigDecimal b2 = new BigDecimal("1.00");
System.out.println(b1.equals(b2));
System.out.println(b1.compareTo(b2));
}
执行结果:
false
0
出现此种结果的原因是,equals不仅比较了值是否相等,还比较了精度是否相同。示例中,由于两个值的精度不同,所有结果也就不相同。而 compareTo 是只比较值的大小。返回的值为-1(小于),0(等于),1(大于)。
结论:
- 如果比较两个BigDecimal值的大小,采用其实现的compareTo方法;
- 如果严格限制精度的比较,那么则可考虑使用equals方法。
(3)无限精度的坑
BigDecimal 并不代表无限精度,当在两个数除不尽的时候,就会出现无限精度的坑,如下所示:
public static void main(String[] args){
BigDecimal b1 = new BigDecimal("1.0");
BigDecimal b2 = new BigDecimal("3.0");
b1.divide(b2);
}
执行结果:
Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
at java.math.BigDecimal.divide(BigDecimal.java:1693)
at com.demo.controller.Test.main(Test.java:29)
大致意思就是,如果在除法(divide)运算过程中,如果商是一个无限小数(如 0.333…),而操作的结果预期是一个精确的数字,那么将会抛出ArithmeticException异常。
此种情况,只需要在使用 divide方法时指定结果的精度即可:
public static void main(String[] args){
BigDecimal b1 = new BigDecimal("1.0");
BigDecimal b2 = new BigDecimal("3.0");
System.out.println(b1.divide(b2,2, RoundingMode.HALF_UP));//0.33
}
结论:
- 在使用BigDecimal进行(所有)运算时,尽量指定精度和舍入模式。