Java Money API (JSR 354): Modern Monetary Operations in Java

The Java Money API (JSR 354) provides a standardized and flexible framework for handling monetary amounts, currencies, and financial calculations in Java applications. It addresses the limitations of using BigDecimal directly and provides a robust solution for financial applications.

Overview and Key Concepts

JSR 354 introduces several core concepts:

  • MonetaryAmount: Represents a monetary value with its currency
  • CurrencyUnit: Represents a currency without amount
  • ExchangeRate: Handles currency conversion
  • MonetaryRounding: Defines rounding rules for monetary operations

Dependencies and Setup

Maven Dependencies

<dependency>
<groupId>org.javamoney</groupId>
<artifactId>moneta</artifactId>
<version>1.4.4</version>
</dependency>
<!-- Optional: for currency conversion -->
<dependency>
<groupId>org.javamoney</groupId>
<artifactId>moneta-conversion</artifactId>
<version>1.4.4</version>
</dependency>

Gradle Dependencies

implementation 'org.javamoney:moneta:1.4.4'
implementation 'org.javamoney:moneta-conversion:1.4.4'

Basic Usage and Core Classes

Example 1: Creating Monetary Amounts

import javax.money.Monetary;
import javax.money.MonetaryAmount;
import javax.money.CurrencyUnit;
import java.util.Locale;
public class MoneyBasics {
public static void main(String[] args) {
// Different ways to create currency units
CurrencyUnit usd = Monetary.getCurrency("USD");
CurrencyUnit eur = Monetary.getCurrency(Locale.GERMANY);
CurrencyUnit jpy = Monetary.getCurrency("JPY");
System.out.println("USD: " + usd.getCurrencyCode() + ", " + 
usd.getNumericCode() + ", " + usd.getDefaultFractionDigits());
System.out.println("EUR: " + eur.getCurrencyCode());
System.out.println("JPY: " + jpy.getCurrencyCode() + ", " + 
jpy.getDefaultFractionDigits() + " fraction digits");
// Creating monetary amounts
MonetaryAmount amount1 = Monetary.getDefaultAmountFactory()
.setCurrency(usd)
.setNumber(123.45)
.create();
MonetaryAmount amount2 = Monetary.getDefaultAmountFactory()
.setCurrency("EUR")
.setNumber(500)
.create();
// Using factory methods (Moneta implementation)
MonetaryAmount amount3 = Money.of(99.99, "USD");
MonetaryAmount amount4 = Money.of(1000, "JPY");
MonetaryAmount amount5 = Money.of(250, eur);
System.out.println("\nAmounts created:");
System.out.println("Amount 1: " + amount1);
System.out.println("Amount 2: " + amount2);
System.out.println("Amount 3: " + amount3);
System.out.println("Amount 4: " + amount4);
System.out.println("Amount 5: " + amount5);
// Accessing amount details
System.out.println("\nAmount details:");
System.out.println("Currency: " + amount1.getCurrency());
System.out.println("Number: " + amount1.getNumber());
System.out.println("Number value: " + amount1.getNumber().doubleValue());
}
}

Example 2: Arithmetic Operations

import javax.money.Monetary;
import javax.money.MonetaryAmount;
import javax.money.MonetaryOperator;
import javax.money.MonetaryQuery;
import org.javamoney.moneta.Money;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class MoneyArithmetic {
public static void main(String[] args) {
MonetaryAmount price = Money.of(100, "USD");
MonetaryAmount tax = Money.of(15, "USD");
MonetaryAmount discount = Money.of(10, "USD");
// Basic arithmetic operations
MonetaryAmount subtotal = price.add(tax);
MonetaryAmount total = subtotal.subtract(discount);
MonetaryAmount doubled = price.multiply(2);
MonetaryAmount half = price.divide(2);
System.out.println("Price: " + price);
System.out.println("Tax: " + tax);
System.out.println("Discount: " + discount);
System.out.println("Subtotal: " + subtotal);
System.out.println("Total: " + total);
System.out.println("Doubled: " + doubled);
System.out.println("Half: " + half);
// More complex calculations
MonetaryAmount quantity = Money.of(3, "USD");
MonetaryAmount unitPrice = Money.of(25.50, "USD");
MonetaryAmount extendedPrice = unitPrice.multiply(quantity.getNumber());
MonetaryAmount withTax = extendedPrice.multiply(1.08); // 8% tax
System.out.println("\nComplex calculations:");
System.out.println("Unit Price: " + unitPrice);
System.out.println("Quantity: " + quantity.getNumber());
System.out.println("Extended Price: " + extendedPrice);
System.out.println("With Tax (8%): " + withTax);
// Using BigDecimal for precise calculations
MonetaryAmount preciseAmount = Money.of(new BigDecimal("123.456789"), "USD");
System.out.println("\nPrecise amount: " + preciseAmount);
// Custom monetary operators
MonetaryOperator tenPercentDiscount = amount -> {
BigDecimal discountAmount = amount.getNumber()
.numberValue(BigDecimal.class)
.multiply(new BigDecimal("0.10"));
return amount.subtract(Money.of(discountAmount, amount.getCurrency()));
};
MonetaryAmount original = Money.of(200, "USD");
MonetaryAmount discounted = original.with(tenPercentDiscount);
System.out.println("\nDiscount calculation:");
System.out.println("Original: " + original);
System.out.println("After 10% discount: " + discounted);
}
}

Advanced Monetary Operations

Example 3: Currency Conversion

import javax.money.Monetary;
import javax.money.MonetaryAmount;
import javax.money.CurrencyUnit;
import javax.money.ConvertibleMoney;
import javax.money.convert.CurrencyConversion;
import javax.money.convert.ExchangeRateProvider;
import javax.money.convert.MonetaryConversions;
import org.javamoney.moneta.Money;
import org.javamoney.moneta.convert.ExchangeRateType;
import java.util.List;
public class CurrencyConversionDemo {
public static void main(String[] args) {
MonetaryAmount usdAmount = Money.of(100, "USD");
MonetaryAmount eurAmount = Money.of(85, "EUR");
MonetaryAmount jpyAmount = Money.of(10000, "JPY");
System.out.println("Original amounts:");
System.out.println("USD: " + usdAmount);
System.out.println("EUR: " + eurAmount);
System.out.println("JPY: " + jpyAmount);
// Get available exchange rate providers
List<String> providers = MonetaryConversions.getConversionProviderNames();
System.out.println("\nAvailable exchange rate providers: " + providers);
try {
// Using default conversion provider
CurrencyConversion usdToEur = MonetaryConversions.getConversion("EUR");
MonetaryAmount convertedUsdToEur = usdAmount.with(usdToEur);
CurrencyConversion eurToUsd = MonetaryConversions.getConversion("USD");
MonetaryAmount convertedEurToUsd = eurAmount.with(eurToUsd);
System.out.println("\nCurrency conversions:");
System.out.println(usdAmount + " = " + convertedUsdToEur);
System.out.println(eurAmount + " = " + convertedEurToUsd);
// Using specific exchange rate provider
ExchangeRateProvider ecbProvider = MonetaryConversions.getExchangeRateProvider("ECB");
CurrencyConversion customConversion = ecbProvider.getCurrencyConversion("GBP");
MonetaryAmount usdToGbp = usdAmount.with(customConversion);
System.out.println("Using ECB provider:");
System.out.println(usdAmount + " = " + usdToGbp);
} catch (Exception e) {
System.out.println("Conversion error: " + e.getMessage());
System.out.println("Note: Real conversion requires internet connection and available providers");
}
// Manual conversion with fixed rates (for testing/demo)
manualConversionDemo();
}
private static void manualConversionDemo() {
System.out.println("\nManual conversion with fixed rates:");
// Fixed exchange rates for demo
MonetaryAmount usdAmount = Money.of(100, "USD");
// Convert USD to EUR (1 USD = 0.85 EUR)
MonetaryAmount eurConverted = Money.of(
usdAmount.getNumber().numberValue(BigDecimal.class).multiply(new BigDecimal("0.85")), 
"EUR"
);
// Convert USD to JPY (1 USD = 110 JPY)
MonetaryAmount jpyConverted = Money.of(
usdAmount.getNumber().numberValue(BigDecimal.class).multiply(new BigDecimal("110")), 
"JPY"
);
System.out.println(usdAmount + " = " + eurConverted + " (manual rate: 1 USD = 0.85 EUR)");
System.out.println(usdAmount + " = " + jpyConverted + " (manual rate: 1 USD = 110 JPY)");
}
}

Example 4: Rounding and Precision

import javax.money.Monetary;
import javax.money.MonetaryAmount;
import javax.money.MonetaryRounding;
import javax.money.RoundingQueryBuilder;
import javax.money.RoundingContext;
import org.javamoney.moneta.Money;
import org.javamoney.moneta.rounding.MonetaryRoundings;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Set;
public class MoneyRounding {
public static void main(String[] args) {
MonetaryAmount preciseAmount = Money.of(new BigDecimal("123.456789"), "USD");
MonetaryAmount jpyAmount = Money.of(new BigDecimal("123.456789"), "JPY");
System.out.println("Original amounts:");
System.out.println("USD: " + preciseAmount);
System.out.println("JPY: " + jpyAmount);
// Default rounding (based on currency)
MonetaryAmount defaultRounded = preciseAmount.with(Monetary.getDefaultRounding());
MonetaryAmount jpyDefaultRounded = jpyAmount.with(Monetary.getDefaultRounding());
System.out.println("\nDefault rounding:");
System.out.println("USD: " + defaultRounded);
System.out.println("JPY: " + jpyDefaultRounded);
// Available rounding types
Set<String> roundingIds = MonetaryRoundings.getRoundingNames();
System.out.println("\nAvailable rounding types: " + roundingIds);
// Custom rounding
MonetaryRounding cashRounding = MonetaryRoundings.getRounding(RoundingQueryBuilder.of()
.setScale(2)
.set(RoundingMode.HALF_UP)
.build());
MonetaryRounding noRounding = MonetaryRoundings.getRounding(RoundingQueryBuilder.of()
.setScale(6)
.set(RoundingMode.HALF_UP)
.build());
MonetaryAmount cashRounded = preciseAmount.with(cashRounding);
MonetaryAmount noRound = preciseAmount.with(noRounding);
System.out.println("\nCustom rounding:");
System.out.println("Original: " + preciseAmount);
System.out.println("Cash rounding (2 decimals): " + cashRounded);
System.out.println("No rounding (6 decimals): " + noRound);
// Financial rounding examples
financialRoundingExamples();
}
private static void financialRoundingExamples() {
System.out.println("\nFinancial rounding examples:");
MonetaryAmount[] amounts = {
Money.of(123.456, "USD"),
Money.of(123.454, "USD"),
Money.of(123.455, "USD"),
Money.of(123.445, "USD")
};
MonetaryRounding halfUp = MonetaryRoundings.getRounding(" rounding:halfUp");
MonetaryRounding halfEven = MonetaryRoundings.getRounding(" rounding:halfEven");
for (MonetaryAmount amount : amounts) {
MonetaryAmount roundedHalfUp = amount.with(halfUp);
MonetaryAmount roundedHalfEven = amount.with(halfEven);
System.out.println(String.format("%s -> HALF_UP: %s, HALF_EVEN: %s", 
amount, roundedHalfUp, roundedHalfEven));
}
}
}

Real-World Applications

Example 5: Shopping Cart Implementation

import javax.money.Monetary;
import javax.money.MonetaryAmount;
import org.javamoney.moneta.Money;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class ShoppingCart {
private final List<CartItem> items = new ArrayList<>();
private final String currency;
public ShoppingCart(String currency) {
this.currency = currency;
}
public void addItem(String productName, double price, int quantity) {
MonetaryAmount unitPrice = Money.of(price, currency);
items.add(new CartItem(productName, unitPrice, quantity));
}
public void addItem(String productName, MonetaryAmount unitPrice, int quantity) {
// Ensure currency consistency
if (!unitPrice.getCurrency().getCurrencyCode().equals(currency)) {
throw new IllegalArgumentException(
"Item currency " + unitPrice.getCurrency() + 
" doesn't match cart currency " + currency);
}
items.add(new CartItem(productName, unitPrice, quantity));
}
public MonetaryAmount getSubtotal() {
MonetaryAmount subtotal = Money.zero(currency);
for (CartItem item : items) {
subtotal = subtotal.add(item.getTotalPrice());
}
return subtotal;
}
public MonetaryAmount getTotalWithTax(double taxRate) {
MonetaryAmount subtotal = getSubtotal();
BigDecimal taxMultiplier = BigDecimal.ONE.add(BigDecimal.valueOf(taxRate / 100));
return subtotal.multiply(taxMultiplier);
}
public MonetaryAmount getTotalWithTax(MonetaryAmount taxAmount) {
return getSubtotal().add(taxAmount);
}
public void printReceipt() {
System.out.println("=== SHOPPING CART RECEIPT ===");
System.out.println("Currency: " + currency);
System.out.println("\nItems:");
for (CartItem item : items) {
System.out.println(String.format("  %-20s %3d x %8s = %10s", 
item.getProductName(),
item.getQuantity(),
item.getUnitPrice(),
item.getTotalPrice()));
}
System.out.println("\nSummary:");
System.out.println("Subtotal: " + getSubtotal());
System.out.println("Tax (8%): " + getTotalWithTax(8).subtract(getSubtotal()));
System.out.println("Total: " + getTotalWithTax(8));
}
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart("USD");
cart.addItem("Laptop", 999.99, 1);
cart.addItem("Mouse", 25.50, 2);
cart.addItem("Keyboard", 75.00, 1);
cart.addItem("Monitor", 299.99, 1);
cart.printReceipt();
}
private static class CartItem {
private final String productName;
private final MonetaryAmount unitPrice;
private final int quantity;
public CartItem(String productName, MonetaryAmount unitPrice, int quantity) {
this.productName = productName;
this.unitPrice = unitPrice;
this.quantity = quantity;
}
public MonetaryAmount getTotalPrice() {
return unitPrice.multiply(quantity);
}
public String getProductName() { return productName; }
public MonetaryAmount getUnitPrice() { return unitPrice; }
public int getQuantity() { return quantity; }
}
}

Example 6: Financial Portfolio

import javax.money.Monetary;
import javax.money.MonetaryAmount;
import org.javamoney.moneta.Money;
import java.math.BigDecimal;
import java.util.*;
public class FinancialPortfolio {
private final Map<String, PortfolioItem> holdings = new HashMap<>();
private final String baseCurrency;
public FinancialPortfolio(String baseCurrency) {
this.baseCurrency = baseCurrency;
}
public void addHolding(String symbol, int shares, MonetaryAmount purchasePrice) {
holdings.put(symbol, new PortfolioItem(symbol, shares, purchasePrice));
}
public MonetaryAmount getCurrentValue(Map<String, MonetaryAmount> currentPrices) {
MonetaryAmount totalValue = Money.zero(baseCurrency);
for (PortfolioItem item : holdings.values()) {
MonetaryAmount currentPrice = currentPrices.get(item.getSymbol());
if (currentPrice != null) {
// Convert to base currency if necessary
MonetaryAmount convertedPrice = currentPrice;
if (!currentPrice.getCurrency().getCurrencyCode().equals(baseCurrency)) {
// In real application, use proper currency conversion
convertedPrice = convertCurrency(currentPrice, baseCurrency);
}
MonetaryAmount positionValue = convertedPrice.multiply(item.getShares());
totalValue = totalValue.add(positionValue);
}
}
return totalValue;
}
public MonetaryAmount getTotalCost() {
MonetaryAmount totalCost = Money.zero(baseCurrency);
for (PortfolioItem item : holdings.values()) {
MonetaryAmount purchasePrice = item.getPurchasePrice();
MonetaryAmount convertedPrice = purchasePrice;
if (!purchasePrice.getCurrency().getCurrencyCode().equals(baseCurrency)) {
convertedPrice = convertCurrency(purchasePrice, baseCurrency);
}
MonetaryAmount positionCost = convertedPrice.multiply(item.getShares());
totalCost = totalCost.add(positionCost);
}
return totalCost;
}
public Map<String, Object> getPerformance(Map<String, MonetaryAmount> currentPrices) {
MonetaryAmount totalCost = getTotalCost();
MonetaryAmount currentValue = getCurrentValue(currentPrices);
MonetaryAmount gainLoss = currentValue.subtract(totalCost);
BigDecimal percentageChange = gainLoss.getNumber()
.numberValue(BigDecimal.class)
.divide(totalCost.getNumber().numberValue(BigDecimal.class), 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"));
Map<String, Object> performance = new HashMap<>();
performance.put("totalCost", totalCost);
performance.put("currentValue", currentValue);
performance.put("gainLoss", gainLoss);
performance.put("percentageChange", percentageChange);
return performance;
}
private MonetaryAmount convertCurrency(MonetaryAmount amount, String targetCurrency) {
// Simplified conversion - in real application, use proper exchange rates
Map<String, BigDecimal> conversionRates = Map.of(
"USD-EUR", new BigDecimal("0.85"),
"EUR-USD", new BigDecimal("1.18"),
"USD-JPY", new BigDecimal("110.0"),
"JPY-USD", new BigDecimal("0.0091")
);
String conversionKey = amount.getCurrency().getCurrencyCode() + "-" + targetCurrency;
BigDecimal rate = conversionRates.get(conversionKey);
if (rate != null) {
return Money.of(
amount.getNumber().numberValue(BigDecimal.class).multiply(rate),
targetCurrency
);
}
throw new IllegalArgumentException("No conversion rate available for " + conversionKey);
}
public void printPortfolioReport(Map<String, MonetaryAmount> currentPrices) {
System.out.println("=== PORTFOLIO REPORT ===");
System.out.println("Base Currency: " + baseCurrency);
System.out.println();
System.out.println("Holdings:");
for (PortfolioItem item : holdings.values()) {
MonetaryAmount currentPrice = currentPrices.get(item.getSymbol());
MonetaryAmount positionValue = currentPrice != null ? 
currentPrice.multiply(item.getShares()) : Money.zero(baseCurrency);
System.out.println(String.format("  %-6s %4d shares %10s (current: %10s)", 
item.getSymbol(),
item.getShares(),
item.getPurchasePrice(),
positionValue));
}
System.out.println();
Map<String, Object> performance = getPerformance(currentPrices);
System.out.println("Performance:");
System.out.println("Total Cost: " + performance.get("totalCost"));
System.out.println("Current Value: " + performance.get("currentValue"));
System.out.println("Gain/Loss: " + performance.get("gainLoss"));
System.out.println("Change: " + performance.get("percentageChange") + "%");
}
public static void main(String[] args) {
FinancialPortfolio portfolio = new FinancialPortfolio("USD");
// Add holdings
portfolio.addHolding("AAPL", 10, Money.of(150.25, "USD"));
portfolio.addHolding("GOOGL", 5, Money.of(2750.00, "USD"));
portfolio.addHolding("SAP", 20, Money.of(125.75, "EUR")); // European stock
portfolio.addHolding("SONY", 15, Money.of(8500.00, "JPY")); // Japanese stock
// Current prices
Map<String, MonetaryAmount> currentPrices = Map.of(
"AAPL", Money.of(175.50, "USD"),
"GOOGL", Money.of(2850.25, "USD"),
"SAP", Money.of(135.20, "EUR"),
"SONY", Money.of(9200.00, "JPY")
);
portfolio.printPortfolioReport(currentPrices);
}
private static class PortfolioItem {
private final String symbol;
private final int shares;
private final MonetaryAmount purchasePrice;
public PortfolioItem(String symbol, int shares, MonetaryAmount purchasePrice) {
this.symbol = symbol;
this.shares = shares;
this.purchasePrice = purchasePrice;
}
public String getSymbol() { return symbol; }
public int getShares() { return shares; }
public MonetaryAmount getPurchasePrice() { return purchasePrice; }
}
}

Best Practices and Considerations

Configuration and Setup

import javax.money.Monetary;
import javax.money.MonetaryAmount;
import javax.money.CurrencyUnit;
import javax.money.format.MonetaryAmountFormat;
import javax.money.format.MonetaryFormats;
import org.javamoney.moneta.Money;
import org.javamoney.moneta.format.CurrencyStyle;
import java.util.Locale;
public class MoneyBestPractices {
public static void main(String[] args) {
// Formatting monetary amounts
MonetaryAmount amount = Money.of(1234.56, "USD");
// Default formatting
MonetaryAmountFormat defaultFormat = MonetaryFormats.getAmountFormat(Locale.US);
System.out.println("Default format: " + defaultFormat.format(amount));
// Custom formatting
MonetaryAmountFormat customFormat = MonetaryFormats.getAmountFormat(
MonetaryFormats.getAmountFormatNameBuilder(Locale.US)
.setStyle(CurrencyStyle.SYMBOL)
.build()
);
System.out.println("Symbol format: " + customFormat.format(amount));
// International formatting
MonetaryAmountFormat germanFormat = MonetaryFormats.getAmountFormat(Locale.GERMANY);
MonetaryAmount eurAmount = Money.of(1234.56, "EUR");
System.out.println("German format: " + germanFormat.format(eurAmount));
// Parsing monetary amounts
try {
MonetaryAmount parsed = defaultFormat.parse("USD 1,234.56");
System.out.println("Parsed amount: " + parsed);
} catch (Exception e) {
System.out.println("Parse error: " + e.getMessage());
}
}
// Validation utility methods
public static boolean isValidAmount(MonetaryAmount amount) {
return amount != null && 
amount.getNumber() != null && 
amount.getCurrency() != null &&
amount.getNumber().doubleValue() >= 0;
}
public static void validateCurrency(MonetaryAmount amount, String expectedCurrency) {
if (!amount.getCurrency().getCurrencyCode().equals(expectedCurrency)) {
throw new IllegalArgumentException(
"Expected currency " + expectedCurrency + 
" but got " + amount.getCurrency().getCurrencyCode());
}
}
}

Benefits of Java Money API

  1. Type Safety: Compile-time checking of monetary operations
  2. Immutability: Thread-safe operations
  3. Extensibility: Custom currencies and rounding rules
  4. Internationalization: Built-in support for global currencies
  5. Precision: Accurate decimal arithmetic
  6. Standardization: JSR standard ensures consistency

Common Use Cases

  • E-commerce applications
  • Financial trading systems
  • Accounting software
  • International business applications
  • Budgeting and personal finance apps
  • Point-of-sale systems

Conclusion

The Java Money API provides a robust, standardized solution for handling monetary values in Java applications. It addresses the shortcomings of using primitive types or BigDecimal directly by providing:

  • Type-safe monetary operations
  • Built-in currency support
  • Flexible rounding strategies
  • Internationalization capabilities
  • Extensible architecture

While there's a learning curve compared to using simple numeric types, the benefits for financial applications are significant: improved code clarity, reduced errors, and better maintainability. For any application dealing with money, the Java Money API is the recommended approach.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper