JSR 354 defines a new Java currency API, which is planned to be officially introduced in Java 9. In this article we will take a look at its reference implementation: the current progress of JavaMoney.
Just like my previous article on Java 8’s new date and time API, this article mainly demonstrates the usage of the new API through some code.
Before I start, I would like to use a paragraph to briefly summarize the purpose of this new set of APIs defined by the specification:
Monetary value is a key feature for many applications, but the JDK does not There is almost no support. Strictly speaking, the existing java.util.Currency class only represents a data structure of the current ISO 4217 currency, but has no associated values or custom currencies. JDK also has no built-in support for currency operations and conversions, let alone a standard type that can represent currency values.
If you are using Maven, you only need to add the following reference to the project to experience the current functions of the reference implementation:
<dependency> <groupId>org.javamoney</groupId> <artifactId>moneta</artifactId> <version>0.9</version> </dependency>
The classes and interfaces mentioned in the specification are all in javax.money.* Package underneath.
Let’s start with the two core interfaces CurrencyUnit and MonetaryAmount.
CurrencyUnit and MonetaryAmount
CurrencyUnit represents currency. It is somewhat similar to the current java.util.Currency class, except that it supports custom implementations. Judging from the specification definition, java.util.Currency can also implement this interface. Instances of CurrencyUnit can be obtained through the MonetaryCurrencies factory:
// 根据货币代码来获取货币单位 CurrencyUnit euro = MonetaryCurrencies.getCurrency("EUR"); CurrencyUnit usDollar = MonetaryCurrencies.getCurrency("USD"); // 根据国家及地区来获取货币单位 CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN); CurrencyUnit canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA);
MontetaryAmount represents the specific amount of a certain currency. Usually it is bound to a CurrencyUnit.
MontetaryAmount, like CurrencyUnit, is also an interface that supports multiple implementations.
The implementation of CurrencyUnit and MontetaryAmount must be immutable, thread-safe and comparable.
/ 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 and FastMoney are two implementations of MonetaryAmount in the JavaMoney library. Money is the default implementation, which uses BigDecimal to store amounts. FastMoney is an optional implementation that uses long type to store amounts. According to the documentation, operations on FastMoney are about 10 to 15 times faster than those on Money. However, the amount size and precision of FastMoney are limited to the long type.
Note that Money and FastMoney here are both specific implementation classes (they are under the org.javamoney.moneta.* package, not javax.money.*). If you do not want to specify a specific type, you can use MonetaryAmountFactory to generate an instance of MonetaryAmount:
MonetaryAmount specAmount = MonetaryAmounts.getDefaultAmountFactory() .setNumber(123.45) .setCurrency("USD") .create();
These two MontetaryAmount instances are considered equal if and only if the implementation class, currency unit, and value are all equal.
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 contains a wealth of methods that can be used to obtain specific currency, amount, precision, etc.:
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;
Usage of MonetaryAmount
You can perform arithmetic operations on 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);
The rounding operation is included in the amount conversion Very important part. MonetaryAmount can be rounded using the rounding operator:
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
Here 12.3456 US dollars will be converted according to the default rounding rules of the current currency.
When operating the MonetaryAmount collection, there are many practical tools and methods that can be used to filter, sort and group. These methods can also be used with Java 8's streams API.
Look at the following collection:
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"));
We can filter the amount based on CurrencyUnit:
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());
We can also filter out the amount greater than or less than a certain threshold:
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());
The sorting is similar:
// 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());
Also Grouping operation:
// 按货币单位进行分组 // {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
MonetaryFunctions also provides a reduction function, which can be used to obtain the maximum value, minimum value, and summation:
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()); //
Customized MonetaryAmount operation
MonetaryAmount also provides a very friendly extension point called MonetaryOperator. MonetaryOperator is a functional interface that receives a MonetaryAmount input parameter and returns a new MonetaryAmount object.
// 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
Standard API features are all implemented through the MonetaryOperator interface. For example, the rounding operation seen earlier is provided in the form of the MonetaryOperator interface.
Exchange rate
Currency exchange rate can be obtained through ExchangeRateProvider. JavaMoney comes with multiple different ExchangeRateProvider implementations. The two most important ones are ECBCurrentRateProvider and IMFRateProvider.
ECBCurrentRateProvider queries the data of the European Central Bank (ECB) and the IMFRateProvider queries the exchange rate of the International Monetary Fund (IMF).
// 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");
If ExchangeRateProvider is not specified, CompoundRateProvider will be returned. The CompoundRateProvider will delegate exchange rate conversion requests to a chain of ExchangeRateProvider and return data from the first provider that returns accurate results.
// 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
Currency Conversion
Conversion between different currencies can be completed through the CurrencyConversions returned by ExchangeRateProvider.
// get the CurrencyConversion from the default provider chain CurrencyConversion dollarConversion = MonetaryConversions.getConversion("USD"); // get the CurrencyConversion from a specific provider CurrencyConversion ecbDollarConversion = ecbExchangeRateProvider.getCurrencyConversion("USD"); MonetaryAmount tenEuro = Money.of(10, "EUR"); // convert 10 euro to us dollar MonetaryAmount inDollar = tenEuro.with(dollarConversion); // "USD 12.537" (at the time writing)
Please note that CurrencyConversion also implements the MonetaryOperator interface. Like other operations, it can also be called through the MonetaryAmount.with() method.
Formatting and parsing
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);
注意,这里的¤符号在模式串中是作为货币的占位符。