我们可以使用double来存储货币字段,并使用BigDecimal进行算术运算吗?

6
我知道使用double / float存在问题,推荐使用BigDecimal代替double / float表示货币字段。但是double / float更有效和节省空间。那么我的问题是: 在Java类中使用double / float表示货币字段是可接受的,但在任何算术运算(即在任何算术之前将double / float转换为BigDecimal)和相等检查中使用BigDecimal进行处理是否可行?原因是为了节省一些空间。 我确实看到许多项目正在使用double / float表示货币字段。这样做有什么缺陷吗?谢谢。

除非你想让钱消失,否则不要在浮点数中存储货币值。 - Christoffer Hammarström
由于我只能接受一个答案,非常感谢你们所有人。 - lostinmoney
6个回答

8
不可以。
假设一个double变量足以存储两个值x和y,那么你将它们转换为安全的BigDecimal并相乘。结果是准确的,然而如果你将乘法结果存回到double中,很有可能会丢失精度。证明如下:
double x = 1234567891234.0;
double y = 1234567891234.0;
System.out.println(x);
System.out.println(y);

BigDecimal bigZ = new BigDecimal(x).multiply(new BigDecimal(y));
double z = bigZ.doubleValue();
System.out.println(bigZ);
System.out.println(z);

结果:

1.234567891234E12          //precise 'x'
1.234567891234E12          //precise 'y'
 1524157878065965654042756  //precise 'x * y'
1.5241578780659657E24      //loosing precision

xy是精确的,使用BigDecimal进行乘法也是精确的。但是,将结果转换回double后,我们会失去一些最不重要的数字。


double 允许您存储最多 16 个最重要的数字,long - 从 0 开始的 18 个数字。double 可能会在没有警告的情况下丢弃最不重要的数字,而 long 则可能会溢出。如果您真的关心准确性和可靠性,请始终使用 BigDecimal(除非您 真的 知道值的下限和上限范围)。毕竟,拥有快速还是正确的程序更好呢? - Tomasz Nurkiewicz
1
如果你真的在将一个美元金额乘以另一个美元金额,那么你面临的问题比你选择的数据类型更大... - ruakh
那么使用long的唯一目的是为了性能吗?我可以得出一个结论:始终使用BigDecimal来表示货币字段,如果有任何性能问题,请尝试使用long。毕竟,在高频交易中使用double是否可接受,因为double比BigDecimal快多次? - lostinmoney
@lostinmoney,大多数交易系统使用double、long或int来表示价格和数量(它们与货币并不完全相同)。 - Peter Lawrey
1
@lostinmoney,使用double可以节省内存、CPU和代码大小。而使用BigDecimal的好处是减少错误,并且具有更可预测的舍入行为。在我看来,哪种方法更好取决于你的项目。我通常使用double时会将结果四舍五入到所需精度。 - Peter Lawrey
显示剩余2条评论

6
我建议您在涉及货币的所有算术运算中仅使用BigDecimal。请确保始终使用BigDecimal的String构造函数。为什么?请在JUnit测试中尝试以下代码:
assertEquals(new BigDecimal("0.01").toString(), new BigDecimal(0.01).toString());

您会得到以下输出:
expected:<0.01[]> but was <0.01[000000000000000020816681711721685132943093776702880859375]>

事实是,你不能将精确的0.01存储为“double”类型。只有BigDecimal可以完全按你想要的方式存储所需的数字。

请记住,BigDecimal是不可变的。以下代码可以编译:

BigDecimal amount = new BigDecimal("123.45");
BigDecimal more = new BigDecimal("12.34");
amount.add(more);
System.out.println("Amount is now: " + amount);

但是生成的输出结果将会是:

现在的金额是:123.45

这是因为你需要将结果赋值给一个新的(或同样的)BigDecimal变量。
换句话说:
amount = amount.add(more)

3

可接受的做法取决于您的项目。在某些项目中,使用double和long可能是预期的,但在其他项目中,这被认为是不可接受的。作为double,您可以表示高达70,000,000,000,000.00美分(比美国国家债务还大)的值,而使用固定位数的long,您可以准确地表示90,000,000,000,000,000.00。

如果您必须处理通货膨胀率极高的货币(在任何情况下都是一个坏主意),但由于某种原因仍然需要考虑每一分钱,那么请使用BigDecimal。

如果您使用double或long或BigDecimal,则必须四舍五入结果。如何实现此目标因每种数据类型而异,而BigDecimal是最不容易出错的,因为您需要指定不同操作的舍入和精度。而对于double或long,您需要自行解决。


2

longdouble/float更好的选择。

你确定使用BigDecimal类型会成为真正的瓶颈吗?


由于这不是一个简单的货币应用程序,它不仅涉及到分钱的问题。可能会有一些价格/金额需要保留至少5个小数点。 - lostinmoney
+1。但是要澄清的是:如果您使用intlong,则必须使用某种常数因子,通常为100或1000,以支持小于一货币单位的表示。例如,您可以将$1表示为1000L,将1¢表示为10L。并且最好将此包装在自己的数据类型中。 - ruakh

0
如果 double 的唯一用途是存储小数值,那么在某些条件下可以:如果您可以保证您的值不超过 15 个小数位,则将值转换为 double(53 位精度),然后使用 15 位小数(或更少)将 double 转换回十进制,将会从 David Matula 在他的文章 In-and-out conversions 中所证明的定理的应用中获得原始值,即没有丢失。请注意,为了适用此结果,必须使用正确舍入进行转换。
请注意,double 可能不是最佳选择:货币值通常不是用浮点数表示,而是用几个小数位 (p) 的固定点表示,在这种情况下,将值转换为一个按 10^p 缩放的整数并存储此整数(正如其他人建议的)更好一些。

0

坑点在于浮点数/双精度浮点数无法存储所有值而不失去精度。即使您使用BigDecimal并在计算过程中保留精度,最终产品仍然以浮点数/双精度浮点数的形式存储。

根据我的经验,“正确”的解决方案是将货币价值存储为表示每美元千分之一的整数(例如Long)。这为大多数任务提供了足够的分辨率,例如利息累积,同时避免了使用浮点数/双精度浮点数的问题。作为额外的“奖励”,这需要与浮点数/双精度浮点数相同的存储量。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接