BigDecimal 对大于 7 或 0 小数的数字给出意想不到的结果

BigDecimal gives unexpected results for numbers larger than 7 or 0 decimal numbers

在尝试计算 2 个物体的体积比时,我注意到计算中有些奇怪,这里有一个示例,您可以自己 运行:

public class TestApplication {

  public static void main(String[] args) {
    BigDecimal first = BigDecimal.valueOf(21099000.0);
    BigDecimal second = BigDecimal.valueOf(13196000.0);

    System.out.println("First: " + first);
    System.out.println("Second: " + second);
    System.out.println("Division: " + first.divide(second, RoundingMode.HALF_UP).doubleValue());
  }
}

结果是:

First: 2.1099E+7
Second: 1.3196E+7
Division: 0.0

我有 3 种方法可以让它给我预期的结果

1.如果我将小数部分从 0 更改为 1(或任何非 0 数字):

First: 21099000.1
Second: 13196000.1
Division: 1.6

2。如果我预先划分数字(将它们设为 7 位数字而不是 8 位数字):

First: 2109900.0
Second: 1319600.0
Division: 1.6

3。如果我指定一个比例进行除法 (first.divide(second, 0, RoundingMode.HALF_UP):

First: 2.1099E+7
Second: 1.3196E+7
Division: 2.0

我认为 BigDecimal 是由一个整数支持的,而我使用的数字远低于 20 亿。谁能解释一下是什么让这 3 个案例与原始结果不同?

解释BigDecimal specification

A BigDecimal consists of an arbitrary precision integer unscaled value and a 32-bit integer scale.

The value of the number represented by the BigDecimal is therefore (unscaledValue × 10-scale).

此外,关于 divide method

Returns a BigDecimal whose value is (this / divisor), and whose scale is this.scale()

您可以使用 unscaledValuescale 方法验证数字的非标度值和标度:

var first = BigDecimal.valueOf(21099000.0);
// ==> 2.1099E+7

first.unscaledValue()
// ==> 21099

first.scale()
// ==> -3

事实证明,unscaledValue 为 21099,scale -3。如您所料,这个数字在数学上等于 21099*10^3 = 21099000,但是因为它的 scale 为 -3,这意味着 first.divide(second, RoundingMode) 的比例也将为 -3。

换句话说,divide()的结果必须四舍五入到1000的倍数。

除法的真实值约为 1.599。根据舍入方式RoundingMode.HALF_UP 必须向下舍入,为0.

要获得不同的行为,您必须将自定义 scale 值传递给 divide,或者更改 first 的比例。例如,您可以更改比例:

first = first.setScale(2)

或者您可以以保证设定比例的方式创建数字:

first = new BigDecimal("21099000"); // sets scale to 0

    or

first = new BigDecimal(21099000); // sets scale to 0

根据documentationdivide​(BigDecimal divisor, RoundingMode roundingMode) returns一个BigDecimal,其值为(this / divisor),其标度为this.scale()

为什么得到 21099000.1 / 13196000.1 的预期结果?

检查以下代码的结果:

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Main {
    public static void main(String[] args) {
        BigDecimal first = BigDecimal.valueOf(21099000.1);
        BigDecimal second = BigDecimal.valueOf(13196000.1);
        System.out.println("First: " + first + ", Scale: " + first.scale());
        System.out.println("Second: " + second + ", Scale: " + second.scale());

        // 21099000.0 / 13196000.0 = 1.5988936041
        System.out.println(BigDecimal.valueOf(1.5988936041).setScale(first.scale(), RoundingMode.HALF_UP));
    }
}

输出:

First: 21099000.1, Scale: 1
Second: 13196000.1, Scale: 1
1.6

如您所见,JVM 已将 first 的比例选择为 1,因此 divide(即 1.5988936041)的结果也设置为1 等于 1.6RoundingMode.HALF_UP.

为什么没有得到 21099000.0 / 13196000.0 的预期结果?

检查以下代码的结果:

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Main {
    public static void main(String[] args) {
        BigDecimal first = BigDecimal.valueOf(21099000.0);
        BigDecimal second = BigDecimal.valueOf(13196000.0);
        System.out.println("First: " + first + ", Scale: " + first.scale());
        System.out.println("Second: " + second + ", Scale: " + second.scale());

        // 21099000.0 / 13196000.0 = 1.5988936041
        System.out.println(BigDecimal.valueOf(1.5988936041).setScale(first.scale(), RoundingMode.HALF_UP));
    }
}

输出:

First: 2.1099E+7, Scale: -3
Second: 1.3196E+7, Scale: -3
0E+3

如您所见,JVM 已将 first 的比例选择为 -3,因此 divide(即 1.5988936041)的结果也设置为-3 等于 0(或 0E+3)和 RoundingMode.HALF_UP.

如何改变这种行为?

如文档中所述,划分的比例设置为this.scale(),这意味着如果将first的比例设置为1,则可以得到预期的结果。

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Main {
    public static void main(String[] args) {
        BigDecimal first = BigDecimal.valueOf(21099000.0).setScale(1);
        BigDecimal second = BigDecimal.valueOf(13196000.0);
        System.out.println("First: " + first + ", Scale: " + first.scale());
        System.out.println("Second: " + second + ", Scale: " + second.scale());
        System.out.println("Division: " + first.divide(second, RoundingMode.HALF_UP).doubleValue());
    }
}

输出:

First: 21099000.0, Scale: 1
Second: 1.3196E+7, Scale: -3
Division: 1.6

最常见的方式是什么?

最后一个例子效果很好,使用没有问题。但是,最常见的方法是使用 divide​(BigDecimal divisor, int scale, RoundingMode roundingMode).

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Main {
    public static void main(String[] args) {
        BigDecimal first = BigDecimal.valueOf(21099000.0);
        BigDecimal second = BigDecimal.valueOf(13196000.0);
        System.out.println("First: " + first + ", Scale: " + first.scale());
        System.out.println("Second: " + second + ", Scale: " + second.scale());
        System.out.println("Division: " + first.divide(second, 1, RoundingMode.HALF_UP).doubleValue());
    }
}

输出:

First: 2.1099E+7, Scale: -3
Second: 1.3196E+7, Scale: -3
Division: 1.6