In der Programmierung stellt sich dann und wann die Herausforderung, finanzmathematische Funktionen umzusetzen. Dies geht von einfachen Berechnungen wie Aufsummierung von Einzelposten zu einer Gesamtrechnung über die Ermittlung von Umsatzsteuer-Anteilen bis hin zu aufwändigeren Berechnungen von Zins und Zinseszins für mehrjährige Tilgungspläne von Krediten und Beiträgen zu Lebensversicherungen und dergleichen. Oberstes Gebot in dieser Disziplin sind die Präzision und Korrektheit der Berechnungen, denn niemand hat es gern, dass er am Ende weniger Geld bekommt, als ihm zusteht. Wie heißt es so schön: Bei Geld hört die Freundschaft auf. ;-)
Währungen
In der Finanzmathematik hat man es mit krummen Werten zu tun, fast jede Währung weltweit ist in eine äquivalente Untereinheit aufgeteilt (z.B. 1 Euro = 100 Eurocent). Wenn man auf dieser Basis dann zusätzlich noch mit relativen Anteilen rechnet, kommt man in den Berechnungen schnell auf mehrstellige Nachkommastellen. Üblicherweise hat man sich in solchen Berechnungen darauf geeinigt, mit vier Nachkommastellen zu rechnen und in der Darstellung auf zwei Nachkommastellen kaufmännisch zu runden.
IEEE 754
Allgemein unterscheidet man in der Programmierung zwischen der Darstellung von Ganzzahlen und von Gleitkommazahlen (auch als Gleitpunktzahl oder Fließkommazahl bezeichnet). Erstere lassen sich recht einfach in Bits und Bytes darstellen, Ganzzahlen fehlen aber die für die Finanzmathematik benötigten Nachkommstellen. Für Darstellung Letzterer gibt es verschiedene Ansätze, von der die Darstellung nach IEEE 754 in Programmiersprachen der am meisten genutzte und verbreitete Ansatz ist. Gemein ist all diesen Darstellungen allerdings, dass sie prinzipbedingt ungenau sind. Die Darstellung einer reellen Zahl kann immer nur als Annäherung mit mehr oder weniger großen Differenzen dargestellt werden. Diese Differenzen spielen bei einzelnen Zahlen meist eine zu vernachlässigbare Rolle, will man allerdings eine bis auf eine bestimmte Anzahl an Nachkommstellen genaue Arithmetik implementieren, kann es dabei schnell zu unerwünschten Ungenauigkeiten kommen.
Gleitkommazahlen in Java
In Java sind Gleitkommazahlen nach IEEE 754 mittels der primitiven Datentypen float
(einfache Genauigkeit) und double
(doppelte Genauigkeit) implementiert. Als primitiver Datentyp haben diese in Java den Vorteil, weniger Speicher als Objekte zu verbrauchen und dass mittels der binären Infix-Operatoren + - / *
einfach Berechnungen implementiert werden können. Die Berechnungen an sich können meist von der unterliegenden Laufzeitumgebung direkt auf CPU-Ebene ausgeführt werden, was einen Geschwindigkeitsvorteil mit sich bringt. Allerdings handelt man sich auch die oben genannten Ungenauigkeiten mit ein, unter denen die Präzision leidet und die durch selbst zu implementierende Korrekturverfahren ausgeglichen werden müssen. Diese müssen natürlich auf Korrektheit überprüft werden, sodass der Gesamtaufwand nicht vernachlässigt werden sollte.
Im folgenden ein paar Beispiele, die die Ungenauigkeiten demonstrieren. Es fällt dabei auf, dass die Abweichungen schon bei kleinen Werten auftreten können.
Addition
double a = 5.6d; double b = 5.8d; double c = a + b; System.out.println(a + " + " + b + " = " + c);
Ausgabe:
5.6 + 5.8 = 11.399999999999999
Subtraktion
double a = 2.0d; double[] array = new double[] { 1.1d, 1.2d, 1.3d, 1.4d, 1.5d, 1.6d, 1.7d, 1.8d, 1.9d, 2d }; for (double b : array) { double c = a - b; System.out.println(a + " - " + b + " = " + c); }
Ausgabe:
2.0 - 1.1 = 0.8999999999999999
2.0 - 1.2 = 0.8
2.0 - 1.3 = 0.7
2.0 - 1.4 = 0.6000000000000001
2.0 - 1.5 = 0.5
2.0 - 1.6 = 0.3999999999999999
2.0 - 1.7 = 0.30000000000000004
2.0 - 1.8 = 0.19999999999999996
2.0 - 1.9 = 0.10000000000000009
2.0 - 2.0 = 0.0
Division
double[] array = new double[] { 1.11d, 1.12d, 1.13d, 1.14d, 1.15d, 1.16d, 1.17d, 1.18d, 1.19d, 2d }; for (double a : array) { double b = a / 100; System.out.println(a + " / 100 = " + b); }
Ausgabe:
1.11 / 100 = 0.0111
1.12 / 100 = 0.011200000000000002
1.13 / 100 = 0.0113
1.14 / 100 = 0.011399999999999999
1.15 / 100 = 0.0115
1.16 / 100 = 0.0116
1.17 / 100 = 0.011699999999999999
1.18 / 100 = 0.0118
1.19 / 100 = 0.011899999999999999
1.20 / 100 = 0.012
Alternative in Java: BigDecimal
Alternativ zu float
oder double
bietet Java die Klasse BigDecimal
an, die den Vorteil bietet, korrekt zu rechnen. Das Geheimnis liegt darin, dass die Vor- und Nachkomma-Stellen in einem BigDecimal
-Objekt exakt abgelegt werden und nicht näherungsweise, wie es bei Fließkommadarstellungen der Fall ist. Neben diversen Rechenoperationen kann man ab Java 5 mittels der Klasse MathContext
komfortabel die Rechengenauigkeit und den Rundungsmodus setzen. Für übliche Anwendungsfälle werden durch die Konstanten MathContext.DECIMAL128
, MathContext.DECIMAL32
, MathContext.DECIMAL64
und MathContext.UNLIMITED
voreingestellte Konfigurationen bereitgestellt. Nachteilig ist allerdings, dass die Berechnungen mit BigDecimal
um den Faktor 100 langsamer sind als mit den primitiven Datentypen – dies gilt es zu berücksichtigen.
Im folgenden die gleichen Beispiele von oben, in denen die Ungenauigkeiten nicht auftauchen.
Addition
BigDecimal a = new BigDecimal("5.6"); BigDecimal b = new BigDecimal("5.8"); BigDecimal c = a.add(b); System.out.println(a + " + " + b + " = " + c);
Ausgabe:
5.6 + 5.8 = 11.4
Subtraktion
BigDecimal a = new BigDecimal("2.0"); BigDecimal[] array = new BigDecimal[] { new BigDecimal("1.1"), new BigDecimal("1.2"), new BigDecimal("1.3"), new BigDecimal("1.4"), new BigDecimal("1.5"), new BigDecimal("1.6"), new BigDecimal("1.7"), new BigDecimal("1.8"), new BigDecimal("1.9"), new BigDecimal("2.0") }; for (BigDecimal b : array) { BigDecimal c = a.subtract(b); System.out.println(a + " - " + b + " = " + c); }
Ausgabe:
2.0 - 1.1 = 0.9
2.0 - 1.2 = 0.8
2.0 - 1.3 = 0.7
2.0 - 1.4 = 0.6
2.0 - 1.5 = 0.5
2.0 - 1.6 = 0.4
2.0 - 1.7 = 0.3
2.0 - 1.8 = 0.2
2.0 - 1.9 = 0.1
2.0 - 2.0 = 0.0
Division
BigDecimal[] array = new BigDecimal[] { new BigDecimal("1.11"), new BigDecimal("1.12"), new BigDecimal("1.13"), new BigDecimal("1.14"), new BigDecimal("1.15"), new BigDecimal("1.16"), new BigDecimal("1.17"), new BigDecimal("1.18"), new BigDecimal("1.19"), new BigDecimal("1.20") }; for (BigDecimal a : array) { BigDecimal b = a.divide(new BigDecimal("100")); System.out.println(a + " / 100 = " + b); }
Ausgabe:
1.11 / 100 = 0.0111
1.12 / 100 = 0.0112
1.13 / 100 = 0.0113
1.14 / 100 = 0.0114
1.15 / 100 = 0.0115
1.16 / 100 = 0.0116
1.17 / 100 = 0.0117
1.18 / 100 = 0.0118
1.19 / 100 = 0.0119
1.20 / 100 = 0.012
Fazit
Sollen Berechnungen präzise und korrekt durchgeführt werden, empfiehlt es sich, den Einsatz von BigDecimal
zu prüfen. Sollte die verringerte Rechengeschwindigkeit nicht problematisch sein, kann die Verwendung sehr sinnvoll sein. Alternative Implementierungen mit double
bedürfen Hilfsklassen, um die nicht vorhandene Präszision wieder auszugleichen. Diese Hilfsklassen sollten gut und möglichst vollständig getestet sein, obwohl ein Restrisiko von falschen Berechnungen immer bleibt.