JSR 354 определяет новый Java API для работы с деньгами и валютами, который планируется включить в Java 9. В этом посте мы рассмотрим текущее состояние эталонной реализации:
JavaMoney .
Как и мой пост об API даты / времени Java 8, этот пост будет в основном основан на коде, который показывает новый API.
Но прежде чем мы начнем, я хочу процитировать небольшой раздел из спецификации, который в значительной степени подводит итог мотивации для этого нового API:
Денежные значения являются ключевой особенностью многих приложений, однако JDK практически не поддерживает их. Существующий класс java.util.Currency является строго структурой, используемой для представления текущих валют ISO 4217, но не связанных значений или пользовательских валют. JDK также не поддерживает денежную арифметику или конвертацию валюты, а также стандартный тип значения для представления денежной суммы.
Если вы используете Maven, вы можете легко попробовать текущее состояние эталонной реализации, добавив следующую зависимость в ваш проект:
1
2
3
4
5
|
< dependency > < groupId >org.javamoney</ groupId > < artifactId >moneta</ artifactId > < version >0.9</ version > </ dependency > |
Все спецификации классов и интерфейсов находятся в пакете javax.money. *.
Мы начнем с двух основных интерфейсов CurrencyUnit и MonetaryAmount. После этого мы рассмотрим обменные курсы, конвертацию и форматирование валют.
CurrencyUnit и MonetaryAmount
CurrencyUnit моделирует валюту. CurrencyUnit очень похож на существующий класс java.util.Currency, за исключением того, что он допускает пользовательские реализации. Согласно спецификации должно быть возможно, что java.util.Currency реализует CurrencyUnit. Экземпляры CurrencyUnit можно получить с помощью фабрики MonetaryCurrencies:
1
2
3
4
5
6
7
|
// getting CurrencyUnits by currency code CurrencyUnit euro = MonetaryCurrencies.getCurrency( "EUR" ); CurrencyUnit usDollar = MonetaryCurrencies.getCurrency( "USD" ); // getting CurrencyUnits by locale CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN); CurrencyUnit canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA); |
MontetaryAmount представляет собой конкретное числовое представление денежной суммы. MonetaryAmount всегда привязан к CurrencyUnit. Как и CurrencyUnit, MonetaryAmount — это интерфейс, который поддерживает различные реализации. Реализации CurrencyUnit и MonetaryAmount должны быть неизменяемыми, поточно-ориентированными, сериализуемыми и сопоставимыми.
1
2
3
4
5
6
7
8
9
|
// 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. Деньги — это реализация по умолчанию, которая хранит числовые значения с помощью BigDecimal. FastMoney — альтернативная реализация, которая хранит суммы в длинных полях. Согласно документации, операции на FastMoney в 10-15 раз быстрее, чем на Money. Тем не менее, FastMoney ограничен размером и точностью типа long.
Обратите внимание, что Money и FastMoney являются классами, специфичными для реализации (расположены в org.javamoney.moneta. * Вместо javax.money. *). Если вы хотите избежать классов, специфичных для реализации, вы должны получить MonetaryAmountFactory для создания экземпляра MonetaryAmount:
1
2
3
4
|
MonetaryAmount specAmount = MonetaryAmounts.getDefaultAmountFactory() .setNumber( 123.45 ) .setCurrency( "USD" ) .create(); |
Два экземпляра MontetaryAmount считаются равными, если классы реализации, денежные единицы и числовые значения равны:
1
2
3
|
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 имеет различные методы, которые позволяют получить доступ к назначенной валюте, числовой сумме, ее точности и многим другим:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
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; |
Работа с MonetaryAmounts
Математические операции могут выполняться с помощью MonetaryAmount:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
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); |
Округление является еще одной важной частью при работе с деньгами. MonetaryAmounts можно округлить с помощью оператора округления:
1
2
3
4
|
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 |
Здесь 12,3456 долларов США округляются с округлением по умолчанию для этой валюты.
При работе с коллекциями MonetaryAmounts доступны некоторые полезные вспомогательные методы для фильтрации, сортировки и группировки. Эти методы могут использоваться вместе с Java 8 Stream API.
Рассмотрим следующую коллекцию:
1
2
3
4
5
6
|
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:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
CurrencyUnit yen = MonetaryCurrencies.getCurrency( "JPY" ); CurrencyUnit dollar = MonetaryCurrencies.getCurrency( "USD" ); // filter by currency, get only dollars // result is [USD 18, USD 7, USD 42] List<MonetaryAmount> onlyDollar = amounts.stream() .filter(MonetaryFunctions.isCurrency(dollar)) .collect(Collectors.toList()); // filter by currency, get only dollars and yen // [USD 18, USD 7, JPY 13.37, USD 42] List<MonetaryAmount> onlyDollarAndYen = amounts.stream() .filter(MonetaryFunctions.isCurrency(dollar, yen)) .collect(Collectors.toList()); |
Мы также можем отфильтровать MonetaryAmounts меньше или больше, чем определенный порог:
1
2
3
4
5
6
7
|
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()); |
Сортировка работает аналогично:
01
02
03
04
05
06
07
08
09
10
11
|
// 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()); |
Группировка функций:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
// Grouping by CurrencyUnit // {USD=[USD 42, USD 7, USD 18], EUR=[EUR 2], JPY=[JPY 13.37]} Map<CurrencyUnit, List<MonetaryAmount>> groupedByCurrency = amounts.stream() .collect(MonetaryFunctions.groupByCurrencyUnit()); // Grouping by summarizing MonetaryAmounts 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 также предоставляет функцию сокращения, которую можно использовать для получения максимума, минимума и суммы коллекции MonetaryAmount:
1
2
3
4
5
6
7
8
|
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()); // "EUR 29.5" |
Пользовательские операции MonetaryAmount
MonetaryAmount предоставляет красивую точку расширения под названием MonetaryOperator. MonetaryOperator — это функциональный интерфейс, который принимает MonetaryAmount в качестве входных данных и создает новый MonetaryAmount на основе входных данных.
01
02
03
04
05
06
07
08
09
10
11
12
|
// 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 |
Некоторые стандартные функции API реализованы как MonetaryOperator. Например, функции округления, которые мы видели выше, реализованы как MonetaryOperator.
Курсы валют
Курсы обмена валют можно получить с помощью ExchangeRateProvider. JavaMoney поставляется с несколькими различными реализациями ExchangeRateProvider. Двумя наиболее важными реализациями являются ECBCurrentRateProvider и IMFRateProvider.
ECBCurrentRateProvider запрашивает поток данных Европейского центрального банка (ЕЦБ) для получения текущих обменных курсов, в то время как IMFRateProvider использует курсы обмена Международного валютного фонда (МВФ).
1
2
3
4
5
6
7
8
9
|
// 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 делегирует запросы обменного курса в цепочку ExchangeRateProviders и возвращает результат от первого поставщика, который возвращает адекватный результат.
1
2
3
4
5
6
|
// 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 |
Обмен валюты
Конвертация между валютами осуществляется с помощью CurrencyConversions, которую можно получить у ExchangeRateProviders:
01
02
03
04
05
06
07
08
09
10
|
// 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) |
Обратите внимание, что CurrencyConversion реализует MonetaryOperator. Как и другие операторы, его можно применять с помощью MonetaryAmount.with ().
Форматирование и анализ
MonetaryAmounts может быть проанализирован / отформатирован из / в строку, используя MonetaryAmountFormat:
01
02
03
04
05
06
07
08
09
10
11
|
// 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 можно создавать собственные форматы:
1
2
3
4
5
6
7
8
9
|
// 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); |
Обратите внимание, что символ ((\ u00A) используется в качестве заполнителя валюты внутри строки шаблона.
Резюме
Мы рассмотрели многие части нового API Money and Currency. Реализация уже выглядит довольно солидно (но, безусловно, нужна дополнительная документация). Я с нетерпением жду, чтобы увидеть этот API в Java 9!
- Вы можете найти все примеры, показанные здесь, на GitHub .
Ссылка: | Изучаем Java 9 Money and Currency API (JSR 354) от нашего партнера по JCG Майкла Шаргага в блоге mscharhag, Programming and Stuff . |