21.4 Genauigkeit, Wertebereich eines Typs und Überlaufkontrolle *
Die Datentypen in Java haben immer eine bestimmte Anzahl von Bytes, und somit ist der Wertebereich bekannt. Allerdings kann eine Operation aus diesem Wertebereich herauslaufen.
21.4.1 Der größte und der kleinste Wert
Für jeden primitiven Datentyp gibt es in Java eine eigene Klasse mit diversen Methoden und Konstanten. Die Klassen Byte, Short, Integer, Long, Float und Double besitzen die Konstanten MIN_VALUE und MAX_VALUE für den minimalen und maximalen Wertebereich. Die Klassen Float und Double verfügen zusätzlich über die wichtigen Konstanten NEGATIVE_INFINITY und POSITIVE_INFINITY für minus und plus unendlich und NaN (Not a Number, undefiniert).
[»] Hinweis
Integer.MIN_VALUE steht mit –2.147.483.648 für den kleinsten Wert, den die Ganzzahl annehmen kann. Double.MIN_VALUE steht jedoch für die kleinste positive Zahl (beste Näherung an 0), die ein Double darstellen kann (4.9E–324).
Wenn uns beim Wort double im Vergleich zu float eine »doppelte Genauigkeit« über die Lippen kommt, müssen wir mit der Aussage vorsichtig sein, denn double bietet zumindest nach der Anzahl der Bits eine mehr als doppelt so präzise Mantisse. Über die Anzahl der Nachkommastellen sagt das jedoch nichts aus.
[zB] Bastelaufgabe
Warum sind die Ausgaben so, wie sie sind?
double d1 = 0.02d;
float f1 = 0.02f;
System.out.println( d1 == f1 ); // false
System.out.println( (float) d1 == f1 ); // true
double d2 = 0.02f;
float f2 = 0.02f;
System.out.println( d2 == f2 ); // true
21.4.2 Überlauf und alles ganz exakt
Bei einigen mathematischen Fragestellungen muss sich feststellen lassen, ob Operationen wie die Addition, Subtraktion oder Multiplikation den Zahlenbereich sprengen, also etwa den Ganzzahlenbereich eines Integers von 32 Bit verlassen. Passt das Ergebnis einer Berechnung nicht in den Wertebereich einer Zahl, so wird dieser Fehler standardmäßig nicht von Java angezeigt; weder der Compiler noch die Laufzeitumgebung melden dieses Problem. Es gibt auch keine Ausnahme: In Java existiert keine eingebaute Überlaufkontrolle.
[zB] Beispiel
Mathematisch gilt a × a ÷ a = a, also zum Beispiel 100.000 × 100.000 ÷ 100.000 = 100.000. In Java ist das anders, da wir bei 100.000 × 100.000 einen Überlauf im int haben.
System.out.println( 100_000 * 100_000 / 100_000 ); // 14100
liefert daher 14100. Wenn wir den Datentyp auf long erhöhen, indem wir hinter ein 100_000 ein L setzen, sind wir bei dieser Multiplikation noch sicher, da ein long das Ergebnis aufnehmen kann:
System.out.println( 100_000L * 100_000 / 100_000 ); // 100000
Multiplizieren von int-Ganzzahlen
Der *-Operator führt bei int keine Anpassung an den Datentypen durch, sodass die Multiplikation von zwei ints wiederum int liefert. Doch das Produkt kann schnell aus dem Wertebereich laufen, sodass es zum Überlauf kommt. Selbst wenn das Produkt in eine long-Variable geschrieben wird, erfolgt die Konvertierung von int in long erst nach der Multiplikation:
int i = Integer.MAX_VALUE * Integer.MAX_VALUE;
long l = Integer.MAX_VALUE * Integer.MAX_VALUE;
System.out.println( i ); // 1
System.out.println( l ); // 1
Sollen zwei ints ohne Überlauf multipliziert werden, ist einer der beiden Faktoren auf long anzupassen, damit die Berechnung zum korrekten Ergebnis 4611686014132420609 führt.
System.out.println( Integer.MAX_VALUE * (long) Integer.MAX_VALUE );
System.out.println( (long) Integer.MAX_VALUE * Integer.MAX_VALUE );
Da diese Typumwandlung schnell vergessen werden kann und nicht besonders explizit ist, bieten ab Java 9 die Klassen Math und StrictMath die statische Methode long multiplyFull(int x, int y), die für uns über (long)x * (long)y die Typumwandlung vornimmt.
Multiplizieren von long-Ganzzahlen
Es gibt keinen primitiven Datentyp, der mehr als 64 Bit (8 Byte) hat, sodass das Ergebnis von long * long mit seinen 128 Bit nur in ein BigInteger komplett passt. Allerdings erlaubt eine Methode, die oberen 64 Bit einer long-Multiplikation getrennt zu erfragen, und zwar ab Java 9 mit der Methode multiplyHigh(long x, long y) in Math und StrictMath – wobei StrictMath nur auf Math leitet.
[zB] Beispiel
BigInteger v = BigInteger.valueOf( Long.MAX_VALUE )
.multiply( BigInteger.valueOf( Long.MAX_VALUE ) );
System.out.println( v ); // 85070591730234615847396907784232501249
long lowLong = Long.MAX_VALUE * Long.MAX_VALUE;
long highLong = Math.multiplyHigh( Long.MAX_VALUE, Long.MAX_VALUE );
BigInteger w = BigInteger.valueOf( highLong )
.shiftLeft( 64 )
.add( BigInteger.valueOf( lowLong ) );
System.out.println( w ); // 85070591730234615847396907784232501249
Überlauf erkennen
Bestimmte Methoden in Math und StrictMath ermöglichen eine Überlauferkennung:
class java.lang.Math
class java.lang.StrictMath
static int addExact(int x, int y)
static int subtractExact(int x, int y)
static long subtractExact(long x, long y)
static int multiplyExact(int x, int y)
static int multiplyExact(long x, int y)
static long multiplyExact(long x, long y)
static int toIntExact(long value)
static int incrementExact(int a)
static long incrementExact(long a)
static int decrementExact(int a)
static long decrementExact(long a)
static int negateExact(int a)
static long negateExact(long a)
Alle Methoden lösen eine ArithmeticException aus, falls die Operation nicht durchführbar ist – die letzte zum Beispiel, wenn (int)value != value ist. Leider deklariert Java keine Unterklassen wie UnderflowException oder OverflowException, und Java meldet nur alles vom Typ ArithmeticException mit der Fehlermeldung »xxx overflow«, auch wenn es eigentlich ein Unterlauf ist.
[zB] Beispiel
Von der kleinsten Ganzzahl mit subtractExact(…) eins abzuziehen, führt zu einer Ausnahme:
subtractExact( Integer.MIN_VALUE, 1 ); // ArithmeticException
[»] Vergleich mit C#
C# verhält sich genauso wie Java und reagiert standardmäßig nicht auf einen Überlauf. Es gibt jedoch spezielle checked-Blöcke, die eine OverflowException melden, wenn es bei arithmetischen Grundoperationen zu einem Überlauf kommt. Folgendes löst diese Ausnahme aus: checked { int val = int.MaxValue; val++; }. Solche checked-Blöcke gibt es in Java nicht – wer diese besondere Überlaufkontrolle braucht, muss die Methoden nutzen und ein val++ dann auch umschreiben zu Math.addExact(val, 1) bzw. Math.incrementExact(val).
21.4.3 Was bitte macht eine ulp?
Die Math-Klasse bietet sehr spezielle Methoden, die für diejenigen interessant sind, die sich sehr genau mit Rechen(un)genauigkeiten beschäftigen und mit numerischen Näherungen arbeiten.
Der Abstand von einer Fließkommazahl zur nächsten ist durch den internen Aufbau nicht immer gleich. Wie groß genau der Abstand einer Zahl zur nächstmöglichen ist, zeigt ulp( double) bzw. ulp(float). Der lustige Methodenname ist eine Abkürzung für Unit in the Last Place. Was genau denn die nächsthöhere/-niedrigere Zahl ist, ermitteln die Methoden nextUp(float|double) und nextDown(float|double), die auf nextAfter(…) zurückgreifen.
[zB] Beispiel
Was kommt nach und vor 1?
System.out.printf( "%.16f%n", Math.nextUp( 1 ) );
System.out.printf( "%.16f%n", Math.nextDown( 1 ) );
System.out.printf( "%.16f%n", Math.nextAfter( 1, Double.POSITIVE_INFINITY ) );
System.out.printf( "%.16f%n", Math.nextAfter( 1, Double.NEGATIVE_INFINITY ) );
Die Ausgabe ist:
1,000000119209289
0,9999999403953552
1,0000001192092896
0,9999999403953552
nextUp(d) ist eine Abkürzung für nextAfter(d, Double.POSITIVE_INFINITY) und nextDown(d) ist eine Abkürzung für nextAfter(d, Double.NEGATIVE_INFINITY). Ist das zweite Argument von Math.nextAfter(…) größer als das erste, dann wird die nächstgrößere Zahl zurückgegeben; ist das zweite Argument kleiner, dann die nächstkleinere Zahl. Bei Gleichheit kommt die gleiche Zahl zurück.
Methode | Rückgabe der ulp |
---|---|
Math.ulp( 0.00001 ) | 0,000000000000000000001694065895 |
Math.ulp( –1 ) | 0,00000011920928955078125 |
Math.ulp( 1 ) | 0,00000011920928955078125 |
Math.ulp( 2 ) | 0,0000002384185791015625 |
Math.ulp( 10E30 ) | 1125899906842624 |
Ein Quantum Ungenauigkeit
Die üblichen mathematischen Fließkommaoperationen haben eine ulp von ½. Das ist so genau wie möglich. Um wie viel ulp die Math-Methoden vom echten Resultat abweichen können, steht in der Javadoc. Rechenfehler lassen sich insbesondere bei komplexen Methoden nicht vermeiden. So darf sin(double) eine mögliche Ungenauigkeit von 1 ulp haben, atan2(double, double) von maximal 2 ulp und sinh(double), cosh(double) sowie tanh(double) von 2,5 ulp.
Die ulp(…)-Methode ist für das Testen interessant, denn mit ihr lassen sich Abweichungen immer in der passenden Größenordnung realisieren. Bei kleinen Zahlen ergibt eine Differenz von vielleicht 0,001 einen Sinn, bei größeren Zahlen kann die Toleranz größer sein.
Java deklariert in den Klassen Double und Float drei besondere Konstanten. Sie lassen sich gut mit nextAfter(…) erklären. Wir sehen das hier am Beispiel von Double:
MIN_VALUE = nextUp(0.0) = Double.longBitsToDouble(0x0010000000000000L)
MIN_NORMAL = MIN_VALUE/(nextUp(1.0)-1.0) = Double.longBitsToDouble(0x1L)
MAX_VALUE = nextAfter(POSITIVE_INFINITY, 0.0) =
Double.longBitsToDouble(0x7fefffffffffffffL)