21.2 Fließkomma-Arithmetik in Java
Zahlen mit einem Komma nennen sich Gleitkomma-, Fließkomma-, Fließpunkt- oder Bruchzahlen (gebrochene Zahlen). Der Begriff »Gleitkommazahl« kommt daher, dass die Zahl durch das Gleiten (Verschieben) des Dezimalpunktes als Produkt aus einer Zahl und einer Potenz der Zahl 10 dargestellt wird. Nehmen wir als Beispiel die Lichtgeschwindigkeit mit 299.792,458 Kilometer in der Sekunde: 299.792,458 = 29.979,2458 × 101 = 2.997,92458 × 102 = 299,792458 × 103 = 29,9792458 × 104 = 2,99792458 × 105.
Java unterstützt für Fließkommazahlen die Typen float und double, die sich nach der Spezifikation IEEE 754 richten. Diesen Standard des Institute of Electrical and Electronics Engineers gibt es seit Mitte der 1980er-Jahre. Ein float hat eine Länge von 32 Bit und ein double eine Länge von 64 Bit. Die Rechenoperationen sind im IEEE Standard for Floating-Point Arithmetic definiert.
[»] Hinweis
Wir sollten uns bewusst sein, dass die Genauigkeit von float wirklich nicht so toll ist. Schnell beginnt die Ungenauigkeit zuzuschlagen:
System.out.println( 2345678.88f ); // 2345679.0
21.2.1 Spezialwerte für Unendlich, Null, NaN
Die Datentypen double und float können nicht nur »Standardzahlen« speichern, sondern
auch eine positive oder negative Null annehmen,
und zudem definiert Java Sonderwerte für
Positive, negative Null
Es gibt eine positive Null (+0,0) und eine negative Null (–0,0), die etwa beim Unterlauf auftauchen.
[zB] Beispiel
Der Unterlauf erzeugt:
System.out.println( 1E-322 * 0.0001 ); // 0.0
System.out.println( 1E-322 * -0.0001 ); // -0.0
Für den Vergleichsoperator == ist die positive Null gleich der negativen Null, sodass 0.0 == ‐0.0 das Ergebnis true ergibt. Damit ist auch 0.0 > -0.0 falsch. Die Bit-Maske ist jedoch unterscheidbar, was der Vergleich Double.doubleToLongBits(0.0) != Double.doubleToLongBits (‐0.0) zeigt.
Es gibt einen weiteren kleinen Unterschied, den die Rechnung 1.0 / -0.0 und 1.0 / 0.0 zeigt. Durch den Grenzwert geht das Ergebnis einmal gegen negativ unendlich und einmal gegen positiv unendlich.
Unendlich
Der Überlauf einer mathematischen Operation führt zu einem positiven oder negativen Unendlich.
[zB] Beispiel
Betrachten wir die Multiplikation zweier wirklich großer Werte und die Division durch null:
System.out.println( 1E300 * 1E20 ); // Infinity
System.out.println( -1E300 * 1E20 ); // -Infinity
System.out.println( 1. / 0. ); // Infinity
System.out.println( 1. / -0. ); // -Infinity
Für die Werte deklariert die Java-Bibliothek in Double und Float zwei Konstanten. Zusammen mit der größten und kleinsten darstellbaren Fließkommazahl sind das:
Wert für | Float | Double |
---|---|---|
positiv unendlich | Float.POSITIVE_INFINITY | Double.POSITIVE_INFINITY |
negativ unendlich | Float.NEGATIVE_INFINITY | Double.NEGATIVE_INFINITY |
kleinster Wert | Float.MIN_VALUE | Double.MIN_VALUE |
größter Wert | Float.MAX_VALUE | Double.MAX_VALUE |
Das Minimum für double-Werte liegt bei etwa 10–324 und das Maximum bei etwa 10308. Außerdem deklarieren Double und Float Konstanten für MAX_EXPONENT/MIN_EXPONENT.
[»] Hinweis
Die Anzeige des Über-/Unterlaufs und des undefinierten Ergebnisses gibt es nur bei Fließkommazahlen, nicht aber bei Ganzzahlen.
public final class java.lang.Float/Double
extends Number
implements Comparable<Float/Double>
static boolean isInfinite(float/double v)
Liefert true, wenn v entweder POSITIVE_INFINITY oder NEGATIVE_INFINITY ist.static boolean isFinite(float/double d)
Liefert true, wenn d eine endliche Zahl ist.
NaN ist eine Zahl, die keine ist
NaN wird als Fehlerindikator für das Ergebnis von undefinierten Rechenoperationen benutzt, etwa 0/0.
[zB] Beispiel
Erzeuge NaN durch den Versuch, die Wurzel einer negativen Zahl zu bilden, und durch eine Nullkommanix-Division:
System.out.println( Math.sqrt(-4) ); // NaN
System.out.println( 0.0 / 0.0); // NaN
NaN ist als Konstante in den Klassen Float/Double deklariert. Die statische Methode isNaN(…) testet, ob eine Zahl NaN ist.
public final class java.lang.Float/Double
extends Number
implements Comparable<Float/Double>
static final float/double NaN = 0.0 / 0.0;
Deklaration von NaN bei Double und Floatstatic boolean isNaN(float/double v)
Liefert true, wenn die übergebene Zahl v NaN ist.boolean isNaN()
Liefert true, wenn das aktuelle Float/Double-Objekt NaN ist.
Die Implementierung von isNan(float/double v) ist einfach: return v != v.
Alles hat seine Ordnung *
Außer für den Wert NaN ist auf allen Fließkommazahlen eine totale Ordnung definiert. Das heißt, sie lassen sich von der kleinsten Zahl bis zur größten aufzählen. Am Rand steht die negative Unendlichkeit, dann folgen die negativen Zahlen, negative Null, positive Null, positive Zahlen und positives Unendlich. Bleibt nur noch die einzige unsortierte Zahl, NaN. Alle numerischen Vergleiche <, <=, >, >= mit NaN liefern false. Der Vergleich mit == ist false, wenn einer der Operanden NaN ist. != verhält sich umgekehrt, ist also true, wenn einer der Operanden NaN ist.
[zB] Beispiel
NaN beim Gleichheitstest:
System.out.println( Double.NaN == Double.NaN ); // false
System.out.println( Double.NaN != Double.NaN ); // true
Da NaN nicht zu sich selbst gleich ist, wird die folgende Konstruktion, die üblicherweise eine Endlosschleife darstellt, mit d als Double.NaN einfach übersprungen:
while ( d == d ) { /* Nie ausgeführt */ }
Ein NaN-Wert auf eine Ganzzahl angepasst, also etwa (int) Double.NaN, ergibt 0 und keine Ausnahme.[ 269 ](http://stackoverflow.com/questions/5876369/why-does-casting-double-nan-to-int-not-throw-an-exception-in-java)
Stille NaNs *
Eine Problematik in der Fließkomma-Arithmetik ist, dass keine Ausnahmen die Fehler anzeigen; NaNs solcher Art heißen auch stille NaNs (engl. quiet NaNs [qNaNs]). Als Entwickler müssen wir also immer selbst schauen, ob das Ergebnis während einer Berechnung korrekt bleibt. Ein durchschnittlicher numerischer Prozessor unterscheidet ein qNaN und ein signaling NaN (sNaN).
21.2.2 Standardnotation und wissenschaftliche Notation bei Fließkommazahlen *
Zur Darstellung der Fließkommaliterale gibt es zwei Notationen: Standard und wissenschaftlich. Die wissenschaftliche Notation ist eine Erweiterung der Standardnotation. Bei ihr folgt hinter den Nachkommastellen ein E (oder e) mit einem Exponenten zur Basis 10. Der Vorkommateil darf durch die Vorzeichen + oder – eingeleitet werden. Auch der Exponent kann positiv oder negativ[ 270 ](LOGO verwendet für negative Exponenten den Buchstaben N anstelle des E. In Java steht das E mit einem folgenden unären Plus- oder Minuszeichen. ) sein, muss aber eine Ganzzahl sein. Tabelle 21.9 stellt drei Beispiele zusammen:
Standard | Wissenschaftlich |
---|---|
123450.0 | 1.2345E5 |
123450.0 | 1.2345E+5 |
0.000012345 | 1.2345E–5 |
[zB] Beispiel
Die wissenschaftliche Notation wird wie folgt genutzt:
double x = 3.00e+8;
float y = 3.00E+8F;
21.2.3 Mantisse und Exponent *
Intern bestehen Fließkommazahlen aus drei Teilen: einem Vorzeichen, einem ganzzahligen Exponenten und einer Mantisse (engl. mantissa). Während die Mantisse die Genauigkeit bestimmt, gibt der Exponent die Größenordnung der Zahl an.
Die Berechnung für Fließkommazahlen aus den drei Elementen ist im Prinzip wie folgt: Vorzeichen × Mantisse × 2 ^ Exponent, wobei das Vorzeichen –1 oder +1 sein kann. Die Mantisse m ist keine Zahl mit beliebigem Wertebereich, sondern normiert mit dem Wertebereich 1 ≤ m < 2, also eine Fließkommazahl, die mit 1 beginnt und daher auch 1-plus-Form heißt.[ 271 ](Es gibt eine Ausnahme durch denormalisierte Zahlen, aber das spielt für das Verständnis keine Rolle. ) Auch der zunächst vorzeichenbehaftete Exponent wird nicht direkt gespeichert, sondern als angepasster Exponent (engl. biased exponent) in der IEEE-kodierten Darstellung abgelegt. Zu unserem Exponenten wird, abhängig von der Genauigkeit, +127 (bei float) und +1.023 (bei double) addiert. Nach der Berechnung steht in der Darstellung immer eine ganze Zahl. 127 und 1.023 nennen sich Bias.
Das Vorzeichen kostet immer 1 Bit, und die Anzahl der Bits für Exponent und Mantisse richtet sich nach dem Datentyp (siehe Tabelle 21.10).
Anzahl Bits | Anzahl Bits | |
---|---|---|
float | 8 | 23 |
double | 11 | 52 |
[zB] Beispiel
Das Folgende sind Kodierungen für die Zahl 123.456,789 als float und double. Das # trennt Vorzeichen, Exponent und Mantisse:
0#10001111#11100010010000001100101
0#10000001111#1110001001000000110010011111101111100111011011001001
Um von dieser Darstellung auf die Zahl zu kommen, schreiben wir:
BigInteger biasedExponent = new BigInteger( "10001111", 2 );
BigInteger mantisse = new BigInteger( "11100010010000001100101", 2 );
int exponent = (int) Math.pow( 2, biasedExponent.longValue() - 127 );
double m = 1. + (mantisse.longValue() / Math.pow( 2, 23 ));
System.out.println( exponent * m ); // 123456.7890625
Den Exponenten (ohne Bias) einer Fließkommazahl liefert Math.getExponent(…); auf unsere Zahl angewendet, ist das also 16.
Zugang zum Bit-Muster liefern die Methoden long doubleToLongBits(double) und int Float.floatToIntBits(float). Die Umkehrung ist double Double.longBitsToDouble(long) bzw. float Float.intBitsToFloat(int).
[zB] Beispiel
double x = 123456.789;
float y = 123456.789f;
out.printf( "%016x%n", Double.doubleToLongBits( x ) ); // 40fe240c9fbe76c9
out.printf( "%08x%n", Float.floatToIntBits( y ) ); // 47f12065
out.println( Long.toBinaryString( Double.doubleToLongBits( x ) ) );
out.println( Integer.toBinaryString( Float.floatToIntBits( y ) ) );