Java 中的货币(Currency)API

1、概览

JSR 354 定义了 Java 中涉及 “货币和金钱” 的标准 API。

其目标是为 Java 生态系统添加一个灵活、可扩展的 API,使货币的处理更简单、更安全。

该 JSR 并未进入 JDK 9,但已成为未来 JDK 版本的候选。

2、设置

首先,在 pom.xml 中定义依赖:

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

最新版的依赖可以 这里 找到。

3、JSR-354 特性

货币 API 的目标是:

  • 提供处理和计算货币金额的 API
  • 定义表示货币和货币金额以及货币舍入的类
  • 处理货币汇率
  • 处理货币和金额的格式化和解析

4、Model

JSR-354 规范的主要类如下图所示:

javax monetary

Model 包含两个主要接口:货币单位(CurrencyUnit)和货币数量(MonetaryAmount),下文将对此进行解释。

5、CurrencyUnit

CurrencyUnit 模拟了货币的最基本属性。可使用 Monetary.getCurrency 方法获取其实例:

@Test
public void givenCurrencyCode_whenString_thanExist() {
    CurrencyUnit usd = Monetary.getCurrency("USD");

    assertNotNull(usd);
    assertEquals(usd.getCurrencyCode(), "USD");
    assertEquals(usd.getNumericCode(), 840);
    assertEquals(usd.getDefaultFractionDigits(), 2);
}

我们创建 CurrencyUnit 时使用的是货币的字符串表示法,如果使用不存在的货币代码创建货币会导致 UnknownCurrency 异常:

@Test(expected = UnknownCurrencyException.class)
public void givenCurrencyCode_whenNoExist_thanThrowsError() {
    Monetary.getCurrency("AAA");
}

6、MonetaryAmount

MonetaryAmount 是货币金额的数字表示。它总是与 CurrencyUnit 相关联,并定义一种货币的货币表示形式。

金额可以通过不同的方式实现,由每个具体用例定义。例如 MoneyFastMoneyMonetaryAmount 接口的实现。

FastMoney 使用 long 作为数字表示法实现 MonetaryAmount,其速度比 BigDecimal 快,但精度有损失;当我们对性能有要求,且允许精度丢失问题存在时,可以使用它。

可以使用默认工厂创建通用实例。如下,获取 MonetaryAmount 实例的不同方式:

@Test
public void givenAmounts_whenStringified_thanEquals() {
 
    CurrencyUnit usd = Monetary.getCurrency("USD");
    MonetaryAmount fstAmtUSD = Monetary.getDefaultAmountFactory()
      .setCurrency(usd).setNumber(200).create();
    Money moneyof = Money.of(12, usd);
    FastMoney fastmoneyof = FastMoney.of(2, usd);

    assertEquals("USD", usd.toString());
    assertEquals("USD 200", fstAmtUSD.toString());
    assertEquals("USD 12", moneyof.toString());
    assertEquals("USD 2.00000", fastmoneyof.toString());
}

7、货币计算

我们可以在 MoneyFastMoney 之间进行货币运算,但在组合这两个类的实例时需要小心。

例如,当我们将 FastMoney 的一个欧元(EUR)实例与 Money 的一个欧元实例进行比较时,结果会发现它们并不相同:

@Test
public void givenCurrencies_whenCompared_thanNotequal() {
    MonetaryAmount oneDolar = Monetary.getDefaultAmountFactory()
      .setCurrency("USD").setNumber(1).create();
    Money oneEuro = Money.of(1, "EUR");

    assertFalse(oneEuro.equals(FastMoney.of(1, "EUR")));
    assertTrue(oneDolar.equals(Money.of(1, "USD")));
}

我们可以使用 MonetaryAmount 类提供的方法进行加、减、乘、除和其他货币算术运算。

如果金额之间的算术运算超出了所用数字表示类型的能力,则算术运算应抛出算术异常。例如,如果我们尝试用 1 除以 3,就会出现 ArithmeticException 算术异常,因为结果是一个无穷大的数字:

@Test(expected = ArithmeticException.class)
public void givenAmount_whenDivided_thanThrowsException() {
    MonetaryAmount oneDolar = Monetary.getDefaultAmountFactory()
      .setCurrency("USD").setNumber(1).create();
    oneDolar.divide(3);
}

在加减金额时,最好使用 MonetaryAmount 实例的参数,因为我们需要确保两个金额具有相同的货币,以便在金额之间执行操作。

7.1、金额计算

计算总金额的方法有多种,其中一种方法是简单地将总金额与其他金额相加:

@Test
public void givenAmounts_whenSummed_thanCorrect() {
    MonetaryAmount[] monetaryAmounts = new MonetaryAmount[] {
      Money.of(100, "CHF"), Money.of(10.20, "CHF"), Money.of(1.15, "CHF")};

    Money sumAmtCHF = Money.of(0, "CHF");
    for (MonetaryAmount monetaryAmount : monetaryAmounts) {
        sumAmtCHF = sumAmtCHF.add(monetaryAmount);
    }

    assertEquals("CHF 111.35", sumAmtCHF.toString());
}

也可以使用链式调用减法运算:

Money calcAmtUSD = Money.of(1, "USD").subtract(fstAmtUSD);

乘:

MonetaryAmount multiplyAmount = oneDolar.multiply(0.25);

除:

MonetaryAmount divideAmount = oneDolar.divide(0.25);

使用字符串比较一下计算结果,因为使用字符串的结果包含货币:

@Test
public void givenArithmetic_whenStringified_thanEqualsAmount() {
    CurrencyUnit usd = Monetary.getCurrency("USD");

    Money moneyof = Money.of(12, usd);
    MonetaryAmount fstAmtUSD = Monetary.getDefaultAmountFactory()
      .setCurrency(usd).setNumber(200.50).create();
    MonetaryAmount oneDolar = Monetary.getDefaultAmountFactory()
      .setCurrency("USD").setNumber(1).create();
    Money subtractedAmount = Money.of(1, "USD").subtract(fstAmtUSD);
    MonetaryAmount multiplyAmount = oneDolar.multiply(0.25);
    MonetaryAmount divideAmount = oneDolar.divide(0.25);

    assertEquals("USD", usd.toString());
    assertEquals("USD 1", oneDolar.toString());
    assertEquals("USD 200.5", fstAmtUSD.toString());
    assertEquals("USD 12", moneyof.toString());
    assertEquals("USD -199.5", subtractedAmount.toString());
    assertEquals("USD 0.25", multiplyAmount.toString());
    assertEquals("USD 4", divideAmount.toString());
}

8、货币舍入

货币舍入是将精确度不确定的金额转换为四舍五入的金额。

我们使用货币类提供的 getDefaultRounding API 进行转换。默认四舍五入值由货币提供:

@Test
public void givenAmount_whenRounded_thanEquals() {
    MonetaryAmount fstAmtEUR = Monetary.getDefaultAmountFactory()
      .setCurrency("EUR").setNumber(1.30473908).create();
    MonetaryAmount roundEUR = fstAmtEUR.with(Monetary.getDefaultRounding());
    
    assertEquals("EUR 1.30473908", fstAmtEUR.toString());
    assertEquals("EUR 1.3", roundEUR.toString());
}

9、货币兑换

该 API 侧重于基于源货币、目标货币和汇率的货币转换的共同方面。

货币转换或汇率访问可以参数化:

@Test
public void givenAmount_whenConversion_thenNotNull() {
    MonetaryAmount oneDollar = Monetary.getDefaultAmountFactory().setCurrency("USD")
      .setNumber(1).create();

    CurrencyConversion conversionEUR = MonetaryConversions.getConversion("EUR");

    MonetaryAmount convertedAmountUSDtoEUR = oneDollar.with(conversionEUR);

    assertEquals("USD 1", oneDollar.toString());
    assertNotNull(convertedAmountUSDtoEUR);
}

转换总是与货币绑定。只需将 CurrencyConversion 传递给金额的 with 方法,就可以对 MonetaryAmount 进行转换。

10、货币格式化

格式化允许基于 java.util.Locale 访问格式。与 JDK 不同,该 API 所定义的 formatter 是线程安全的:

@Test
public void givenLocale_whenFormatted_thanEquals() {
    MonetaryAmount oneDollar = Monetary.getDefaultAmountFactory()
      .setCurrency("USD").setNumber(1).create();

    MonetaryAmountFormat formatUSD = MonetaryFormats.getAmountFormat(Locale.US);
    String usFormatted = formatUSD.format(oneDollar);

    assertEquals("USD 1", oneDollar.toString());
    assertNotNull(formatUSD);
    assertEquals("USD1.00", usFormatted);
}

如上,我们使用预定义格式,为货币创建自定义格式。使用 MonetaryFormats 类的 format 方法可以直接使用标准格式。通过设置格式查询生成器的 pattern 属性来定义自定义格式。

和以前一样,由于货币包含在结果中,我们使用字符串来测试结果:

@Test
public void givenAmount_whenCustomFormat_thanEquals() {
    MonetaryAmount oneDollar = Monetary.getDefaultAmountFactory()
            .setCurrency("USD").setNumber(1).create();

    MonetaryAmountFormat customFormat = MonetaryFormats.getAmountFormat(AmountFormatQueryBuilder.
      of(Locale.US).set(CurrencyStyle.NAME).set("pattern", "00000.00 ¤").build());
    String customFormatted = customFormat.format(oneDollar);

    assertNotNull(customFormat);
    assertEquals("USD 1", oneDollar.toString());
    assertEquals("00001.00 US Dollar", customFormatted);
}

11、总结

本文介绍了 Java 中 Money(金钱) 和 Currency(货币) JSR 的基础知识。


Ref:https://www.baeldung.com/java-money-and-currency