我总是被告知永远不要用双类型或浮点类型来表示货币,这一次我向你提出一个问题:为什么?

我相信有一个很好的理由,我只是不知道是什么。


当前回答

这不是精确与否的问题,也不是精确与否的问题。这是一个满足以10为底而不是以2为底计算的人的期望的问题。例如,在财务计算中使用双精度值不会产生数学意义上的“错误”答案,但它可以产生财务意义上不期望的答案。

即使您在输出前的最后一分钟舍入结果,您仍然可以偶尔使用与期望不匹配的双精度结果。

Using a calculator, or calculating results by hand, 1.40 * 165 = 231 exactly. However, internally using doubles, on my compiler / operating system environment, it is stored as a binary number close to 230.99999... so if you truncate the number, you get 230 instead of 231. You may reason that rounding instead of truncating would have given the desired result of 231. That is true, but rounding always involves truncation. Whatever rounding technique you use, there are still boundary conditions like this one that will round down when you expect it to round up. They are rare enough that they often will not be found through casual testing or observation. You may have to write some code to search for examples that illustrate outcomes that do not behave as expected.

Assume you want to round something to the nearest penny. So you take your final result, multiply by 100, add 0.5, truncate, then divide the result by 100 to get back to pennies. If the internal number you stored was 3.46499999.... instead of 3.465, you are going to get 3.46 instead 3.47 when you round the number to the nearest penny. But your base 10 calculations may have indicated that the answer should be 3.465 exactly, which clearly should round up to 3.47, not down to 3.46. These kinds of things happen occasionally in real life when you use doubles for financial calculations. It is rare, so it often goes unnoticed as an issue, but it happens.

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

其他回答

如果你的计算涉及到不同的步骤,任意的精度算法都不能100%覆盖你。

使用完美的结果表示(使用自定义Fraction数据类型,将除法操作批处理到最后一步)并且仅在最后一步转换为十进制的唯一可靠方法。

任意精度不会有帮助,因为总有可能有很多小数点后的数字,或者一些结果,如0.6666666……最后一个例子没有任意的表示法。所以每一步都会有小误差。

这些错误会累积起来,最终可能变得不再容易被忽视。这被称为错误传播。

摘自Bloch, J., Effective Java,(第二版,第48项。第3版,项目60):

float和double类型是 尤其不适用于货币 因为这是不可能的 表示0.1(或任何其他。 10的负次方)作为浮点数或 完全的两倍。 例如,假设您有1.03美元 你花了42c。多少钱? 你走了? System.out.println(1.03 - .42); 输出0.6100000000000001。 解决这个问题的正确方法是 使用BigDecimal, int或long 用于货币计算。

虽然BigDecimal有一些警告(请参阅当前接受的答案)。

虽然浮点类型确实只能表示近似的十进制数据,但如果在表示数字之前将数字舍入到必要的精度,则可以获得正确的结果。通常。

通常是因为双排精度小于16位。如果你要求更高的精度,这不是一个合适的类型。近似也可以累积。

必须指出的是,即使您使用定点算术,您仍然必须对数字进行四舍五入,如果不是因为BigInteger和BigDecimal在获得周期性小数时会给出错误。所以这里也有一个近似。

例如,历史上用于财务计算的COBOL的最大精度为18位数字。所以通常会有一个隐含的舍入。

总之,在我看来,双精度主要不适合它的16位精度,这可能是不够的,而不是因为它是近似值。

考虑以下后续程序的输出。它表明,在舍入double后,得到与BigDecimal相同的结果,精度为16。

Precision 14
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611
Double                        : 56789.012345 / 1111111111 = 0.000051110111115611

Precision 15
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110
Double                        : 56789.012345 / 1111111111 = 0.0000511101111156110

Precision 16
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101
Double                        : 56789.012345 / 1111111111 = 0.00005111011111561101

Precision 17
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611011
Double                        : 56789.012345 / 1111111111 = 0.000051110111115611013

Precision 18
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110111
Double                        : 56789.012345 / 1111111111 = 0.0000511101111156110125

Precision 19
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101111
Double                        : 56789.012345 / 1111111111 = 0.00005111011111561101252

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.MathContext;

public class Exercise {
    public static void main(String[] args) throws IllegalArgumentException,
            SecurityException, IllegalAccessException,
            InvocationTargetException, NoSuchMethodException {
        String amount = "56789.012345";
        String quantity = "1111111111";
        int [] precisions = new int [] {14, 15, 16, 17, 18, 19};
        for (int i = 0; i < precisions.length; i++) {
            int precision = precisions[i];
            System.out.println(String.format("Precision %d", precision));
            System.out.println("------------------------------------------------------");
            execute("BigDecimalNoRound", amount, quantity, precision);
            execute("DoubleNoRound", amount, quantity, precision);
            execute("BigDecimal", amount, quantity, precision);
            execute("Double", amount, quantity, precision);
            System.out.println();
        }
    }

    private static void execute(String test, String amount, String quantity,
            int precision) throws IllegalArgumentException, SecurityException,
            IllegalAccessException, InvocationTargetException,
            NoSuchMethodException {
        Method impl = Exercise.class.getMethod("divideUsing" + test, String.class,
                String.class, int.class);
        String price;
        try {
            price = (String) impl.invoke(null, amount, quantity, precision);
        } catch (InvocationTargetException e) {
            price = e.getTargetException().getMessage();
        }
        System.out.println(String.format("%-30s: %s / %s = %s", test, amount,
                quantity, price));
    }

    public static String divideUsingDoubleNoRound(String amount,
            String quantity, int precision) {
        // acceptance
        double amount0 = Double.parseDouble(amount);
        double quantity0 = Double.parseDouble(quantity);

        //calculation
        double price0 = amount0 / quantity0;

        // presentation
        String price = Double.toString(price0);
        return price;
    }

    public static String divideUsingDouble(String amount, String quantity,
            int precision) {
        // acceptance
        double amount0 = Double.parseDouble(amount);
        double quantity0 = Double.parseDouble(quantity);

        //calculation
        double price0 = amount0 / quantity0;

        // presentation
        MathContext precision0 = new MathContext(precision);
        String price = new BigDecimal(price0, precision0)
                .toString();
        return price;
    }

    public static String divideUsingBigDecimal(String amount, String quantity,
            int precision) {
        // acceptance
        BigDecimal amount0 = new BigDecimal(amount);
        BigDecimal quantity0 = new BigDecimal(quantity);
        MathContext precision0 = new MathContext(precision);

        //calculation
        BigDecimal price0 = amount0.divide(quantity0, precision0);

        // presentation
        String price = price0.toString();
        return price;
    }

    public static String divideUsingBigDecimalNoRound(String amount, String quantity,
            int precision) {
        // acceptance
        BigDecimal amount0 = new BigDecimal(amount);
        BigDecimal quantity0 = new BigDecimal(quantity);

        //calculation
        BigDecimal price0 = amount0.divide(quantity0);

        // presentation
        String price = price0.toString();
        return price;
    }
}

美国货币可以很容易地用美元和美分来表示。整数是100%精确的,而浮点二进制数并不完全匹配浮点小数。

这个问题的许多答案都讨论了IEEE和围绕浮点算法的标准。

我的背景不是计算机科学(物理和工程),我倾向于从不同的角度看问题。对我来说,我在数学计算中不使用double或float的原因是我会丢失太多的信息。

有什么替代方案?有很多(还有很多我不知道的!)

Java中的BigDecimal原产于Java语言。 Apfloat是另一个用于Java的任意精度库。

c#中的十进制数据类型是微软的. net中28位有效数字的替代方案。

SciPy (Scientific Python)可能还可以处理财务计算(我还没有尝试过,但我怀疑是这样)。

GNU多精度库(GMP)和GNU MFPR库是C和c++的两个免费的开源资源。

还有用于JavaScript(!)和PHP的精确数值库,我认为它们可以处理财务计算。

对于许多计算机语言,也有专有的(特别是Fortran)和开源的解决方案。

我不是训练出来的计算机科学家。然而,我倾向于在Java中使用BigDecimal,在c#中使用decimal。我还没有尝试过我列出的其他解决方案,但它们可能也非常好。

对我来说,我喜欢BigDecimal是因为它支持的方法。c#的十进制非常好,但我还没有机会尽可能多地使用它。我在业余时间做我感兴趣的科学计算,BigDecimal似乎工作得很好,因为我可以设置浮点数的精度。BigDecimal的缺点是什么?它有时会很慢,特别是当你使用除法的时候。

为了提高速度,您可以查看C、c++和Fortran中的免费和专有库。