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 Fließkommazahlen float und double.

Was sind Fließkommazahlen eigentlich?

Erst einmal sind Fließkommazahlen 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 Fließkommazahlen 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 Fließkommazahl 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 Fließkommazahlen

Sie haben es vermutlich bereits befürchtet: Dadurch, dass ein Computer eigentlich nur 0 und 1 kennt, kann es beim Rechnen mit Fließkommazahlen zu minimalen Abweichungen kommen.

System.out.println(32.3 * 100); // 3229.9999999999995
System.out.println(32.3f * 100); // 3230.0

Dies 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 (Fließkommazahl 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); // 3230

Alternativ können Sie den Betrag auch mit Hilfe der statischen Methode ceil der Klasse java.lang.Math runden (diese Klasse wird Ihnen später im Detail vorgestellt):

System.out.println(Math.ceil(32.3 * 100)); // 3230

Eine weitere Möglichkeit dieses Problem zu umgehen besteht darin, auf float und double 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 Klasse BigDecimal ersetzen (siehe Kapitel 11.03 Rechnen mit großen Zahlen).

Besondere Fließkommazahlen

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 Fließkommzahl durch 0, oder teilen Sie eine Zahl durch die Fließkommazahl 0, wird keine Fehlermeldung geworfen:

System.out.println(1 / 0.); // Infinity => Unendlich

Teilen Sie hingegen 0 durch 0 (eine von beiden Zahlen muss eine Fließkommazahl sein), gibt das keinen Fehler und auch nicht unendlich, sondern eine nicht definierte Zahl:

System.out.println(0. / 0); // NaN => Not a Number

Dies ist eine Besonderheit, die beim Rechnen mit Fließkommazahlen beachtet werden muss, wurde aber bereits Im Kapitel 06.06 Wrapper-Klassen behandelt.

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.