在添加之前将 double 转换为 BigDecimal 是否足以保留原始精度?
Is it sufficient to convert a double to a BigDecimal just before addition to retain original precision?
我们正在解决与数字精度相关的错误。我们的系统收集一些数字并吐出它们的总和。
问题是 系统不保留数字精度, 例如300.7 + 400.9 = 701.599...,而预期结果为 701.6。精度应该适应输入值,因此我们不能只将结果四舍五入到固定精度。
问题很明显,我们使用双精度值,加法从十进制值的二进制表示中累积误差。
数据路径如下:
- XML 文件,输入 xsd:decimal
- 解析为 java 原始双精度数。它的小数点后 15 位应该足够了,我们希望值总共不超过 10 位,5 位小数。
- 存入DB MySql 5.5, type double
- 通过 Hibernate 加载到 JPA 实体中,即仍然是原始的 double
- 这些值的总和
- 将总和打印到另一个 XML 文件
现在,我假设最佳解决方案是将所有内容都转换为十进制格式。毫不奇怪,存在采用最便宜的解决方案的压力。事实证明,在添加几个数字之前将双打转换为 BigDecimal 在以下示例中的情况 B 中有效:
import java.math.BigDecimal;
public class Arithmetic {
public static void main(String[] args) {
double a = 0.3;
double b = -0.2;
// A
System.out.println(a + b);//0.09999999999999998
// B
System.out.println(BigDecimal.valueOf(a).add(BigDecimal.valueOf(b)));//0.1
// C
System.out.println(new BigDecimal(a).add(new BigDecimal(b)));//0.099999999999999977795539507496869191527366638183593750
}
}
更多相关信息:
Why do we need to convert the double into a string, before we can convert it into a BigDecimal?
Unpredictability of the BigDecimal(double) constructor
我担心这样的解决方法会成为一颗定时炸弹。
首先,我不太确定这种算法是否适用于所有情况。
其次,仍然存在一些风险,将来有人可能会实施一些更改并将 B 更改为 C,因为这个陷阱远非显而易见,甚至单元测试也可能无法揭示错误。
我愿意接受第二点,但问题是:此解决方法是否会提供正确的结果?会不会有这样的情况
Double.valueOf("12345.12345").toString().equals("12345.12345")
是假的?鉴于 Double.toString,根据 javadoc,仅打印唯一表示基础双精度值所需的数字,因此再次解析时,它会给出相同的双精度值?对于我只需要添加数字并使用这种神奇的 Double.toString(Double d) 方法打印总和的用例来说,这还不够吗?需要明确的是,我确实更喜欢我认为 干净的解决方案 ,到处都使用 BigDecimal,但我有点缺乏推销它的理由,我的意思是理想情况下转换为的示例添加前的 BigDecimal 无法完成上述工作。
这取决于您使用的数据库。如果您使用 SQL 服务器,您可以使用 numeric(12, 8) 作为数据类型,其中 12 表示数值,8 表示精度。同样,对于我的 SQL DECIMAL(5,2) 你可以使用。
如果您使用上述数据类型,您不会丢失任何精度值。
Java 休眠 Class :
你可以定义
私人双纬;
数据库:
如果无法避免解析为原始 double
或存储为 double,则应尽早转换为 BigDecimal
。
double
不能准确表示小数。 double x = 7.3;
中的值永远不会正好是 7.3,而是非常非常接近它的值,从第 16 位左右可见差异(给出 50 位小数点左右)。不要被打印可能给出准确的“7.3”这一事实所误导,因为打印已经进行了某种舍入并且没有准确显示数字。
如果您对 double
个数字进行大量计算,微小的差异最终会累加起来,直到超出您的容忍度。因此,在需要小数部分的计算中使用双精度数确实是一颗定时炸弹。
[...] we expect values no longer than 10 digits total, 5 fraction digits.
我读到该断言意味着您处理的所有数字都是 0.00001
的精确倍数,没有任何其他数字。您可以使用
将双打转换为此类 BigDecimals
new BigDecimal.valueOf(Math.round(doubleVal * 100000), 5)
这将为您提供一个具有 5 个小数位的数字的精确表示,即最接近输入 doubleVal
的 5 个小数位。通过这种方式,您可以纠正 doubleVal
和您最初表示的十进制数之间的微小差异。
如果您只是使用 BigDecimal.valueOf(double val)
,您将遍历所使用的双精度值的字符串表示形式,这不能保证它就是您想要的。它取决于 Double class 内部的舍入过程,它试图用最合理的小数位数来表示 7.3 的双倍近似值(可能是 7.30000000000000123456789123456789125)。它恰好导致“7.3”(并且,开发人员的荣誉,经常匹配 "expected" 字符串)而不是“7.300000000000001”或“7.3000000000000012”,这两个对我来说似乎同样合理。
这就是为什么我建议不要依赖该四舍五入,而是通过将小数点移动 5 位,然后四舍五入到最接近的 long,并构建一个按比例缩小 5 位小数的 BigDecimal 来自己进行四舍五入。这保证您获得(最多)5 个小数位的精确值。
然后使用 BigDecimals 进行计算(如有必要,使用适当的 MathContext 进行舍入)。
当您最终必须将数字存储为双精度数时,请使用 BigDecimal.doubleValue()
。结果 double 将足够接近小数点,以至于上述转换肯定会给你与以前相同的 BigDecimal (除非你有非常大的数字,比如小数点前 10 位数字 - 你迷失了 double
无论如何)。
P.S。请务必仅在 小数 分数与您相关时才使用 BigDecimal
- 曾几何时,英国先令货币由 12 便士组成。将小数磅表示为 BigDecimal
会比使用双打更糟糕。
我们正在解决与数字精度相关的错误。我们的系统收集一些数字并吐出它们的总和。 问题是 系统不保留数字精度, 例如300.7 + 400.9 = 701.599...,而预期结果为 701.6。精度应该适应输入值,因此我们不能只将结果四舍五入到固定精度。
问题很明显,我们使用双精度值,加法从十进制值的二进制表示中累积误差。
数据路径如下:
- XML 文件,输入 xsd:decimal
- 解析为 java 原始双精度数。它的小数点后 15 位应该足够了,我们希望值总共不超过 10 位,5 位小数。
- 存入DB MySql 5.5, type double
- 通过 Hibernate 加载到 JPA 实体中,即仍然是原始的 double
- 这些值的总和
- 将总和打印到另一个 XML 文件
现在,我假设最佳解决方案是将所有内容都转换为十进制格式。毫不奇怪,存在采用最便宜的解决方案的压力。事实证明,在添加几个数字之前将双打转换为 BigDecimal 在以下示例中的情况 B 中有效:
import java.math.BigDecimal;
public class Arithmetic {
public static void main(String[] args) {
double a = 0.3;
double b = -0.2;
// A
System.out.println(a + b);//0.09999999999999998
// B
System.out.println(BigDecimal.valueOf(a).add(BigDecimal.valueOf(b)));//0.1
// C
System.out.println(new BigDecimal(a).add(new BigDecimal(b)));//0.099999999999999977795539507496869191527366638183593750
}
}
更多相关信息: Why do we need to convert the double into a string, before we can convert it into a BigDecimal? Unpredictability of the BigDecimal(double) constructor
我担心这样的解决方法会成为一颗定时炸弹。 首先,我不太确定这种算法是否适用于所有情况。 其次,仍然存在一些风险,将来有人可能会实施一些更改并将 B 更改为 C,因为这个陷阱远非显而易见,甚至单元测试也可能无法揭示错误。
我愿意接受第二点,但问题是:此解决方法是否会提供正确的结果?会不会有这样的情况
Double.valueOf("12345.12345").toString().equals("12345.12345")
是假的?鉴于 Double.toString,根据 javadoc,仅打印唯一表示基础双精度值所需的数字,因此再次解析时,它会给出相同的双精度值?对于我只需要添加数字并使用这种神奇的 Double.toString(Double d) 方法打印总和的用例来说,这还不够吗?需要明确的是,我确实更喜欢我认为 干净的解决方案 ,到处都使用 BigDecimal,但我有点缺乏推销它的理由,我的意思是理想情况下转换为的示例添加前的 BigDecimal 无法完成上述工作。
这取决于您使用的数据库。如果您使用 SQL 服务器,您可以使用 numeric(12, 8) 作为数据类型,其中 12 表示数值,8 表示精度。同样,对于我的 SQL DECIMAL(5,2) 你可以使用。
如果您使用上述数据类型,您不会丢失任何精度值。
Java 休眠 Class :
你可以定义 私人双纬;
数据库:
如果无法避免解析为原始 double
或存储为 double,则应尽早转换为 BigDecimal
。
double
不能准确表示小数。 double x = 7.3;
中的值永远不会正好是 7.3,而是非常非常接近它的值,从第 16 位左右可见差异(给出 50 位小数点左右)。不要被打印可能给出准确的“7.3”这一事实所误导,因为打印已经进行了某种舍入并且没有准确显示数字。
如果您对 double
个数字进行大量计算,微小的差异最终会累加起来,直到超出您的容忍度。因此,在需要小数部分的计算中使用双精度数确实是一颗定时炸弹。
[...] we expect values no longer than 10 digits total, 5 fraction digits.
我读到该断言意味着您处理的所有数字都是 0.00001
的精确倍数,没有任何其他数字。您可以使用
new BigDecimal.valueOf(Math.round(doubleVal * 100000), 5)
这将为您提供一个具有 5 个小数位的数字的精确表示,即最接近输入 doubleVal
的 5 个小数位。通过这种方式,您可以纠正 doubleVal
和您最初表示的十进制数之间的微小差异。
如果您只是使用 BigDecimal.valueOf(double val)
,您将遍历所使用的双精度值的字符串表示形式,这不能保证它就是您想要的。它取决于 Double class 内部的舍入过程,它试图用最合理的小数位数来表示 7.3 的双倍近似值(可能是 7.30000000000000123456789123456789125)。它恰好导致“7.3”(并且,开发人员的荣誉,经常匹配 "expected" 字符串)而不是“7.300000000000001”或“7.3000000000000012”,这两个对我来说似乎同样合理。
这就是为什么我建议不要依赖该四舍五入,而是通过将小数点移动 5 位,然后四舍五入到最接近的 long,并构建一个按比例缩小 5 位小数的 BigDecimal 来自己进行四舍五入。这保证您获得(最多)5 个小数位的精确值。
然后使用 BigDecimals 进行计算(如有必要,使用适当的 MathContext 进行舍入)。
当您最终必须将数字存储为双精度数时,请使用 BigDecimal.doubleValue()
。结果 double 将足够接近小数点,以至于上述转换肯定会给你与以前相同的 BigDecimal (除非你有非常大的数字,比如小数点前 10 位数字 - 你迷失了 double
无论如何)。
P.S。请务必仅在 小数 分数与您相关时才使用 BigDecimal
- 曾几何时,英国先令货币由 12 便士组成。将小数磅表示为 BigDecimal
会比使用双打更糟糕。