1. 概述

BigDecimal 是为了处理浮点数设计的。它提供了一种方便的方式来管理精度,并且特别解决了浮点数计算中的舍入误差问题。

然而,在某些情况下,我们需要将它作为简单的整数来使用,并将其转换为 Integerint。在这个教程中,我们将学习如何正确地进行这种转换,并理解其中的一些潜在问题。

2. 窄化转换

BigDecimal 可以存储的数值范围远大于 Integerint。这通常会导致在转换过程中丢失精度

2.1. 截断

BigDecimal 提供了 intValue() 方法,可以将其转换为 int

@ParameterizedTest
@ArgumentsSource(SmallBigDecimalConversionArgumentsProvider.class)
void givenSmallBigDecimalWhenConvertToIntegerThenWontLosePrecision(BigDecimal given, int expected) {
    int actual = given.intValue();
    assertThat(actual).isEqualTo(expected);
}

BigDecimal 可能包含小数值,但 int 不行。这就是为什么 intValue() 方法会截断小数点后的所有数字:

@ParameterizedTest
@ValueSource(doubles = {0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5})
void givenLargeBigDecimalWhenConvertToIntegerThenLosePrecision(double given) {
    BigDecimal decimal = BigDecimal.valueOf(given);
    int integerValue = decimal.intValue();
    double actual = Integer.valueOf(integerValue).doubleValue();
    assertThat(actual)
      .isEqualTo((int) given)
      .isNotEqualTo(given);
}

这个行为类似于将 double 转换为 intlong。因此,可能会丢失精度。尽管如此,对于某些应用来说,可能可以接受精度损失。但我们始终应该考虑到这一点。

2.2. 溢出

另一个问题是使用 intValue() 时可能出现的溢出。这与前面的问题类似,但结果完全不正确:

@ParameterizedTest
@ValueSource(longs = {Long.MAX_VALUE, Long.MAX_VALUE - 1, Long.MAX_VALUE - 2,
  Long.MAX_VALUE - 3, Long.MAX_VALUE - 4, Long.MAX_VALUE - 5,
  Long.MAX_VALUE - 6, Long.MAX_VALUE - 7, Long.MAX_VALUE - 8})
void givenLargeBigDecimalWhenConvertToIntegerThenLosePrecision(long expected) {
    BigDecimal decimal = BigDecimal.valueOf(expected);
    int actual = decimal.intValue();
    assertThat(actual)
      .isNotEqualTo(expected)
      .isEqualTo(expected - Long.MAX_VALUE - 1);
}

考虑到二进制数的表示,这种行为是合理的。我们不能存储超过 int 可容纳的比特信息。在某些场景下,这两种问题都会出现:

@ParameterizedTest
@ValueSource(doubles = {Long.MAX_VALUE - 0.5, Long.MAX_VALUE - 1.5, Long.MAX_VALUE - 2.5,
  Long.MAX_VALUE - 3.5, Long.MAX_VALUE - 4.5, Long.MAX_VALUE - 5.5,
  Long.MAX_VALUE - 6.5, Long.MAX_VALUE - 7.5, Long.MAX_VALUE - 8.5})
void givenLargeBigDecimalWhenConvertToIntegerThenLosePrecisionFromBothSides(double given) {
    BigDecimal decimal = BigDecimal.valueOf(given);
    int integerValue = decimal.intValue();
    double actual = Integer.valueOf(integerValue).doubleValue();
    assertThat(actual)
      .isNotEqualTo(Math.floor(given))
      .isNotEqualTo(given);
}

虽然 intValue() 在某些情况下可能有效,但我们需要一个更好的解决方案来避免意外的错误。

3. 精度损失

我们可以采取几种方法来解决我们讨论的问题。虽然无法完全避免精度损失,但可以使过程更显明确。

3.1. 检查小数位数

最直接的方法之一是检查 BigDecimal 的小数位数。我们可以识别出是否包含小数点,并假设其后有值。这种方法在大多数情况下适用。然而,它只是标识了小数点的存在,而不是判断其后是否有非零值:

@ParameterizedTest
@ValueSource(doubles = {
  0.0, 1.00, 2.000, 3.0000, 
  4.00000, 5.000000, 6.00000000, 
  7.000000000, 8.0000000000})
void givenLargeBigDecimalWhenCheckScaleThenItGreaterThanZero(double given) {
    BigDecimal decimal = BigDecimal.valueOf(given);
    assertThat(decimal.scale()).isPositive();
    assertThat(decimal.toBigInteger()).isEqualTo((int) given);
}

在这个例子中,数字 0.0 将有一个小数位数为 1。如果我们基于小数位数的值来定义转换行为,可能会遇到一些边缘情况。

3.2. 定义舍入策略

如果精度损失是可以接受的,我们可以将小数位数设为 0 并确定舍入策略。这比简单地调用 intValue() 有优势。我们将明确定义舍入行为:

@ParameterizedTest
@ValueSource(doubles = {0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5})
void givenLargeBigDecimalWhenConvertToIntegerWithRoundingUpThenLosePrecision(double given) {
    BigDecimal decimal = BigDecimal.valueOf(given);
    int integerValue = decimal.setScale(0, RoundingMode.CEILING).intValue();
    double actual = Integer.valueOf(integerValue).doubleValue();
    assertThat(actual)
      .isEqualTo(Math.ceil(given))
      .isNotEqualTo(given);
}

我们可以使用 RoundingMode 枚举来定义规则。它提供了预定义的策略,让我们对转换有更多的控制。

4. 溢出预防

溢出问题则有所不同。虽然精度损失可能对应用来说是可以接受的,但得到一个完全错误的数字永远不可接受。

4.1. 范围检查

我们可以检查 BigDecimal 值是否可以适当地放入 int 中。如果可以,我们使用 intValue() 转换。否则,我们可以使用默认值,例如最小或最大的 int

@ParameterizedTest
@ValueSource(longs = {
  Long.MAX_VALUE, Long.MAX_VALUE - 1, Long.MAX_VALUE - 2,
  Long.MIN_VALUE, Long.MIN_VALUE + 1, Long.MIN_VALUE + 2,
  0, 1, 2
})
void givenLargeBigDecimalWhenConvertToIntegerThenSetTheMaxOrMinValue(long expected) {
    BigDecimal decimal = BigDecimal.valueOf(expected);
    boolean tooBig = isTooBig(decimal);
    boolean tooSmall = isTooSmall(decimal);
    int actual;
    if (tooBig) {
        actual = Integer.MAX_VALUE;
    } else if (tooSmall) {
        actual = Integer.MIN_VALUE;
    } else {
        actual = decimal.intValue();
    }
    assertThat(tooBig).isEqualTo(actual == Integer.MAX_VALUE);
    assertThat(tooSmall).isEqualTo(actual == Integer.MIN_VALUE);
    assertThat(!tooBig && !tooSmall).isEqualTo(actual == expected);
}

如果无法确定合理的默认值,我们可以抛出异常。BigDecimal API 已经提供了类似的方法。

4.2. 精确值

BigDecimal 有一个更安全的 intValue() 版本——intValueExact()。这个方法在小数部分出现任何溢出时会抛出 ArithmeticException

@ParameterizedTest
@ValueSource(longs = {Long.MAX_VALUE, Long.MAX_VALUE - 1, Long.MAX_VALUE - 2,
  Long.MAX_VALUE - 3, Long.MAX_VALUE - 4, Long.MAX_VALUE - 5,
  Long.MAX_VALUE - 6, Long.MAX_VALUE - 7, Long.MAX_VALUE - 8})
void givenLargeBigDecimalWhenConvertToExactIntegerThenThrowException(long expected) {
    BigDecimal decimal = BigDecimal.valueOf(expected);
    assertThatExceptionOfType(ArithmeticException.class)
      .isThrownBy(decimal::intValueExact);
}

这样,我们可以确保我们的应用能够处理溢出,不会允许不正确的状态发生。

5. 总结

数值转换看似简单,但即使是简单的转换也可能在应用中引入难以调试的问题。因此,我们在进行窄化转换时应谨慎,并始终考虑精度损失和溢出。

BigDecimal 提供了各种便利的方法来简化转换,并让我们对过程有更多的控制。

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