11.01 Berechnungen mit Fließkommazahlen
Aus den Einstiegskapiteln zu Operatoren kennen Sie bereits die Rechenzeichen + (addieren), – (subtrahieren), * (multiplizieren), / (dividieren) und % (Modulo), mit denen es möglich ist, einfache Rechenoperationen auf primitive Zahlen (byte, short, int, long, float, double
) anzuwenden. Auch wissen Sie über Grundlagen der Mathematik in Java bescheid. Bspw. wissen Sie was passiert, wenn der Wertebereich einer Zahl überschritten wird. Auf die Basics wird deshalb im Mathematik-Kapitel verzichtet. Zu Beginn lernen Sie stattdessen etwas über die Eigenheiten beim Rechnen mit den Gleitkommazahlen float
und double
.
Was sind Gleitkommazahlen eigentlich?
Erst einmal sind Gleitkommazahlen Zahlen, die ein Dezimaltrennzeichen beinhalten und folglich keine Ganzzahlen sein müssen. Jedoch versteht ein Computer bekanntlich nur das Binärsystem mit Nullen und Einsen. Bruchzahlen und Zahlen mit Vorzeichen (signed) werden nicht unterstützt. Die Programmiersprachen behelfen sich hier mit einem Trick: Für die benötigten Zusatzinformationen werden vordefinierte Bytes reserviert, die diese speichern.
Für das Vorzeichen handelt es sich dabei um das erste Bit der Zahl. Steht dieses auf 0, handelt es sich um eine positive Zahl, bei einer negativen Zahl wird eine 1 gesetzt. Dies ist nicht nur bei Gleitkommazahlen so, sondern gilt auch bei gewöhnlichen Ganzzahlen. Bspw. steht 0 1111111111111111111111111111111
für den höchsten, positiven Integer
und 1 0000000000000000000000000000000
für den niedrigsten, negativen Integer
.
Bei Bruchzahlen ist es ähnlich. Die primitiven Datentypen float
(32 Bit) und double
(64 Bit) (siehe auch Kapitel 02.03 Primitive Datentypen) richten sich in Java nach der Spezifikation IEEE 754 des Institute of Electrical and Electronics Engineers. Zusätzlich zum Vorzeichen bestehen diese jedoch noch aus einer Mantisse (bei einem float
die Bits 2 – 9 (8 Bit insgesamt), bzw. bei einem double
die Bits 2 – 13 (11 Bit insgesamt)) und einem Exponent (restliche Bits). Aus diesen wird wie folgt eine Gleitkommazahl gebildet:
Vorzeichen * 2 ^ Exponent * Mantisse
Wobei das Vorzeichen entweder -1 (erstes Bit auf 1) oder 1 (erstes Bit auf 0) entspricht.
Mantisse und Exponent sind ebenfalls codiert. Um vom gespeicherten Wert einer Mantisse auf den realen Wert zu kommen, wird diese in eine Dezimalzahl umgewandelt, durch 2 hoch Anzahl der Bits der Mantisse (float
: 23, double
: 52) geteilt, und zu 1 dazugezählt. Von der Dezimalzahl des Exponenten muss die so genannte Bias abgezogen werden (127 bei float
und 1023 bei double
).
Bspw. soll die Zahl 42,0815 einmal in binärer foat
– (01000010001010000101001101110101
), und double
-Schreibweise (0100000001000101000010100110111010010111100011010100111111011111
) zurück in eine Dezimalzahl gewandelt werden. Zuerst wird die Zahl in Ihre Komponenten zerlegt:
0 10000100 01010000101001101110101
0 10000000100 0101000010100110111010010111100011010100111111011111
In Dezimalschreibweise entspricht das
0 132 2642805
0 1028 1418844988854239
Das Vorzeichen entspricht also in beiden Fällen +1
. Bei der Berechnung des Exponenten ergibt sich folgendes:
132 - 127 = 5
1028 - 1023 = 5
Der Exponent beträgt also in beiden Fällen 5. Als letzter Zwischenschritt muss nun noch die Mantisse berechnet werden:
1 + (2642805 / (2^23)) = FloatMantisse
1 + (2642805 / 8388608) = FloatMantisse
1 + 0,3150469 = FloatMantisse
1,3150459 = FloatMantisse
1 + (1418844988854239 / (2^52)) = DoubleMantisse
1 + (1418844988854239 / 4503599627370496) = DoubleMantisse
1 + 0,3150469 = DoubleMantisse
1,3150469 = DoubleMantisse
Werden diese Zahlen in die obige Formel eingesetzt, erhält man als Ergebnis rund 42,0815.
(2^5) * 1,3150459 = 42,0814688
(2^5) * 1,3150469 = 42,0815008
Wie Sie an diesem einfachen Beispiel erkennen, ergeben sich (abhängig von der Genauigkeit) kleine Rundungsdifferenzen.
Ungenauigkeiten beim Rechnen mit Gleitkommazahlen
Sie haben es vermutlich bereits befürchtet: Dadurch, dass ein Computer eigentlich nur 0 und 1 kennt, kann es beim Rechnen mit Gleitkommazahlen zu minimalen Abweichungen kommen.
System.out.println(32.3 * 100); // 3229.9999999999995 System.out.println(32.3f * 100); // 3230.0Dies ist jedoch kein Problem von Java, sondern von Programmiersprachen/CPUs allgemein und liegt an einer ungünstigen Binärdarstellung.
Um dieses Problem zu umgehen, gibt es mehrere Ansätze:
Besitzen Zahlen eine feste Anzahl an Nachkommastellen (bspw. bei Geldbeträgen zwei Nachkommastellen), kann mit Ganzzahlen (Gleitkommazahl multipliziert mit der Anzahl der maximalen Nachkommastellen) gearbeitet, und bei der Ausgabe des Ergebnisses das Komma um die Anzahl der gewünschten Nachkommastellen verschoben werden:
long euro = 3230; // Cent, Anstatt 32,3 Euro euro = euro * 100; // Berechnung, die im letzten Beispiel Probleme bereitet hat double ergebnis = euro / 100D; // Von Cent wieder in Euro umrechnen System.out.println(ergebnis); // 3230 // Zweiter Weg: Komma setzen und parsen String temp = String.valueOf(euro); temp = temp.substring(0, temp.length() - 2) + "." + temp.substring(temp.length() - 2); ergebnis = Double.parseDouble(temp); System.out.println(ergebnis); // 3230Alternativ können Sie den Betrag auch mit Hilfe der statischen Methode
ceil
der Klassejava.lang.Math
runden (diese Klasse wird Ihnen später im Detail vorgestellt):System.out.println(Math.ceil(32.3 * 100)); // 3230Eine weitere Möglichkeit dieses Problem zu umgehen besteht darin, auf
float
unddouble
zu verzichten, und stattdessen auf entsprechende Klassen (bspw.BigDecimal
) zurückzugreifen. Auch bei der Berechnung von größeren Zahlen sollten Sie aufgrund der höheren Genauigkeit und des uneingeschränkten Wertebereichs die primitiven Datentypen durch die KlasseBigDecimal
ersetzen (siehe Kapitel 11.03 Rechnen mit großen Zahlen).Besondere Gleitkommazahlen
Teilen Sie eine Ganzzahl durch 0, erhalten Sie eine Fehlermeldung:
System.out.println(1 / 0);Exception in thread "main" java.lang.ArithmeticException: / by zero
Logisch – durch 0 kann man auch nicht teilen! Teilen Sie allerdings eine Gleitkommzahl durch 0, oder teilen Sie eine Zahl durch die Gleitkommazahl 0, wird keine Fehlermeldung geworfen:
System.out.println(1 / 0.); // Infinity => UnendlichTeilen Sie hingegen 0 durch 0 (eine von beiden Zahlen muss eine Gleitkommazahl sein), gibt das keinen Fehler und auch nicht unendlich, sondern eine nicht definierte Zahl:
System.out.println(0. / 0); // NaN => Not a NumberDies ist eine Besonderheit, die beim Rechnen mit Gleitkommazahlen beachtet werden muss, wurde aber bereits Im Kapitel 06.06 Wrapper-Klassen behandelt.