shuijibaobao

导航

< 2025年2月 >
26 27 28 29 30 31 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 1
2 3 4 5 6 7 8
统计
 

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

执行顺序交换后,产生的结果可能不同,会导致一定的问题,使用顺序建议先乘后除。

 

posted on   水吉z  阅读(90)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
 
点击右上角即可分享
微信分享提示