1. 概述

BigDecimal 是为了处理大浮点数而设计的。它解决了浮点数算术中的精度问题,并提供了控制精度的方法。此外,它还提供了一系列对数字进行操作的传统方法。

我们可以利用 BigDecimal 的特性,通过将 Integer 转换为 BigDecimal。在这篇教程中,我们将学习几种转换方式,并讨论它们的优缺点。

2. 构造函数转换

最直接的使用方法是构造函数转换。BigDecimal 提供了可以从多种输入进行转换的构造函数,因此我们可以将给定的 Integer 传递给 BigDecimal 构造函数:

@ParameterizedTest
@ArgumentsSource(BigDecimalConversionArgumentsProvider.class)
void giveIntegerWhenConvertWithConstructorToBigDecimalThenConversionCorrect(Integer given, BigDecimal expected) {
    BigDecimal actual = new BigDecimal(given);
    assertThat(actual).isEqualTo(expected);
}

然而,这种做法会迫使 BigDecimal 每次都创建一个新的对象:

@ParameterizedTest
@ValueSource(ints = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
void giveIntegerWhenConvertWithConstructorToBigDecimalThenConversionWithoutCaching(Integer given) {
    BigDecimal firstBigDecimal = new BigDecimal(given);
    BigDecimal secondBigDecimal = new BigDecimal(given);
    assertThat(firstBigDecimal)
      .isEqualTo(secondBigDecimal)
      .isNotSameAs(secondBigDecimal);
}

3. 静态工厂转换

另一种方法是使用静态工厂,类似于前面的例子:

@ParameterizedTest
@ArgumentsSource(BigDecimalConversionArgumentsProvider.class)
void giveIntegerWhenConvertWithValueOfToBigDecimalThenConversionCorrect(Integer given, BigDecimal expected) {
    BigDecimal actual = BigDecimal.valueOf(given);
    assertThat(actual).isEqualTo(expected);
}

它的优点是可以缓存值,与构造函数转换不同。因此,我们可以在不同的上下文中重用同一个对象。由于 BigDecimal 是不可变的,这不会带来任何问题。

4. 缓存

BigIntegers.valueOf() 工厂会缓存从 0 到 10 的值。所有这些值都在 BigDecimal 类的静态 ZERO_THROUGH_TEN 数组中定义:

private static final BigDecimal[] ZERO_THROUGH_TEN = {
  new BigDecimal(BigInteger.ZERO,       0,  0, 1),
  new BigDecimal(BigInteger.ONE,        1,  0, 1),
  new BigDecimal(BigInteger.TWO,        2,  0, 1),
  new BigDecimal(BigInteger.valueOf(3), 3,  0, 1),
  new BigDecimal(BigInteger.valueOf(4), 4,  0, 1),
  new BigDecimal(BigInteger.valueOf(5), 5,  0, 1),
  new BigDecimal(BigInteger.valueOf(6), 6,  0, 1),
  new BigDecimal(BigInteger.valueOf(7), 7,  0, 1),
  new BigDecimal(BigInteger.valueOf(8), 8,  0, 1),
  new BigDecimal(BigInteger.valueOf(9), 9,  0, 1),
  new BigDecimal(BigInteger.TEN,        10, 0, 2),
};

valueOf(long) 工厂在内部使用这个数组:

public static BigDecimal valueOf(long val) {
    if (val >= 0 && val < ZERO_THROUGH_TEN.length)
        return ZERO_THROUGH_TEN[(int)val];
    else if (val != INFLATED)
        return new BigDecimal(null, val, 0, 0);
    return new BigDecimal(INFLATED_BIGINT, val, 0, 0);
}

我们可以看到,对于某些值,BigDecimal 对象是相同的:

@ParameterizedTest
@ValueSource(ints = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
void giveIntegerWhenConvertWithValueOfToBigDecimalThenConversionCachesTheResults(Integer given) {
    BigDecimal firstBigDecimal = BigDecimal.valueOf(given);
    BigDecimal secondBigDecimal = BigDecimal.valueOf(given);
    assertThat(firstBigDecimal).isSameAs(secondBigDecimal);
}

如果我们的应用中频繁使用 0 到 10 之间的 BigDecimal 值,这可能会提高性能。同时,由于 BigDecimal 对象是不可变的,我们还可以为在应用中反复使用的数值实现自己的缓存。

然而,这个范围之外的数值将不会使用缓存:

@ParameterizedTest
@ValueSource(ints = {11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21})
void giveIntegerWhenConvertWithValueOfToBigDecimalThenConversionWontCacheTheResults(Integer given) {
    BigDecimal firstBigDecimal = BigDecimal.valueOf(given);
    BigDecimal secondBigDecimal = BigDecimal.valueOf(given);
    assertThat(firstBigDecimal)
      .isEqualTo(secondBigDecimal)
      .isNotSameAs(secondBigDecimal);
}

因此,在生产代码中依赖于引用相等(即对象是否指向同一个内存地址)是不推荐的。

5. 总结

当需要在浮点数上进行操作并避免舍入误差时,BigDecimal 是一个很好的选择。它还能处理无法用其他方式表示的大数值。BigDecimal 提供了多种从其他类型(如 Integer)转换的方法。

如往常一样,本教程的所有代码可在 GitHub 上找到。