![]() |
|
|||||||
|
|
|
Themen-Optionen | Thema durchsuchen | Ansicht |
| #1 (permalink) | |
|
Stammbenutzer
Megabyte
Registriert seit: 23.12.2005
Fachbeiträge: 1.746
Abgegebene Danke: 74
Erhielt 147 Danke für 134 Beiträge
|
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.
|
|
|
|
| #2 (permalink) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Stammbenutzer
Megabyte
Themenstarter
Registriert seit: 23.12.2005
Fachbeiträge: 1.746
Abgegebene Danke: 74
Erhielt 147 Danke für 134 Beiträge
|
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 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:
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: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.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:
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:
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:
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:
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: 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:
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.).
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|
| #3 (permalink) | |||||||||||||||||||||||||||||||||||
|
Stammbenutzer
Megabyte
Themenstarter
Registriert seit: 23.12.2005
Fachbeiträge: 1.746
Abgegebene Danke: 74
Erhielt 147 Danke für 134 Beiträge
|
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: Code:
x = 1.1 y = 0.1 x+y = 1.2000000000000002 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.
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:
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ß: 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:
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 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):
Beispiele im Binärsystem (b = 2):
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. |
||||||||||||||||||||||||||||||||||
|
|
|
| #4 (permalink) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Stammbenutzer
Megabyte
Themenstarter
Registriert seit: 23.12.2005
Fachbeiträge: 1.746
Abgegebene Danke: 74
Erhielt 147 Danke für 134 Beiträge
|
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:
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) |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|
| #5 (permalink) | |
|
Stammbenutzer
Megabyte
Themenstarter
Registriert seit: 23.12.2005
Fachbeiträge: 1.746
Abgegebene Danke: 74
Erhielt 147 Danke für 134 Beiträge
|
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. |
|
|
|
| #6 (permalink) | |||
|
Java-Forum Team
Moderator
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
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) |
||
|
|
|
|
| Themen-Optionen | Thema durchsuchen |
| 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 |
|
|