为何最小有符号64位整数的BigDecimal转Double再转回会失准?
问题
预期可精确表示为Double的BigDecimal值能在两者间干净往返(即BigDecimal转Double再转回BigDecimal后与原值相等),但在openjdk 11.0.19 2023-04-18 LTS中,以下测试用例失败,报错信息为org.opentest4j.AssertionFailedError: expected: <0> but was: <192>。
测试代码:
@Test void checkThatMin64BitIntegerAsBigDecimalRoundTripsThroughDouble() { final var min64BitIntegerAsBigDecimal = new BigDecimal( "-9223372036854775808" ); final var min64BitIntegerAsDouble = min64BitIntegerAsBigDecimal.doubleValue(); final var min64BitIntegerAsBigDecimalAgain = BigDecimal.valueOf( min64BitIntegerAsDouble ); assertEquals( BigDecimal.ZERO, min64BitIntegerAsBigDecimal.subtract(min64BitIntegerAsBigDecimalAgain) ); }
-2^63(即-9223372036854775808)是-1乘以2的幂,指数足够小,可精确表示为double,为何无法实现干净往返?
原因分析
问题出在BigDecimal.valueOf(double)的实现逻辑上:
BigDecimal.doubleValue()确实能把原BigDecimal正确转换为代表-2^63的double值,这个值本身是可以被double精确存储的。- 但
BigDecimal.valueOf(double)在转换回BigDecimal时,会先调用Double.toString(double)把double值转为字符串,再用该字符串构造BigDecimal。而对于-2^63这个double值,Double.toString()返回的是"-9223372036854775616"——这是因为该方法会优先选择更短的十进制字符串来表示double值,而这个字符串对应的数值和原BigDecimal相差192,最终导致往返后的结果不一致。
解决办法
要实现这类整数型BigDecimal与double的精确往返,需要绕过依赖字符串转换的路径,改用基于整数类型的转换方式:
修改后的测试代码示例:
@Test void checkThatMin64BitIntegerAsBigDecimalRoundTripsThroughDouble() { final var min64BitIntegerAsBigDecimal = new BigDecimal("-9223372036854775808"); // 先转为精确的BigInteger,再转double,避免字符串转换的精度丢失 final var min64BitIntegerAsDouble = min64BitIntegerAsBigDecimal.toBigIntegerExact().doubleValue(); // 从double转回long,再构造BigDecimal final var min64BitIntegerAsBigDecimalAgain = new BigDecimal(BigInteger.valueOf((long) min64BitIntegerAsDouble)); assertEquals(BigDecimal.ZERO, min64BitIntegerAsBigDecimal.subtract(min64BitIntegerAsBigDecimalAgain)); }
另一种思路:如果只是需要判断两者是否相等,也可以直接使用BigDecimal.compareTo()方法,而非通过减法判断是否为零,但这只是规避了表象问题,上述基于整数的转换才是解决往返精度问题的根本方案。
内容的提问来源于stack exchange,提问作者Eponymous
相关产品推荐
相关产品推荐

