首页 文章

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

提问于
浏览
800

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

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

14 回答

  • 3

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

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

    即使在基数10中,这种表示法也不能准确地表示最简单的分数 . 例如,你可以't represent 1/3: the decimal representation is repeating (0.3333...), so there is no finite integer that you can multiply by a power of 10 to get 1/3. You could settle on a long sequence of 3'和一个小指数,如333333333 * 10-10,但它不准确:如果你乘以3,你就不会得到1 .

    但是,为了计算货币,至少对于货币 Value 在美元数量级范围内的国家来说,通常你所需要的就是能够存储10-2的倍数,所以它不会被代表 .

    浮点数和双打数的问题在于,绝大多数类似于钱的数字都可以完全表示为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类型 .

  • 7

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

    float和double类型特别不适合进行货币计算,因为不可能将0.1(或任何其他10的负幂)表示为float或double . 例如,假设您有1.03美元,而您花费42c . 你还剩多少钱? System.out.println(1.03 - .42);
    打印出0.6100000000000001 . 解决此问题的正确方法是使用BigDecimal,int或long进行货币计算 .

  • 65

    这不是准确性问题,也不是精确问题 . 这是一个满足使用基数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进行内部计算而不是双精度数,那么答案总是完全符合人类的预期,假设您的代码中没有其他错误 .

  • 15

    我对其中的一些反应感到不安 . 我认为双打和花车在财务计算中占有一席之地 . 当然,在添加和减去非小数货币金额时,使用整数类或BigDecimal类时不会有精度损失 . 但是,当执行更复杂的操作时,无论您如何存储数字,通常都会得到几个或多个小数位的结果 . 问题是你如何呈现结果 .

    如果你的结果是在向上舍入和向下舍入之间的边界,并且最后一分钱真正重要,你可能应该告诉 Spectator 答案几乎在中间 - 通过显示更多小数位 .

    双精度问题,以及浮点数更多的问题是,当它们用于组合大数和小数时 . 在java中,

    System.out.println(1000000.0f + 1.2f - 1000000.0f);
    

    结果是

    1.1875
    
  • 35

    浮点数和双打数是近似值 . 如果你创建一个BigDecimal并将一个浮点数传递给构造函数,你会看到float实际上等于:

    groovy:000> new BigDecimal(1.0F)
    ===> 1
    groovy:000> new BigDecimal(1.01F)
    ===> 1.0099999904632568359375
    

    这可能不是你想要代表1.01美元的方式 .

    问题是IEEE规范没有办法准确地表示所有分数,其中一些最终成为重复分数,因此最终得出近似误差 . 会计师喜欢把事情准确地说出来一分钱,如果他们付账单就会对客户感到烦恼,而且付款处理完毕后他们欠他们.01他们会收取一定的费用或无法关闭账户,最好使用确切的类型,如十进制(在C#中)或Java中的java.math.BigDecimal .

    它是's not that the error isn'如果你是圆的可控制的:see this article by Peter Lawrey . 它需要大量的数学运算,操作包括添加东西或分配数量到不同的桶 . 引入浮点和舍入只会使事情复杂化 .

  • 826

    浮点数的结果不准确,这使得它们不适用于任何需要精确结果而非近似的财务计算 . float和double是为工程和科学计算而设计的,并且很多次都没有产生精确的结果,浮点计算的结果也可能因JVM到JVM而异 . 请看下面用于表示货币 Value 的BigDecimal和double原语的示例,很明显浮点计算可能不准确,并且应该使用BigDecimal进行财务计算 .

    // 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
    
  • 1

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

    通常因为double类型的精度小于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;
        }
    }
    
  • 2

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

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

    当然,你必须保持理智;例如一个简单的网上商店可能永远不会遇到任何双精度浮动的问题,但如果你这样做,例如会计或任何其他需要添加大量(不受限制的)数字的内容,您不希望触及10英尺极点的浮点数 .

  • 1

    正如之前所说的那样“首先将货币表示为双重或浮动可能看起来很好,因为软件可以完成微小的错误,但是当您对不精确的数字执行更多的加法,减法,乘法和除法时,您将失去越来越多的精度因为错误加起来 . 这使浮动和双打不足以处理金钱,其中需要基本10倍数倍的完美准确性 . “

    Finally Java has a standard way to work with Currency And Money!

    JSR 354: Money and Currency API

    JSR 354提供了一个API,用于使用货币和货币表示,传输和执行综合计算 . 您可以从以下链接下载:

    JSR 354: Money and Currency API Download

    The specification consists of the following things:

    用于处理e的API . G . 货币金额和货币API支持可互换的实现用于创建实现类实例的工厂用于计算,转换和格式化货币金额的函数用于处理货币和货币的Java API,计划包含在Java 9中 . 所有规范类和接口位于javax.money . *包中 .

    Sample Examples of JSR 354: Money and Currency API:

    创建MonetaryAmount并将其打印到控制台的示例如下所示::

    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));
    

    使用参考实现API时,必要的代码更简单:

    MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
    MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
    System.out.println(format.format(monetaryAmount));
    

    API还支持使用MonetaryAmounts进行计算:

    MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
    MonetaryAmount otherMonetaryAmount = monetaryAmount.divide(2).add(Money.of(5, "EUR"));
    

    CurrencyUnit和MonetaryAmount

    // getting CurrencyUnits by locale
    CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN);
    CurrencyUnit canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA);
    

    MonetaryAmount有多种方法可以访问指定的货币,数量,精度等等:

    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;
    

    可以使用舍入运算符舍入MonetaryAmounts:

    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
    

    使用MonetaryAmounts集合时,可以使用一些很好的过滤,排序和分组实用方法 .

    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"));
    

    自定义MonetaryAmount操作

    // 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
    

    资源:

    Handling money and currencies in Java with JSR 354

    Looking into the Java 9 Money and Currency API (JSR 354)

    另见:JSR 354 - Currency and Money

  • 1

    如果你的计算涉及各种步骤,任意精度算术都不会覆盖你100% .

    使用完美结果表示的唯一可靠方法(使用自动分数数据类型,将批处理操作分配到最后一步)并仅在最后一步中转换为十进制表示法 .

    任意精度都无济于事,因为总会有数字有这么多小数位,或者有些结果如0.6666666 ......任意表示都不会涵盖最后一个例子 . 因此,每一步都会出现小错误 .

    这种错误会加起来,最终可能不容易被忽视 . 这称为Error Propagation .

  • 16

    大多数答案都强调了为什么人们不应该使用双打进行货币和货币计算的原因 . 我完全赞同他们 .

    这并不意味着双打永远不会用于此目的 .

    我曾参与过许多gc要求非常低的项目,并且拥有BigDecimal对象是造成这种开销的重要因素 .

    缺乏对双重表征的理解以及缺乏处理准确性和精确度的经验,这带来了明智的建议 .

    如果您能够处理项目的精度和准确度要求,则可以使其工作,这必须基于正在处理的双值范围来完成 .

    您可以参考 Guava 的FuzzyCompare方法来获得更多想法 . 参数容差是关键 . 我们为证券交易应用程序处理了这个问题,我们对不同范围内不同数值的公差进行了详尽的研究 .

    此外,在某些情况下,您可能会想要使用Double包装器作为映射键,并将哈希映射作为实现 . 这是非常危险的,因为Double.equals和哈希代码例如值“0.5”和“0.6 - 0.1”将导致一个大混乱 .

  • 16

    我更喜欢使用Integer或Long来代表货币 . BigDecimal太多了源代码 .

    你只需要知道你所有的 Value 都是美分 . 或者您正在使用的任何货币的最低值 .

  • 276

    发布到这个问题的许多答案都讨论了IEEE和标准周围的浮点运算 .

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

    有哪些替代方案?有很多(其中许多我不知道!) .

    Java中的BigDecimal是Java语言的原生 . Apfloat是Java的另一个任意精度库 .

    C#中的十进制数据类型是Microsoft的.NET替代方案,可获得28位重要数据 .

    SciPy(科学Python)也可以处理财务计算(我没试过,但我怀疑如此) .

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

    还有JavaScript(!)的数值精度库,我认为PHP可以处理财务计算 .

    对于许多计算机语言,还有专有(特别是,我认为,对于Fortran)和开源解决方案 .

    我不是通过培训的计算机科学家 . 但是,我倾向于倾向于Java中的BigDecimal或C#中的decimal . 我没有尝试过我列出的其他解决方案,但它们也可能非常好 .

    对我来说,我喜欢BigDecimal,因为它支持的方法 . C#的小数非常好,但我没有机会像我想的那样使用它 . 我在业余时间对我感兴趣的科学计算,BigDecimal似乎工作得很好,因为我可以设置我的浮点数的精度 . BigDecimal的缺点是什么?它有时会很慢,特别是如果你使用除法 .

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

  • 42

    几个例子......这对几乎所有的编程语言都有效(实际上并没有按预期工作)...我已经尝试过使用Delphi,VBScript,Visual Basic,JavaScript和Java / Android:

    double total = 0.0;
    
        // do 10 adds of 10 cents
        for (int i = 0; i < 10; i++) {
            total += 0.1;  // adds 10 cents
        }
    
        Log.d("round problems?", "current total: " + total);
    
        // looks like total equals to 1.0, don't?
    
        // now, do reverse
        for (int i = 0; i < 10; i++) {
            total -= 0.1;  // removes 10 cents
        }
    
        // looks like total equals to 0.0, don't?
        Log.d("round problems?", "current total: " + total);
        if (total == 0.0) {
            Log.d("round problems?", "is total equal to ZERO? YES, of course!!");
        } else {
            Log.d("round problems?", "is total equal to ZERO? NO... thats why you should not use Double for some math!!!");
        }
    

    OUTPUT:

    round problems?: current total: 0.9999999999999999 round problems?: current total: 2.7755575615628914E-17 round problems?: is total equal to ZERO? NO... thats why you should not use Double for some math!!!

相关问题