在Java中,用什么数据类型来表示货币?

232

在Java中,你应该使用什么数据类型来表示货币?


2
这取决于你要执行哪些操作。请提供更多信息。 - eversor
@eversor,你能给我描述一下应该为不同的操作使用什么数据类型吗? - questborn
1
我正在进行需要精确表示美分的计算。 - questborn
你能预测你的应用程序需要处理的最大金额吗?而且,你的计算会是简单的(加法等)还是更复杂的财务操作? - eversor
它不接受货币答案。 - MC Emperor
我不理解为什么这篇帖子会被关闭,而这篇关于在 C# 中应该使用哪种类的问题却得到了498个赞。 - tsh
11个回答

179

Java有Currency类,可以表示ISO 4217货币代码。

BigDecimal是最适合表示货币小数值的类型。

Joda Money提供了一个库来表示货币。


8
为什么我们不能使用 float 或 double 来代替? - Erran Morad
26
@Borat Sagdiyev 这就是为什么会出现浮点数精度问题的原因。此外,您可以参考这个链接:https://www.securecoding.cert.org/confluence/display/java/NUM04-J.+Do+not+use+floating-point+numbers+if+precise+computation+is+required。 - Buhake Sindi
3
@Borat:如果你知道你在做什么,就可以这样做,参见 Peter Lawrey 的这篇文章(http://vanillajava.blogspot.com/2011/08/double-your-money-again.html)。但是看起来进行所有的舍入和使用 BigDecimals 一样麻烦。 - Nathan Hughes
69
如果我能拿到每次看到有人使用FLOAT来存储货币的十分之一美元,我将会有999.997634美元。-- Bill Karwin - Collin Krawll

48

您可以使用Money and Currency API (JSR 354)。只要将适当的依赖项添加到您的项目中,就可以在其中使用此API。

对于Java 8,请将以下参考实现作为依赖项添加到您的pom.xml中:

<dependency>
    <groupId>org.javamoney</groupId>
    <artifactId>moneta</artifactId>
    <version>1.0</version>
</dependency>

这个依赖项将自动添加 javax.money:money-api 作为依赖项。

然后,您就可以使用该API:

package com.example.money;

import static org.junit.Assert.assertThat;
import static org.hamcrest.CoreMatchers.is;

import java.util.Locale;

import javax.money.Monetary;
import javax.money.MonetaryAmount;
import javax.money.MonetaryRounding;
import javax.money.format.MonetaryAmountFormat;
import javax.money.format.MonetaryFormats;

import org.junit.Test;

public class MoneyTest {

    @Test
    public void testMoneyApi() {
        MonetaryAmount eurAmount1 = Monetary.getDefaultAmountFactory().setNumber(1.1111).setCurrency("EUR").create();
        MonetaryAmount eurAmount2 = Monetary.getDefaultAmountFactory().setNumber(1.1141).setCurrency("EUR").create();

        MonetaryAmount eurAmount3 = eurAmount1.add(eurAmount2);
        assertThat(eurAmount3.toString(), is("EUR 2.2252"));

        MonetaryRounding defaultRounding = Monetary.getDefaultRounding();
        MonetaryAmount eurAmount4 = eurAmount3.with(defaultRounding);
        assertThat(eurAmount4.toString(), is("EUR 2.23"));

        MonetaryAmountFormat germanFormat = MonetaryFormats.getAmountFormat(Locale.GERMAN);
        assertThat(germanFormat.format(eurAmount4), is("EUR 2,23") );
    }
}

2
序列化和保存到数据库怎么办?发送到网络上应该使用什么格式? - Paweł Szczur
1
我相信 Oracle 决定在 Java 9 中不包含 Java Money。真是太遗憾了,但这是个好答案。我们仍然可以使用 Maven。 - borjab
4
你有没有Oracle决定在Java 9中不包括Java Money的来源? - Abdull
1
@PawełSzczur 这取决于您用于序列化和持久化的工具,但是例如,您可以在Jackson中使用jackson-datatype-money,并在Hibernate中使用Jadira - Dario Seidl
@Abdull,曾经有关于将Java Money包含在JDK中的讨论,但这从未发生。我没有来自Oracle的消息来源,但这里有一些信息:https://dev59.com/Cq_la4cB1Zd3GeqPr1-s#53181704 - Dario Seidl
这个图书馆是否已经过时了?它已经很久没有更新了。 - Greyshack

29

整数类型,表示最小的可能值。换句话说,您的程序应该以分为单位而不是以美元/欧元为单位。

这不应阻止您让GUI将其转换回美元/欧元。


5
@eversor 那需要超过2000万美元,大多数应用程序不需要那么多,如果它们做得足够好,即使我们的政府也无法处理那么多钱。 - ratchet freak
5
最好使用 long。 - le3th4x0rbot
6
许多银行每天处理的金额比2000万美元要大得多。这甚至不考虑像日元这样与美元有巨大汇率差异的货币。整数类型可能是避免四舍五入问题的最佳选择,尽管在利率和汇率计算方面会变得混乱。然而,根据应用程序的不同,您可能需要一个64位整数类型。 - Alchymist
不要忘记考虑发达国家,他们的货币有很多零。 - stuckedoverflow
2022年已经到了。我们现在拥有的加密货币没有任何原子下限。祝你好运,同样命中注定的程序员。 - Hubert Grzeskowiak
显示剩余2条评论

17

16

JSR 354: 货币和货币API

JSR 354提供了一个API,用于表示、传输和执行有关货币和货币进行全面计算的操作。您可以从此链接下载:

JSR 354:货币和货币API下载

该规范包括以下内容:

  1. 处理货币金额和货币等的API
  2. 支持可互换实现的API
  3. 创建实现类实例的工厂
  4. 用于货币金额计算、转换和格式化功能
  5. Java API用于处理货币和货币,计划将其包含在Java 9中。
  6. 所有规范类和接口都位于javax.money.*包中。

JSR 354: 货币和货币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"));

货币单位和货币金额

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

自定义货币金额操作

// 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-货币和货币


这一切都很不错,但正如Federico上面建议的那样,它看起来比BigDecimal慢:-))一个不好的玩笑,但是我会在一年后进行测试... - kensai

8

我进行了一个微基准测试(JMH),比较了Moneta(Java货币JSR 354实现)和BigDecimal在性能方面的差异。

令人惊讶的是,BigDecimal的表现似乎要优于Moneta。 我使用了以下Moneta配置:

org.javamoney.moneta.Money.defaults.precision=19 org.javamoney.moneta.Money.defaults.roundingMode=HALF_UP

package com.despegar.bookedia.money;

import org.javamoney.moneta.FastMoney;
import org.javamoney.moneta.Money;
import org.openjdk.jmh.annotations.*;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.concurrent.TimeUnit;

@Measurement(batchSize = 5000, iterations = 10, time = 2, timeUnit =     TimeUnit.SECONDS)
@Warmup(iterations = 2)
@Threads(value = 1)
@Fork(value = 1)
@State(Scope.Benchmark)
@BenchmarkMode(Mode.Throughput)
public class BigDecimalBenchmark {

private static final Money MONEY_BASE = Money.of(1234567.3444, "EUR");
private static final Money MONEY_SUBSTRACT = Money.of(232323, "EUR");
private static final FastMoney FAST_MONEY_SUBSTRACT = FastMoney.of(232323, "EUR");
private static final FastMoney FAST_MONEY_BASE = FastMoney.of(1234567.3444, "EUR");
MathContext mc = new MathContext(10, RoundingMode.HALF_UP);

@Benchmark
public void bigdecimal_string() {
    new BigDecimal("1234567.3444").subtract(new BigDecimal("232323")).multiply(new BigDecimal("3.4"), mc).divide(new BigDecimal("5.456"), mc);
}

@Benchmark
public void bigdecimal_valueOf() {
    BigDecimal.valueOf(12345673444L, 4).subtract(BigDecimal.valueOf(232323L)).multiply(BigDecimal.valueOf(34, 1), mc).divide(BigDecimal.valueOf(5456, 3), mc);
}
@Benchmark
public void fastmoney() {
    FastMoney.of(1234567.3444, "EUR").subtract(FastMoney.of(232323, "EUR")).multiply(3.4).divide(5.456);
}

@Benchmark
public void money() {
    Money.of(1234567.3444, "EUR").subtract(Money.of(232323, "EUR")).multiply(3.4).divide(5.456);
}

@Benchmark
public void money_static(){
    MONEY_BASE.subtract(MONEY_SUBSTRACT).multiply(3.4).divide(5.456);
}

@Benchmark
public void fastmoney_static() {
    FAST_MONEY_BASE.subtract(FAST_MONEY_SUBSTRACT).multiply(3.4).divide(5.456);
    }
}

导致
Benchmark                                Mode  Cnt     Score    Error  Units
BigDecimalBenchmark.bigdecimal_string   thrpt   10   479.465 ± 26.821  ops/s
BigDecimalBenchmark.bigdecimal_valueOf  thrpt   10  1066.754 ± 40.997  ops/s
BigDecimalBenchmark.fastmoney           thrpt   10    83.917 ±  4.612  ops/s
BigDecimalBenchmark.fastmoney_static    thrpt   10   504.676 ± 21.642  ops/s
BigDecimalBenchmark.money               thrpt   10    59.897 ±  3.061  ops/s
BigDecimalBenchmark.money_static        thrpt   10   184.767 ±  7.017  ops/s

如果我漏掉了什么,请随意纠正我


有趣,我将使用JDK9上的最新工具运行相同的测试。 - kensai

7

在表示货币价值时,您应该使用BigDecimal。它允许您使用各种舍入模式,在金融应用中,舍入模式通常是强制要求的,甚至可能是法律规定。


6

对于简单的情况(一种货币),使用int/long就足够了。 将货币保存在分(...)或百分之一/千分之一美分中(使用固定的分隔符可以获得所需的任何精度)。


6

我会使用Joda Money

虽然它仍处于0.6版本,但看起来非常有前途。


4

BigDecimal是处理货币最好的数据类型。

虽然有很多货币容器,但它们都使用BigDecimal作为底层数据类型。使用BigDecimal并采用BigDecimal.ROUND_HALF_EVEN舍入方式,你不会出错。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接