floating-point - wertebereich - komplexe datentypen java




Warum nicht Double oder Float zur Darstellung der Währung verwenden? (10)

Das Ergebnis der Fließkommazahl ist nicht exakt, was sie für eine finanzielle Berechnung ungeeignet macht, die ein genaues Ergebnis und keine Annäherung erfordert. float und double sind für die technische und wissenschaftliche Berechnung konzipiert und liefern oft kein exaktes Ergebnis. Auch das Ergebnis der Gleitkommaberechnung kann von JVM zu JVM variieren. Sehen Sie sich das folgende Beispiel von BigDecimal und Double Primitive an, das zur Darstellung von Geldwerten verwendet wird. Es ist ziemlich klar, dass die Gleitkommaberechnung nicht exakt sein kann und BigDecimal für Finanzberechnungen verwendet werden sollte.

    // floating point calculation
    final double amount1 = 2.0;
    final double amount2 = 1.1;
    System.out.println("difference between 2.0 and 1.1 using double is: " + (amount1 - amount2));

    // Use BigDecimal for financial calculation
    final BigDecimal amount3 = new BigDecimal("2.0");
    final BigDecimal amount4 = new BigDecimal("1.1");
    System.out.println("difference between 2.0 and 1.1 using BigDecimal is: " + (amount3.subtract(amount4)));

Ausgabe:

difference between 2.0 and 1.1 using double is: 0.8999999999999999
difference between 2.0 and 1.1 using BigDecimal is: 0.9

Mir wurde immer gesagt, dass ich niemals Geld mit double oder float Typen darstellen soll, und dieses Mal stelle ich Ihnen die Frage: Warum?

Ich bin mir sicher, dass es einen sehr guten Grund gibt, ich weiß einfach nicht, was es ist.


Dies ist keine Frage der Genauigkeit, noch ist es eine Frage der Präzision. Es geht darum, die Erwartungen von Menschen zu erfüllen, die Basis 10 für Berechnungen anstelle von Basis 2 verwenden. Zum Beispiel führt die Verwendung von Doppelrechnungen für Finanzberechnungen nicht zu Antworten, die im mathematischen Sinne "falsch" sind, aber sie kann Antworten liefern nicht was im finanziellen Sinn erwartet wird.

Selbst wenn Sie Ihre Ergebnisse in der letzten Minute vor der Ausgabe abrunden, können Sie immer noch gelegentlich ein Ergebnis erhalten, wenn Sie Doubles verwenden, die nicht den Erwartungen entsprechen.

Mit einem Taschenrechner, oder Berechnung der Ergebnisse von Hand, 1.40 * 165 = 231 genau. Bei der Verwendung von Doubles auf meiner Compiler- / Betriebssystemumgebung wird es jedoch als Binärzahl nahe 230.99999 gespeichert. Wenn Sie also die Zahl abschneiden, erhalten Sie 230 anstelle von 231. Sie könnten also annehmen, dass das Runden statt Abschneiden das wäre haben das gewünschte Ergebnis von 231 gegeben. Das stimmt, aber das Runden beinhaltet immer Verkürzung. Welche Rundungstechnik Sie auch verwenden, es gibt immer noch Randbedingungen wie diese, die abgerundet werden, wenn Sie erwarten, dass sie sich zusammenrunden. Sie sind selten genug, dass sie oft nicht durch zufällige Tests oder Beobachtungen gefunden werden. Möglicherweise müssen Sie Code schreiben, um nach Beispielen zu suchen, die Ergebnisse darstellen, die sich nicht wie erwartet verhalten.

Angenommen, Sie möchten etwas auf den nächsten Cent runden. Also nimmst du dein Endergebnis, multiplizierst mit 100, addierst 0.5, trennst und teilst das Ergebnis dann durch 100, um wieder zu den Pennies zu kommen. Wenn die interne Nummer 3,46499999 war .... statt 3,465, erhalten Sie 3,46 statt 3,47, wenn Sie die Zahl auf den nächsten Cent runden. Aber Ihre Basis-10-Berechnungen könnten darauf hindeuten, dass die Antwort 3,465 genau sein sollte, was deutlich auf 3,47 runden sollte, nicht auf 3,46. Diese Art von Dingen passieren gelegentlich im wirklichen Leben, wenn Sie Doppel für finanzielle Berechnungen verwenden. Es ist selten, so dass es oft unbemerkt bleibt, aber es passiert.

Wenn Sie die Basis 10 für Ihre internen Berechnungen anstelle von Doppeln verwenden, sind die Antworten immer genau das, was von Menschen erwartet wird, vorausgesetzt, es gibt keine anderen Fehler in Ihrem Code.


Es ist zwar richtig, dass Fließkomma-Typ nur ungefähre Dezimalzahlen darstellen kann, aber wenn man Zahlen vor der Präsentation auf die notwendige Genauigkeit rundet, erhält man das korrekte Ergebnis. Gewöhnlich.

Normalerweise, weil der Doppeltyp eine Genauigkeit von weniger als 16 Ziffern hat. Wenn Sie eine bessere Genauigkeit benötigen, ist es kein geeigneter Typ. Auch Annäherungen können sich ansammeln.

Es muss gesagt werden, dass Sie auch dann, wenn Sie Festkommaarithmetik verwenden, Zahlen runden müssen, wenn BigInteger und BigDecimal keine Fehler geben, wenn Sie periodische Dezimalzahlen erhalten. Also gibt es auch hier eine Annäherung.

Zum Beispiel hat COBOL, historisch für Finanzberechnungen verwendet, eine maximale Genauigkeit von 18 Zahlen. Es kommt also oft zu einer impliziten Rundung.

Zusammenfassend ist das Double meines Erachtens vor allem wegen seiner 16-stelligen Genauigkeit ungeeignet, was nicht ausreichen kann, nicht weil es ungefähr ist.

Betrachten Sie die folgende Ausgabe des nachfolgenden Programms. Es zeigt, dass nach der Doppelung das gleiche Ergebnis wie BigDecimal bis zur Genauigkeit 16 erhalten wird.

Precision 14
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611
Double                        : 56789.012345 / 1111111111 = 0.000051110111115611

Precision 15
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110
Double                        : 56789.012345 / 1111111111 = 0.0000511101111156110

Precision 16
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101
Double                        : 56789.012345 / 1111111111 = 0.00005111011111561101

Precision 17
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611011
Double                        : 56789.012345 / 1111111111 = 0.000051110111115611013

Precision 18
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110111
Double                        : 56789.012345 / 1111111111 = 0.0000511101111156110125

Precision 19
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101111
Double                        : 56789.012345 / 1111111111 = 0.00005111011111561101252
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.MathContext;

public class Exercise {
    public static void main(String[] args) throws IllegalArgumentException,
            SecurityException, IllegalAccessException,
            InvocationTargetException, NoSuchMethodException {
        String amount = "56789.012345";
        String quantity = "1111111111";
        int [] precisions = new int [] {14, 15, 16, 17, 18, 19};
        for (int i = 0; i < precisions.length; i++) {
            int precision = precisions[i];
            System.out.println(String.format("Precision %d", precision));
            System.out.println("------------------------------------------------------");
            execute("BigDecimalNoRound", amount, quantity, precision);
            execute("DoubleNoRound", amount, quantity, precision);
            execute("BigDecimal", amount, quantity, precision);
            execute("Double", amount, quantity, precision);
            System.out.println();
        }
    }

    private static void execute(String test, String amount, String quantity,
            int precision) throws IllegalArgumentException, SecurityException,
            IllegalAccessException, InvocationTargetException,
            NoSuchMethodException {
        Method impl = Exercise.class.getMethod("divideUsing" + test, String.class,
                String.class, int.class);
        String price;
        try {
            price = (String) impl.invoke(null, amount, quantity, precision);
        } catch (InvocationTargetException e) {
            price = e.getTargetException().getMessage();
        }
        System.out.println(String.format("%-30s: %s / %s = %s", test, amount,
                quantity, price));
    }

    public static String divideUsingDoubleNoRound(String amount,
            String quantity, int precision) {
        // acceptance
        double amount0 = Double.parseDouble(amount);
        double quantity0 = Double.parseDouble(quantity);

        //calculation
        double price0 = amount0 / quantity0;

        // presentation
        String price = Double.toString(price0);
        return price;
    }

    public static String divideUsingDouble(String amount, String quantity,
            int precision) {
        // acceptance
        double amount0 = Double.parseDouble(amount);
        double quantity0 = Double.parseDouble(quantity);

        //calculation
        double price0 = amount0 / quantity0;

        // presentation
        MathContext precision0 = new MathContext(precision);
        String price = new BigDecimal(price0, precision0)
                .toString();
        return price;
    }

    public static String divideUsingBigDecimal(String amount, String quantity,
            int precision) {
        // acceptance
        BigDecimal amount0 = new BigDecimal(amount);
        BigDecimal quantity0 = new BigDecimal(quantity);
        MathContext precision0 = new MathContext(precision);

        //calculation
        BigDecimal price0 = amount0.divide(quantity0, precision0);

        // presentation
        String price = price0.toString();
        return price;
    }

    public static String divideUsingBigDecimalNoRound(String amount, String quantity,
            int precision) {
        // acceptance
        BigDecimal amount0 = new BigDecimal(amount);
        BigDecimal quantity0 = new BigDecimal(quantity);

        //calculation
        BigDecimal price0 = amount0.divide(quantity0);

        // presentation
        String price = price0.toString();
        return price;
    }
}

Floats und Doubles sind ungefähre Werte. Wenn Sie ein BigDecimal erstellen und ein Float in den Konstruktor übergeben, sehen Sie, was der Float eigentlich gleich ist:

groovy:000> new BigDecimal(1.0F)
===> 1
groovy:000> new BigDecimal(1.01F)
===> 1.0099999904632568359375

Dies ist wahrscheinlich nicht, wie Sie $ 1,01 darstellen möchten.

Das Problem ist, dass die IEEE-Spezifikation keine Möglichkeit bietet, alle Brüche exakt darzustellen, einige von ihnen enden als sich wiederholende Brüche, so dass Sie mit Approximationsfehlern enden. Da Buchhalter mögen Dinge genau auf den Pfennig kommen, und Kunden werden genervt sein, wenn sie ihre Rechnung bezahlen und nachdem die Zahlung verarbeitet wird, schulden sie .01 und sie bekommen eine Gebühr oder können ihr Konto nicht schließen, es ist besser zu benutzen exakte Typen wie dezimal (in C #) oder java.math.BigDecimal in Java.

Es ist nicht so, dass der Fehler nicht kontrollierbar ist, wenn man umgeht : siehe diesen Artikel von Peter Lawrey . Es ist einfach einfacher, nicht an erster Stelle zu sein. Die meisten Anwendungen, die mit Geld umgehen, erfordern nicht viel Mathematik. Die Operationen bestehen darin, Dinge hinzuzufügen oder Beträge verschiedenen Buckets zuzuordnen. Die Einführung von Fließkomma und Rundung macht die Dinge nur komplizierter.


Ich bin beunruhigt über einige dieser Antworten. Ich denke Doppelgänger und Schwimmer haben einen Platz in finanziellen Berechnungen. Wenn Sie nicht-gebrochene Währungsbeträge addieren und subtrahieren, wird es sicher keinen Genauigkeitsverlust geben, wenn Sie Integer-Klassen oder BigDecimal-Klassen verwenden. Wenn Sie jedoch komplexere Operationen ausführen, erhalten Sie oft Ergebnisse, die mehrere oder viele Dezimalstellen überschreiten, unabhängig davon, wie Sie die Zahlen speichern. Das Problem ist, wie Sie das Ergebnis präsentieren.

Wenn Ihr Ergebnis an der Grenze zwischen Aufrunden und Abrunden liegt und der letzte Penny wirklich zählt, sollten Sie dem Zuschauer wahrscheinlich sagen, dass die Antwort fast in der Mitte liegt - indem Sie mehr Dezimalstellen anzeigen.

Das Problem mit Doubles und mehr mit Floats ist, wenn sie verwendet werden, um große Zahlen und kleine Zahlen zu kombinieren. In Java,

System.out.println(1000000.0f + 1.2f - 1000000.0f);

Ergebnisse in

1.1875

Ich werde riskieren, downvoted, aber ich denke, dass die Ungeeignetheit von Gleitkommazahlen für Währungsberechnungen überbewertet wird. Solange Sie sicherstellen, dass Sie die Cent-Rundung korrekt durchführen und genügend signifikante Ziffern haben, um mit der durch zneak erklärten binären Dezimal-Darstellung zu arbeiten, wird es kein Problem geben.

Leute, die mit einer Währung in Excel rechnen, haben immer Gleitkommazahlen mit doppelter Genauigkeit benutzt (es gibt keinen Währungstyp in Excel) und ich habe noch niemanden gesehen, der sich über Rundungsfehler beschwert.

Natürlich musst du in der Vernunft bleiben; zB würde ein einfacher Webshop wahrscheinlich niemals Probleme mit Double-Genauigkeit-Floats haben, aber wenn Sie zB Buchhaltung oder irgendetwas anderes machen, das das Hinzufügen einer großen (unbeschränkten) Menge an Zahlen erfordert, möchten Sie Gleitkommazahlen mit einem zehn Fuß nicht berühren Pole.


Weil Floats und Doubles die Basis 10 Multiples, die wir für Geld verwenden, nicht genau darstellen können. Dieses Problem betrifft nicht nur Java, sondern jede Programmiersprache, die Basis-Gleitkommatypen verwendet.

In der Basis 10 können Sie 10,25 als 1025 * 10 -2 (eine ganze Zahl mal eine Potenz von 10) schreiben. IEEE-754 Fließkommazahlen sind unterschiedlich, aber eine sehr einfache Art, darüber nachzudenken, besteht darin, mit einer Zweierpotenz zu multiplizieren. Zum Beispiel könnten Sie 164 * 2 -4 (eine ganze Zahl mal eine Potenz von zwei) betrachten, was auch gleich 10,25 ist. So werden die Zahlen nicht im Speicher dargestellt, aber die Mathematik funktioniert genauso.

Selbst in der Basis 10 kann diese Notation die meisten einfachen Brüche nicht genau darstellen. Zum Beispiel können Sie 1/3 nicht darstellen: Die dezimale Darstellung wiederholt sich (0.3333 ...), also gibt es keine endliche ganze Zahl, die Sie mit einer Potenz von 10 multiplizieren können, um 1/3 zu erhalten. Du könntest dich auf eine lange Folge von Dreiern und einen kleinen Exponenten wie 333333333 * 10 -10 festlegen, aber es ist nicht korrekt: Wenn du das mit 3 multiplizierst, erhältst du nicht 1.

Zum Zählen von Geld, zumindest für Länder, deren Geld in einer Größenordnung des US-Dollars geschätzt wird, ist es normalerweise alles, was Sie brauchen, um ein Vielfaches von 10 -2 zu speichern, also spielt es keine Rolle das 1/3 kann nicht dargestellt werden.

Das Problem mit Floats und Doubles ist, dass die überwiegende Mehrheit der geldähnlichen Zahlen keine exakte Darstellung als eine ganze Zahl mal eine Potenz von 2 hat. Tatsächlich sind die einzigen Vielfachen von 0.01 zwischen 0 und 1 (die im Handel signifikant sind) mit Geld, weil sie ganzzahlige Cents sind), die genau als eine IEEE-754-Binär-Fließkommazahl dargestellt werden können, sind 0, 0,25, 0,5, 0,75 und 1. Alle anderen sind um einen kleinen Betrag ausgeschaltet. Als eine Analogie zu dem Beispiel 0.333333, wenn Sie den Gleitkommawert für 0,1 nehmen und Sie ihn mit 10 multiplizieren, erhalten Sie nicht 1.

Die Darstellung von Geld als double oder float wird wahrscheinlich zuerst gut aussehen, da die Software die winzigen Fehler abrundet, aber wenn Sie mehr Additionen, Subtraktionen, Multiplikationen und Divisionen auf ungenauen Zahlen durchführen, werden sich Fehler addieren und Sie werden mit Werten enden sind sichtbar nicht genau. Dies macht Floats und Doubles für den Umgang mit Geld ungeeignet, wo eine perfekte Genauigkeit für ein Vielfaches von Basis 10 Potenzen erforderlich ist.

Eine Lösung, die in fast jeder Sprache funktioniert, besteht darin, ganze Zahlen zu verwenden und Cent zu zählen. Zum Beispiel wäre 1025 10,25 $. Mehrere Sprachen haben auch eingebaute Arten, um mit Geld umzugehen. Unter anderem hat Java die BigDecimal Klasse und C # den decimal .


Wenn Ihre Berechnung verschiedene Schritte beinhaltet, wird die Arithmetik mit beliebiger Genauigkeit Sie nicht zu 100% abdecken.

Die einzige zuverlässige Methode, um die perfekte Darstellung der Ergebnisse zu verwenden (Verwenden Sie einen benutzerdefinierten Fraction-Datentyp, der Divisionsoperationen im letzten Schritt durchführt), und konvertieren Sie im letzten Schritt nur in eine Dezimalschreibweise.

Willkürliche Präzision wird nicht helfen, weil es immer Zahlen geben kann, die so viele Dezimalstellen haben, oder einige Ergebnisse wie 0.6666666 ... Keine willkürliche Darstellung wird das letzte Beispiel abdecken. Sie werden also in jedem Schritt kleine Fehler haben.

Diese Fehler werden sich addieren, können eventuell nicht mehr einfach ignoriert werden. Dies wird als Fehlerfortpflanzung bezeichnet .


Von Bloch, J., Effektives Java, 2. Ausgabe, Punkt 48:

Die float und double Typen sind für monetäre Berechnungen besonders ungeeignet, da es unmöglich ist, 0,1 (oder eine andere negative Zehnerpotenz) als float oder double genau darzustellen.

Angenommen, Sie haben $ 1,03 und Sie geben 42 Cent aus. Wie viel Geld hast du noch?

System.out.println(1.03 - .42);

druckt 0.6100000000000001 .

Der richtige Weg zur Lösung dieses Problems ist die Verwendung von BigDecimal , int oder long für monetäre Berechnungen.


Most answers have highlighted the reasons why one should not use doubles for money and currency calculations. And I totally agree with them.

It doesn't mean though that doubles can never be used for that purpose.

I have worked on a number of projects with very low gc requirements, and having BigDecimal objects was a big contributor to that overhead.

It's the lack of understanding about double representation and lack of experience in handling the accuracy and precision that brings about this wise suggestion.

You can make it work if you are able to handle the precision and accuracy requirements of your project, which has to be done based on what range of double values is one dealing with.

You can refer to guava's FuzzyCompare method to get more idea. The parameter tolerance is the key. We dealt with this problem for a securities trading application and we did an exhaustive research on what tolerances to use for different numerical values in different ranges.

Es kann auch Situationen geben, in denen Sie versucht sind, Double-Wrapper als Map-Schlüssel zu verwenden, wobei Hash-Map die Implementierung ist. Es ist sehr riskant, weil Double.equals und Hash-Code für die Werte "0.5" und "0.6 - 0.1" ein großes Durcheinander verursachen.







currency