Blog

Korrekte Berechnungen mit Präzision in Java

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.

Holisticon AG — Teile diesen Artikel

Über den Autor

Antwort hinterlassen