java-forum.org - Java programmieren aus Leidenschaft

Zurück   java-forum.org - Java programmieren aus Leidenschaft > Java-Forum FAQs > FAQ - Übersicht > Java-FAQ Beiträge > Allgemeines

Antwort
Themen-Optionen Thema durchsuchen Ansicht
Alt 30.07.2011, 17:35   #1 (permalink)
Ark
Stammbenutzer
Megabyte
 
Benutzerbild von Ark
 
Registriert seit: 23.12.2005
Fachbeiträge: 1.746
Abgegebene Danke: 74
Erhielt 147 Danke für 134 Beiträge
Standard Ungenauigkeit von double und float: Gleitkommazahlen und Alternativen

Ungenauigkeit von double und float: Gleitkommazahlen und Alternativen

Weil in letzter Zeit immer wieder Fragen auftauchen, warum double so ungenau sei etc., habe ich mich schon vor längerer Zeit dazu entschlossen, einen FAQ-Beitrag zu erstellen.

Hinweis: Im gesamten Beitrag sind mit Gleitkommazahlen immer binäre Gleitkommazahlen gemeint. Entsprechend verhält es sich mit Begriffen wie Gleitkommaarithmetik, Gleitkommadarstellung etc. Außerdem sind mit double-Literalen wie 0.0 oder 1.0 je nach Kontext auch immer die jeweiligen float-Literale wie 0.0f oder 1.0f gemeint.
Ark ist offline  
Bei Google nach dem markiertem Wort suchen Bei Wikipedia nach dem markiertem Wort suchen Im Forum nach dem markiertem Wort suchen
Mit Zitat antworten
Danke sagen:
LiVo (21.12.2011), polarpro (04.01.2012)
Alt 31.07.2011, 19:53   #2 (permalink)
Ark
Stammbenutzer
Megabyte
Themenstarter
 
Benutzerbild von Ark
 
Registriert seit: 23.12.2005
Fachbeiträge: 1.746
Abgegebene Danke: 74
Erhielt 147 Danke für 134 Beiträge
Standard Ungenauigkeit von double und float: Gleitkommazahlen und Alternativen

Abschnitt A: Typische Fehler im Umgang mit Gleitkommazahlen, seltsame und scheinbar falsche Ergebnisse


Frage A.1: Bei Division zweier Zahlen kommt immer ein falsches Ergebnis (z.B. 0) raus. Mache ich etwas falsch? Wie löse ich das Problem?

Wahrscheinlich wird eine Ganzzahldivision statt einer Gleitkommadivision durchgeführt. Siehe dazu folgendes Beispiel:
Java Code: Quelltext in neuem Fenster öffnen
1
2
3
int a = 10;
int b = 3;
double x = a / b; // Ergebnis: 3.0 statt 3.3333333…
Java kommt hier auf 3, weil die Division a / b durchgeführt wird, bevor in Gleitkommadarstellung umgerechnet wird. Dieses Phänomen tritt immer dann auf, wenn beide Operanden Ganzzahlen sind, also vom Typ byte, short, char, int oder long. Weil a und b vom Typ int sind, wird hier eine Ganzzahldivision durchgeführt, d.h., es wird nur der Vorkommateil ermittelt. Der Nachkommateil wird verworfen, das Ergebnis ist immer eine Ganzzahl. Es wird nicht kaufmännisch gerundet, sondern immer zur Null hin: 7 / 8 ergibt nicht 1, sondern 0.

Abhilfe schafft hier, die Umrechnung in Gleitkommazahlen zu erzwingen, bevor die Division durchgeführt wird. Dazu gibt es je nach Situation verschiedene Möglichkeiten. Wichtig ist, dass im Quelltext mindestens einer der beiden Operanden eine Gleitkommazahl ist:
Java Code: Quelltext in neuem Fenster öffnen
1
2
3
4
5
6
7
int a = 10;
int b = 3;
double x = (double)a / b; // a wird vor Division zu double gecastet.
float y = (float)a / b; // Gleiches mit float.
double nochEinBeispiel = 12 / Math.PI; // Math.PI ist bereits double.
double undNochEinBeispiel = a / 3.0; // Die Zahl 3.0 wird durch die Schreibung
                                     // mit Punkt als double angenommen.


Frage A.2: Obwohl zwei Zahlen gleich sein müssten, sind sie es nicht. Wie vergleiche ich Gleitkommazahlen richtig?

Vergleiche zwei Gleitkommazahlen a und b nicht mit a == b , wahrscheinlich wird false ermittelt, denn Gleitkommazahlen sind fast immer ungenau (siehe Frage B.1). Vergleiche besser den Abstand zu einer hinreichend kleinen positiven Zahl (üblicherweise mit Epsilon bezeichnet): Math.abs(a - b) < epsilon . Wie groß Epsilon gewählt werden muss (ob z.B. 1e-4 oder 1e-12), hängt von der Problemstellung und der erwarteten/möglichen Genauigkeit ab. Wenn du zwischen 0.0 und -0.0 unterscheiden oder mit NaN umgehen musst, siehe Frage A.5.


Frage A.3: Trigonometrische Funktionen geben scheinbar falsche Ergebnisse aus. Woran liegt das?

Wahrscheinlich gehst du davon aus, dass die Winkel im Gradmaß ein- bzw. ausgegeben werden. Diese Annahme ist jedoch falsch, denn Methoden wie Math.sin(double) oder Math.cos(double) erwarten den Winkel im Bogenmaß. Zur Umrechnung vom Gradmaß ins Bogenmaß kannst du Math.toRadians(double) verwenden. Genauso gibt dir z.B. Math.atan(double) den Winkel im Bogenmaß zurück. Diesen kannst du mit Math.toDegrees(double) wieder ins Gradmaß umrechnen. Hier ein Beispiel:
Java Code: Quelltext in neuem Fenster öffnen
1
2
double sinusVonNeunzigGrad = Math.sin(Math.toRadians(90)); // 1.0
double arkustangensVonEinsInGrad = Math.toDegrees(Math.atan(1)); // 45.0
Beachte dabei auch Frage B.3.


Frage A.4: Beim Konvertieren von Gleitkommazahlen in Ganzzahlen (cast) wird seltsam gerundet. Was geht da vor?

Siehe Frage A.8.


Frage A.5: Ich bekomme als Ergebnis -0.0, Infinity, -Infinity oder NaN. Was hat es damit auf sich? Wie gehe ich damit um?

Die negative Null, -0.0, kann bei Berechnungen auftreten, wenn das Ergebnis betragsmäßig zu klein für double bzw. float, aber negativ ist. Genauso kann 0.0 ermittelt werden, wenn das Ergebnis vom Betrag her zu klein, aber positiv ist. Meistens können 0.0 und -0.0 als gleich angesehen werden, es gilt auch 0.0 == -0.0 . Unterschiede gibt es jedoch in Grenzfällen, so ist z.B. 1.0 / 0.0 == Double.POSITIVE_INFINITY , aber 1.0 / -0.0 == Double.NEGATIVE_INFINITY . Siehe dazu auch Frage A.6 und Frage B.3.

Infinity heißt unendlich, genauer: positiv unendlich. -Infinity steht entsprechend für negativ unendlich. Solche Werte werden von Methoden ermittelt, wenn das Ergebnis betragsmäßig zu groß für double bzw. float ist. Es kann aber auch sein, dass das Ergebnis grundsätzlich nicht als endlich große Zahl erfasst werden kann. So ist 1.0 / 0.0 == Double.POSITIVE_INFINITY oder Math.log(0.0) == Double.NEGATIVE_INFINITY .

NaN steht für "Not a Number", also keine Zahl. Das Ergebnis einer Berechnung ist üblicherweise dann NaN, wenn keine durch double bzw. float darstellbare Zahl und weder Infinity noch -Infinity brauchbare Annäherungen sind. So liefern z.B. 0.0 / 0.0 oder Math.sqrt(-1.0) NaN. Es gibt viele verschiedene NaNs für double bzw. float. Java behandelt alle NaNs gleich, auch wenn sie sich hinsichtlich ihres Bitmusters unterscheiden. (Es wird auch nicht zwischen signaling NaNs und quiet NaNs unterschieden, und die Verwendung von NaN in Rechenoperationen führt im Allgemeinen auch nicht zu einer Ausnahme.)

Wenn NaN ermittelt wird, heißt das aber nicht unbedingt, dass du niemals einen passenden Wert finden könntest. So kannst du bei Bedarf z.B. auf komplexe Zahlen ausweichen (siehe Frage D.1). In vielen anderen Fällen kann die Analysis weiterhelfen. Dies sei hier nur kurz am Beispiel des nicht normierten Sinus cardinalis demonstriert. Eine naive Implementierung könnte so aussehen:
Java Code: Quelltext in neuem Fenster öffnen
1
2
3
public static double sinc(double x){
    return Math.sin(x) / x; // unzureichend
}
Obwohl die sinc-Funktion im ganzen Bereich der reellen Zahlen definiert ist und im Unendlichen gegen Null konvergiert, liefert diese Implementierung für 0.0, -0.0, Infinity und -Infinity nur NaN. Eine Implementierung, die diese Grenzfälle berücksichtigt, kann für jedes x (außer NaN) einen sinnvollen Wert liefern:
Java Code: Quelltext in neuem Fenster öffnen
1
2
3
4
5
public static double sinc(double x){
    return x == 0.0 ? 1.0 :
           Double.isInfinite(x) ? 0.0 :
           Math.sin(x) / x;
}
Um unerwünschte Effekte zu vermeiden, sollte man also bei eigenen Methoden, die double- oder float-Werte entgegennehmen, auch an Infinity, -Infinity, -0.0 und NaN als mögliche Argumente denken und entsprechend reagieren (z.B. ausschließen). Das genaue Verhalten einer (nicht selbst geschriebenen) Methode, z.B. in java.lang.Math , bei Eingabe dieser speziellen Werte musst du ihrer Dokumentation entnehmen, soweit eine solche vorliegt. Üblicherweise ist das Ergebnis jeder Operation NaN, wenn bereits mindestens ein Operand NaN ist.

Das Vergleichen von Gleitkommazahlen gestaltet sich ähnlich schwierig, wenn spezielle Werte im Spiel sind. Hier eine Übersicht:
  • Für die Relationen <, >, <=, >= gilt insbesondere:
    • NaN ist nicht vergleichbar, d.h., das Ergebnis ist immer false.
    • Infinity ist gleich Infinity und größer als jeder endliche Wert und -Infinity.
    • -Infinity ist gleich -Infinity und kleiner als jeder endliche Wert und Infinity.
    • 0.0 und -0.0 sind gleich.
  • Für die Relationen ==, != gilt insbesondere:
    • NaN ist ungleich NaN und ungleich jedem endlichen Wert.
    • Infinity ist gleich Infinity.
    • -Infinity ist gleich -Infinity.
    • 0.0 und -0.0 sind gleich.
  • Abweichend von oben Genanntem gilt für Double.compare(double, double) , Double.compareTo(Double) und Double.equals(Object) insbesondere:
    • NaN ist gleich NaN.
    • NaN ist vergleichbar und gilt immer als größer als alles, was nicht NaN ist.
    • 0.0 und -0.0 sind ungleich.
    • 0.0 ist echt größer als -0.0.
(Analog gilt dies dann auch für Float.compare(float, float) , Float.compareTo(Float) und Float.equals(Object) .)

Es ist also nur mit einem Trick möglich, auf NaN zu testen, denn x == Double.NaN ergibt immer false, unabhängig davon, ob x tatsächlich NaN ist oder nicht. Der Ausdruck x != x wird genau dann zu true ausgewertet, wenn x NaN ist. Zur Erhöhung der Lesbarkeit ist dem jedoch die Methode Double.isNaN(double) (bzw. Float.isNaN(float) ) vorzuziehen.


Frage A.6: Warum sollte ich Methoden der Standard-API wie Math.abs(double) oder java.util.Arrays.sort(double[]) statt meiner eigenen benutzen?

Einerseits können andere den Code leichter nachvollziehen, wenn Methoden der Standard-API benutzt werden. Andererseits ist eine korrekte Implementierung, die auch mit speziellen Werten (siehe Frage A.5) umgehen kann, selten trivial. Zur Verdeutlichung hier eine scheinbar einfach zu implementierende Methode in java.lang.Math , die die größte der beiden eingegebenen Zahlen zurückgeben soll:
Java Code: Quelltext in neuem Fenster öffnen
1
2
3
4
5
6
7
8
    public static double max(double a, double b) {
        if (a != a) return a;   // a is NaN
        if ((a == 0.0d) && (b == 0.0d)
            && (Double.doubleToLongBits(a) == negativeZeroDoubleBits)) {
            return b;
        }
        return (a >= b) ? a : b;
    }


Frage A.7: Wie komme ich an den Vorkommateil/Nachkommateil einer Zahl? Wie teste ich auf Ganzzahl?

Um den Vorkommateil einer Zahl x zu ermitteln, kannst du einfach den Nachkommateil von x abziehen, also z.B. x - x % 1.0 . Bei Frage A.8 wird eine trunc() -Methode vorgestellt, die in Grenzfällen (siehe Frage A.5) genauer und unter Umständen sogar schneller ist.

Um an den Nachkommateil zu kommen, rechnest du im einfachsten Fall x % 1.0 . Das Ergebnis hat dann das Vorzeichen von x. Ist der Vorkommateil schon bekannt, kann durch Abziehen desselben von x der Nachkommateil ermittelt werden, also z.B. x - trunc(x) . Allerdings geht hier das Vorzeichen verloren, wenn das Ergebnis Null ist. Wenn das Vorzeichen wichtig sein sollte, kann es mit Math.copySign(double, double) wieder hinzugefügt werden, also Math.copySign(x - trunc(x), x) .

Beachte, dass der Nachkommateil allein nicht genauer ist als der der ursprünglichen Zahl, auch wenn mehr Ziffern in der Ausgabe erscheinen sollten.

Eine Zahl x ist genau dann eine Ganzzahl, wenn ihr Nachkommateil gleich Null ist, also x % 1.0 == 0.0 gilt. Alternativ kannst du zeigen, dass durch Runden von x wieder x herauskommt, also z.B. Math.floor(x) == x oder trunc(x) == x .

Je nach geforderter Geschwindigkeit oder Genauigkeit musst du verschiedene Verfahren geschickt miteinander kombinieren, um alle notwendigen Ergebnisse zu erhalten.


Frage A.8: Worin unterscheiden sich die verschiedenen Möglichkeiten, um Zahlen zu runden? Wie runde ich auf eine bestimmte Anzahl an Stellen nach bzw. vor dem Komma?

In Java werden verschiedene Möglichkeiten geboten, um eine Gleitkommazahl x zu runden. Um die richtige Auswahl zu treffen, sollte man ihre Eigenheiten kennen:
  • Eine Konvertierung (Cast) in eine Ganzzahl (long oder int), z.B. mit (long)x , schneidet den Nachkommateil ab, es wird zu Null hin gerundet ("round towards zero"). Beachte, dass das Ergebnis in den gewählten Ganzzahltyp passen muss, sonst ist der Betrag des Ergebnisses falsch. Konvertierungen in short, char oder byte funktionieren dagegen nicht so, wie man es erwarten würde: Vor der Konvertierung in einen solchen Zieltyp erfolgt eine Konvertierung nach int. So wird (byte)Double.POSITIVE_INFINITY ausgewertet wie (byte)(int)Double.POSITIVE_INFINITY , also zu (byte)Integer.MAX_VALUE , und das ist -1 (und nicht etwa 127). Bei einer Konvertierung nach char geht das Vorzeichen gänzlich verloren.
  • Math.floor(x) rundet zur nächstkleineren Ganzzahl ("round down").
  • Math.ceil(x) rundet zur nächstgrößeren Ganzzahl ("round up").
  • Math.round(x) rundet (wie aus der Schule gewohnt) kaufmännisch zur nächsten Ganzzahl. Im Zweifelsfall ist das Ergebnis aber eine größere Zahl, d.h., Math.round(1.5) == 2 und Math.round(-1.5) == -1 . Beachte, dass das Ergebnis in ein long oder int passen muss (je nachdem, ob x vom Typ double oder float ist), sonst ist der Betrag des Ergebnisses falsch, denn im Gegensatz zu den anderen Methoden führt Math.round(x) eine Konvertierung in einen Ganzzahltypen durch. Eine Alternative, die sich daraus ergebende Probleme umgeht, wird weiter unten beschrieben.
  • Math.rint(x) rundet mathematisch zur nächsten Ganzzahl ("round half to even"). Im Zweifelsfall ist der Betrag des Ergebnisses gerade, d.h., Math.rint(1.5) == 2.0 und Math.rint(2.5) == 2.0 .
Unabhängig vom gewählten Verfahren ist das Ergebnis für eine Zahl (also nicht Infinity, -Infinity oder NaN, siehe Frage A.5) immer eine Ganzzahl im mathematischen Sinne. Das Vorzeichen entspricht dem Vorzeichen von x, geht aber für x == -0.0 bei Casts (und damit auch bei Math.round(x) ) verloren. Für Infinity, -Infinity und NaN ist das Ergebnis gleich x (bzw. das Ergebnis, wenn man x nach long bzw int konvertiert, im Fall von Math.round(x) ). Vorsicht, bei Auslöschung durch Subtraktion können Math.ceil(x) und Math.floor(x) unerwartete Ergebnisse liefern, siehe dazu auch Frage B.3.

Sowohl Casts als auch Math.round(x) haben einen Nachteil gegenüber den andereren vorgestellten Methoden: Bei betragsmäßig sehr großen Zahlen, -0.0, Infinity, -Infinity und NaN gehen unnötigerweise Informationen verloren. Hier zwei Alternativen, die auch mit diesen Fällen zurechtkommen:
Java Code: Quelltext in neuem Fenster öffnen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * Schneidet den Nachkommateil ab ("round towards zero").
 */
public static double trunc(double x){
    return x < 0.0 ? Math.ceil(x) : Math.floor(x);
}
 
/**
 * Rundet kaufmännisch zur nächsten Ganzzahl ("round half up").
 */
public static double round(double x){
    // Für x = 0x1.fffffffffffffp-2 (dezimal 0.49999999999999994) ergibt
    // Math.floor(x + 0.5) tatsächlich 1.0 statt 0.0
    return Double.isNaN(x) ? x :
           x == 0x1.fffffffffffffp-2 ? 0.0 :
           Math.copySign(Math.floor(x + 0.5), x);
}
Hier eine Zusammenfassung der Eigenschaften der Methoden inklusive Verhalten bei speziellen Werten (siehe Frage A.5):

AusdruckBeschreibungErhält Betrag für Ganzzahl?Erhält negatives Vorzeichen?Erhält Infinity?Erhält NaN?
(byte)xwie (byte)(int)xbedingtbedingtneinnein
(char)xwie (char)(int)xbedingtneinneinnein
(short)xwie (short)(int)xbedingtbedingtneinnein
(int)xzur Nullbedingtbedingtneinnein
(long)xzur Nullbedingtbedingtneinnein
trunc(x) (hier vorgestellt)zur Nulljajajaja
Math.floor(x)zur kleineren Zahljajajaja
round(x) (hier vorgestellt)kaufmännischjajajaja
Math.round(x)kaufmännischbedingtbedingtneinnein
Math.ceil(x)zur größeren Zahljajajaja
Math.rint(x)mathematischjajajaja

Wenn du auf eine bestimmte Anzahl Stellen nach dem Komma runden willst, musst du x vor dem Runden mit der entsprechenden Potenz der Basis multiplizieren und das gerundete Ergebnis wieder durch dieselbe Potenz teilen. Beim Runden auf Stellen vor dem Komma kommt entsprechend der Kehrwert der Potenz zum Einsatz, bzw. Multiplizieren und Dividieren werden vertauscht. Hier ein paar Beispiele:

Java Code: Quelltext in neuem Fenster öffnen
1
2
3
double a = 1234.5678;
double aufHundertstel = Math.round(a * 100.0) / 100.0; // 1234.57
double aufHunderter = Math.round(a / 100.0) * 100.0; // 1200.0

Beachte hierzu auch Frage B.1.


Frage A.9: Was ist der Unterschied zwischen Math.atan(double) und Math.atan2(double, double) ? Wie rechne ich kartesische Koordinaten in Polarkoordinaten um (und anders herum)?

Mit Math.atan2(y, x) ist es möglich, den Arkustangens von y/x im Bereich von -Math.PI bis Math.PI zu berechnen. Da sich Math.atan(double) nur für das Verhältnis y/x interessiert, "fehlt" dieser Methode ein Vorzeichen, um aus allen vier Quadranten wählen zu können: Sie kann nur einen Wertebereich von -Math.PI / 2.0 bis Math.PI / 2.0 abdecken.

Zusammen mit Math.hypot(double, double) lässt sich Math.atan2(double, double) gut dazu verwenden, kartesische Koordinaten in Polarkoordinaten umzurechnen. Beachte jedoch, dass Math.atan2(y, x) als erstes Argument die y-Koordinate bekommen möchte, während dies bei Math.hypot(x, y) egal ist.

Um Polarkoordinaten r (Radius) und phi (Winkel) wieder in kartesische Koordinaten umzurechnen, berechnest du für x r * Math.cos(phi) und für y r * Math.sin(phi) .


Frage A.10: Was ist der Unterschied zwischen dem Modulo-Operator % für Gleitkommazahlen und Math.IEEEremainder(double, double) ? Wozu ist letztere Methode überhaupt gut?

Das Vorzeichen von a % d ist immer das von a. Um dabei alle möglichen Reste darzustellen, wird ein geeigneter Betrag zwischen 0 (inkl.) und d (exkl.) gewählt.

Der von Math.IEEEremainder(a, d) ausgewählte Rest ist immer der betragsmäßig kleinste. Der Betrag ist also nur höchstens so groß wie die Hälfte des Betrags von d. Um alle möglichen Reste darzustellen, wird ein geeignetes Vorzeichen gewählt. Es kann also passieren, dass das Ergebnis negativ ist, obwohl beide Operanden positiv sind.

Für beide Operationen gilt:
  • Sie sind (bei festem zweiten Argument) ungerade im ersten Argument: a % d == -(-a % d) bzw. Math.IEEEremainder(a, d) == -Math.IEEEremainder(-a, d)
  • Sie sind (bei festem ersten Argument) gerade im zweiten Argument, d.h., das Vorzeichen von d spielt keine Rolle: a % d == a % -d bzw. Math.IEEEremainder(a, d) == Math.IEEEremainder(a, -d)
  • Wenn a gleich Infinity, -Infinity oder NaN ist, oder wenn d gleich Null oder NaN ist, dann ist das Ergebnis immer NaN.
  • Für a == 0.0 hat das Ergebnis das gleiche Vorzeichen wie a (es sei denn, auch d == 0.0 , dann ist das Ergebnis NaN).

Math.IEEEremainder(double, double) ist gut dazu geeignet, um z.B. einen vom Nutzer eingegebenen Winkel phi (im Bogenmaß) auf einen äquivalenten Winkel im Intervall von -Math.PI bis Math.PI (inkl.) abzubilden. Dazu berechnest du einfach Math.IEEEremainder(phi, Math.PI * 2) . Das Ergebnis liegt dann immer in demselben Intervall wie z.B. die Werte von Math.atan2(double, double) . Würdest du da stattdessen den %-Operator verwenden, kämst du für phi mit positivem Vorzeichen auf Werte zwischen 0.0 und Math.PI * 2 (exkl.) und für phi mit negativem Vorzeichen auf Werte zwischen -Math.PI * 2 (exkl.) und -0.0 (inkl.).
Ark ist offline  
Bei Google nach dem markiertem Wort suchen Bei Wikipedia nach dem markiertem Wort suchen Im Forum nach dem markiertem Wort suchen
Mit Zitat antworten
Danke sagen:
LiVo (21.12.2011), polarpro (04.01.2012)
Alt 31.07.2011, 19:54   #3 (permalink)
Ark
Stammbenutzer
Megabyte
Themenstarter
 
Benutzerbild von Ark
 
Registriert seit: 23.12.2005
Fachbeiträge: 1.746
Abgegebene Danke: 74
Erhielt 147 Danke für 134 Beiträge
Standard Ungenauigkeit von double und float: Gleitkommazahlen und Alternativen

Abschnitt B: Woher Rundungsfehler kommen und wie man mit ihnen umgeht


Frage B.1: Woher kommen die Rundungsfehler?

Bei der Verwendung von Gleitkommazahlen, in Java repräsentiert durch die Datentypen double und float, kommt es sehr häufig zu Rundungsfehlern. Ein typisches Beispiel liefert dieser Code:

Java Code: Quelltext in neuem Fenster öffnen
1
2
3
4
5
double x = 1.1;
double y = 0.1;
System.out.println("x   = "+ x);
System.out.println("y   = "+ y);
System.out.println("x+y = "+ (x + y));
Ausgabe:
Code:
x   = 1.1
y   = 0.1
x+y = 1.2000000000000002
Offensichtlich ist die Ausgabe von x+y falsch bzw. ungenau. Der Grund: die Ziffern der Gleitkommazahlen werden intern im Binärsystem gespeichert. Bruchzahlen, die im Dezimalsystem endlich erscheinen, sind im Binärsystem häufig periodisch, würden also unendlich viele Stellen nach dem Komma benötigen (siehe Frage B.8). Da aber der Speicherplatz für eine Zahl immer begrenzt ist, muss sowohl bei periodischen Binärbrüchen als auch bei irrationalen Zahlen (z.B. Pi) gerundet werden.

Im Beispiel sind die Zahlen 1,1 und 0,1 im Dezimalsystem endlich, im Binärsystem jedoch ist der Nachkommateil beider(!) Zahlen periodisch: dezimal 0,1 entspricht binär 0,000110011001100110011…. Sowohl 1,1 als auch 0,1 werden also intern nur näherungsweise gespeichert. Bei der Ausgabe von x und y als Dezimalzahlen ist nur deshalb kein Fehler zu erkennen, weil die Berechnung nach spätestens 16 Dezimalstellen (bei double) bzw. 8 Dezimalstellen (bei float) hinter dem Komma abgebrochen und das Ergebnis gerundet ausgegeben wird. Bereits bei der Addition von x und y vergrößert sich jedoch der Fehler, sodass sich dieser auch bei der Ausgabe auswirkt.


Frage B.2: Kann ich diese Rundungsfehler verhindern?

Jein. Eine Dezimalzahl ist nur dann exakt als Gleitkommazahl darstellbar, wenn die Anzahl der zur Darstellung benötigten Binärstellen klein genug ist (siehe Frage B.4 und Frage B.6). Bei irrationalen Zahlen wie Pi oder Wurzel aus 2 kann diese Bedingung niemals erfüllt werden. Eine rationale Zahl q = x/y (maximal gekürzt) hat genau dann endlich viele binäre Nachkommastellen, wenn y eine Zweierpotenz (1, 2, 4, 8, 16, …) ist, sonst eben nicht. Siehe dazu auch Frage B.8.

Im Allgemeinen ist von der Verwendung von Gleitkommazahlen dringend abzuraten, wenn exakte Ergebnisse erforderlich sind. Stattdessen solltest du nötigenfalls auf andere Darstellungsformen ausweichen, siehe dazu Frage D.1.


Frage B.3: In meinem Fall sind Rundungsfehler durch Gleitkommaarithmetik durchaus erlaubt. Was sollte ich dennoch beachten?

Hier sind einige Tipps aufgelistet, um Rundungsfehler zu reduzieren oder mit ihnen besser umzugehen. Natürlich kannst du hiervon abweichen, wenn die Problemstellung es erfordert (z.B. kaufmännisches Runden). Diese Liste erhebt weder Anspruch auf Vollständigkeit noch auf unumstößliche Richtigkeit.
  • Rechne möglichst lange mit exakten Darstellungen (z.B. Ganzzahlen).
  • Führe Gleitkommaarithmetik nur mit (mindestens) double-Genauigkeit durch.
  • Minimiere die Anzahl der Operationen, vermeide unnötiges Umrechnen (z.B. zwischen Grad- und Bogenmaß, 100% und 1, float und double).
  • Verwende Gleitkommazahlen nicht als Schleifenzähler. Versuche stattdessen, die Abbruchbedingung mit Ganzzahlen zu beschreiben und die vom Schleifenzähler abhängigen Gleitkommazahlen direkt zu berechnen.
  • Durch Runden kannst du einen Fehler sogar vergrößern, nicht nur verkleinern (ganz egal, wie hübsch die gerundete Zahl gegenüber der ungerundeten aussieht). Runde erst zum Schluss (bzw. nur bei der Ausgabe des Ergebnisses) oder wenn du genau weißt, wie du runden musst (für einige Rundungsverfahren siehe Frage A.8). Wenn du dennoch ein Problem mit Rundungsfehlern hast, versuche, sie gar nicht erst entstehen zu lassen.
  • Vorsicht bei Unstetigkeitsstellen und an den Rändern von Definitionsbereichen: Ist eine Funktion f an der Stelle x nicht stetig oder nicht definiert, kann f(x) komplett falsch sein! Dies betrifft sehr häufig die Division durch Null oder fast Null, aber auch andere Funktionen. Beispiele:
    Java Code: Quelltext in neuem Fenster öffnen
    1
    2
    3
    4
    5
    6
    
    double fastNull =; // Eine Zahl, die z.B. durch vorangegangene Operationen
                         // fast 0.0 ist - kann auch negativ sein!
    double vorsicht1 = x / fastNull;
    double vorsicht2 = Math.tan(Math.PI / 2.0 + fastNull);
    double vorsicht3 = Math.sqrt(fastNull);
    double vorsicht4 = Math.log(fastNull);
  • Verschiedene Gesetzmäßigkeiten gelten bei Gleitkommazahlen nicht, dies betrifft insbesondere das Assoziativgesetz und das Distributivgesetz:
    Java Code: Quelltext in neuem Fenster öffnen
    1
    2
    3
    4
    5
    6
    
    // Mit Dank an SlaterB für dieses Beispiel
    float a = 2.8788f;
    float b = 0.711f;
    float c = 2.5286f;
    float x = a + b + c; // 6.1183996
    float y = a + c + b; // 6.1184
  • Vermeide Addition bzw. Subtraktion zweier Zahlen, wenn sich ihre Beträge um mehrere Größenordnungen voneinander unterscheiden. Es besteht die Gefahr der Absorption:
    Java Code: Quelltext in neuem Fenster öffnen
    1
    
    boolean seltsam = 20000000000000000.0 + 1 == 20000000000000000.0; // true
  • Vorsicht bei Subtraktion zweier fast gleich großer Zahlen: Das Ergebnis hat aufgrund von Auslöschung möglicherweise ausschließlich Rundungsfehler und damit keinerlei Aussagekraft mehr. Unter Umständen stimmt nicht einmal das Vorzeichen:
    Java Code: Quelltext in neuem Fenster öffnen
    1
    2
    3
    4
    
    double x = 4 + 0.1 + 1.1 - 2.2 + 1; // 3.999999999999999 statt 4.0
    double y = x - 4; // -8.881784197001252E-16 statt 0.0
    double z = Math.sqrt(y); // NaN statt 0.0
    boolean won = z < 1e-6; // false statt true
  • Verwende nach Möglichkeit betragsmäßig kleine Winkel bei trigonometrischen Funktionen (Math.sin(double) , Math.cos(double) , Math.tan(double) ). Eventuell hilft dir dabei Math.IEEEremainder(double, double) , siehe Frage A.10. Je nach Hardware können Berechnungen sogar schneller ausgeführt werden, wenn die eingegebenen Winkel in einen bestimmten Bereich fallen.
  • Neben Math.exp(double) und Math.log(double) gibt es für kleine Argumente auch Math.expm1(double) und Math.log1p(double) , diese sind unter Umständen genauer.
  • Zur Berechnung des Abstands zweier Punkte im zweidimensionalen Raum gibt es Math.hypot(double, double) . Diese Methode vermeidet Fehler, die sonst durch Überläufe entstanden wären.
  • Zur Multiplikation einer Gleitkommazahl mit einer Zweierpotenz gibt es Math.scalb(double, int) bzw. Math.scalb(float, int) .
Für weitere Hinweise siehe Frage B.6 und Frage B.9.


Frage B.4: Wann nehme ich double, wann float? Wo ist da der Unterschied?

Der wesentliche Unterschied zwischen double und float liegt in der Genauigkeit und im Speicherverbrauch. double benötigt 8 Byte bzw. 64 Bit pro Zahl. Diese 64 Bit verteilen sich auf 1 Bit fürs Vorzeichen, 11 Bit für den Exponenten und 52 Bit für die Mantisse. Bei float werden nur 4 Byte bzw. 32 Bit belegt: 1 Bit Vorzeichen, 8 Bit Exponent und 23 Bit Mantisse. Diese Verteilung folgt der IEEE-754-Norm.

Dabei gibt die Länge der Mantisse an, wie lang die binäre Ziffernfolge maximal sein kann. Im Normalfall (bei normalisierten Zahlen) kommt dabei noch ein "hidden bit" hinzu, double berücksichtigt also bis zu 53 Bit, float bis zu 24 Bit. Der Exponent steuert die Position des Kommas, also ob der Betrag der Zahl sehr klein oder sehr groß sein soll, daher auch die Bezeichnung "Gleitkomma". Das Vorzeichenbit steuert, ob die Zahl negativ oder positiv ist. (Zu speziellen Werten siehe Frage A.5.)

Der normalerweise in Java verwendete Typ zur Darstellung von Gleitkommazahlen ist double, weil er eine sehr viel höhere Genauigkeit als float aufweist. Beweggründe, die dennoch für float sprechen, sind:
  • APIs, die float verlangen (kann z.B. auch Hardware sein),
  • Speicherverbrauch (normalerweise erst bei sehr großen Arrays spürbar),
  • Geschwindigkeit (nur in besonderen Situationen, da Geschwindigkeitsvorteil für gewöhnlich kaum messbar),
  • Energieverbrauch (z.B. bei akkubetriebenen Geräten interessant),
  • verringerter Synchronisationsaufwand (nur bei entsprechenden Multithreading-Szenarien interessant).
Der letzte Punkt entspringt einer Vorschrift der Java Language Specification (JLS), nach der der Zugriff auf 32-Bit-Primitive immer atomar erfolgen muss. Zugriffe auf Variablen größerer primitiver Datentypen (dazu zählen long und double mit jeweils 64 Bit) können dagegen auch in zwei Zügen durchgeführt werden. Eventuell kann aber auch das Schlüsselwort volatile helfen.

Ferner sollte bedacht werden, dass viele Methoden in java.lang.Math mit double rechnen. Sollte es also keine triftigen Gründe für die Verwendung von float geben, ist double vorzuziehen.


Frage B.5: Warum verliere ich Genauigkeit, wenn ich eine Ganzzahl in eine Gleitkommazahl konvertiere (cast)?

Ganzzahlen vom Typ long speichern bis zu 63 Binärstellen, double aber nur bis zu 53. Ein long kann also nicht immer verlustfrei in double konvertiert werden. Analog gilt dies auch für int (31 Bit) und float (24 Bit). Es passt aber problemlos z.B. ein int in ein double.


Frage B.6: Hat die Position des Kommas einen Einfluss auf die Genauigkeit? Wie groß ist die Lücke zwischen zwei benachbarten Gleitkommazahlen?

Wie in der Antwort zu Frage B.4 dargestellt, berücksichtigt double bis zu 53 Binärstellen, float bis zu 24 Binärstellen. Da die Anzahl der gespeicherten Binärstellen begrenzt ist, legt die Position des Kommas fest, mit welcher Genauigkeit Vor- und Nachkommateil gespeichert werden: Eine hohe Anzahl an Vorkommastellen bringt eine niedrige Anzahl an Nachkommastellen mit sich (und anders herum).

So benötigt eine Zahl mit Vorkommateil 1012 zehn binäre Vorkommastellen, bei double bleiben also nur noch 43 Binärstellen für den Nachkommateil übrig. Ist der Vorkommateil jedoch z.B. 1, so ist nur ein Bit für den Vorkommateil nötig, die verbleibenden 52 Binärstellen können für den Nachkommateil verwendet werden.

Zur Orientierung: Im Bereich zwischen 0.0 und 1.0 gibt es ungefähr so viele darstellbare Zahlen wie zwischen 1.0 und Infinity. Analog gilt dies für den negativen Bereich. In der Nähe der Null kommen allerdings so genannte denormalisierte (IEEE 754-2008: subnormale) Zahlen zum Einsatz. Diese sind betragsmäßig kleiner als 2^-1022 (double) bzw. 2^-126 (float) und verlieren an Genauigkeit, je näher sie der Null sind.

Aufgrund dieser Zusammenhänge ist auch der Abstand zwischen einer darstellbaren Zahl und der betragsmäßig nächstgrößeren darstellbaren Zahl nicht für alle Zahlen gleich groß:
Java Code: Quelltext in neuem Fenster öffnen
1
2
3
4
5
6
7
double x = 1e-42;
double y = Math.PI; // 3.141592653589793
double z = 1e20;
 
double xUlp = Math.ulp(x); // 1.5930919111324523E-58
double yUlp = Math.ulp(y); // 4.440892098500626E-16
double zUlp = Math.ulp(z); // 16384.0

Im Beispiel benötigt x gar keine Stellen vor dem Komma, sondern ist weit hinter dem Komma genau. Dagegen benötigt y Stellen vor dem Komma und bietet hinter dem Komma entsprechend weniger Genauigkeit. Der Wert von z ist betragsmäßig so groß, dass er weit davon entfernt ist, auch nur eine einzige Stelle nach dem Komma aufnehmen zu können: Die nächstgrößere darstellbare Zahl nach z wäre bereits um 16384 größer.


Frage B.7: Wie verteilen sich die Binärstellen auf Dezimalstellen?

Um zehn verschiedene Ziffern (0 bis 9) im Binärsystem zu unterscheiden, bräuchte man theoretisch etwa 3,322 Binärstellen (genauer: Zweierlogarithmus von 10). Entsprechend benötigt man für beispielsweise 4 Dezimalstellen etwa 13,288 Binärstellen. Anders herum kommt man mit double-Genauigkeit auf etwa 15,955 Dezimalstellen, also knapp 16. Mit float-Genauigkeit sind es nur 7,225 Dezimalstellen. Der nachfolgende Code verdeutlicht den Zusammenhang:
Java Code: Quelltext in neuem Fenster öffnen
1
2
3
4
5
6
7
8
9
final double log2_10 = Math.log(10) / Math.log(2);
double x = 0;
for(int i = 1; i <= 53; i++){
    x += Math.scalb(1.0, -i);
    System.out.println(i + "\t" + // Anzahl verwendeter Binärstellen
        (int)(i / log2_10) + "\t" + // Anzahl Dezimalstellen, zu deren Kodierung
                                    // genügend Bits zur Verfügung stehen
        +x); // Zahl mit entsprechend vielen Einsen hinter dem Komma im Binärsystem
}
Ausgabe (gekürzt):
Code:
1	0	0.5
2	0	0.75
3	0	0.875
4	1	0.9375
5	1	0.96875
[…]
13	3	0.9998779296875
14	4	0.99993896484375
15	4	0.999969482421875
[…]
23	6	0.9999998807907104
24	7	0.9999999403953552
25	7	0.9999999701976776
[…]
50	15	0.9999999999999991
51	15	0.9999999999999996
52	15	0.9999999999999998
53	15	0.9999999999999999
Für die Vorkommastellen gilt dies analog, beachte hierzu auch Frage B.6.


Frage B.8: Warum sind verschiedene Bruchzahlen in verschiedenen Zahlensystemen mal periodisch und mal nicht?

Das liegt an den Primfaktoren der Basis des Zahlensystems und denen des Nenners des gleichwertigen, maximal gekürzten Bruches: Sei z eine rationale Zahl und x/y ein maximal gekürzter Bruch mit x/y = z. Dann ist z in der Darstellung als b-adische Zahl genau dann endlich, wenn alle Primfaktoren von y auch Primfaktoren von b sind. Dabei ist b die Basis des Zahlensystems.

Beispiele im Dezimalsystem (b = 10):
BruchzahlErläuterungNachkommateil
1/4Primfaktor von 4 ist zweimal die 2. Dieser kommt auch in b vor.endlich
1/10Primfaktoren von 10 sind 2 und 5. Diese Primfaktoren kommen auch in b vor.endlich
2/5Primfaktor von 5 ist 5. Dieser kommt auch in b vor.endlich
1/6Primfaktoren von 6 sind 2 und 3. Die 2 kommt in b vor, nicht aber die 3.periodisch

Beispiele im Binärsystem (b = 2):
BruchzahlErläuterungNachkommateil
1/4Primfaktor von 4 ist zweimal die 2. Dieser kommt auch in b vor.endlich
1/10Primfaktoren von 10 sind 2 und 5. Die 2 kommt in b vor, nicht aber die 5.periodisch
2/5Primfaktor von 5 ist 5. Dieser kommt nicht in b vor.periodisch
1/6Primfaktoren von 6 sind 2 und 3. Die 2 kommt in b vor, nicht aber die 3.periodisch

Zur Orientierung: Wenn in der Dezimaldarstellung ein endlicher Nachkommateil existiert, der nicht auf 5 endet, dann ist der Nachkommateil im Binärsystem garantiert periodisch. (Die Umkehrung ist im Allgemeinen falsch!) Ebenso ist ein Nachkommateil, der schon in Dezimaldarstellung periodisch ist, auch im Binärsystem periodisch. (Anders herum gesagt: Wenn der Nachkommateil schon im Binärsystem endlich ist, dann ist er es auch im Dezimalsystem.) Ob eine Zahl im Binär-, Oktal- oder Hexadezimalsystem dargestellt wird, hat keinen Einfluss auf die Periodizität, denn alle diese Zahlensysteme haben Zweierpotenzen als Basis.


Frage B.9: Wozu gibt es die Klasse java.lang.StrictMath und das Schlüsselwort strictfp ?

Wenn eine Methode mit dem Modifizierer strictfp ausgezeichnet wird, werden alle Gleitkommaoperationen, die in dieser Methode notiert sind, streng nach IEEE 754 durchgeführt. Damit wird sichergestellt, dass die Ergebnisse der Gleitkommaoperationen auf allen Maschinen gleich sind. Ohne das Schlüsselwort ist es möglich, dass in Abhängigkeit von der tatsächlich verwendeten Maschine z.B. mit Zwischenergebnissen in viel höherer Genauigkeit gerechnet wird und dadurch von Maschine zu Maschine verschieden genaue Ergebnisse möglich sind.

Wird eine Klasse mit dem Modifizierer strictfp ausgezeichnet, dann wird mit allen Methoden dieser Klasse so umgegangen, als wären sie selbst mit diesem Schlüsselwort ausgezeichnet worden. Entsprechend verwendet die Klasse StrictMath für nahezu alle Methoden eine Bibliothek namens fdlibm, die Berechnungen streng nach dieser Norm durchführt, während dies bei Math nicht der Fall ist.

Java-Compiler, die Bytecode aus einem Quelltext erzeugen, führen Konstantenfaltungen immer "strict" (in diesem Sinne) durch, um sicherzustellen, dass der Compiler bei gleichem Quelltext immer das gleiche Kompilat liefert, unabhängig davon, auf welcher Maschine kompiliert wird.


Frage B.10: Obwohl auf zwei verschiedenen Maschinen das gleiche Programm läuft, kommen beide Maschinen bei Gleitkommazahlen zu leicht unterschiedlichen Ergebnissen. Woran liegt das und wie kann ich das beheben?

Siehe Frage B.9.
Ark ist offline  
Bei Google nach dem markiertem Wort suchen Bei Wikipedia nach dem markiertem Wort suchen Im Forum nach dem markiertem Wort suchen
Mit Zitat antworten
Danke sagen:
LiVo (21.12.2011), polarpro (04.01.2012)
Alt 04.08.2011, 18:14   #4 (permalink)
Ark
Stammbenutzer
Megabyte
Themenstarter
 
Benutzerbild von Ark
 
Registriert seit: 23.12.2005
Fachbeiträge: 1.746
Abgegebene Danke: 74
Erhielt 147 Danke für 134 Beiträge
Standard Ungenauigkeit von double und float: Gleitkommazahlen und Alternativen

Abschnitt C: Eingabe und Ausgabe von Gleitkommazahlen


Frage C.1: Wie kann ich Gleitkommazahlen für den Anwender ausgeben?

Für die Ausgabe von Gleitkommazahlen gegenüber dem Anwender bietet sich die Verwendung eines Formatters (java.util.Formatter ) an. Zur einfacheren Verwendung im Zusammenhang mit der Standardausgabe (z.B. Bildschirm/Konsole) bieten PrintStreams (java.io.PrintStream ) entsprechende printf() -Methoden an. Ein wesentlicher Vorteil eines Formatters kann in seiner Locale-Abhängigkeit liegen: regionsabhängige Unterschiede in der Zahlendarstellung (z.B. Komma statt Punkt zwischen Vor- und Nachkommateil) können so leicht berücksichtigt werden. Eine Alternative ist die Klasse DecimalFormat (java.text.DecimalFormat ), welche auch mit Währungssymbolen und Prozentsätzen umgehen kann. Zur regionsunabhängigen Ausgabe von Gleitkommazahlen (z.B. für persistente Speicherung, Übertragung oder Weiterverarbeitung) siehe Frage C.3.


Frage C.2: Wie kann ich Gleitkommazahlen vom Anwender eingeben lassen?

Für die Eingabe von Gleitkommazahlen durch den Anwender kannst du einen Scanner (java.util.Scanner ) verwenden. Dieser arbeitet locale-abhängig (siehe Frage C.1) und verwendet standardmäßig die Einstellungen der JVM. Soll eine andere Locale verwendet werden, kann der Scanner via useLocale(Locale) dazu angewiesen werden. Mit hasNextDouble() kannst du festellen, ob das nächste Token wie ein double aussieht, und mit nextDouble() kannst du dieses Token einlesen. Eine Alternative ist die Klasse DecimalFormat (java.text.DecimalFormat ), welche auch mit Währungssymbolen und Prozentsätzen umgehen kann.


Frage C.3: Wie kann ich Gleitkommazahlen extern speichern?

Java speichert float- bzw. double-Werte in 4 bzw. 8 aufeinanderfolgenden Bytes im Arbeitsspeicher. Um die Werte z.B. in Dateien zu speichern, gibt es verschiedene Möglichkeiten, von denen einige in den nachfolgenden Tabellen aufgelistet sind:

Methode für doublelocale-abhängig?Genauigkeitsverlust?NaN invertierbar?Länge fest?binär?Exponentialschreibweise?Beispiel Math.PI als StringInvertierung
new Formatter(Locale).format("%f", double).out().toString()jabedingtneinneinneinnein3,141593new Scanner(String).useLocale(Locale).nextDouble()
new Formatter(Locale).format("%g", double).out().toString()jabedingtneinneinneinbedingt3,141593new Scanner(String).useLocale(Locale).nextDouble()
new Formatter(Locale).format("%e", double).out().toString()jabedingtneinneinneinja3,141593e+00new Scanner(String).useLocale(Locale).nextDouble()
Double.toString(double)neinneinneinneinneinbedingt3.14159265358979Double.parseDouble(String)
Double.toHexString(double)neinneinneinneinneinja0x1.921fb54442d18p1Double.parseDouble(String)
Double.doubleToLongBits(double)neinneinneinja (8 Bytes)jaja4614256656552045848Double.longBitsToDouble(long)
Double.doubleToRawLongBits(double)neinneinjaja (8 Bytes)jaja4614256656552045848Double.longBitsToDouble(long)

Methode für floatlocale-abhängig?Genauigkeitsverlust?NaN invertierbar?Länge fest?binär?Exponentialschreibweise?Beispiel (float)Math.PI als StringInvertierung
new Formatter(Locale).format("%f", float).out().toString()jabedingtneinneinneinnein3,141593new Scanner(String).useLocale(Locale).nextFloat()
new Formatter(Locale).format("%g", float).out().toString()jabedingtneinneinneinbedingt3,141593new Scanner(String).useLocale(Locale).nextFloat()
new Formatter(Locale).format("%e", float).out().toString()jabedingtneinneinneinja3,141593e+00new Scanner(String).useLocale(Locale).nextFloat()
Float.toString(float)neinneinneinneinneinbedingt3.1415927Float.parseFloat(String)
Float.toHexString(float)neinneinneinneinneinja0x1.921fb6p1Float.parseFloat(String)
Float.floatToIntBits(float)neinneinneinja (4 Bytes)jaja1078530011Float.intBitsToFloat(int)
Float.floatToRawIntBits(float)neinneinjaja (4 Bytes)jaja1078530011Float.intBitsToFloat(int)

Je nach Anforderung kannst du eine entsprechende Darstellung wählen. Zum Speichern in Binärdateien bieten sich Methoden aus java.io.DataOutputStream und java.io.RandomAccessFile an. Diese heißen dann writeDouble(double) bzw. writeFloat(float) . Beachte, dass diese Methoden auf Double.doubleToLongBits(double) bzw. Float.floatToIntBits(float) zurückgreifen, also NaN nicht bitgenau invertierbar machen (siehe Frage A.5). Zu Formatter und Scanner siehe Frage C.1 bzw. Frage C.2.


Frage C.4: Wie kann ich extern gespeicherte Gleitkommazahlen einlesen?

Um Gleitkommazahlen wieder einzulesen, kannst du z.B. die entsprechenden Methoden in der Spalte "Invertierung" der Tabelle zur Frage C.3 verwenden. Zum Einlesen aus Binärdateien bieten sich Methoden aus java.io.DataInputStream und java.io.RandomAccessFile an. Diese heißen dann readDouble() bzw. readFloat() .


Frage C.5: Mein Scanner (java.util.Scanner ) erkennt keine double-Werte. Was mache ich falsch?

Wahrscheinlich verwendet der Scanner die falsche Locale. Möglicherweise handelt es sich aber auch um einen Bug (im Formatter oder Scanner). In diesem Fall kann es helfen, eine andere Klasse oder eine neuere Java-Version zu verwenden. Siehe dazu auch Frage C.2 und Frage C.4.


Frage C.6: Ich möchte einen Formatter oder Scanner benutzen, aber locale-unabhängig arbeiten. Wie mache ich das?

Verwende für deinen Formatter bzw. Scanner java.util.Locale.ROOT als Locale.

Geändert von Ark (05.08.2011 um 16:07 Uhr)
Ark ist offline  
Bei Google nach dem markiertem Wort suchen Bei Wikipedia nach dem markiertem Wort suchen Im Forum nach dem markiertem Wort suchen
Mit Zitat antworten
Danke sagen:
LiVo (21.12.2011), polarpro (04.01.2012)
Alt 04.08.2011, 18:15   #5 (permalink)
Ark
Stammbenutzer
Megabyte
Themenstarter
 
Benutzerbild von Ark
 
Registriert seit: 23.12.2005
Fachbeiträge: 1.746
Abgegebene Danke: 74
Erhielt 147 Danke für 134 Beiträge
Standard Ungenauigkeit von double und float: Gleitkommazahlen und Alternativen

Abschnitt D: Alternativen zu Gleitkommazahlen


Frage D.1: Welche Alternativen gibt es zu den Gleitkommazahltypen double und float? Was ist mit komplexen Zahlen?

In der Standard-API wird als Alternative java.math.BigDecimal angeboten. Im Gegensatz zu double und float können Berechnungen mit BigDecimal mit beliebig wählbarer Genauigkeit durchgeführt werden. Wie der Name schon andeutet, kann mit BigDecimals wie mit Dezimalzahlen umgegangen werden. Folglich haben sie aber auch die gleichen Schwächen, wie etwa eigentlich periodische Nachkommateile, die gerundet werden müssen (siehe Frage B.8).

Dieses Problem kann umgangen werden, indem die Zahl als gemeiner Bruch gespeichert wird, also als Kombination aus Nenner und Zähler. Entsprechende Implementierungen gibt es bereits, hier ein paar Beispiele:
Die genannten Bibliotheken stellen auch Implementierungen für komplexe Zahlen bereit. Bei Apfloat ist die Basis des Zahlensystems für jede Zahl frei wählbar zwischen 2 und 36. Für Gleitkommazahlen zur Basis 10000 mit wählbarer Genauigkeit bietet sich dfp.sourceforge.net an. Zur Rolle der Basis siehe Frage B.8. Zu Festkommazahlen siehe Frage D.2.

Es sei darauf hingewiesen, dass die hier vorgestellten Alternativen typischerweise deutlich langsamer und speicherhungriger als double bzw. float sind.


Frage D.2: Was ist mit Festkommazahlen?

Wiederum eine Alternative zu den in Frage D.1 genannten sind Festkommazahlen, genauer: Festkommazahlen mit vorgegebener Genauigkeit. Im Gegensatz zu Gleitkommazahlen (siehe Frage B.4) ist dann die Position des Kommas fest vorgegeben. Folglich haben solche Zahlen immer die gleiche Anzahl an Vor- bzw. Nachkommastellen. Eine Zahl x kannst du in eine Festkommazahl konvertieren, indem du eine Ganzzahl z suchst, sodass z/s = x gilt, wobei s eine für alle Festkommazahlen gleich große, ganze Zahl ist. Die Zahl z wird dabei typischerweise durch primitive Datentypen wie long oder int repräsentiert.

Beispiel: Es soll mit einem Geldbetrag in einer bestimmten Währung (z.B. Euro) gerechnet werden. Um die Ungenauigkeiten von double zu umgehen, wird der Betrag in Cent umgerechnet (also mit s = 100 multipliziert) und intern als long gespeichert. Auch alle Berechnungen werden in ganzen Cent durchgeführt und sind entsprechend exakt. Um wieder auf den Betrag in Euro zu kommen, muss der gespeicherte long-Wert durch s (also 100) dividiert werden.

Festkommaoperationen können meist durch einfachere Ganzzahloperationen ausgedrückt werden, was insbesondere bei Prozessoren ohne entsprechende Hardware für Gleitkommaarithmetik oder in Bezug auf Energieverbrauch von Vorteil sein kann. Zudem kannst du die Genauigkeit von Festkommazahlen durch geschickte Wahl von s der jeweiligen Problemstellung anpassen. So kannst du z.B. in Mikrovolt statt Volt (s = 1000000) oder Winkelsekunden statt Grad (s = 3600) rechnen. Anderes Beispiel: wenn du sicherstellen musst, dass die Brüche 1/3 und 1/10 exakt (also mit endlich vielen Nachkommastellen) dargestellt werden können, kannst du für s eine Dreißigerpotenz wählen, denn 30 enthält alle Primfaktoren von 3 und 10. Siehe dazu auch Frage B.8.
Ark ist offline  
Bei Google nach dem markiertem Wort suchen Bei Wikipedia nach dem markiertem Wort suchen Im Forum nach dem markiertem Wort suchen
Mit Zitat antworten
Danke sagen:
LiVo (21.12.2011), polarpro (04.01.2012)
Alt 27.02.2013, 12:45   #6 (permalink)
Java-Forum Team
Moderator
 
Benutzerbild von SlaterB
 
Registriert seit: 13.11.2005
Fachbeiträge: 32.026
Abgegebene Danke: 0
Erhielt 2.623 Danke für 2.583 Beiträge
kein Gleitkommazahlen-Fehler, evtl. zu verschieben, aber vorerst eingetragen:

Vorsicht beim Einsatz des DecimalFormats zum Runden
Java Code: Quelltext in neuem Fenster öffnen
1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
    public static void main(String[] args) {
        String st = "0.5005";
 
        BigDecimal notRounded = new BigDecimal(st);
        DecimalFormat df = new DecimalFormat("0.000");
        System.out.println(df.format(notRounded)); // 0,500
 
        BigDecimal rounded = notRounded.setScale(3, RoundingMode.HALF_UP);
        System.out.println(rounded); // 0.501
    }
}
DecimalFormat nutzt standardmäßig RoundingMode.HALF_EVEN

ab Java 1.6 gibt es setRoundingMode()-Methode, vorher evtl. lieber per BigDecimal runden,
kann danach immer noch an DecimalFormat übergeben werden
__________________
Hansa wird Meister.

Geändert von SlaterB (27.02.2013 um 12:47 Uhr)
SlaterB ist gerade online  
Bei Google nach dem markiertem Wort suchen Bei Wikipedia nach dem markiertem Wort suchen Im Forum nach dem markiertem Wort suchen
Mit Zitat antworten
Antwort

Themen-Optionen Thema durchsuchen
Thema durchsuchen:

Erweiterte Suche
Ansicht

Ähnliche Themen
Thema Autor Forum Antworten Letzter Beitrag
GUI Parkplatz zeichnen Blondie Java Basics - Anfänger-Themen 10 06.07.2011 14:23
(Methoden) Problem mit get-methode im Quelltext hans1 Java Basics - Anfänger-Themen 17 28.02.2011 17:32
Software Rendering Steev Spiele- und Multimedia-Programmierung 3 20.09.2010 21:44
Eigener Graphics-Context Steev Spiele- und Multimedia-Programmierung 10 07.05.2010 19:07
Tabelle in bestimmter Form ausgeben Taramsis Java Basics - Anfänger-Themen 1 20.04.2010 15:09


Lesezeichen

Forumregeln
Es ist Ihnen nicht erlaubt, neue Themen zu verfassen.
Es ist Ihnen nicht erlaubt, auf Beiträge zu antworten.
Es ist Ihnen nicht erlaubt, Anhänge hochzuladen.
Es ist Ihnen nicht erlaubt, Ihre Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.
Trackbacks are aus
Pingbacks are aus
Refbacks are aus


Alle Zeitangaben in WEZ +1. Es ist jetzt 11:56 Uhr.


Powered by vBulletin® Version 3.8.6 (Deutsch)
Copyright ©2000 - 2013, Jelsoft Enterprises Ltd.
Search Engine Friendly URLs by vBSEO 3.3.2
Thanks for Smilies by smilies.4-user.de