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
- Type Safety: Compile-time checking of monetary operations
- Immutability: Thread-safe operations
- Extensibility: Custom currencies and rounding rules
- Internationalization: Built-in support for global currencies
- Precision: Accurate decimal arithmetic
- 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.