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

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


当前回答

这不是精确与否的问题,也不是精确与否的问题。这是一个满足以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为基数进行内部计算,而不是使用双数,则如果您的代码中没有其他错误,那么结果总是完全符合人类的预期。

其他回答

我将冒着被否决的风险,但我认为浮点数在货币计算中的不适用性被高估了。只要确保正确地进行了舍入,并且有足够的有效数字来处理zneak解释的二进制十进制表示不匹配,就不会有问题。

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

当然,你必须在合理范围内;例如,一个简单的网络商店可能永远不会遇到双精度浮点数的任何问题,但如果你做会计或其他需要添加大量(无限制)数字的事情,你不会想要用十英尺的杆子触摸浮点数。

摘自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;
    }
}

如前所述,“把钱表示为双位数或浮点数,一开始可能看起来不错,因为软件会消除微小的错误,但当你对不精确的数字进行更多的加减乘除时,随着错误的增加,你会失去越来越多的精度。”这使得浮点数和双精度数不适用于处理货币,因为货币需要精确计算以10为底数的倍数。”

最后,Java有一个标准的方法来处理货币和金钱!

JSR 354:货币和货币API

JSR 354提供了一个API,用于表示、传输和执行Money和Currency的综合计算。你可以从以下连结下载:

JSR 354:货币和货币API下载

该规范包括以下内容:

用于处理例如货币数量和货币的API 支持可互换实现的api 用于创建实现类实例的工厂 用于计算、转换和格式化货币金额的功能 用于处理Money和Currencies的Java API,计划包含在Java 9中。 所有规范类和接口都位于javax.money中。*包。

JSR 354: Money and Currency API示例:

创建一个moneyaryamount并将其打印到控制台的示例如下:

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还支持monetaryamount的计算:

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

CurrencyUnit和moneyaryamount

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

moneyaryamount有各种方法,允许访问指定的货币,数字金额,其精度和更多:

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;

monearyamount可以使用舍入运算符进行舍入:

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

当使用monearyamount的集合时,可以使用一些不错的实用程序方法进行过滤、排序和分组。

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

自定义monearyamount操作

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

资源:

使用JSR 354在Java中处理金钱和货币

Java 9货币和货币API (JSR 354)

参见:JSR 354 -货币和货币

我对其中一些回答感到困扰。我认为双数和浮点数在财务计算中占有一席之地。当然,在使用整数类或BigDecimal类时,在加减非分数货币金额时,不会损失精度。但是,当执行更复杂的操作时,无论您如何存储这些数字,您经常会得到小数点后几位或许多位的结果。问题在于你如何呈现结果。

如果你的结果是在四舍五入和四舍五入之间的边缘,最后一分真的很重要,你可能应该告诉观众答案几乎在中间——通过显示更多的小数点后数位。

双精度浮点数的问题是,当它们被用来组合大数和小数时。在java中,

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

结果

1.1875