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

一直有人告诉我,永远不要double floatfloat型表示货币,这一次我向您提出一个问题:为什么?

我敢肯定有一个很好的理由,我根本不知道这是什么。

答案

因为浮点数和双精度数不能准确表示我们用于赚钱的基数 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。

首先,将钱表示为double floatfloat可能会很好,因为该软件会消除微小的误差,但是当您对不精确的数字执行更多的加法,减法,乘法和除法运算时,误差将加重,最终您会得到显然不准确。这使得浮子和翻倍数不足以处理金钱,在这种情况下,要求十进制乘数的完美精度。

适用于几乎所有语言的解决方案是改用整数,然后计算分。例如,1025 为 $ 10.25。几种语言还具有内置类型来处理金钱。其中,Java 具有BigDecimal类,而 C#具有decimal类型。

摘自 Bloch,J.,《有效 Java》,第二版,第 48 项:

floatdouble类型特别不适用于货币计算,因为不可能精确地将 0.1(或其他任何负数的 10 的幂)表示为floatdouble

例如,假设您有 $ 1.03 并且花费了 42c。你还剩下多少钱?

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

打印出0.6100000000000001

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

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

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

即使您在输出前的最后一分钟对结果进行四舍五入,您仍然偶尔会使用不符合预期的双精度来获得结果。

使用计算器或手动计算结果,则精确地为 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 为基数进行内部计算而不是使用双精度数,那么假设代码中没有其他错误,答案总是完全是人类期望的结果。

System.out.println(1000000.0f + 1.2f - 1000000.0f);
1.1875
groovy:000> new BigDecimal(1.0F)
===> 1
groovy:000> new BigDecimal(1.01F)
===> 1.0099999904632568359375

我冒着被低估的风险,但是我认为浮点数不适合用于货币计算被高估了。只要您确保正确进行分位数舍入并且有足够的有效数字来处理 zneak 解释的二进制小数表示不匹配,就不会有问题。

在 Excel 中使用货币进行计算的人们一直使用双精度浮点数(Excel 中没有货币类型),而且我还没有看到有人抱怨舍入错误。

当然,您必须保持理性。例如,一个简单的网上商店可能永远不会遇到双精度浮点数的任何问题,但是如果您进行会计或其他任何需要添加大量(不受限制的)数字的操作,则您不希望碰到十英尺的浮点数极。

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;
    }
}
// floating point calculation
    final double amount1 = 2.0;
    final double amount2 = 1.1;
    System.out.println("difference between 2.0 and 1.1 using double is: " + (amount1 - amount2));

    // Use BigDecimal for financial calculation
    final BigDecimal amount3 = new BigDecimal("2.0");
    final BigDecimal amount4 = new BigDecimal("1.1");
    System.out.println("difference between 2.0 and 1.1 using BigDecimal is: " + (amount3.subtract(amount4)));
difference between 2.0 and 1.1 using double is: 0.8999999999999999
difference between 2.0 and 1.1 using BigDecimal is: 0.9
MonetaryAmountFactory<?> amountFactory = Monetary.getDefaultAmountFactory();
MonetaryAmount monetaryAmount = amountFactory.setCurrency(Monetary.getCurrency("EUR")).setNumber(12345.67).create();
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));
MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));
MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmount otherMonetaryAmount = monetaryAmount.divide(2).add(Money.of(5, "EUR"));
// getting CurrencyUnits by locale
CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN);
CurrencyUnit canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA);
MonetaryAmount monetaryAmount = Money.of(123.45, euro);
CurrencyUnit currency = monetaryAmount.getCurrency();
NumberValue numberValue = monetaryAmount.getNumber();

int intValue = numberValue.intValue(); // 123
double doubleValue = numberValue.doubleValue(); // 123.45
long fractionDenominator = numberValue.getAmountFractionDenominator(); // 100
long fractionNumerator = numberValue.getAmountFractionNumerator(); // 45
int precision = numberValue.getPrecision(); // 5

// NumberValue extends java.lang.Number. 
// So we assign numberValue to a variable of type Number
Number number = numberValue;
CurrencyUnit usd = MonetaryCurrencies.getCurrency("USD");
MonetaryAmount dollars = Money.of(12.34567, usd);
MonetaryOperator roundingOperator = MonetaryRoundings.getRounding(usd);
MonetaryAmount roundedDollars = dollars.with(roundingOperator); // USD 12.35
List<MonetaryAmount> amounts = new ArrayList<>();
amounts.add(Money.of(2, "EUR"));
amounts.add(Money.of(42, "USD"));
amounts.add(Money.of(7, "USD"));
amounts.add(Money.of(13.37, "JPY"));
amounts.add(Money.of(18, "USD"));
// A monetary operator that returns 10% of the input MonetaryAmount
// Implemented using Java 8 Lambdas
MonetaryOperator tenPercentOperator = (MonetaryAmount amount) -> {
  BigDecimal baseAmount = amount.getNumber().numberValue(BigDecimal.class);
  BigDecimal tenPercent = baseAmount.multiply(new BigDecimal("0.1"));
  return Money.of(tenPercent, amount.getCurrency());
};

MonetaryAmount dollars = Money.of(12.34567, "USD");

// apply tenPercentOperator to MonetaryAmount
MonetaryAmount tenPercentDollars = dollars.with(tenPercentOperator); // USD 1.234567

如果您的计算涉及多个步骤,那么任意精度的算术都不会 100%覆盖您。

使用结果完美表示的唯一可靠方法(使用自定义的 Fraction 数据类型,该数据类型将对最后一步进行除法运算)并且仅在最后一步中转换为十进制表示法。

任意精度将无济于事,因为总会有小数位那么多的数字,或者诸如 0.6666666 之类的结果。因此,您在每个步骤中都会有一些小错误。

此错误将加总,最终可能变得不容易被忽略。这称为错误传播