JSR 354定義了一套新的Java貨幣API,計劃會在Java 9中正式引入。本文中我們將來看看它的參考實作:JavaMoney的當前進展。
正如我在之前那篇Java 8新的日期時間API一文中那樣,本文主要也是透過一些程式碼來示範下新的API的用法 。
在開始之前,我想先用一段話來簡短地總結一下規範定義的這套新的API的用意何在:
對許多應用而言貨幣價值都是一個關鍵的特性,但JDK對此卻幾乎沒有任何支持。嚴格來講,現有的java.util.Currency類別只是代表了目前ISO 4217貨幣的一個資料結構,但並沒有關聯的值或自訂貨幣。 JDK對貨幣的運算及轉換也沒有內建的支持,更別說有一個能夠代表貨幣值的標準類型了。
如果你用的是Maven的話,只需把下面的引用添加到工裡面便能夠體驗下該參考實現的當前功能了:
<dependency> <groupId>org.javamoney</groupId> <artifactId>moneta</artifactId> <version>0.9</version> </dependency>
規範中提到的類及接口都在javax.money.*包下面。
我們先從核心的兩個介面CurrencyUnit與MonetaryAmount開始講起。
CurrencyUnit及MonetaryAmount
CurrencyUnit代表的是貨幣。它有點類似現在的java.util.Currency類,不同之處在於它支援自訂的實作。從規範的定義來看,java.util.Currency也是可以實現該介面的。 CurrencyUnit的實例可以透過MonetaryCurrencies工廠來取得:
// 根据货币代码来获取货币单位 CurrencyUnit euro = MonetaryCurrencies.getCurrency("EUR"); CurrencyUnit usDollar = MonetaryCurrencies.getCurrency("USD"); // 根据国家及地区来获取货币单位 CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN); CurrencyUnit canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA);
MontetaryAmount代表的是某種貨幣的具體金額。通常它都會與某個CurrencyUnit綁定。
MontetaryAmount和CurrencyUnit一樣,也是一個能支援多種實作的介面。
CurrencyUnit與MontetaryAmount的實作必須是不可變,執行緒安全且可比較的。
/ get MonetaryAmount from CurrencyUnit CurrencyUnit euro = MonetaryCurrencies.getCurrency("EUR"); MonetaryAmount fiveEuro = Money.of(5, euro); // get MonetaryAmount from currency code MonetaryAmount tenUsDollar = Money.of(10, "USD"); // FastMoney is an alternative MonetaryAmount factory that focuses on performance MonetaryAmount sevenEuro = FastMoney.of(7, euro);
Money與FastMoney是JavaMoney庫中MonetaryAmount的兩種實作。 Money是預設實現,它使用BigDecimal來儲存金額。 FastMoney是可選的另一個實現,它用long類型來儲存金額。根據文檔來看,FastMoney上的操作要比Money的快10到15倍左右。然而,FastMoney的金額大小與精確度都受限於long類型。
注意了,這裡的Money和FastMoney都是具體的實作類別(它們在org.javamoney.moneta.*套件下面,而不是javax.money.*)。如果你不希望指定具體類型的話,可以透過MonetaryAmountFactory來產生一個MonetaryAmount的實例:
MonetaryAmount specAmount = MonetaryAmounts.getDefaultAmountFactory() .setNumber(123.45) .setCurrency("USD") .create();
當且僅當實現類,貨幣單位,以及數值全部相等時才認為這兩個MontetaryAmount實例是相等的。
MonetaryAmount oneEuro = Money.of(1, MonetaryCurrencies.getCurrency("EUR")); boolean isEqual = oneEuro.equals(Money.of(1, "EUR")); // true boolean isEqualFast = oneEuro.equals(FastMoney.of(1, "EUR")); // false
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;
MonetaryAmount的使用
可以在MonetaryAmount上進行算術運算:非常重要的一部分。 MonetaryAmount可以使用捨去運算子來進行四捨五入:
MonetaryAmount twelveEuro = fiveEuro.add(sevenEuro); // "EUR 12" MonetaryAmount twoEuro = sevenEuro.subtract(fiveEuro); // "EUR 2" MonetaryAmount sevenPointFiveEuro = fiveEuro.multiply(1.5); // "EUR 7.5" // MonetaryAmount can have a negative NumberValue MonetaryAmount minusTwoEuro = fiveEuro.subtract(sevenEuro); // "EUR -2" // some useful utility methods boolean greaterThan = sevenEuro.isGreaterThan(fiveEuro); // true boolean positive = sevenEuro.isPositive(); // true boolean zero = sevenEuro.isZero(); // false // Note that MonetaryAmounts need to have the same CurrencyUnit to do mathematical operations // this fails with: javax.money.MonetaryException: Currency mismatch: EUR/USD fiveEuro.add(tenUsDollar);
這裡12.3456美金就會依照目前貨幣預設的捨入規則來進行換算。
在操作MonetaryAmount集合時,有許多實用的工具方法可以用來進行過濾,排序以及分組。這些方法也可以與Java 8的串流API一起搭配使用。
看一下下面這個集合:
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
我們可以根據CurrencyUnit來進行金額過濾:
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"));
我們也可以過濾出大於或小於某個閾值的金額:
CurrencyUnit yen = MonetaryCurrencies.getCurrency("JPY"); CurrencyUnit dollar = MonetaryCurrencies.getCurrency("USD"); // 根据货币过滤,只返回美金 // result is [USD 18, USD 7, USD 42] List<MonetaryAmount> onlyDollar = amounts.stream() .filter(MonetaryFunctions.isCurrency(dollar)) .collect(Collectors.toList()); // 根据货币过滤,只返回美金和日元 // [USD 18, USD 7, JPY 13.37, USD 42] List<MonetaryAmount> onlyDollarAndYen = amounts.stream() .filter(MonetaryFunctions.isCurrency(dollar, yen)) .collect(Collectors.toList());
我們也可以過濾出大於或小於某個閾值的金額:
MonetaryAmount tenDollar = Money.of(10, dollar); // [USD 42, USD 18] List<MonetaryAmount> greaterThanTenDollar = amounts.stream() .filter(MonetaryFunctions.isCurrency(dollar)) .filter(MonetaryFunctions.isGreaterThan(tenDollar)) .collect(Collectors.toList());
排序也是類似的:分組操作:
// Sorting dollar values by number value // [USD 7, USD 18, USD 42] List<MonetaryAmount> sortedByAmount = onlyDollar.stream() .sorted(MonetaryFunctions.sortNumber()) .collect(Collectors.toList()); // Sorting by CurrencyUnit // [EUR 2, JPY 13.37, USD 42, USD 7, USD 18] List<MonetaryAmount> sortedByCurrencyUnit = amounts.stream() .sorted(MonetaryFunctions.sortCurrencyUnit()) .collect(Collectors.toList());
MonetaryFunctions還提供了歸約函數,可以用來獲取最大值,最小值,以及求和:
// 按货币单位进行分组 // {USD=[USD 42, USD 7, USD 18], EUR=[EUR 2], JPY=[JPY 13.37]} Map<CurrencyUnit, List<MonetaryAmount>> groupedByCurrency = amounts.stream() .collect(MonetaryFunctions.groupByCurrencyUnit()); // 分组并进行汇总 Map<CurrencyUnit, MonetarySummaryStatistics> summary = amounts.stream() .collect(MonetaryFunctions.groupBySummarizingMonetary()).get(); // get summary for CurrencyUnit USD MonetarySummaryStatistics dollarSummary = summary.get(dollar); MonetaryAmount average = dollarSummary.getAverage(); // "USD 22.333333333333333333.." MonetaryAmount min = dollarSummary.getMin(); // "USD 7" MonetaryAmount max = dollarSummary.getMax(); // "USD 42" MonetaryAmount sum = dollarSummary.getSum(); // "USD 67" long count = dollarSummary.getCount(); // 3
自訂的MonetaryAmount操作
MonetaryAmount還提供了一個非常友好的擴展點作MonetaryOperator。 MonetaryOperator是一個函數式接口,它接收一個MonetaryAmount入參並傳回一個新的MonetaryAmount物件。
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 7.5" Optional<MonetaryAmount> min = amounts.stream().reduce(MonetaryFunctions.min()); // "EUR 12" Optional<MonetaryAmount> sum = amounts.stream().reduce(MonetaryFunctions.sum()); //
標準的API特性都是透過MonetaryOperator的介面來實現的。比方說,前面看到的捨去操作就是以MonetaryOperator介面的形式來提供的。
匯率
貨幣兌換率可以透過ExchangeRateProvider來取得。 JavaMoney自備了多個不同的ExchangeRateProvider的實作。其中最重要的兩個是ECBCurrentRateProvider與 IMFRateProvider。
ECBCurrentRateProvider查詢的是歐洲中央銀行(European Central Bank,ECB)的資料而IMFRateProvider查詢的是國際貨幣基金組織(International Monetary Fund,IMF)的匯率。
// 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
如果沒有指定ExchangeRateProvider的話回傳的就是CompoundRateProvider。 CompoundRateProvider會將匯率轉換請求委派給一個ExchangeRateProvider鏈並將第一個傳回準確結果的提供者的資料傳回。
// 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回傳的CurrencyConversions來完成。
// get the exchange rate from euro to us dollar 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
請注意CurrencyConversion也實作了MonetaryOperator介面。正如其它操作一樣,它也能透過MonetaryAmount.with()方法來呼叫。
格式化及解析
MonetaryAmount可以通过MonetaryAmountFormat来与字符串进行解析/格式化。
// formatting by locale specific formats MonetaryAmountFormat germanFormat = MonetaryFormats.getAmountFormat(Locale.GERMANY); MonetaryAmountFormat usFormat = MonetaryFormats.getAmountFormat(Locale.CANADA); MonetaryAmount amount = Money.of(12345.67, "USD"); String usFormatted = usFormat.format(amount); // "USD12,345.67" String germanFormatted = germanFormat.format(amount); // 12.345,67 USD // A MonetaryAmountFormat can also be used to parse MonetaryAmounts from strings MonetaryAmount parsed = germanFormat.parse("12,4 USD");
可以通过AmountFormatQueryBuilder来生成自定义的格式。
// Creating a custom MonetaryAmountFormat MonetaryAmountFormat customFormat = MonetaryFormats.getAmountFormat( AmountFormatQueryBuilder.of(Locale.US) .set(CurrencyStyle.NAME) .set("pattern", "00,00,00,00.00 ¤") .build()); // results in "00,01,23,45.67 US Dollar" String formatted = customFormat.format(amount);
注意,这里的¤符号在模式串中是作为货币的占位符。