Статьи

Изучение API денег и валюты Java 9 (JSR 354)

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 .