1. Einleitung
Man könnte meinen: Wenn der Computer „klug“ ist, dann sollte 0.1 + 0.2 einfach 0.3 sein. Ganz so ist es jedoch nicht. Schauen wir uns ein einfaches Beispiel an.
double x = 0.1;
double y = 0.2;
double sum = x + y;
System.out.println(sum); // Was wird das Programm ausgeben?
Und nun versuchen Sie, mit 0.3 zu vergleichen:
System.out.println(sum == 0.3); // Wird das hier true oder false sein?
Wenn Sie false sehen, wundern Sie sich nicht!
Die Ursache liegt in der Darstellung von Zahlen im Speicher
Computer arbeiten mit Zahlen im Binärsystem. Doch längst nicht alle Dezimalbrüche lassen sich als endliche Binärbrüche darstellen – so wie sich 1/3 nicht exakt als Dezimalbruch schreiben lässt (0.333...). Zum Beispiel ist 0.1 im Binärsystem ein unendlicher Bruch und muss für die Speicherung „gerundet“ werden.
Vereinfacht gesagt speichert double manchmal scheinbar exakt, tatsächlich aber nur einen sehr nahen Näherungswert.
2. Welche „Macken“ treten bei der Arithmetik mit double auf?
Schauen wir uns praktische Beispiele an.
Beispiel 1. Die klassische „Magie“ 0.1 + 0.2
double a = 0.1;
double b = 0.2;
double sum = a + b;
System.out.println(sum); // 0.30000000000000004
System.out.println(sum == 0.3); // false
Der Rechner gab nicht 0.3 aus, sondern 0.30000000000000004. Der Unterschied ist klein, aber wenn Sie zum Beispiel mit Finanzen arbeiten – ist das bereits kritisch.
Beispiel 2. Iteratives Addieren
double result = 0;
for (int i = 0; i < 10; i++)
{
result += 0.1;
}
System.out.println(result); // 0.9999999999999999
Gewünscht war 1.0 – heraus kommt etwas weniger. Wieder ist die Ursache die Rundung innerhalb von double.
Warum das in der Praxis wichtig ist
Viele denken: „Na und, ein kleiner Fehler, halb so wild!“ Betrachten wir ein Beispiel aus der Welt der Zahlungen.
Angenommen, Ihr Online-Banking addiert 100 Transaktionen je 0.1 Euro. Wenn Ihr Programm bei jeder Iteration um ein hunderttausendstel Euro „verliert“, verlieren Sie im Maßstab einer Bank echtes Geld. Da kommt die Buchhaltung sofort auf Sie zu und fragt: „Wo ist unser Geld?!“
3. Wie man Fließkommazahlen richtig vergleicht
Da double häufig nicht genau den Wert speichern kann, den Sie erwarten, kann der direkte Vergleich mit == in die Irre führen. Stattdessen vergleicht man üblicherweise den Betrag der Differenz mit einer sehr kleinen Zahl (Epsilon).
Beispiel: Vergleich mit Toleranz
double a = 0.1 + 0.2;
double b = 0.3;
double epsilon = 0.000001;
if (Math.abs(a - b) < epsilon)
{
System.out.println("Fast gleich!"); // So zu vergleichen ist sicherer
}
Hier sagen wir: „Wenn der Unterschied zwischen den Zahlen kleiner als ein Millionstel ist, betrachten wir sie als gleich.“
Hinweis: Die Funktion Math.abs(value) gibt den Absolutwert (Betrag) der übergebenen Zahl zurück.
4. Sonderwerte von double: Infinity, NaN, -Infinity
Der Typ double speichert nicht nur Zahlen, sondern auch besondere Werte. Diese entstehen in Situationen, die ein Mathematiker einer Studentin streng verboten hätte.
Unendlichkeit (Infinity)
Was passiert, wenn man 1 durch 0 teilt?
double result = 1.0 / 0.0;
System.out.println(result); // Infinity
In Java (und vielen anderen Sprachen) führt die Division durch 0 bei double nicht zu einer Exception! Stattdessen wird das Ergebnis zu einem speziellen Wert „positive Unendlichkeit“.
Minus-Unendlichkeit (-Infinity)
Teilt man eine negative Zahl durch 0, erhält man negative Unendlichkeit:
double result = -1.0 / 0.0;
System.out.println(result); // -Infinity
„Keine Zahl“ (NaN – Not a Number)
Wenn man etwas wirklich Ungewöhnliches tut, etwa die Quadratwurzel aus einer negativen Zahl berechnet:
double result = Math.sqrt(-1);
System.out.println(result); // NaN
Oder das Ergebnis der Division 0.0 / 0.0:
double result = 0.0 / 0.0;
System.out.println(result); // NaN
NaN ist gewissermaßen „alles, was im wirklichen Leben keine Zahl wäre“.
Prüfung auf Sonderwerte
In Java gibt es Funktionen zur Prüfung auf Sonderwerte:
System.out.println(Double.isInfinite(result)); // true, wenn Unendlichkeit
System.out.println(Double.isNaN(result)); // true, wenn NaN
Tabelle: Wie double auf ungewöhnliche Operationen reagiert
| Operation | Ergebnis | Was in double gespeichert wird |
|---|---|---|
|
Infinity | +∞ |
|
-Infinity | -∞ |
|
NaN | Keine Zahl |
|
NaN | Keine Zahl |
5. Typische Fehler im Umgang mit Fließkommazahlen
Fehler Nr. 1: Vergleich von Gleitkommazahlen mit ==
Die häufigste Falle: zu prüfen, ob zwei berechnete Gleitkommazahlen mit dem normalen Vergleich gleich sind. Aufgrund von auflaufenden Rundungsfehlern erhalten Sie fast immer ein unerwartetes Ergebnis. Verwenden Sie immer einen Vergleich mit Toleranz (epsilon).
Fehler Nr. 2: Unerwartete NaN und Infinity in Berechnungen
Wenn Sie die Division durch Null oder Wurzeln aus negativen Zahlen nicht kontrollieren, können in Ihrem Programm NaN oder Infinity auftauchen und alle weiteren Berechnungen „anstecken“. Prüfen Sie verdächtige Werte mit Double.isNaN() und Double.isInfinite() – so vermeiden Sie böse Überraschungen.
Fehler Nr. 3: NaN als „Marker“ verwenden
Einige Einsteiger nutzen NaN als „Spezial“-Wert, etwa „falls nicht gefunden – geben wir NaN zurück“. Denken Sie daran: NaN ist tückisch, und der Vergleich mit == funktioniert nicht! Prüfen Sie ausschließlich mit den speziellen Methoden.
Fehler Nr. 4: Eine Exception beim Teilen durch 0.0 erwarten
Anders als bei Ganzzahlen wirft die Division durch 0.0 bei double keine Exception, sondern liefert Infinity oder NaN. Das kann zu leisen, schwer auffindbaren Bugs führen – ein Ergebnis ist da, aber nicht das erwartete.
GO TO FULL VERSION