Java: Go for the Money – Währungen mit JSR 354
Mit der Standardisierung von Währungen und Geldbeträgen in Java befasst sich JSR 354. Ziel ist es, das Java Eco-System mit einem flexiblen und erweiterbaren API zu ergänzen, welches das Arbeiten mit monetären Beträgen einfacher, aber auch sicherer macht. Es löst das Problem unterschiedlicher numerischer Anforderungen, definiert Erweiterungspunkte für weitergehende Funktionen und stellt auch Funktionen für die Währungsumrechnung und Formatierung zur Verfügung.
JSR 354 [1] wurde im Frühjahr 2012 gestartet, ein Jahr später wurde der Early Draft Review veröffentlicht. Ursprünglich war es die Absicht, den JSR in Java 8 als Teil der Plattform zu integrieren. Es hat sich aber schnell gezeigt, dass dies aus verschiedenen Gründen kurzfristig nicht möglich ist. Somit wird der JSR als eigenständige Spezifikation getrieben. Im Schatten von Java EE 7 und Java 8 hat sich JSR 354 kontinuierlich weiterentwickelt, so dass der JSR hoffentlich bis Ende des Jahres finalisiert werden kann.
Währungen in Java: Ausgangslage und Motivation
Die meisten Java-Entwickler werden früher oder später in irgendeiner Form mit Geldbeträgen konfrontiert werden, da Geld doch ein weit verbreitetes Konzept darstellt. Somit dürfte den meisten auch die Klasse java.util.Currency im JDK ein Begriff sein. Diese ist seit Java 1.4 Bestandteil der Plattform und modelliert Währungen auf Basis von ISO-4217 [2]. Weiter unterstützt java.text.DecimalFormat auch bereits die Formatierung von Geldbeträgen. Wer jedoch etwas genauer hinschaut, wird bemerken, dass viele Anwendungsfälle nur ungenügend abgedeckt werden können:
So enthält die Klasse java.util.Currency nicht alle ISO Währungen, z.B. fehlen die Codes CHE (Schweizer Franken Euro) und CHW (Schweizer Franken WIR). Die unterschiedlichen Codes für US Dollar (USD, USS, USN) sind im Gegensatz dazu alle vorhanden. Seit Java 7 ist es auch mit vertretbarem Aufwand möglich, weitere Währungen hinzuzufügen, jedoch muss dazu eine sogenannte Java Extension implementiert werden (ein Java-Archiv, welches in den lib-Ordner der JRE installiert wird). Für viele Unternehmungen kein akzeptabler Weg. Auch ist es nicht möglich, Anwendungsfälle wie Mandantenfähigkeit, virtuelle Währungen oder kontext-abhängiges Verhalten im Java EE Umfeld zu modellieren. Weiter lässt die Formatierung von Geldbeträgen ebenfalls zu wünschen übrig. Die Klasse java.text.DecimalFormat bietet zwar viele grundlegende Funktionen an. Leider sind diese für viele Anwendungen aber zu starr, da die Klasse nur eine Unterscheidung im Muster für positive und negative Zahlen kennt. Viele Anwendungen erfordern jedoch dynamisches Verhalten, z.B. in Abhängigkeit der Größe des Geldbetrages. Auch lassen sich auch keine unterschiedlichen Gruppierungen definieren, wie sie z.B. bei Indischen Rupien nötig wären (z.B. INR 12,23,123.34). Die fehlende Thread Sicherheit der Formatklassen sei hier nur der Vollständigkeit halber erwähnt.
Eine Abstraktion für Geldbeträge fehlt sodann vollends. Natürlich kann man BigDecimal verwenden, aber jeder Entwickler muss selbst dafür sorgen, dass die Währung immer mit ihrem Betrag transportiert wird. Sollte sich jemand auf double als numerischen Typen abstützen, hat das rasch fehlerhafte Resultate zur Folge, da sich die Fließkomma-Arithmetik nicht für Finanzanwendungen eignet [3]. Dass weitergehende Funktionen wie Währungsumrechnung und finanzmathematisches Runden ebenfalls fehlen, ist letztendlich nichts als logische Konsequenz.
Java: Modellierung von Währungen
Wie erwähnt, basiert die bestehende Currency-Klasse auf ISO 4217 [2]. Im JSR ist intensiv diskutiert worden, welche Eigenschaften zusätzlich benötigt werden, um auch soziale, virtuelle und historische Währungen unterstützen zu können. Zudem hat ISO 4217 selbst Limitierungen: einerseits hinkt ein Standard typischerweise eine gewisse Zeit hinter der Realität her, andererseits gibt es bspw. auch Mehrdeutigkeiten (z.B. der Code CFA) oder nicht modellierte Aspekte (unterschiedliches Runden, Legal Tender, historische Währungen). Hinzu kommt noch, dass ausrangierte Codes nach einer gewissen Zeit neu vergeben werden können. Dennoch hat sich gezeigt, dass sich die Essenz einer Währung mit nur vier Methoden abbilden lässt. Diese sind mehrheitlich bereits in der bestehenden Currency-Klasse vorhanden:
public interface CurrencyUnit { String getCurrencyCode(); int getNumericCode(); int getDefaultFractionDigits(); CurrencyContext getCurrencyContext(); // neu }
Neu enthält eine Währung auch einen CurrencyContext, welcher es erlaubt, zusätzliche Metadaten mitzugeben, wie z.B. zeitliche/regionale Gültigkeit oder äquivalente Codes. Im Gegensatz zur aktuellen Currency-Klasse ist der Währungscode für nicht ISO-Währungen frei wählbar. Somit können auch eigene Codesysteme integriert oder Codes gruppiert werden. Alle folgenden Codes sind somit bspw. möglich:
ISO:CHF // ISO mit Gruppierung CS:23345 // Eigener Namespace mit Gruppierung 23345-1 // Eigener Code
Währungen selbst lassen sich vom MonetaryCurrencies-Singleton analog wie bei java.util.Currency beziehen:
CurrencyUnit eur = MonetaryCurrencies.getCurrency(“EUR”); Set<CurrencyUnit> usCurrencies = MonetaryCurrencies.getCurrencies(Locale.US);
Bemerkenswert ist der Unterschied zur bisherigen Currency-Klasse, in der immer nur eine Währung für eine Locale zurückgegeben wird. Das API von JSR 354 unterstützt generell wesentlich komplexere Szenarien, in dem eine CurrencyQuery mit beliebigen Attributen übergeben werden kann. Somit könnten z.B. alle europäischen Währungen, welche im Jahre 1970 gültig waren, wie folgt abgefragt werden:
Set<CurrencyUnit> currencies = MonetaryCurrencies .getCurrencies(CurrencyQueryBuilder.of() .set(“continent”, “Europe”) .set(“year”,1970).build());
Im Hintergrund arbeitet ein zugehöriges SPI, wobei mehrere Provider entsprechend passende Währungen liefern können. Die Abfragemöglichkeiten hängen dabei einzig von den registrierten Providern ab. Für weitere Details verweisen wir auf die Dokumentation [4] und die Referenz-Implementation [5].
Java: Modellierung von monetären Beträgen
Intuitiv würde man wohl einfach eine Währung mit einem numerischen Wert, z.B. BigDecimal zu einem neuen immutable-Werttypen verbinden. Aber leider hat sich gezeigt, dass dieses Modell an der enormen Bandbreite an Anforderungen an einen Betragstypen scheitert. So muss ein idealer Typ extrem kurze Rechenzeiten bei geringem Speicherverbrauch garantieren (Handel), dafür können aber gewisse Abstriche bei den numerischen Fähigkeiten in Kauf genommen werden (z.B. Nachkommastellen). Hingegen benötigen Produkterechnungen oft sehr hohe Genauigkeiten, wobei die Laufzeit weit weniger kritisch ist. Risikoberechnungen oder Statistiken hingegen produzieren sehr große Zahlen.
Zusammenfassend wird es kaum möglich sein, alle Anforderungen mit nur einem einzigen Implementationstypen abzudecken. Aus diesem Grunde haben wir uns im JSR dazu entschieden, mehrere Implementationen gleichzeitig zu unterstützen. Ein Betrag wird dabei mit dem MonetaryAmount-Interface modelliert. Es wurden Interoperabilitätsregeln definiert, welche verhindern, dass unerwartete Rundungsfehler auftreten. Damit diese sinnvoll implementiert werden können, wurde dem Geldbetrag nebst Währung und numerischem Wert ein sogenannter MonetaryContext hinzugefügt. Dieser kann die numerischen Grenzen einer Implementation (Präzision und Skalierung) und weitere Eigenschaften aufnehmen:
public interface MonetaryAmount extends CurrencySupplier, NumberSupplier, Comparable<MonetaryAmount> { CurrencyUnit getCurrency(); NumberValue getNumber(); MonetaryContext getMonetaryContext(); <R> R query(MonetaryQuery<R> query){ MonetaryAmount with(MonetaryOperator operator); MonetaryAmountFactory<? extends MonetaryAmount> getFactory(); … }
Der numerische Wert eines Betrages wird als javax.money.NumberValue zurückgegeben: NumberValue erweitert java.lang.Number mit weiteren Funktionen um den numerischen Wert korrekt und verlustfrei exportieren zu können. Die beiden Methoden with und query definieren Erweiterungspunkte, um zusätzliche, externe Funktionen wie z.B. Runden, Währungsumrechnung oder Prädikate mit einem Geldbetrag zu kombinieren. MonetaryAmount bietet auch Operationen, um Beträge miteinander zu vergleichen oder arithmetische Operationen analog zu BigDecimal anzuwenden. Schließlich stellt jeder Betrag eine MonetaryAmountFactory zur Verfügung, dank welcher auf Basis des gleichen Implementationstypen beliebige weitere Beträge erzeugt werden können.
Java: Erzeugen von monetären Beträgen
Geldbeträge werden mit Hilfe einer MonetaryAmountFactory erzeugt. Nebst einem konkreten Betrag können Factory-Instanzen auch über das MonetaryAmounts-Singleton bezogen werden. Im einfachsten Fall benutzt man die (konfigurierbare) default Factory:
MonetaryAmount amt = MonetaryAmounts.getDefaultAmountFactory() .setCurrency(“EUR”) .setNumber(200.5).create();
Auch kann man eine MonetaryAmountFactory direkt adressieren, indem man den gewünschten Implementationstypen als Parameter an MonetaryAmounts übergibt. Analog zu Währungen können passende Factories oder Implementationstypen aber auch mit Hilfe einer MonetaryAmountFactoryQuery abgefragt werden:
MonetaryAmountFactory<?> factory = MonetaryAmounts .getAmountFactory(MonetaryAmountFactoryQueryBuilder.of() .setPrecision(200).setMaxScale(10).build());
Java: Runden von monetären Beträgen
Jeder Instanz von MonetaryAmount kann eine Instanz eines MonetaryOperator übergeben werden, um beliebige weitere externe Funktionen auf einem Betrag auszuführen (Methode with):
@FunctionalInterface public interface MonetaryOperator extends UnaryOperator<MonetaryAmount> {}
Dieser Mechanismus wird auch für das Runden von Geldbeträgen benutzt, wobei das Interface MonetaryRounding noch zusätzlich einen RoundingContext bereitstellt. Dabei werden folgende Arten des Rundens berücksichtigt:
Internes Runden geschieht implizit aufgrund des numerischen Modells des benutzten Implementationstypen. Wenn eine Implementation definiert, dass sie eine Skalierung von maximal 5 Stellen nach dem Komma unterstützt, darf sie das Resultat einer Division durch 7 auf genau diese 5 Stellen implizit runden.
Externes Runden wird benötigt, um einen numerischen Wert aus NumberValue in eine numerische Repräsentation, welche u.U. geringere numerische Fähigkeiten besitzt, zu exportieren. Bspw. darf ein Betrag von 255.15, auf 255 gerundet werden, wenn der Wert als Byte exportiert wird (was natürlich zu hinterfragen ist).
Beim Formatierungsrunden kann ein Betrag in eine arbiträre andere Form “gerundet” werden. Z.B. kann CHF 2’030’043 einfach als CHF > 1 Mio angezeigt werden.
Grundsätzlich ist internes Runden nur in wenigen Fällen, ähnlich dem oben erwähnten, erlaubt. Alle anderen Rundungsvarianten sind hingegen explizit, d.h. der Benutzer wendet diese aktiv an. Dies macht Sinn, da je nach Anwendungsfall zu unterschiedlichen Zeitpunkten und in verschiedener Weise gerundet werden muss. Somit hat der Entwickler die maximale Kontrolle über das Geschehen.
Unterschiedliche Rundungen können über das MonetaryRoundings-Singleton bezogen werden. Dabei können Rundungen für Währungen oder auch passend zu einem MathContext bezogen werden. Analog zu anderen Bereichen kann für komplexe Fälle auch eine RoundingQuery übergeben werden. Als Beispiel nehmen wir das Runden von CHF bei Barzahlungen in der Schweiz. Die kleinste Münzeinheit sind 5 Rappen, somit muss auf eben diese 5 Rappen gerundet werden. Das folgende Beispiel zeigt, wie auch dies mit dem API gelöst werden kann:
MonetaryRounding rounding = MonetaryRoundings.getRounding( RoundingQueryBuilder.of() .setCurrency(MonetaryCurrencies.getCurrency(“CHF”)) .set(“cashRounding”, true).build());
Die Rundung selbst kann sehr einfach auf jeden Betrag angewendet werden:
MonetaryAmount amount = …; MonetaryAmount roundedCHFCashAmount = amount.with(rounding);
Java: Währungsumrechnung
Herzstück der Währungsumrechnung bildet die Rate ExchangeRate. Diese beinhaltet neben den beteiligten Quell- und Zielwährungen den Umrechnungsfaktor und weitere Metadaten. Auch mehrstufige Umrechnungen (z.B. triangular rates) werden unterstützt. Raten sind dabei immer unidirektional und werden von sogenannten ExchangeRateProvider-Instanzen geliefert. Dabei kann optional ebenfalls eine ConversionQuery mitgegeben werden, welche es erlaubt, die gewünschte Umrechnung detaillierter zu parametrisieren. Die Währungsumrechnung selbst wird als CurrencyConversion modelliert, welche MonetaryOperator erweitert und an einen ExchangeRateProvider und eine Zielwährung gebunden wird.
Beide Interfaces lassen sich über das MonetaryConversions-Singleton beziehen:
CurrencyConversion conversion = MonetaryConversions .getConversion(“USD”); ExchangeRateProvider prov = MonetaryConversions .getExchangeRateProvider();
Zusätzlich kann beim Bezug einer CurrencyConversion oder eines ExchangeRateProvider auch die gewünschte Kette von Providern dynamisch konfiguriert oder eine ConversionQuery übergeben werden. Die Anwendung einer Konversion schließlich funktioniert analog wie das Runden:
MonetaryAmount amountCHF = …; MonetaryAmount convertedAmountUSD = amount.with(conversion);
Die aktuelle Referenz-Implementation bringt zwei vorkonfigurierte Provider mit, welche Umrechnungsfaktoren auf Basis der öffentlichen Datenfeeds der Europäischen Zentralbank und des Internationalen Währungsfonds zur Verfügung stellen. Für gewisse Währungen reichen diese zurück bis 1990.
Java: Formatierung von Währungen
Beim Formatieren von Beträgen wurde ebenfalls darauf geachtet, das API möglichst einfach aber dennoch flexibel zu halten:
public interface MonetaryAmountFormat extends MonetaryQuery<String>{ AmountFormatContext getAmountFormatContext(); String format(MonetaryAmount amount); void print(Appendable appendable, MonetaryAmount amount) throws IOException; MonetaryAmount parse(CharSequence text) throws MonetaryParseException; }
Analog zu den anderen Bereichen lassen sich entsprechende Instanzen über ein MonetaryFormats-Singleton beziehen:
MonetaryAmountFormat fmt = MonetaryFormats.getAmountFormat(Locale.US);
Auch hier gibt es die Möglichkeit, eine AmountFormatQuery zu übergeben, mit welcher beliebige Attribute zur Kontrolle der Formatierung mitgegeben werden können:
DecimalFormatSymbols symbols = …; MonetaryAmountFormat fmt = MonetaryFormats.getAmountFormat( AmountFormatQueryBuilder.of(Locale.US) .setObject(symbols).build());
Somit können Formate z.B. mit Namen oder Enum-Typen identifiziert werden und beliebig konfiguriert werden. Die Fähigkeiten der registrierten SPI Instanzen legen hierbei fest, was möglich ist.
SPIs: Währungen, Konversionen, Rundungen und Formate
Zusätzlich zum beschriebenen API definiert der JSR auch ein komplettes SPI, mit welchem die gesamte angebotene Funktionalität den konkreten Bedürfnissen angepasst werden kann. So können zusätzliche Währungen, Konversionen, Rundungen, Formate oder Implementationen für Beträge ergänzt oder das Komponenten-Bootstrapping angepasst werden.
Fazit: Währungen in Java und JSR 354
JSR 354 definiert ein übersichtliches und zugleich mächtiges API, welches den Umgang mit Währungen und Geldbeträgen stark vereinfacht, aber auch die teilweise widersprüchlichen Anforderungen abzudecken vermag. Auch fortgeschrittene Themen wie Runden und Währungsumrechnung werden adressiert und es ist möglich, Geldbeträge beliebig zu formatieren. Die bestehenden Erweiterungspunkte schließlich erlauben es, weitere Funktionen elegant zu integrieren. Diesbezüglich lohnt sich auch ein Blick in das OSS Projekt [6], wo neben finanzmathematischen Formeln auch eine mögliche Integration mit CDI experimentell zur Verfügung steht. JSR 354 sollte bis spätestens Ende des Jahres finalisiert sein. Für Anwender, welche noch mit Java 7 arbeiten, soll später zusätzlich ein vorwärtskompatibler Backport zur Verfügung gestellt werden.