Java避坑指南:涉及金钱计算时使用BigDecimal如何避坑


简介


在涉及金钱交易交易计算时,为了避免浮点数计算对精度的影响,我们都会选择BigDecimal来处理。但是BigDecimal的有些方法是有坑的,稍不注意也会产生精度计算的问题。

BigDecimal坑一: 禁止使用java.math.BigDecimal#BigDecimal(double)构造函数方法


示例:

代码语言:javascript
复制
/**
 * @author 认知科技技术团队
 * 微信公众号:认知科技技术团队
 */
public class Demo {
    public static void main(String[] args)  {
        BigDecimal bigDecimal = new BigDecimal(1.019);
        System.out.println(bigDecimal);
    }
}

输出:

结果肯定不是我们想要的。我们看一下javadoc给的解释:

浮点数在计算机硬件中存储会丢失精度风险。为了避免这种情况,我们需要使用字符串参数的构造函数

代码语言:javascript
复制
java.math.BigDecimal#BigDecimal(java.lang.String)

示例:

代码语言:javascript
复制
/**
 * @author 认知科技技术团队
 * 微信公众号:认知科技技术团队
 */
public class Demo {
    public static void main(String[] args)  {
        BigDecimal bigDecimal = new BigDecimal("1.019");
        System.out.println(bigDecimal);
    }
}

输出:

BigDecimal坑二: 禁止使用java.math.BigDecimal#valueOf(double)静态方法


示例:

代码语言:javascript
复制
/**
 * @author 认知科技技术团队
 * 微信公众号:认知科技技术团队
 */
public class Demo {
    public static void main(String[] args)  {
        BigDecimal bigDecimal =   BigDecimal.valueOf(1.019111456677999999);
        System.out.println(bigDecimal);
    }
}

输出:

1.019111456677999999 输入,变成了1.019111456678。这也是由于我们输入的是double类型的精度问题导致。这和坑一情况一致。

BigDecimal坑三:禁止使用java.math.BigDecimal#equals比较


示例:

代码语言:javascript
复制
/**
 * @author 认知科技技术团队
 * 微信公众号:认知科技技术团队
 */
public class Demo {
    public static void main(String[] args)  {
        BigDecimal a =  new  BigDecimal("1.0");
        BigDecimal b = new BigDecimal("1");
        System.out.println(a.equals(b));
    }
}

输出:

我们看一下javadoc官方解释:

BigDecimal的equals方法会比较比较其“无标度值”和“标度值”,只有两者都相等才相等,而非我们业务逻辑即金钱关系的相等。为了避免这种情况的发生,我们使用compareTo方法即可。

BigDecimal坑四:禁止使用java.math.BigDecimal#round 进行舍入运算


示例:

代码语言:javascript
复制
/**
 * @author 认知科技技术团队
 * 微信公众号:认知科技技术团队
 */
public class Demo {
    public static void main(String[] args)  {
        BigDecimal x = new BigDecimal("123.56789");
        x = x.round(new MathContext(2, RoundingMode.HALF_UP));
        System.out.println(x);
        System.out.println("x=" + x.toPlainString());
        System.out.println("scale=" + x.scale());
    }
}

输出结果:

期望结果123.57,而结果真是意料之外。

因为此round方法只是从左到右进行舍入位数。

比如:123111.56789,从左到右舍入2位,小数部分,结果为:120000

代码语言:javascript
复制
/**
 * @author 认知科技技术团队
 * 微信公众号:认知科技技术团队
 */
public class Demo {
    public static void main(String[] args)  {
        BigDecimal x = new BigDecimal("123111.56789");
        x = x.round(new MathContext(2, RoundingMode.HALF_UP));
        System.out.println(x);
        System.out.println("x=" + x.toPlainString());
        System.out.println("scale=" + x.scale());
    }
}

进行舍入运算时,我们可以使用方法:

代码语言:javascript
复制
java.math.BigDecimal#setScale(int, java.math.RoundingMode)

示例:

代码语言:javascript
复制
/**
 * @author 认知科技技术团队
 * 微信公众号:认知科技技术团队
 */
public class Demo {
    public static void main(String[] args)  {
        BigDecimal x = new BigDecimal("1235.6789");
        x = x.setScale(2, RoundingMode.HALF_UP);
        System.out.println("x=" + x);
    }
}

输出:

小结


【🈲】禁止使用java.math.BigDecimal#BigDecimal(double)构造函数方法;

【🈲】禁止使用java.math.BigDecimal#valueOf(double)静态方法;

【🈲】禁止使用java.math.BigDecimal#equals比较;

【🈲】禁止使用java.math.BigDecimal#round 进行舍入运算;

【👍🏻】使用字符串参数的构造函数java.math.BigDecimal#BigDecimal(java.lang.String)构造BigDecimal,防止精度丢失;

【👍🏻】使用compareTo方法比较BigDecimal对象;

【👍🏻】使用java.math.BigDecimal#setScale(int, java.math.RoundingMode)进行舍入运算;