概述

JSR 354 – “货币与金额”规范旨在为Java生态系统提供灵活且可扩展的货币处理API,简化和增强对货币金额的操作。

该标准并未在JDK 9中实现,但有望在未来版本中加入。

设置

首先,让我们在pom.xml文件中添加依赖:

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

最新的依赖版本可在这里查看。

JSR-354 功能

“货币与金额”API的主要目标如下:

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

模型

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

javax monetary3 1

模型包含两个主要接口CurrencyUnitMonetaryAmount,接下来将分别介绍。

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作为数值表示,比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的欧元实例与一个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类提供的方法执行加法、减法、乘法、除法等货币算术运算。

如果算术运算超出了所使用的数值表示类型的处理能力,比如试图除以三,会抛出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. 货币四舍五入

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

我们将使用Monetary类提供的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);
}

转换始终与货币相关。MonetaryAmount可以通过将CurrencyConversion传递给金额的with方法来进行简单转换。

10. 货币格式化

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

@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货币与汇率JSR的基础知识。

货币值无处不在,Java现在开始支持和处理货币值,包括货币算术和货币转换。

如往常一样,可以从文章的GitHub代码库获取代码。