1. 概述
BigDecimal
是为了处理浮点数设计的。它提供了一种方便的方式来管理精度,并且特别解决了浮点数计算中的舍入误差问题。
然而,在某些情况下,我们需要将它作为简单的整数来使用,并将其转换为 Integer
或 int
。在这个教程中,我们将学习如何正确地进行这种转换,并理解其中的一些潜在问题。
2. 窄化转换
BigDecimal
可以存储的数值范围远大于 Integer
或 int
。这通常会导致在转换过程中丢失精度。
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
转换为 int
或 long
。因此,可能会丢失精度。尽管如此,对于某些应用来说,可能可以接受精度损失。但我们始终应该考虑到这一点。
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上找到。