一、BigDecimal概述
BigDecimal是Java在java.math包中提供的线程安全的API类,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数,但在实际应用中,可能需要对更大或者更小的数进行运算和处理。一般情况下,对于那些不需要准确计算精度的数字,我们可以直接使用Float和Double处理,但是Double.valueOf(String) 和Float.valueOf(String)会丢失精度。所以开发中,如果我们需要精确计算的结果,则必须使用BigDecimal类来操作。
BigDecimal所创建的是对象,故我们不能使用传统的+、-、*、/等算术运算符直接对其对象进行数学运算,而必须调用其相对应的方法。方法中的参数也必须是BigDecimal的对象。构造器是类的特殊方法,专门用来创建对象,特别是带有参数的对象。
二、BigDecimal常用构造方法
方法 | 含义 |
---|---|
BigDecimal(int val) | 创建一个具有参数所指定整数值的对象。 |
BigDecimal(double val) | 创建一个具有参数所指定双精度值的对象。不推荐使用,因为存在精度丢失问题 |
BigDecimal(long val) | 创建一个具有参数所指定长整数值的对象。 |
BigDecimal(String val) | 创建一个具有参数所指定以字符串表示的数值的对象。 推荐使用 |
三、BigDecimal常用方法
注意:BigDecimal进行运算时必须要保证对象本身不能是null,否则就会抛空指针异常。
方法 | 含义 |
---|---|
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 |
代码示例
import java.math.BigDecimal;
public class Test {
public static void main(String[] args){
BigDecimal b1 = new BigDecimal("1");
BigDecimal b2 = new BigDecimal("2");
BigDecimal b3 = new BigDecimal("4");
System.out.println("相加:"+b1.add(b2));
System.out.println("相减:"+b1.subtract(b2));
System.out.println("相乘:"+b2.multiply(b3));
System.out.println("相除:"+b2.divide(b3));
}
}
四、BigDecimal进阶
BigDecimal的八种舍入模式
BigDecimal.setScale()方法用于格式化小数点
setScale(1)表示保留一位小数,默认用四舍五入方式
setScale(1,BigDecimal.ROUND_DOWN)直接删除多余的小数位,如2.35会变成2.3
setScale(1,BigDecimal.ROUND_UP)进位处理,2.35变成2.4
setScale(1,BigDecimal.ROUND_HALF_UP)四舍五入,2.35变成2.4
setScaler(1,BigDecimal.ROUND_HALF_DOWN)四舍五入,2.35变成2.3,如果是5则向下舍
setScaler(1,BigDecimal.ROUND_CEILING)接近正无穷大的舍入
setScaler(1,BigDecimal.ROUND_FLOOR)接近负无穷大的舍入,数字>0和ROUND_UP作用一样,数字<0和ROUND_DOWN作用一样
setScaler(1,BigDecimal.ROUND_HALF_EVEN)向最接近的数字舍入,如果与两个相邻数字的距离相等,则向相邻的偶数舍入。
1、ROUND_UP,向远离0的方向舍入,在丢弃非零部分之前始终增加数字(始终对非零舍弃部分前面的数字加1)。
注意,此舍入模式始终不会减少计算值的大小。
eg: 保留1位小数 1.60->1.6 1.61->1.7 1.66->1.7 , -1.62->-1.7
2、ROUND_DOWN,向接近0的方向舍入,在丢弃某部分之前,始终不增加数据(即,截断),该方式是只减不加。
eg: 保留1位小数 1.60->1.6 1.61->1.6 1.66->1.6 , -1.62->-1.6
3、ROUND_CEILING,向正无穷方向舍入,如果数值为正,舍入方式与ROUND_UP一致,如果为负,舍入方式与ROUND_DOWN一致,该模式始终不会减少计算数值。
eg: 保留1位小数 1.60->1.6 1.61->1.7 1.66->1.7 , -1.62->-1.6
4、ROUND_FLOOR,向负无穷方向舍入,如果数值为正,舍入行为与 ROUND_DOWN 相同;如果为负,则舍入行为与 ROUND_UP 相同。该模式始终不会增加计算数值。
eg: 保留1位小数 1.60->1.6 1.61->1.6 1.66->1.6 , -1.62->-1.7
5、ROUND_HALF_UP,向“最接近的”数字舍入,也就是四舍五入。
eg: 保留1位小数 1.61->1.6 1.65->1.7 1.66->1.7 , -1.62->-1.6
6、ROUND_HALF_DOWN,向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则为上舍入的舍入模式,也就是五舍六入。
eg: 保留1位小数 1.61->1.6 1.65->1.6 1.66->1.7 , -1.62->-1.6
7、ROUND_HALF_EVEN,向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则向相邻的偶数舍入。如果舍弃部分左边的数字为奇数,则舍入行为与 ROUND_HALF_UP 相同;如果为偶数,则舍入行为与 ROUND_HALF_DOWN 相同。
注意,在重复进行一系列计算时,此舍入模式可以将累加错误减到最小。
此舍入模式也称为“银行家舍入法”,主要在美国使用。四舍六入,五分两种情况。如果前一位为奇数,则入位,否则舍去。以下例子为保留小数点1位,那么这种舍入方式下的结果。
eg. 1.15->1.2, 1.25->1.2
8、ROUND_UNNECESSARY,计算结果是精确的,不需要舍入模式。如果对获得精确结果的操作指定此舍入模式,则抛出ArithmeticException。
代码示例
BigDecimal b = new BigDecimal("1.6666");
System.out.println("result b:" + b.setScale(2, BigDecimal.ROUND_HALF_UP)); // 1.67
System.out.println("result b:" + b.setScale(2)); // 精度错误
//执行结果:
result b:1.67
Exception in thread "main" java.lang.ArithmeticException: Rounding necessary
/**原因分析:
* setScale方法默认使用的roundingMode是ROUND_UNNECESSARY,不需要使用舍入模式,设置精度2位,但是小数点后有4位肯定会抛异常。
* BigDecimal格式化、小数点转换
* BigDecimal可以与DecimalFormat结合使用,从而对金额格式化,如小数点后面统一保留两位,不够两位的补零,多余两位的舍入。
*/
import java.math.BigDecimal;
import java.text.DecimalFormat;
public class Test {
public static void main(String[] s){
System.out.println(formatToNumber(new BigDecimal("12333.435")));
System.out.println(formatToNumber(new BigDecimal(0)));
System.out.println(formatToNumber(new BigDecimal("0.00")));
System.out.println(formatToNumber(new BigDecimal("0.001")));
System.out.println(formatToNumber(new BigDecimal("0.006")));
System.out.println(formatToNumber(new BigDecimal("0.206")));
System.out.println(formatToNumber(new BigDecimal("1.22")));
}
/**
* @desc
* @param obj 传入的小数
* @return
*/
public static String formatToNumber(BigDecimal obj) {
// DecimalFormat默认使用的是进位方式是RoundingMode.HALF_EVEN,此舍入模式也称为“银行家算法”,主要在美国使用。
//银行家算法:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一
DecimalFormat df = new DecimalFormat("###,##0.00");
return df.format(obj);
}
}
//执行结果:
12,333.44
0.00
0.00
0.00
0.01
0.21
1.22
需注意:
DecimalFormat的默认进位方式不是四舍五入,所以当小数点后面需要舍去的时候,肯能跟预想的不一样,具体可参考《关于DecimalFormat的取舍问题,DecimalFormat四舍五入的坑》
new DecimalFormat(“###,##0.00”)小数点前面需要有个0,这样0-1之间的数字才会正常格式化;若##0.00的小数点前面没有0,则0-1之间的数字会被丢失掉小数点前的0,代码如下:
import java.math.BigDecimal;
import java.text.DecimalFormat;
public class Test {
public static void main(String[] s){
System.out.println(formatToNumber(new BigDecimal("12333.435")));
System.out.println(formatToNumber(new BigDecimal(0)));
System.out.println(formatToNumber(new BigDecimal("0.00")));
System.out.println(formatToNumber(new BigDecimal("0.001")));
System.out.println(formatToNumber(new BigDecimal("0.006")));
System.out.println(formatToNumber(new BigDecimal("0.206")));
System.out.println(formatToNumber(new BigDecimal("1.22")));
}
/**
* @desc
* @param obj 传入的小数
* @return
*/
public static String formatToNumber(BigDecimal obj) {
// DecimalFormat默认使用的是进位方式是RoundingMode.HALF_EVEN,此舍入模式也称为“银行家算法”,主要在美国使用。
//银行家算法:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一
DecimalFormat df = new DecimalFormat("###,##.00");
return df.format(obj);
}
}
//执行结果:
1,23,33.44
.00
.00
.00
.01
.21
1.22
货币格式化与百分比格式化
经常能看到金额用¥120.00表示,利率用0.8%表示,这里扩展一下BigDecimal的货币格式化与百分比格式化
NumberFormat类的format()方法可以使用BigDecimal对象作为其参数,可以利用BigDecimal对超出16位有效数字的货币值,百分值,以及一般数值进行格式化控制。
NumberFormat对象:
getCompactNumberInstance();返回FORMAT带有"SHORT"格式样式的默认语言环境 的紧凑数字格式 。
getCurrencyInstance(Locale inLocale);返回指定语言环境的货币格式。若是不指定参数,则以默认语言为参数。
getInstance(Locale inLocale);返回指定语言环境的通用数字格式。若是不指定参数,则以默认语言为参数。
getPercentInstance(Locale inLocale);返回指定语言环境的百分比格式。若是不指定参数,则以默认语言为参数。
import java.math.BigDecimal;
import java.text.NumberFormat;
public class Test {
public static void main(String[] args){
NumberFormat currency = NumberFormat.getCurrencyInstance(); //建立货币格式化引用
NumberFormat percent = NumberFormat.getPercentInstance(); //建立百分比格式化引用
percent.setMinimumFractionDigits(2);//设置数的小数部分所允许的最小位数(如果不足后面补0)
percent.setMaximumFractionDigits(3);//设置数的小数部分所允许的最大位数(如果超过会四舍五入)
BigDecimal amount = new BigDecimal("250600.42"); //金额
BigDecimal interestRate = new BigDecimal("0.0004"); //利率
BigDecimal interest = amount .multiply(interestRate); //相乘
System.out.println("金额: " + currency.format(loanAmount));
System.out.println("利率: " + percent.format(interestRate));
System.out.println("利息: " + currency.format(interest));
}
}
//执行结果:
金额: ¥250,600.42
利率: 0.04%
利息: ¥100.24
NumberFormat提供了多种货币格式的引用,如¥(人民币),$(美元、英元)等等
五、BigDecimal常见问题
踩坑一:创建 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位精度还是可能会出现问题的
踩坑二:等值比较的坑
一般在比较两个值是否相等时,都是用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方法。
踩坑三:无限精度的坑
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)
在官方文档中对该异常有如下说明:
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.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进行(所有)运算时,尽量指定精度和舍入模式。
踩坑四:BigDecimal三种字符串输出的坑
在BigDecimal 转换成字符串时,有可能输出非你预期的结果。如下所示:
public static void main(String[] args){
BigDecimal bg = new BigDecimal("1E11");
System.out.println(bg.toString()); // 1E+11
System.out.println(bg.toPlainString()); // 100000000000
System.out.println(bg.toEngineeringString()); // 100E+9
}
执行结果:
1E+11
100000000000
100E+9
可以看到三种方式输出的结果可能都不相同,可能这个并不是预期的结果 ,BigDecimal 有三个方法可以转为相应的字符串类型,切记不要用错:
以下内容介绍java.math.BigDecimal下的三个toString方法的区别及用法
toPlainString() : 不使用任何指数。
toString() :有必要时使用科学计数法。
toEngineeringString():有必要时使用工程计数法。 工程记数法是一种工程计算中经常使用的记录数字的方法,与科学技术法类似,但要求10的幂必须是3的倍数
踩坑五:使用BigDecimal进行计算时参数不能为NULL
在使用BigDecimal类型进行计算时,进行加、减、乘、除、比较大小时,一定要保证参与计算的两个值不能为空,否则会抛出java.lang.NullPointerException异常。
//代码示例:
BigDecimal b1 = new BigDecimal("1");
BigDecimal b2 = null;
System.out.println("相加:"+b2.add(b1));
结果:
Exception in thread "main" java.lang.NullPointerException
at com.demo.controller.Test.main(Test.java:14)
踩坑六:使用BigDecimal进行除法计算时被除数不能为0
代码示例:
BigDecimal b1 = new BigDecimal("1");
BigDecimal b2 = new BigDecimal("0");
System.out.println(b1.divide(b2));
执行结果:
Exception in thread "main" java.lang.ArithmeticException: Division by zero
踩坑七:执行顺序不能调换(乘法交换律失效)
乘法满足交换律是一个常识,但是在计算机的世界里,会出现不满足乘法交换律的情况;
代码示例:
BigDecimal b1 = BigDecimal.valueOf(1.0);
BigDecimal b2 = BigDecimal.valueOf(3.0);
BigDecimal b3 = BigDecimal.valueOf(3.0);
System.out.println(b1.divide(b2, 2, RoundingMode.HALF_UP).multiply(b3)); // 0.990
System.out.println(b1.multiply(b3).divide(b2, 2, RoundingMode.HALF_UP)); // 1.00
执行结果:
0.990
1.00
执行顺序交换后,产生的结果可能不同,会导致一定的问题,使用顺序建议先乘后除。