Java: Warum sollten wir BigDecimal anstelle von Double in der realen Welt verwenden?

Wenn es sich um reale monetäre Werte handelt, wird mir empfohlen, BigDecimal anstelle von Double zu verwenden. Aber ich habe keine überzeugende Erklärung außer “Es ist normalerweise so gemacht”.

Können Sie bitte diese Frage beleuchten?

Das nennt man Präzisionsverlust und macht sich sehr bemerkbar, wenn man mit sehr großen Zahlen oder sehr kleinen Zahlen arbeitet. Die binäre Darstellung von Dezimalzahlen mit einem Radix ist in vielen Fällen eine Annäherung und kein absoluter Wert. Um zu verstehen, warum Sie die Darstellung der Gleitkommazahl in Binärdateien lesen müssen. Hier ist ein Link: http://en.wikipedia.org/wiki/IEEE_754-2008 . Hier ist eine kurze Demonstration:
in bc (Eine beliebige Präzisionsrechnersprache) mit der Genauigkeit = 10:

(1/3 + 1/12 + 1/8 + 1/30) = 0,6083333332
(1/3 + 1/12 + 1/8) = 0,541666666666666
(1/3 + 1/12) = 0,416666666666666

Java doppelt:
0,6083333333333333
0.5416666666666666
0.41666666666666663

Java-Float:

0,60833335
0.5416667
0.4166667

Wenn Sie eine Bank sind und für Tausende von Transaktionen pro Tag verantwortlich sind, obwohl sie nicht von und zu ein und demselben Konto sind (oder vielleicht auch nicht), müssen Sie zuverlässige Nummern haben. Binäre Schwimmer sind nicht zuverlässig – es sei denn, Sie verstehen, wie sie funktionieren und ihre Grenzen.

Ich denke das beschreibt eine Lösung für dein Problem: Java Traps: Big Decimal und das Problem mit Double hier

Aus dem ursprünglichen Blog, der jetzt zu sein scheint.

Java Traps: doppelt

Viele Fallen liegen vor dem Lehrling Programmierer, als er den Weg der Softwareentwicklung geht. Dieser Artikel veranschaulicht anhand einer Reihe praktischer Beispiele die Hauptfallen bei der Verwendung von Javas einfachen Typen double und float. Beachten Sie jedoch, dass Sie, um die Genauigkeit in numerischen Berechnungen vollständig zu erfassen, ein (oder zwei) Lehrbuch zu diesem Thema benötigen. Folglich können wir nur die Oberfläche des Themas zerkratzen. Das Wissen, das hier vermittelt wird, sollte Ihnen das grundlegende Wissen vermitteln, das erforderlich ist, um Bugs in Ihrem Code aufzuspüren oder zu identifizieren. Es ist ein Wissen, von dem sich jeder professionelle Softwareentwickler bewusst sein sollte.

  1. Dezimalzahlen sind Näherungswerte

    Während alle natürlichen Zahlen zwischen 0 – 255 mit 8 Bit genau beschrieben werden können, erfordert die Beschreibung aller reellen Zahlen zwischen 0.0 – 255.0 eine unendliche Anzahl von Bits. Erstens gibt es unendlich viele Zahlen, die in diesem Bereich zu beschreiben sind (selbst im Bereich von 0.0 – 0.1), und zweitens können bestimmte irrationale Zahlen überhaupt nicht numerisch beschrieben werden. Zum Beispiel e und π. Mit anderen Worten, die Zahlen 2 und 0,2 sind im Computer stark unterschiedlich dargestellt.

    Ganzzahlen werden durch Bits repräsentiert, die Werte 2n repräsentieren, wobei n die Position des Bits ist. Somit wird der Wert 6 als 23 * 0 + 22 * 1 + 21 * 1 + 20 * 0 · 23 * 0 + 22 * 1 + 21 * 1 + 20 * 0 · 23 * 0 + 22 * 1 + 21 * 1 + 20 * 0 · 23 * 0 + 22 * 1 + 21 * 1 + 20 * 0 · 23 * 0 + 22 * 1 + 21 * 1 + 20 * 0 entsprechend der Bitsequenz 0110 dargestellt. Dezimale werden andererseits durch Bits beschrieben, die 2-n darstellen, das sind die Brüche 1/2, 1/4, 1/8,... Die Zahl 0,75 entspricht 2-1 * 1 + 2-2 * 1 + 2-3 * 0 + 2-4 * 0 ergibt die Bitfolge 1100 (1/2 + 1/4) .

    Mit diesem Wissen können wir die folgende Faustregel formulieren: Jede Dezimalzahl wird durch einen approximierten Wert dargestellt.

    Lassen Sie uns die praktischen Konsequenzen untersuchen, indem wir eine Reihe von Trivial-Multiplikationen durchführen.

     System.out.println( 0.2 + 0.2 + 0.2 + 0.2 + 0.2 ); 1.0 

    1.0 ist gedruckt. Obwohl dies in der Tat richtig ist, kann es uns ein falsches Gefühl der Sicherheit geben. Zufälligerweise ist 0.2 einer der wenigen Werte, die Java korrekt darstellen kann. Lassen Sie uns Java erneut mit einem weiteren trivialen arithmetischen Problem herausfordern, indem wir die Zahl 0,1 zehnmal addieren.

     System.out.println( 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f ); System.out.println( 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d ); 1.0000001 0.9999999999999999 

    Laut Folien von Joseph D. Darcy’s Blog sind die Summen der beiden Berechnungen 0.100000001490116119384765625 bzw. 0.1000000000000000055511151231... Diese Ergebnisse sind für eine begrenzte Anzahl von Ziffern korrekt. Floats haben eine Genauigkeit von 8 führenden Ziffern, während Double eine Genauigkeit von 17 führenden Ziffern hat. Nun, wenn die konzeptionelle Diskrepanz zwischen dem erwarteten Ergebnis 1.0 und den auf den Bildschirmen gedruckten Ergebnissen nicht ausgereicht hat, um Ihre Alarmglocken in Gang zu setzen, dann beachten Sie, wie die Zahlen von mr. Darcys Folien scheinen nicht den gedruckten Zahlen zu entsprechen! Das ist eine andere Falle. Mehr dazu weiter unten.

    Wenn man in scheinbar einfachen Szenarien auf Fehlberechnungen aufmerksam gemacht wird, ist es vernünftig, darüber nachzudenken, wie schnell sich der Eindruck einstellt. Lassen Sie uns das Problem vereinfachen, indem wir nur drei Zahlen hinzufügen.

     System.out.println( 0.3 == 0.1d + 0.1d + 0.1d ); false 

    Erschreckend ist, dass die Ungenauigkeit bereits bei drei Hinzufügungen einsetzt!

  2. Doppelter Überlauf

    Wie bei jedem anderen einfachen Java-Typ wird ein Double durch eine endliche Menge von Bits dargestellt. Folglich kann das Addieren eines Werts oder das Multiplizieren eines Doppels zu überraschenden Ergebnissen führen. Zugegebenermaßen müssen Zahlen ziemlich groß sein, um überlaufen zu können, aber es passiert. Versuchen wir, eine große Zahl zu multiplizieren und dann zu teilen. Mathematische Intuition sagt, dass das Ergebnis die ursprüngliche Zahl ist. In Java erhalten wir möglicherweise ein anderes Ergebnis.

     double big = 1.0e307 * 2000 / 2000; System.out.println( big == 1.0e307 ); false 

    Das Problem hier ist, dass groß zuerst multipliziert wird, überläuft, und dann wird die übergelaufene Zahl geteilt. Schlimmer noch, es gibt keine Ausnahme oder andere Arten von Warnungen an den Programmierer. Grundsätzlich macht dies den Ausdruck x * y völlig unzuverlässig, da im allgemeinen Fall für alle durch x, y dargestellten Doppelwerte keine Angabe oder Garantie erfolgt.

  3. Groß und Klein sind keine Freunde!

    Laurel und Hardy waren sich oft über viele Dinge nicht einig. In der Computertechnik sind Groß und Klein keine Freunde. Eine Konsequenz der Verwendung einer festen Anzahl von Bits zur Darstellung von Zahlen besteht darin, dass das Arbeiten mit sehr großen und sehr kleinen Zahlen in denselben Berechnungen nicht wie erwartet funktioniert. Versuchen wir, etwas Großes zu etwas Großem hinzuzufügen.

     System.out.println( 1234.0d + 1.0e-13d == 1234.0d ); true 

    Der Zusatz hat keine Wirkung! Dies widerspricht jeder (vernünftigen) mathematischen Intuition der Addition, die besagt, dass bei gegebenen zwei Zahlen positive Zahlen d und f, dann d + f> d.

  4. Dezimalzahlen können nicht direkt verglichen werden

    Was wir bisher gelernt haben, ist, dass wir jede Intuition, die wir im Mathematikunterricht und in der Programmierung mit Ganzzahlen erlangt haben, wegcasting müssen. Verwenden Sie Dezimalzahlen vorsichtig. Zum Beispiel ist die Aussage for(double d = 0.1; d != 0.3; d += 0.1) tatsächlich eine verschleierte endlose Schleife! Der Fehler besteht darin, Dezimalzahlen direkt miteinander zu vergleichen. Sie sollten sich an die folgenden Richtlinien halten.

    Vermeiden Sie Gleichheitsprüfungen zwischen zwei Dezimalzahlen. Refrain von if(a == b) {..} , benutze if(Math.abs(ab) < tolerance) {..} wobei Toleranz eine Konstante sein könnte, die zB als public static definiert ist final double tolerance = 0.01 Als Alternative betrachten die Operatoren < ,> zu verwenden, da sie natürlich mehr beschreiben können, was Sie express möchten. Zum Beispiel bevorzuge ich die Form for(double d = 0; d < = 10.0; d+= 0.1) über die unbeholfener for(double d = 0; Math.abs(10.0-d) < tolerance; d+= 0.1) Both Formen haben je nach Situation jedoch ihre Vorzüge: Beim assertEquals(2.5, d, tolerance) ziehe ich es vor, das assertEquals(2.5, d, tolerance) über das Sprichwort assertTrue(d > 2.5) auszudrücken, nicht nur liest das erste Formular besser, es ist oft die Überprüfung, die Sie durchführen möchte tun (dh dass d nicht zu groß ist).

  5. WYSINWYG - Was Sie sehen, ist nicht das, was Sie bekommen

    WYSIWYG ist ein Ausdruck, der normalerweise in Anwendungen für grafische Benutzeroberflächen verwendet wird. Es bedeutet "Was Sie sehen, ist was Sie bekommen" und wird in der Informatik verwendet, um ein System zu beschreiben, in dem während der Bearbeitung angezeigter Inhalt der endgültigen Ausgabe, die ein gedrucktes Dokument, eine Webseite usw. sein kann, sehr ähnlich ist Phrase war ursprünglich ein populärer Slogan, der von Flip Wilsons Drag-Persona "Geraldine" stammt, die oft sagen würde "Was du siehst ist, was du bekommst", um ihr eigenartiges Verhalten zu entschuldigen (aus wikipedia).

    Eine andere ernsthafte Falle, in die Programmierer oft hineinfallen, ist, dass Dezimalzahlen WYSIWYG sind. Es ist unbedingt zu beachten, dass beim Drucken oder Schreiben einer Dezimalzahl nicht der angenäherte Wert gedruckt / geschrieben wird. Anders gesagt, Java macht viele Annäherungen hinter den Kulissen und versucht beharrlich, dich davon abzuhalten, es jemals zu wissen. Es gibt nur ein Problem. Sie müssen über diese Annäherungen Bescheid wissen, sonst könnten Sie in Ihrem Code mit allen möglichen mysteriösen Fehlern konfrontiert werden.

    Mit etwas Einfallsreichtum können wir jedoch untersuchen, was wirklich hinter der Szene vor sich geht. Inzwischen wissen wir, dass die Zahl 0.1 mit einer gewissen Annäherung dargestellt wird.

     System.out.println( 0.1d ); 0.1 

    Wir wissen, 0,1 ist nicht 0,1, aber 0,1 ist auf dem Bildschirm gedruckt. Fazit: Java ist WYSINWYG!

    Aus Gründen der Vielfalt, wählen wir eine andere unschuldig aussehende Nummer, sagen wir 2,3. Wie 0.1, 2.3 ist ein angenäherter Wert. Es überrascht nicht, dass beim Drucken der Nummer Java die Approximation verbirgt.

     System.out.println( 2.3d ); 2.3 

    Um zu untersuchen, wie der interne approximierte Wert von 2,3 sein kann, können wir die Zahl mit anderen Zahlen in einem engen Bereich vergleichen.

     double d1 = 2.2999999999999996d; double d2 = 2.2999999999999997d; System.out.println( d1 + " " + (2.3d == d1) ); System.out.println( d2 + " " + (2.3d == d2) ); 2.2999999999999994 false 2.3 true 

    2,2999999999999997 ist also genauso viel 2,3 wie der Wert 2,3! Beachten Sie auch, dass aufgrund der Annäherung der Drehpunkt bei ..99997 liegt und nicht bei ..99995, wo Sie normalerweise Runden in Mathe runden. Eine weitere Möglichkeit, sich mit dem angenäherten Wert vertraut zu machen, besteht darin, die Dienste von BigDecimal in Anspruch zu nehmen.

     System.out.println( new BigDecimal(2.3d) ); 2.29999999999999982236431605997495353221893310546875 

    Nun, ruhen Sie sich nicht auf Ihren Lorbeeren aus, weil Sie denken, Sie können einfach auf das Schiff springen und nur BigDecimal verwenden. BigDecimal hat eine eigene Sammlung von Traps, die hier dokumentiert sind.

    Nichts ist einfach und selten kommt etwas umsonst. Und "natürlich", floats und doubles ergeben unterschiedliche Ergebnisse, wenn sie gedruckt / geschrieben werden.

     System.out.println( Float.toString(0.1f) ); System.out.println( Double.toString(0.1f) ); System.out.println( Double.toString(0.1d) ); 0.1 0.10000000149011612 0.1 

    Gemäß den Folien von Joseph D. Darcys Blog hat eine Float-Approximation 24 signifikante Bits, während eine doppelte Approximation 53 signifikante Bits aufweist. Die Moral ist das. Um Werte zu erhalten, müssen Sie Dezimalzahlen im selben Format lesen und schreiben.

  6. Division durch 0

    Viele Entwickler wissen aus Erfahrung, dass das Teilen einer Zahl durch null zu einem plötzlichen Abbruch ihrer Anwendungen führt. Ein ähnliches Verhalten ist Java, wenn es mit Ints arbeitet, aber ziemlich überraschend, nicht wenn es auf Double's arbeitet. Jede Zahl, mit Ausnahme von Null, dividiert durch Null ergibt jeweils ∞ oder -∞. Das Zerlegen von Null mit Null ergibt das spezielle NaN, den Not a Number-Wert.

     System.out.println(22.0 / 0.0); System.out.println(-13.0 / 0.0); System.out.println(0.0 / 0.0); Infinity -Infinity NaN 

    Das Teilen einer positiven Zahl mit einer negativen Zahl ergibt ein negatives Ergebnis, während das Teilen einer negativen Zahl mit einer negativen Zahl ein positives Ergebnis ergibt. Da Division durch Null möglich ist, erhalten Sie unterschiedliche Ergebnisse abhängig davon, ob Sie eine Zahl mit 0,0 oder -0,0 teilen. Ja, es ist wahr! Java hat eine negative Null! Lassen Sie sich jedoch nicht täuschen, die zwei Nullwerte sind gleich wie unten gezeigt.

     System.out.println(22.0 / 0.0); System.out.println(22.0 / -0.0); System.out.println(0.0 == -0.0); Infinity -Infinity true 
  7. Die Unendlichkeit ist seltsam

    In der Welt der Mathematik war die Unendlichkeit ein Konzept, das ich schwer zu verstehen fand. Zum Beispiel habe ich niemals eine Intuition dafür erlangt, wenn eine Unendlichkeit unendlich größer als eine andere war. Sicher ist Z> N, die Menge aller rationalen Zahlen ist unendlich größer als die Menge der natürlichen Zahlen, aber das war in dieser Hinsicht ungefähr die Grenze meiner Intuition!

    Glücklicherweise ist die Unendlichkeit in Java ungefähr so ​​unberechenbar wie die Unendlichkeit in der mathematischen Welt. Sie können die üblichen Verdächtigen (+, -, *, / auf einem unendlichen Wert ausführen, aber Sie können eine Unendlichkeit nicht auf eine Unendlichkeit anwenden.

     double infinity = 1.0 / 0.0; System.out.println(infinity + 1); System.out.println(infinity / 1e300); System.out.println(infinity / infinity); System.out.println(infinity - infinity); Infinity Infinity NaN NaN 

    Das Hauptproblem besteht darin, dass der NaN-Wert ohne Warnungen zurückgegeben wird. Sollten Sie also törichterweise untersuchen, ob ein bestimmtes Double gerade oder ungerade ist, können Sie wirklich in eine haarige Situation geraten. Vielleicht wäre eine Laufzeitausnahme geeigneter gewesen?

     double d = 2.0, d2 = d - 2.0; System.out.println("even: " + (d % 2 == 0) + " odd: " + (d % 2 == 1)); d = d / d2; System.out.println("even: " + (d % 2 == 0) + " odd: " + (d % 2 == 1)); even: true odd: false even: false odd: false 

    Plötzlich ist deine Variable weder ungerade noch gerade! NaN ist noch seltsamer als Infinity Ein unendlicher Wert unterscheidet sich von dem maximalen Wert eines Double und NaN ist wieder anders als der unendliche Wert.

     double nan = 0.0 / 0.0, infinity = 1.0 / 0.0; System.out.println( Double.MAX_VALUE != infinity ); System.out.println( Double.MAX_VALUE != nan ); System.out.println( infinity != nan ); true true true 

    Wenn ein Doppel den Wert NaN erhalten hat, führt jede Operation darauf zu einem NaN.

     System.out.println( nan + 1.0 ); NaN 
  8. Schlussfolgerungen

    1. Dezimalzahlen sind Näherungswerte, nicht der Wert, den Sie zuweisen. Jede Intuition, die in der Mathematikwelt gewonnen wird, gilt nicht mehr. Erwarte a+b = a und a != a/3 + a/3 + a/3
    2. Vermeiden Sie es, das == zu verwenden, vergleichen Sie es mit einer Toleranz oder verwenden Sie die Operatoren> = oder < =
    3. Java ist WYSINWYG! Glauben Sie niemals, dass der Wert, den Sie drucken / schreiben, ein angenäherter Wert ist, daher lesen / schreiben Sie immer Dezimalzahlen im selben Format.
    4. Achten Sie darauf, dass Sie Ihr Double nicht überlaufen, damit Ihr Double nicht in einen Zustand von ± Infinity oder NaN versetzt wird. In beiden Fällen werden Ihre Berechnungen möglicherweise nicht so ausgeführt, wie Sie es erwarten würden. Es kann sinnvoll sein, diese Werte immer zu überprüfen, bevor Sie einen Wert in Ihren Methoden angeben.

Während BigDecimal mehr Genauigkeit als Double speichern kann, ist dies normalerweise nicht erforderlich. Der wahre Grund, warum es verwendet wurde, weil es deutlich macht, wie die Rundung durchgeführt wird, einschließlich einer Anzahl von verschiedenen Rundungsstrategien. Sie können in den meisten Fällen die gleichen Ergebnisse mit Double erzielen, aber wenn Sie die erforderlichen Techniken nicht kennen, ist BigDecimal in diesem Fall der richtige Weg.

Ein allgemeines Beispiel ist Geld. Auch wenn das Geld in 99% der Anwendungsfälle nicht groß genug ist, um die Genauigkeit von BigDecimal zu erreichen, wird BigDecimal oft als die beste Methode angesehen, da die Steuerung der Rundung in der Software erfolgt, wodurch das Risiko des Entwicklers vermieden wird ein Fehler beim Runden. Auch wenn Sie sicher sind, dass Sie mit double Runden umgehen können, schlage ich vor, dass Sie die Rundung mit Hilfe von Hilfsmethoden durchführen, die Sie gründlich testen.

Dies geschieht hauptsächlich aus Gründen der Genauigkeit. BigDecimal speichert Gleitkommazahlen mit unbegrenzter Genauigkeit. Sie können sich diese Seite ansehen, die es gut erklärt. http://blogs.oracle.com/CoreJavaTechTips/entry/the_need_for_bigdecimal

Wenn BigDecimal verwendet wird, kann es viel mehr Daten als Double speichern, wodurch es genauer und nur eine bessere Wahl für die reale Welt ist.

Obwohl es viel langsamer und länger ist, ist es das wert.

Wetten, du würdest deinem Chef nicht falsche Informationen geben wollen, oder?

Eine andere Idee: Verfolgen Sie die Anzahl der Cent in einer long . Dies ist einfacher und vermeidet die umständliche Syntax und die langsame Ausführung von BigDecimal .

Präzision in Finanzberechnungen ist besonders wichtig, weil die Leute sehr verärgert sind, wenn ihr Geld aufgrund von Rundungserrorsn verschwindet, weshalb double eine schreckliche Wahl für den Umgang mit Geld ist.