1. 概述
BigDecimal是Java在java.math包中提供的线程安全的API类。BigDecimal是Java中用于表示任意精度数字的类,它可以表示无限长度的小数,BigDecimal 通常支持任意位数的小数部分,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数,但在实际应用中,可能需要对更大或者更小的数进行运算和处理。一般情况下,对于那些不需要准确计算精度的数字,我们可以直接使用Float和Double处理,但是Double.valueOf(String) 和Float.valueOf(String)会丢失精度。所以开发中,如果我们需要精确计算的结果,则必须使用BigDecimal类来操作。
BigDecimal所创建的是对象,故我们不能使用传统的+、-、*、/等算术运算符直接对其对象进行数学运算,而必须调用其相对应的方法。方法中的参数也必须是BigDecimal的对象。构造器是类的特殊方法,专门用来创建对象,特别是带有参数的对象。
2. 常用构造方法
3. BigDecimal常用方法
注意:BigDecimal进行运算时必须要保证对象本身不能是null,否则就会抛空指针异常。
代码示例:
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)); } }
4. 进阶
4.1、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 necessar
原因分析:
setScale方法默认使用的roundingMode是ROUND_UNNECESSARY,不需要使用舍入模式,设置精度2位,但是小数点后有4位肯定会抛异常。
4.2、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
4.3、货币格式化与百分比格式化
经常能看到金额用¥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常见问题
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位精度还是可能会出现问题的5.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方法。
5.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进行(所有)运算时,尽量指定精度和舍入模式。
5.4、踩坑四: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的倍数
5.5、踩坑五:使用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)
5.6、踩坑六:使用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
执行顺序交换后,产生的结果可能不同,会导致一定的问题,使用顺序建议先乘后除。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战