使用BigDecimal处理货币


67

我试图使用多头建立自己的货币类,但显然我应该改BigDecimal而使用。有人可以帮我入门吗?将BigDecimals用作美元货币的最佳方法是什么,例如使它至少但不超过2个小数位,等等。用于的APIBigDecimal很大,我不知道使用哪种方法。而且,它BigDecimal具有更好的精度,但是如果它通过double?会不会全部丢失?如果我做新的BigDecimal(24.99),那和使用a有double什么不同?还是应该改用使用a的构造函数String


1
一切看起来都不错,但是最后一件事-当在BigDecimal实例上使用setScale()时,尤其是如果稍后将其用于加法时,请提供n + 1的舍入因子,其中n是有效数字的数量。例如,将舍入系数设置为3,以下添加0.043 + 0.043将产生0.09而不是0.08(如果您选择2个有效数字进行舍入/存储)。
Vineet Reynolds

在上一个示例中,如果您选择2个有效数字,并在加法之前进行了四舍五入(作为中间运算),则将获得0.04 + 0.04 = 0.08
Vineet Reynolds

我存储要从价格中删除的百分比的变量是否也应该是BigDecimal?
09年

Answers:


81

这里有一些提示:

  1. 使用BigDecimal的计算,如果你需要的精度,它提供了(货币价值往往需要这个)。
  2. 使用NumberFormat该类进行显示。此类将处理不同货币金额的本地化问题。但是,它将仅接受原语。因此,如果您接受由于转换为导致的精度的微小变化double,则可以使用此类。
  3. 使用NumberFormat类时,请scale()BigDecimal实例上使用方法设置精度和舍入方法。

PS:如果您想知道的话,BigDecimal总是比double要用Java表示货币值更好

PPS:

创建BigDecimal实例

这非常简单,因为BigDecimal它提供了构造函数以接受原始值String对象。您可以使用那些对象最好是String使用对象的对象。例如,

BigDecimal modelVal = new BigDecimal("24.455");
BigDecimal displayVal = modelVal.setScale(2, RoundingMode.HALF_EVEN);

显示BigDecimal实例

您可以使用setMinimumFractionDigitssetMaximumFractionDigits方法调用来限制所显示的数据量。

NumberFormat usdCostFormat = NumberFormat.getCurrencyInstance(Locale.US);
usdCostFormat.setMinimumFractionDigits( 1 );
usdCostFormat.setMaximumFractionDigits( 2 );
System.out.println( usdCostFormat.format(displayVal.doubleValue()) );

3
特别要同意数字2。将视图(显示格式)与模型(BigDecimal)分开。您始终可以编写自定义java.text.Format来处理您的特定数据类型。
Steve Kuo

您不是说DecimalFormat,不是NumberFormat吗?
2009年

8
-1,表示使用BigDecimal(double)构造函数..即使文档说此构造函数是“不可预测的”
Ryan Fernandes

1
让我将您的问题改写为“我什么时候应该舍入BigDecimal值?” 这将取决于您如何使用数字-加法和减法需要存储较少的十进制值,而乘法和除法则需要更多。实际上,您需要1个额外的小数位来进行加减(处理溢出的舍入)。单位成本不应四舍五入,转换单位也不应四舍五入。总而言之,这取决于您在BigDecimal实例中存储的内容。如果需要在整个操作过程中保持精度,请最后进行舍入。
Vineet Reynolds

1
理想情况下,您应该在操作结束时四舍五入,并将该值存储在数据库中(通常用于金融服务领域的审计),以使显示的值和存储在数据库中的值保持相同。因此,您可以在那里找到答案,仅将数字格式用于货币格式,并在开头(如有必要)和结尾(在存储之前)使用setScale。舍入运算的删除将使计算的数学正确性得以验证。
Vineet Reynolds,

39

我建议您对货币模式进行一些研究。Martin Fowler在他的《分析模式》一书中对此进行了更详细的介绍。

public class Money {

    private static final Currency USD = Currency.getInstance("USD");
    private static final RoundingMode DEFAULT_ROUNDING = RoundingMode.HALF_EVEN;

    private final BigDecimal amount;
    private final Currency currency;   

    public static Money dollars(BigDecimal amount) {
        return new Money(amount, USD);
    }

    Money(BigDecimal amount, Currency currency) {
        this(amount, currency, DEFAULT_ROUNDING);
    }

    Money(BigDecimal amount, Currency currency, RoundingMode rounding) {
        this.currency = currency;      
        this.amount = amount.setScale(currency.getDefaultFractionDigits(), rounding);
    }

    public BigDecimal getAmount() {
        return amount;
    }

    public Currency getCurrency() {
        return currency;
    }

    @Override
    public String toString() {
        return getCurrency().getSymbol() + " " + getAmount();
    }

    public String toString(Locale locale) {
        return getCurrency().getSymbol(locale) + " " + getAmount();
    }   
}

用途:

您将使用Moneyobject代表所有款项BigDecimal。将货币表示为大十进制数将意味着您将必须在显示它的每个位置格式化货币。试想一下显示标准是否发生了变化。您将不得不在整个地方进行编辑。取而代之的是使用Money模式,将货币格式集中到一个位置。

Money price = Money.dollars(38.28);
System.out.println(price);

4
这样做的一个问题-不是所有货币都默认使用金额前的符号,或在两者之间留有空格。那应该是fomatting的一部分。
·塞尚

没错,@ GabeSechan-您可能会感兴趣drivy.engineering/multi-currency-java
Mick


1

1)如果限于double精度,则使用BigDecimals的原因之一是要使用从BigDecimals创建的doubles实现运算。

2)BigDecimal由一个任意精度的整数无标度值和一个非负的32位整数标度组成,而double将原始类型的值包装double在一个对象中。类型的对象Double包含一个类型为double

3)应该没有区别

您应该对$和精度没有任何困难。一种方法是使用System.out.printf


1

使用BigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP)时要四舍五入到小数点后2分美分。但是,进行计算时请注意四舍五入的错误。在进行货币价值舍入时,您需要保持一致。在所有计算完成后,只在最后一次舍入一次舍入,或者在执行任何计算之前对每个值应用舍入。使用哪个选项取决于您的业务需求,但是总的来说,我认为最后进行四舍五入对我来说似乎更好。

String当您构造BigDecimal货币价值时,请使用。如果使用double,则末尾将带有尾随浮点值。这是由于计算机体系结构有关如何以二进制格式表示double/float值。


11
请注意,对于金融应用程序,ROUND_HALF_EVEN是最常见的舍入模式,因为它避免了偏差。
Michael Borgwardt

关于避免偏见的好点。不知道它能实现这一目标。
Vineet Reynolds,2009年

@迈克尔:谢谢你的提示。我不知道 我一直想知道如何最好地处理偏差/累积错误。今天学到了新东西。:)
tim_wonil

1

基本数字类型对于在内存中存储单个值很有用。但是在使用double和float类型进行计算时,四舍五入会出现问题,这是因为内存表示不完全映射到该值。例如,双精度值本应采用64位,但Java不会使用全部64位,它仅存储其认为是数字重要部分的内容。因此,在将float或double类型的值相加时,可能会得出错误的值。

请查看短片https://youtu.be/EXxUSz9x7BM


0

在javapractices.com上有一个有关如何执行此操作的广泛示例。请特别参阅Money该类,该类旨在使货币计算比BigDecimal直接使用更简单。

此类的设计Money旨在使表达式更自然。例如:

if ( amount.lt(hundred) ) {
 cost = amount.times(price); 
}

WEB4J工具有一个类似的类,称为Decimal,比Money该类更精细。


0
NumberFormat.getNumberInstance(java.util.Locale.US).format(num);

我喜欢这个答案的简单性,但是它将返回1.5而不是1.50而不是最后的零。
JesseBoyd

试试:NumberFormat.getCurrencyInstance(java.util.Locale.US).format(num);代替。
塞德里克

0

我会很激进的。没有BigDecimal。

这是一篇很棒的文章 https://lemnik.wordpress.com/2011/03/25/bigdecimal-and-your-money/

来自这里的想法。

import java.math.BigDecimal;

public class Main {

    public static void main(String[] args) {
        testConstructors();
        testEqualsAndCompare();
        testArithmetic();
    }

    private static void testEqualsAndCompare() {
        final BigDecimal zero = new BigDecimal("0.0");
        final BigDecimal zerozero = new BigDecimal("0.00");

        boolean zerosAreEqual = zero.equals(zerozero);
        boolean zerosAreEqual2 = zerozero.equals(zero);

        System.out.println("zerosAreEqual: " + zerosAreEqual + " " + zerosAreEqual2);

        int zerosCompare = zero.compareTo(zerozero);
        int zerosCompare2 = zerozero.compareTo(zero);
        System.out.println("zerosCompare: " + zerosCompare + " " + zerosCompare2);
    }

    private static void testArithmetic() {
        try {
            BigDecimal value = new BigDecimal(1);
            value = value.divide(new BigDecimal(3));
            System.out.println(value);
        } catch (ArithmeticException e) {
            System.out.println("Failed to devide. " + e.getMessage());
        }
    }

    private static void testConstructors() {
        double doubleValue = 35.7;
        BigDecimal fromDouble = new BigDecimal(doubleValue);
        BigDecimal fromString = new BigDecimal("35.7");

        boolean decimalsEqual = fromDouble.equals(fromString);
        boolean decimalsEqual2 = fromString.equals(fromDouble);

        System.out.println("From double: " + fromDouble);
        System.out.println("decimalsEqual: " + decimalsEqual + " " + decimalsEqual2);
    }
}

它打印

From double: 35.7000000000000028421709430404007434844970703125
decimalsEqual: false false
zerosAreEqual: false false
zerosCompare: 0 0
Failed to devide. Non-terminating decimal expansion; no exact representable decimal result.

如何将BigDecimal存储到数据库中?地狱,它也存储为双值???至少,如果我在没有任何高级配置的情况下使用mongoDb,它将存储BigDecimal.TEN1E1

可能的解决方案?

我来了一个-使用String将Java中的BigDecimal作为String存储到数据库中。您已经进行了验证,例如@NotNull@Min(10)等等。然后可以在更新或保存时使用触发器,以检查当前字符串是否是您需要的数字。虽然没有mongo的触发器。 Mongodb触发函数调用有内置方法吗?

我在玩乐中有一个缺点-Swagger防御中的BigDecimal为String

我需要招摇摇晃,所以我们的前端团队知道我给他们传递了一个以String表示的数字。例如,DateTime以字符串形式显示。

我在上一篇文章中阅读了另一个很酷的解决方案...使用存储精确的数字。

标准的长值可以存储6477次美国国债的当前值(以美分而不是美元为单位),而不会发生任何溢出。更重要的是:它是整数类型,而不是浮点数。这样可以更轻松,准确地进行操作并保证行为。


更新资料

https://stackoverflow.com/a/27978223/4587961

也许将来MongoDb将添加对BigDecimal的支持。 https://jira.mongodb.org/browse/SERVER-1393 3.3.8似乎已完成此操作。

这是第二种方法的一个例子。使用缩放。 http://www.technology-ebay.de/the-teams/mobile-de/blog/mapping-bigdecimals-with-morphia-for-mongodb.html

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.