Java Money 与 Currency API 浅谈

920 阅读5分钟

一、概述

JSR 354 - “金钱和货币” 解决了 Java 中货币和货币金额的标准化问题。

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

JSR 没有进入 JDK 9,而是未来 JDK 版本的候选人。虽然在 Java9 有 Currency 类简单实现但是实际开发中满足不了需求

二、引入

在 maven 的 pom.xml 中做如下引入

      <dependency>
        <groupId>org.javamoney</groupId>
        <artifactId>moneta</artifactId>
        <version>最新版本</version>
     </dependency>

在 gradle 中

    compile group: 'org.javamoney', name: 'moneta', version: '最新版本'

最新版本依赖,可以查看,点击这里

三、JSR-354 功能

“货币和金钱” API 的目标:

  1. 提供处理和计算货币金额的 API

  2. 定义货币和货币金额的类别,以及货币四舍五入

  3. 处理汇率

  4. 处理货币和货币金额的格式化和解析

四、API 分析与使用

  1. 规范中提到的类及接口都在 javax.money.* 包下面。

  2. 先从核心的两个接口 CurrencyUnit 与 MonetaryAmount 开始剖析

3.CurrencyUnit 及 MonetaryAmount

CurrencyUnit

代表的是货币。它有点类似于现在的 java.util.Currency 类,不同之处在于它支持自定义的实现。从规范的定义来看,java.util.Currency 也是可以实现该接口的。CurrencyUnit 的实例可以通过 Monetary.getCurrency () 方法获取,如下:

    //据货币代码来获取货币单位        
    CurrencyUnit currencyUnit = Monetary.getCurrency("USD");
   //亦或根据国家及地区来获取货币单位
   CurrencyUnit   unit    = Monetary.getCurrency(Locale.US);

CurrencyUnit 模拟货币的最小属性,我们使用货币的字符串表示形式创建 CurrencyUnit ,这可能会导致我们尝试使用不存在的代码创建货币的情况。使用不存在的代码创建货币会引发 UnknownCurrency 异常。

MonetaryAmount

MonetaryAmount 是货币金额的数字表示。它始终与 CurrencyUnit 关联,并定义货币的货币表示形式。

金额可以用不同的方式来实现,重点放在由每个具体用例所定义的货币表示要求的行为上。例如。Money 和 FastMoney 是 MonetaryAmount 接口的实现。

FastMoney 实现 MonetaryAmount 使用长为数字表示,并且比更快的 BigDecimal 在精度的成本;它可以在我们需要性能时使用,精度不是问题。

Money 与 FastMoney 是 JavaMoney 库中 MonetaryAmount 的两种实现。Money 是默认实现,它使用 BigDecimal 来存储金额。FastMoney 是可选的另一个实现,它用 long 类型来存储金额。根据文档来看,FastMoney 上的操作要比 Money 的快 10 到 15 倍左右。然而,FastMoney 的金额大小与精度都受限于 long 类型。

注意:这里的 Money 和 FastMoney 都是具体的实现类(它们在 org.javamoney.moneta. 包下面,而不是 javax.money.)。如果你不希望指定具体类型的话,可以通过 MonetaryAmountFactory 来生成一个 MonetaryAmount 的实例

通用实例可以使用默认工厂创建。

e.g:

    CurrencyUnit currencyUnit = Monetary.getCurrency(Locale.US);
    
    //金额表示
    MonetaryAmount fstAmtUSD = Monetary.getDefaultAmountFactory().setCurrency(currencyUnit).setNumber(200).create();
    
    Money money = Money.of(12, currencyUnit);
    
    FastMoney fastMoney = FastMoney.of(2, currencyUnit);

注意:当且仅当实现类,货币单位,以及数值全部相等时才认为这两个 MontetaryAmount 实例是相等的。

MonetaryAmount 内包含丰富的方法,可以用来获取具体的货币,金额,精度等等。

    //货币计算
    MonetaryAmount oneDolar = Monetary.getDefaultAmountFactory().setCurrency(currencyUnit).setNumber(1).create();
    Money oneEuro = Money.of(1, "EUR");

    //"+"
    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);
    }     
    
    //"-"
     Money calcAmtUSD = Money.of(1, "USD").subtract(fstAmtUSD);  
     
   //"*"
    MonetaryAmount multiplyAmount = oneDolar.multiply(0.25);
    
   //"" 
   MonetaryAmount divideAmount = oneDolar.divide(0.25);
   
   Money moneyOf = Money.of(12, currencyUnit);
   fstAmtUSD = Monetary.getDefaultAmountFactory().setCurrency(currencyUnit).setNumber(200.50).create();
   oneDolar = Monetary.getDefaultAmountFactory().setCurrency("USD").setNumber(1).create();
   Money subtractedAmount = Money.of(1, "USD").subtract(fstAmtUSD);
   multiplyAmount = oneDolar.multiply(0.25);
   divideAmount = oneDolar.divide(0.25);
   
   //四舍五入
   MonetaryAmount fstAmtEUR = Monetary.getDefaultAmountFactory().setCurrency("EUR").setNumber(1.30473908).create();
   MonetaryAmount roundEUR = fstAmtEUR.with(Monetary.getDefaultRounding());
   
   MonetaryAmount oneDollar = Monetary.getDefaultAmountFactory().setCurrency("USD").setNumber(1).create();
   
   //货币格式化以及解析
   MonetaryAmountFormat formatUSD = MonetaryFormats.getAmountFormat(Locale.US);
   String usFormatted = formatUSD.format(oneDollar);
   MonetaryAmount parsed = germanFormat.parse("12,4 USD");

可以通过 AmountFormatQueryBuilder 来生成自定义的格式。

   MonetaryAmountFormat customFormat = MonetaryFormats.getAmountFormat(
   AmountFormatQueryBuilder.of(Locale.US).set(CurrencyStyle.NAME).set("pattern", "00,00,00,00.00 #").build());

注意,这里的 #符号在模式串中是作为货币的占位符。

在操作在操作 MonetaryAmount 集合时,有许多实用的工具方法可以用来进行过滤,排序以及分组。这些方法还可以与 Java 8 的流 API 一起配套使用。

example:

    List<MonetaryAmount> amounts = new ArrayList<>();
    amounts.add(Money.of(2000.00, "EUR"));
    amounts.add(Money.of(4200.00, "USD"));
    amounts.add(Money.of(700.00, "USD"));
    amounts.add(Money.of(13.37, "JPY"));
    amounts.add(Money.of(188000.80, "USD"));

根据 CurrencyUnit 来进行金额过滤:

    CurrencyUnit yen = Monetary.getCurrency("JPY");
    
    CurrencyUnit dollar = Monetary.getCurrency("USD");       

   // 根据货币过滤,只返回美金      
   List<MonetaryAmount> onlyDollar = amounts.stream().filter(MonetaryFunctions.isCurrency(dollar)).collect(Collectors.toList());
   
   // 根据货币过滤,只返回美金和日元
   
   List<MonetaryAmount> onlyDollarAndYen = amounts.stream().filter(MonetaryFunctions.isCurrency(dollar, yen)).collect(Collectors.toList());

还可以过滤出大于或小于某个阈值的金额:

MonetaryAmount tenDollar = Money.of(1000, dollar);

List<MonetaryAmount> greaterThanTenDollar = amounts.stream().filter(MonetaryFunctions.isCurrency(dollar))
    .filter(MonetaryFunctions.isGreaterThan(tenDollar)).collect(Collectors.toList());

排序与分组

// Sorting dollar values by number value    
List<MonetaryAmount> sortedByAmount = onlyDollar.stream().sorted(MonetaryFunctions.sortNumber()).collect(Collectors.toList());

//Sorting by CurrencyUnit  
List<MonetaryAmount> sortedByCurrencyUnit = amounts.stream().sorted(MonetaryFunctions.sortCurrencyUnit()).collect(Collectors.toList());

//按货币单位进行分组   
Map<CurrencyUnit, List<MonetaryAmount>> groupedByCurrency = amounts.stream().collect(MonetaryFunctions.groupByCurrencyUnit());

// 分组并进行汇总
Map<CurrencyUnit, MonetarySummaryStatistics> summary = amounts.stream().collect(MonetaryFunctions.groupBySummarizingMonetary()).get();

MonetarySummaryStatistics dollarSummary = summary.get(dollar);
MonetaryAmount average = dollarSummary.getAverage();
MonetaryAmount min = dollarSummary.getMin();
MonetaryAmount max = dollarSummary.getMax();
MonetaryAmount sum = dollarSummary.getSum();
long count = dollarSummary.getCount();

MonetaryFunctions 还提供了归约函数,可以用来获取最大值,最小值,以及求和:

List<MonetaryAmount> amounts = new ArrayList<>();
amounts.add(Money.of(10, "EUR"));
amounts.add(Money.of(7.5, "EUR"));
amounts.add(Money.of(12, "EUR"));

Optional<MonetaryAmount> max = amounts.stream().reduce(MonetaryFunctions.max()); // "EUR 12"
Optional<MonetaryAmount> min = amounts.stream().reduce(MonetaryFunctions.min()); // "EUR 7.5"
Optional<MonetaryAmount> sum = amounts.stream().reduce(MonetaryFunctions.sum()); // "EUR 29.5"

自定义的 MonetaryAmount 操作

MonetaryAmount 还提供了一个非常友好的扩展点叫作 MonetaryOperator。MonetaryOperator 是一个函数式接口,它接收一个 MonetaryAmount 入参并返回一个新的 MonetaryAmount 对象。

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

MonetaryAmount tenPercentDollars = dollars.with(tenPercentOperator);

标准的 API 特性都是通过 MonetaryOperator 的接口来实现的。比方说,前面看到的舍入操作就是以 MonetaryOperator 接口的形式来提供的。

五、汇率

货币兑换率可以通过 ExchangeRateProvider 来获取。JavaMoney 自带了多个不同的 ExchangeRateProvider 的实现。其中最重要的两个是 ECBCurrentRateProvider 与 IMFRateProvider。

ECBCurrentRateProvider 查询的是欧洲中央银行 (European Central Bank,ECB) 的数据而 IMFRateProvider 查询的是国际货币基金组织(International Monetary Fund,IMF)的汇率。

e.g:

//get the default ExchangeRateProvider (CompoundRateProvider)  
ExchangeRateProvider exchangeRateProvider = MonetaryConversions.getExchangeRateProvider();

// get the names of the default provider chain
// [IDENT, ECB, IMF, ECB-HIST]
List<String> defaultProviderChain = MonetaryConversions.getDefaultProviderChain();

// get a specific ExchangeRateProvider (here ECB)
ExchangeRateProvider ecbExchangeRateProvider = MonetaryConversions.getExchangeRateProvider("ECB");

如果没有指定 ExchangeRateProvider 的话返回的就是 CompoundRateProvider。CompoundRateProvider 会将汇率转换请求委派给一个 ExchangeRateProvider 链并将第一个返回准确结果的提供商的数据返回。

ExchangeRate rate = exchangeRateProvider.getExchangeRate("EUR", "USD");

NumberValue factor = rate.getFactor(); // 1.2537 (at time writing)
CurrencyUnit baseCurrency = rate.getBaseCurrency(); // EUR
CurrencyUnit targetCurrency = rate.getCurrency(); // USD

六、币种转换

不同货币间的转换可以通过 ExchangeRateProvider 返回的 CurrencyConversions 来完成。

CurrencyConversion dollarConversion = MonetaryConversions.getConversion("USD");

CurrencyConversion ecbDollarConversion = ecbExchangeRateProvider.getCurrencyConversion("USD");

MonetaryAmount tenEuro = Money.of(10, "EUR");

MonetaryAmount inDollar = tenEuro.with(dollarConversion);

注意:CurrencyConversion 也实现了 MonetaryOperator 接口。正如其它操作一样,它也能通过 MonetaryAmount.with () 方法来调用。

七、类结构图

输入图片说明