协慌网

登录 贡献 社区

为什么不使用 Double 或 Float 来表示货币?

我总是被告知永远不要doublefloat类型代表钱,这次我向你提出问题:为什么?

我确信有一个很好的理由,我根本不知道它是什么。

答案

因为浮点数和双打数不能准确地代表我们用于赚钱的基数 10 倍数。这个问题不仅适用于 Java,也适用于任何使用 base 2 浮点类型的编程语言。

在基数 10 中,您可以将 10.25 写为 1025 * 10 -2 (10 的幂的整数倍)。 IEEE-754 浮点数是不同的,但考虑它们的一种非常简单的方法是乘以 2 的幂。例如,你可能会看到 164 * 2 -4 (2 的幂的整数倍),也等于 10.25。这不是数字在记忆中的表现方式,但数学含义是相同的。

即使在基数 10 中,这种表示法也不能准确地表示最简单的分数。例如,你不能代表 1/3:十进制表示重复(0.3333 ...),因此没有有限整数可以乘以 10 的幂来得到 1/3。你可以选择一个 3 的长序列和一个小指数,如 333333333 * 10 -10 ,但它不准确:如果你乘以 3,你就不会得到 1。

但是,为了计算货币,至少对于货币价值在美元数量级范围内的国家来说,通常你所需要的就是能够存储 10 -2 的倍数,所以它并不重要 1/3 无法表示。

浮点数和双打数的问题在于, 绝大多数类似货币的数字没有精确表示为 2 的幂的整数倍。实际上,0 到 1 之间的唯一倍数为 0.01(这在交易时很重要)有钱,因为它们是整数分钱),可以完全表示为 IEEE-754 二进制浮点数,分别是 0,0.25,0.5,0.75 和 1. 所有其他的都是少量的。与 0.333333 示例类似,如果将浮点值设为 0.1 并将其乘以 10,则不会得到 1。

当软件完成微小的错误时,代表钱作为doublefloat可能看起来很好,但是当你对不精确的数字执行更多的加法,减法,乘法和除法时,错误会复合,你最终会得到值显然不准确。这使浮动和双打不足以处理金钱,其中需要基本 10 倍数的倍数的完美精确度。

几乎适用于任何语言的解决方案是使用整数,并计算美分。例如,1025 将是 10.25 美元。几种语言也有内置类型来处理钱。其中,Java 具有BigDecimal类,C#具有decimal类型。

来自 Bloch,J.,Effective Java,2nd ed,Item 48:

floatdouble类型特别不适合进行货币计算,因为不可能将 0.1(或任何其他 10 的负幂)表示为floatdouble

例如,假设您有 1.03 美元,而您花费 42c。你还剩多少钱?

System.out.println(1.03 - .42);

打印出0.6100000000000001

解决此问题的正确方法是使用BigDecimalintlong进行货币计算。

这不是准确性问题,也不是精确问题。这是一个满足使用基数 10 进行计算而不是基数 2 的人们的期望的问题。例如,使用双精度进行财务计算不会产生数学意义上的 “错误” 答案,但它可以产生答案。不是财务意义上的预期。

即使您在输出之前的最后一分钟完成结果,您仍然可以偶尔使用与预期不符的双打来获得结果。

使用计算器或手动计算结果,确切地说 1.40 * 165 = 231。但是,在我的编译器 / 操作系统环境中内部使用双打,它被存储为接近 230.99999 的二进制数... 所以如果你截断数字,你会得到 230 而不是 231。你可能会认为舍入而不是截断已经给出了 231 的预期结果。这是事实,但是舍入总是涉及截断。无论你使用什么样的舍入技术,仍然存在像这样的边界条件,当你期望它向上舍入时它会向下舍入。它们非常罕见,通常不会通过随意测试或观察发现它们。您可能必须编写一些代码来搜索说明结果不符合预期的结果的示例。

假设您要将某些内容舍入到最近的便士。因此,你得到你的最终结果,乘以 100,加 0.5,截断,然后将结果除以 100,以便回到便士。如果您存储的内部数字是 3.46499999 .... 而不是 3.465,那么当您将数字四舍五入到最近的便士时,您将得到 3.46 而不是 3.47。但你的基数 10 计算可能表明答案应该是 3.465,这显然应该是 3.47,而不是 3.46。当您使用双打进行财务计算时,这些事情偶尔会在现实生活中发生。这是罕见的,因此它经常被忽视作为一个问题,但它发生了。

如果您使用基数 10 进行内部计算而不是双精度数,那么答案总是完全符合人类的预期,假设您的代码中没有其他错误。