前言

BigDecimal 是 java.math 包中提供的一种可以用来进行精确运算的类型。所以,在支付、电商等业务中,BigDecimal 的使用非常频繁。而且其内部自带了很多方法,如加,减,乘,除等运算方法都是可以直接调用的。除了需要用 BigDecimal 表示数字和进行数字运算以外,代码中还经常需要对于数字进行相等判断。

image.png

不能使用 BigDecimal 的 equals 方法做等值比较

那么为什么会有这样的要求呢🤔~ 其中的奥秘是什么呢🤔~ 请各位小伙伴听我娓娓道来…

BigDecimal 的 equals 方法

  1. public static void main(String[] args) {
  2. BigDecimal bigDecimal1 = new BigDecimal(1);
  3. BigDecimal bigDecimal2 = new BigDecimal(1);
  4. if(bigDecimal1 == bigDecimal2){
  5. //等值比较
  6. }
  7. }

相信聪明的小伙伴一眼就可以看出来上面的代码是有问题的,因为 BigDecimal 是对象,不能使用 == 来做等值判断。

如果我们使用 BigDecimal 的 equals 方法做等值比较是不是可以呢?👇

  1. public static void main(String[] args) {
  2. BigDecimal bigDecimal1 = new BigDecimal(1);
  3. BigDecimal bigDecimal2 = new BigDecimal(1);
  4. if(bigDecimal1.equals(bigDecimal2)){
  5. //等值比较
  6. }
  7. }

这里我先卖个关子,咱们跑跑代码来看看能不能用 BigDecimal 的 equals 方法做等值比较(●’◡’●),

  1. public static void main(String[] args) {
  2. BigDecimal bigDecimal1 = new BigDecimal(1);
  3. BigDecimal bigDecimal2 = new BigDecimal(1);
  4. System.out.println(bigDecimal1.equals(bigDecimal2));
  5. BigDecimal bigDecimal3 = new BigDecimal(1);
  6. BigDecimal bigDecimal4 = new BigDecimal(1.0);
  7. System.out.println(bigDecimal3.equals(bigDecimal4));
  8. BigDecimal bigDecimal5 = new BigDecimal("1");
  9. BigDecimal bigDecimal6 = new BigDecimal("1.0");
  10. System.out.println(bigDecimal5.equals(bigDecimal6));
  11. }

BigDecimal精度问题 - 图2

我们可以发现,在使用 BigDecimal 的 equals 方法对 1 和 1.0 进行比较的时候:使用 int、double 定义 BigDecimal 结果是 true;使用 String 定义 BigDecimal 结果是false,为什么会出现这种情况呢?

我们一起来看看 equals 方法的源码 👇

  1. /**
  2. * Compares this {@code BigDecimal} with the specified
  3. * {@code Object} for equality. Unlike {@link
  4. * #compareTo(BigDecimal) compareTo}, this method considers two
  5. * {@code BigDecimal} objects equal only if they are equal in
  6. * value and scale (thus 2.0 is not equal to 2.00 when compared by
  7. * this method).
  8. *
  9. * @param x {@code Object} to which this {@code BigDecimal} is
  10. * to be compared.
  11. * @return {@code true} if and only if the specified {@code Object} is a
  12. * {@code BigDecimal} whose value and scale are equal to this
  13. * {@code BigDecimal}'s.
  14. * @see #compareTo(java.math.BigDecimal)
  15. * @see #hashCode
  16. */
  17. @Override
  18. public boolean equals(Object x) {
  19. if (!(x instanceof BigDecimal))
  20. return false;
  21. BigDecimal xDec = (BigDecimal) x;
  22. if (x == this)
  23. return true;
  24. if (scale != xDec.scale)
  25. return false;
  26. long s = this.intCompact;
  27. long xs = xDec.intCompact;
  28. if (s != INFLATED) {
  29. if (xs == INFLATED)
  30. xs = compactValFor(xDec.intVal);
  31. return xs == s;
  32. } else if (xs != INFLATED)
  33. return xs == compactValFor(this.intVal);
  34. return this.inflated().equals(xDec.inflated());
  35. }

其实咱们从方法的注释中就能找到答案:equals 方法会比较两部分内容,分别是值(value)和标度(scale),也就是说 bigDecimal5 和 bigDecimal6 的值虽然相同,但是标度是不一样的。

咱们打个断点,debug 一下看看~

BigDecimal精度问题 - 图3

我们可以看见 bigDecimal5 的标度值是0,而bigDecimal6的标度值是1,所以 bigDecimal5 和 bigDecimal6 的比较结果是false (●ˇ∀ˇ●)

那么这时候又产生了一个疑问:为什么标度不同呢?🤔

double类型计算误差

案例:double类型的三个数0.2、0.5、0.1相加,期望得到的结果应该是0.8,实际输出的0.7999999999999999。

  1. double a = 0.2;
  2. double b = 0.5;
  3. double c = 0.1;
  4. double res = a+b+c;
  5. System.out.println("0.2+0.5+0.1 = "+res);

结果:
Screen Shot 2022-02-17 at 10.40.47 AM.png

原因:一位小数,小数位不是0或者5的,用2进制来标识,长度是无限长的。double属于floating binary point types,也就是说都double型的数值在相加减的时候,会将数值转换成浮点数再做相加减。但是在根据
IEEE 754标准,转换成二进制代码表示时,存储小数部分的位数会有不够的现象,即无限循环小数,这就是造成微差距的主要原因。

double小数转BigDecimal

  1. double g = 10.35;
  2. String gg = String.format("%.3f",g);
  3. BigDecimal originG = new BigDecimal(g);
  4. System.out.println("double BigDecimal default is :"+originG);
  5. System.out.println("double BigDecimal default scale is :"+originG.scale());
  6. BigDecimal mon = originG.setScale(1, RoundingMode.HALF_EVEN);
  7. System.out.println("double Bigdecimal in one decimal place :"+mon.doubleValue());
  8. mon = new BigDecimal(gg).setScale(1,RoundingMode.HALF_EVEN);
  9. System.out.println("original in one decimal place :"+mon.doubleValue());

结果:
Screen Shot 2022-02-17 at 11.53.23 AM.png

原因:定义double g = 10.35在计算机中二进制表示成定义g = 10.349999999999999,当new BigDecimal(g),g还是10.349999999999999,所以会出现上面的情况。

正确的定义方式是使用字符串构造函数:new BigDecimal("10.35").setScale(1, RoundingMode.HALF_EVEN),这样得到的是10.4;也就是说一定要用BigDecimal(String)构造器,而千万不要用BigDecimal(double),通过正确使用BigDecimal,程序就可以打印出我们所期望的结果。

通过测试发现,当使用 double 或者 float 这些浮点数据类型时,会丢失精度,String、int 则不会,这是为什么呢?

我们点开构造器方法看下源码:

  1. public static long doubleToLongBits(double value) {
  2. long result = doubleToRawLongBits(value);
  3. // Check for NaN based on values of bit fields, maximum
  4. // exponent and nonzero significand.
  5. if ( ((result & DoubleConsts.EXP_BIT_MASK) ==
  6. DoubleConsts.EXP_BIT_MASK) &&
  7. (result & DoubleConsts.SIGNIF_BIT_MASK) != 0L)
  8. result = 0x7ff8000000000000L;
  9. return result;
  10. }

问题就处在 doubleToRawLongBits 这个方法上,在 jdk 中 double 类(float 与 int 对应)中提供了 double 与 long 转换,doubleToRawLongBits 就是将 double 转换为 long,这个方法是原始方法(底层不是 java 实现,是 c++ 实现的)。

double 之所以会出问题,是因为小数点转二进制丢失精度。

BigDecimal精度问题 - 图6

BigDecimal 在处理的时候把十进制小数扩大 N 倍让它在整数上进行计算,并保留相应的精度信息。

float 和 double 类型,主要是为了科学计算和工程计算而设计的,之所以执行二进制浮点运算,是为了在广泛的数值范围上提供较为精确的快速近和计算。

并没有提供完全精确的结果,所以不应该被用于精确的结果的场合。

当浮点数达到一定大的数,就会自动使用科学计数法,这样的表示只是近似真实数而不等于真实数。

当十进制小数位转换二进制的时候也会出现无限循环或者超过浮点数尾数的长度。

BigDecimal标度Scale

BigDecimal 一共有以下 4 个构造方法:

  • BigDecimal(int)
  • BigDecimal(double)
  • BigDecimal(long)
  • BigDecimal(String)

其中最容易理解的就是 BigDecimal(int)BigDecimal(long),因为是整数,所以标度就是 0 (源码如下👇):

  1. /**
  2. * Translates an {@code int} into a {@code BigDecimal}. The
  3. * scale of the {@code BigDecimal} is zero.
  4. *
  5. * @param val {@code int} value to be converted to
  6. * {@code BigDecimal}.
  7. * @since 1.5
  8. */
  9. public BigDecimal(int val) {
  10. this.intCompact = val;
  11. this.scale = 0;
  12. this.intVal = null;
  13. }
  14. /**
  15. * Translates a {@code long} into a {@code BigDecimal}. The
  16. * scale of the {@code BigDecimal} is zero.
  17. *
  18. * @param val {@code long} value to be converted to {@code BigDecimal}.
  19. * @since 1.5
  20. */
  21. public BigDecimal(long val) {
  22. this.intCompact = val;
  23. this.intVal = (val == INFLATED) ? INFLATED_BIGINT : null;
  24. this.scale = 0;
  25. }

而对于 BigDecimal (double) 来说,当我们使用 new BigDecimal (0.1) 创建一个对象的时候,其实创建出来的对象的值并不是等于0.1,而是等于0.1000000000000000055511151231257827021181583404541015625

BigDecimal精度问题 - 图7

我们再打个断点,debug一下看看标度值是多少

BigDecimal精度问题 - 图8

我们可以看到标度值是55,这个值是怎么来的呢?其实很简单,这个标度值就是这个数字的位数,其他的浮点数也同样的道理。对于 new BigDecimal (1.0),和new BigDecimal (1.00) 这样的形式来说,因为他本质上也是个整数,所以他创建出来的数字的标度就是0。

最后我们再看看 BigDecimal(String) ,当我们使用 new BigDecimal ("0.1") 创建一个 BigDecimal 的时候,其实创建出来的值正好就是等于 0.1 的。那么他的标度也就是 1;如果使用 new BigDecimal ("0.10000"),那么创建出来的数就是 0.10000,标度也就是 5。

讲到这里相信各位小伙伴也明白了为什么 bigDecimal5 和 bigDecimal6 用equals 方法做等值比较的结果是false了 O(∩_∩)O

BigDecimal的等值比较

如果我们只想判断两个 BigDecimal 的值是否相等,那么该如何判断呢?

在 BigDecimal 中也为我们提供了一个方法 —— compareTo方法,这个方法就可以只比较两个数字的值,如果两个数相等,则返回 0。

BigDecimal精度问题 - 图9

我们把 equals 换成 compareTo 后可以发现,bigDecimal5 和 bigDecimal6 等值比较的结果是0,也就是说明这二者的值是相等的。

P.S. 所以我们在做等值比较的时候不要随便用 BigDecimal 的 equals 方法,如果只是要对数值作比较,就果断选择 compareTo 方法就搞定

总结

所以,在涉及到精度计算的过程中,我们尽量使用 String 类型来进行转换。

正确用法如下:

  1. BigDecimal bigDecimal2=new BigDecimal("8.8");
  2. BigDecimal bigDecimal3=new BigDecimal("8.812");
  3. System.out.println( bigDecimal2.compareTo(bigDecimal3));
  4. System.out.println( bigDecimal2.add(bigDecimal3));

BigDecimal 创建出来的是对象,我们不能用传统的加减乘除对其进行运算,必须使用他的方法,在我们数据库存储里,如果我们使用的是 double 或者 float 类型,需要进行来回的转换后进行计算,非常不方便。

工具分享

所以在这里整理出一个 util 类供大家使用:

  1. import java.math.BigDecimal;
  2. /**
  3. * @Author shuaige
  4. * @Date 2022/4/17
  5. * @Version 1.0
  6. **/
  7. public class BigDecimalUtils {
  8. public static BigDecimal doubleAdd(double v1, double v2) {
  9. BigDecimal b1 = new BigDecimal(Double.toString(v1));
  10. BigDecimal b2 = new BigDecimal(Double.toString(v2));
  11. return b1.add(b2);
  12. }
  13. public static BigDecimal floatAdd(float v1, float v2) {
  14. BigDecimal b1 = new BigDecimal(Float.toString(v1));
  15. BigDecimal b2 = new BigDecimal(Float.toString(v2));
  16. return b1.add(b2);
  17. }
  18. public static BigDecimal doubleSub(double v1, double v2) {
  19. BigDecimal b1 = new BigDecimal(Double.toString(v1));
  20. BigDecimal b2 = new BigDecimal(Double.toString(v2));
  21. return b1.subtract(b2);
  22. }
  23. public static BigDecimal floatSub(float v1, float v2) {
  24. BigDecimal b1 = new BigDecimal(Float.toString(v1));
  25. BigDecimal b2 = new BigDecimal(Float.toString(v2));
  26. return b1.subtract(b2);
  27. }
  28. public static BigDecimal doubleMul(double v1, double v2) {
  29. BigDecimal b1 = new BigDecimal(Double.toString(v1));
  30. BigDecimal b2 = new BigDecimal(Double.toString(v2));
  31. return b1.multiply(b2);
  32. }
  33. public static BigDecimal floatMul(float v1, float v2) {
  34. BigDecimal b1 = new BigDecimal(Float.toString(v1));
  35. BigDecimal b2 = new BigDecimal(Float.toString(v2));
  36. return b1.multiply(b2);
  37. }
  38. public static BigDecimal doubleDiv(double v1, double v2) {
  39. BigDecimal b1 = new BigDecimal(Double.toString(v1));
  40. BigDecimal b2 = new BigDecimal(Double.toString(v2));
  41. // 保留小数点后两位 ROUND_HALF_UP = 四舍五入
  42. return b1.divide(b2, 2, BigDecimal.ROUND_HALF_UP);
  43. }
  44. public static BigDecimal floatDiv(float v1, float v2) {
  45. BigDecimal b1 = new BigDecimal(Float.toString(v1));
  46. BigDecimal b2 = new BigDecimal(Float.toString(v2));
  47. // 保留小数点后两位 ROUND_HALF_UP = 四舍五入
  48. return b1.divide(b2, 2, BigDecimal.ROUND_HALF_UP);
  49. }
  50. /**
  51. * 比较v1 v2大小
  52. * @param v1
  53. * @param v2
  54. * @return v1>v2 return 1 v1=v2 return 0 v1<v2 return -1
  55. */
  56. public static int doubleCompareTo(double v1, double v2) {
  57. BigDecimal b1 = new BigDecimal(Double.toString(v1));
  58. BigDecimal b2 = new BigDecimal(Double.toString(v2));
  59. return b1.compareTo(b2);
  60. }
  61. public static int floatCompareTo(float v1, float v2) {
  62. BigDecimal b1 = new BigDecimal(Float.toString(v1));
  63. BigDecimal b2 = new BigDecimal(Float.toString(v2));
  64. return b1.compareTo(b2);
  65. }
  66. }