Warum nicht Double oder Float verwenden, um die Währung darzustellen?


939

Mir wurde immer gesagt, ich solle niemals Geld mit doubleoder floatTypen repräsentieren , und diesmal stelle ich Ihnen die Frage: Warum?

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


4
Siehe diese SO-Frage: Rundungsfehler?
Jeff Ogata

80
Um ganz klar zu sein, sollten sie nicht für etwas verwendet werden, das Genauigkeit erfordert - nicht nur für Währung.
Jeff

152
Sie sollten nicht für Dinge verwendet werden, die Genauigkeit erfordern . Die 53 signifikanten Bits von double (~ 16 Dezimalstellen) sind jedoch normalerweise gut genug für Dinge, die lediglich Genauigkeit erfordern .
Dan04

21
@jeff Ihr Kommentar stellt völlig falsch dar, wofür binärer Gleitkomma gut ist und wofür er nicht gut ist. Lesen Sie die Antwort von zneak unten und löschen Sie Ihren irreführenden Kommentar.
Pascal Cuoq

Antworten:


985

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

In Basis 10 können Sie 10.25 als 1025 * 10 -2 schreiben (eine ganze Zahl mal eine Potenz von 10). IEEE-754-Gleitkommazahlen sind unterschiedlich, aber eine sehr einfache Möglichkeit, über sie nachzudenken, besteht darin, stattdessen mit einer Zweierpotenz zu multiplizieren. Zum Beispiel könnten Sie 164 * 2 -4 (eine ganze Zahl mal eine Zweierpotenz) betrachten, was ebenfalls 10,25 entspricht. So werden die Zahlen nicht im Speicher dargestellt, aber die mathematischen Implikationen sind dieselben.

Selbst in Basis 10 kann diese Notation die einfachsten Brüche nicht genau darstellen. Zum Beispiel können Sie nicht 1/3 darstellen: Die Dezimaldarstellung wiederholt sich (0,3333 ...), sodass es keine endliche Ganzzahl gibt, die Sie mit einer Potenz von 10 multiplizieren können, um 1/3 zu erhalten. Sie könnten sich auf eine lange Folge von 3en und einen kleinen Exponenten wie 333333333 * 10 -10 festlegen , aber es ist nicht genau: Wenn Sie dies mit 3 multiplizieren, erhalten Sie keine 1.

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

Das Problem bei Floats und Doubles ist, dass die überwiegende Mehrheit der geldähnlichen Zahlen keine exakte Darstellung als Ganzzahl mal Potenz von 2 hat. Tatsächlich sind die einzigen Vielfachen von 0,01 zwischen 0 und 1 (die beim Handel von Bedeutung sind) mit Geld, weil es sich um ganzzahlige Cent handelt), die genau als binäre Gleitkommazahl nach IEEE-754 dargestellt werden können, sind 0, 0,25, 0,5, 0,75 und 1. Alle anderen sind um einen kleinen Betrag versetzt. In Analogie zum Beispiel 0.333333 erhalten Sie keine 1, wenn Sie den Gleitkommawert für 0.1 nehmen und mit 10 multiplizieren.

Die Darstellung von Geld als doubleoder floatwird wahrscheinlich zunächst gut aussehen, wenn die Software die winzigen Fehler abrundet. Wenn Sie jedoch mehr Additionen, Subtraktionen, Multiplikationen und Divisionen für ungenaue Zahlen durchführen, werden sich die Fehler verstärken und Sie erhalten Werte, die sichtbar sind nicht genau. Dies macht Floats und Doubles für den Umgang mit Geld unzureichend, wo eine perfekte Genauigkeit für ein Vielfaches der Basis-10-Kräfte erforderlich ist.

Eine Lösung, die in nahezu jeder Sprache funktioniert, besteht darin, stattdessen Ganzzahlen zu verwenden und Cent zu zählen. Zum Beispiel wäre 1025 10,25 $. Einige Sprachen haben auch eingebaute Typen, um mit Geld umzugehen. Java hat unter anderem die BigDecimalKlasse und C # den decimalTyp.


3
@Fran Sie werden Rundungsfehler bekommen und in einigen Fällen, in denen große Mengen an Währung verwendet werden, können Zinsberechnungen stark
abweichen

5
... also die meisten Basis-10-Fraktionen. Zum Beispiel hat 0.1 keine exakte binäre Gleitkommadarstellung. So 1.0 / 10 * 10kann nicht das gleiche wie 1.0.
Chris Jester-Young

6
@ linuxuser27 Ich denke Fran hat versucht lustig zu sein. Wie auch immer, die Antwort von zneak ist die beste, die ich je gesehen habe, sogar besser als die klassische Version von Bloch.
Isaac Rabinovitch

5
Wenn Sie die Präzision kennen, können Sie das Ergebnis natürlich immer runden und so das gesamte Problem vermeiden. Dies ist viel schneller und einfacher als die Verwendung von BigDecimal. Eine andere Alternative ist die Verwendung von int oder long mit fester Genauigkeit.
Peter Lawrey

2
@zneak du weißt, dass die rationalen Zahlen eine Teilmenge der reellen Zahlen sind, oder? IEEE-754 reelle Zahlen sind reelle Zahlen. Sie sind einfach auch rational.
Tim Seguine

314

Von Bloch, J., Effective Java, 2. Auflage, Punkt 48:

Die Typen floatund doublesind für monetäre Berechnungen besonders ungeeignet, da es unmöglich ist, 0,1 (oder eine andere negative Zehnerpotenz) als floatoder doublegenau darzustellen .

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

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

druckt aus 0.6100000000000001.

Der richtige Weg , um dieses Problem zu lösen , ist die Verwendung BigDecimal, intoder long für das Geld Berechnungen.

Es BigDecimalgibt jedoch einige Einschränkungen (siehe derzeit akzeptierte Antwort).


6
Ich bin ein wenig verwirrt über die Empfehlung, int oder long für monetäre Berechnungen zu verwenden. Wie stellen Sie 1.03 als int oder long dar? Ich habe versucht "long a = 1.04;" und "long a = 104/100;" umsonst.
Peter

49
@Peter, Sie verwenden long a = 104und zählen in Cent anstelle von Dollar.
Zneak

@zneak Was ist, wenn ein Prozentsatz wie Zinseszins oder ähnliches angewendet werden muss?
Trusktr

3
@trusktr, ich würde mit dem Dezimaltyp Ihrer Plattform gehen. In Java ist das so BigDecimal.
Zneak

13
@maaartinus ... und du denkst nicht, dass die Verwendung von double für solche Dinge fehleranfällig ist? Ich habe gesehen, dass das Problem der Float-Rundung echte Systeme hart getroffen hat . Auch im Bankgeschäft. Bitte empfehlen es nicht, oder wenn Sie das tun, sehen vor , dass als eine separate Antwort (so wir es können downvote: P)
uvb

75

Dies ist weder eine Frage der Genauigkeit noch der Präzision. Es geht darum, die Erwartungen von Menschen zu erfüllen, die Basis 10 für Berechnungen anstelle von Basis 2 verwenden. Beispielsweise führt die Verwendung von Doppel für Finanzberechnungen nicht zu Antworten, die im mathematischen Sinne "falsch" sind, sondern zu Antworten, die es sind nicht das, was im finanziellen Sinne erwartet wird.

Selbst wenn Sie Ihre Ergebnisse in der letzten Minute vor der Ausgabe abrunden, können Sie gelegentlich ein Ergebnis mit Doppelwerten erzielen, die nicht den Erwartungen entsprechen.

Verwenden eines Taschenrechners oder Berechnen der Ergebnisse von Hand: 1,40 * 165 = 231 genau. Bei interner Verwendung von Doubles wird es in meiner Compiler- / Betriebssystemumgebung jedoch als Binärzahl in der Nähe von 230.99999 gespeichert. Wenn Sie also die Zahl abschneiden, erhalten Sie 230 anstelle von 231. Sie können argumentieren, dass Rundung statt Abschneiden wäre haben das gewünschte Ergebnis von 231 gegeben. Das ist wahr, aber das Runden beinhaltet immer das Abschneiden. Unabhängig davon, welche Rundungstechnik Sie verwenden, gibt es immer noch Randbedingungen wie diese, die sich abrunden, wenn Sie erwarten, dass sie sich aufrunden. Sie sind selten genug, dass sie oft nicht durch gelegentliche Tests oder Beobachtungen gefunden werden. Möglicherweise müssen Sie Code schreiben, um nach Beispielen zu suchen, die Ergebnisse veranschaulichen, die sich nicht wie erwartet verhalten.

Angenommen, Sie möchten etwas auf den nächsten Cent runden. Sie nehmen also Ihr Endergebnis, multiplizieren es mit 100, addieren 0,5, kürzen es und dividieren das Ergebnis durch 100, um wieder auf ein paar Cent zu kommen. Wenn die interne Nummer, die Sie gespeichert haben, 3.46499999 ... statt 3.465 war, erhalten Sie 3.46 statt 3.47, wenn Sie die Nummer auf den nächsten Cent runden. Ihre Basis-10-Berechnungen haben jedoch möglicherweise ergeben, dass die Antwort genau 3,465 sein sollte, was eindeutig auf 3,47 und nicht auf 3,46 aufrunden sollte. Solche Dinge passieren gelegentlich im wirklichen Leben, wenn Sie Doppel für finanzielle Berechnungen verwenden. Es ist selten, daher bleibt es als Problem oft unbemerkt, aber es passiert.

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


2
Verwandte, interessante: In meiner Chrome JS-Konsole: Math.round (.4999999999999999): 0 Math.round (.49999999999999999): 1
Curtis Yallop

16
Diese Antwort ist irreführend. 1,40 * 165 = 231. Jede andere Zahl als genau 231 ist im mathematischen Sinne (und in allen anderen Sinnen) falsch.
Karu

2
@Karu Ich denke, deshalb sagt Randy, dass Floats schlecht sind ... Meine Chrome JS-Konsole zeigt als Ergebnis 230.99999999999997 an. Das ist falsch, worauf es in der Antwort ankommt.
Trusktr

6
@ Karu: Imho die Antwort ist nicht mathematisch falsch. Es werden nur zwei Fragen beantwortet, von denen eine nicht gestellt wird. Die Frage, die Ihr Compiler beantwortet, ist 1.39999999 * 164.99999999 usw., was mathematisch korrekt ist, entspricht 230.99999 .... Offensichtlich ist dies nicht die Frage, die zuerst gestellt wurde ....
Markus

2
@CurtisYallop, da der doppelte Doppelwert auf 0,49999999999999999 0,5 beträgt. Warum wird Math.round(0.49999999999999994)1 zurückgegeben?
Phuclv

53

Einige dieser Antworten beunruhigen mich. Ich denke, dass Double und Floats einen Platz in Finanzberechnungen haben. Wenn Sie nicht gebrochene Geldbeträge addieren und subtrahieren, tritt bei Verwendung von Ganzzahlklassen oder BigDecimal-Klassen kein Genauigkeitsverlust auf. Wenn Sie jedoch komplexere Operationen ausführen, erhalten Sie häufig 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 dieser letzte Cent wirklich wichtig ist, sollten Sie dem Betrachter wahrscheinlich sagen, dass die Antwort fast in der Mitte liegt - indem Sie mehr Dezimalstellen anzeigen.

Das Problem bei Doubles und insbesondere bei Floats besteht darin, dass sie zum Kombinieren großer und kleiner Zahlen verwendet werden. In Java,

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

führt zu

1.1875

16
DIESE!!!! Ich habe alle Antworten gesucht, um diese RELEVANTE FAKT zu finden !!! In normalen Berechnungen kümmert es niemanden, ob Sie um einen Bruchteil eines Cent sind, aber hier mit hohen Zahlen gehen leicht einige Dollar pro Transaktion verloren!
Falco

22
Und jetzt stellen Sie sich vor, jemand erzielt einen Tagesumsatz von 0,01% auf seine 1 Million Dollar - er würde jeden Tag nichts bekommen - und nach einem Jahr, in dem er keine 1000 Dollar bekommen hat, ist das wichtig
Falco

6
Das Problem ist nicht die Genauigkeit, aber dieser Float sagt Ihnen nicht, dass er ungenau wird. Eine Ganzzahl kann nur bis zu 10 Stellen enthalten, ein Float kann bis zu 6 Stellen enthalten, ohne ungenau zu werden (wenn Sie sie entsprechend schneiden). Dies ist zulässig, während eine Ganzzahl einen Überlauf erhält und eine Sprache wie Java Sie warnt oder nicht zulässt. Wenn Sie ein Double verwenden, können Sie bis zu 16 Stellen eingeben, was für viele Anwendungsfälle ausreicht.
Sigi

39

Floats und Doubles sind ungefähr. Wenn Sie ein BigDecimal erstellen und einen Float an den Konstruktor übergeben, sehen Sie, was der Float tatsächlich entspricht:

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

Auf diese Weise möchten Sie wahrscheinlich nicht 1,01 USD darstellen.

Das Problem ist, dass die IEEE-Spezifikation nicht alle Brüche genau darstellen kann. Einige von ihnen werden als sich wiederholende Brüche ausgegeben, sodass Sie Approximationsfehler erhalten. Da Buchhalter es mögen, wenn die Dinge genau auf den Punkt kommen, und Kunden sich ärgern, wenn sie ihre Rechnung bezahlen und nachdem die Zahlung verarbeitet wurde, schulden sie 0,01 und ihnen eine Gebühr berechnet wird oder sie ihr Konto nicht schließen können, ist es besser, sie zu verwenden genaue Typen wie Dezimal (in C #) oder java.math.BigDecimal in Java.

Es ist nicht so, dass der Fehler nicht kontrollierbar ist, wenn Sie runden: siehe diesen Artikel von Peter Lawrey . Es ist einfach einfacher, überhaupt nicht runden zu müssen. Die meisten Anwendungen, die mit Geld umgehen, erfordern nicht viel Mathematik. Die Operationen bestehen darin, Dinge hinzuzufügen oder Beträge verschiedenen Buckets zuzuweisen. Das Einführen von Gleitkomma und Runden macht die Sache nur komplizierter.


5
float, doubleUnd BigDecimalsind vertreten genaue Werte. Die Konvertierung von Code in Objekt ist ebenso ungenau wie andere Vorgänge. Die Typen selbst sind nicht ungenau.
chux

1
@chux: Wenn ich das noch einmal lese, denke ich, dass Sie einen Punkt haben, an dem mein Wortlaut verbessert werden könnte. Ich werde das bearbeiten und umformulieren.
Nathan Hughes

28

Ich werde riskieren, herabgestimmt zu werden, aber ich denke, die Ungeeignetheit von Gleitkommazahlen für Währungsberechnungen wird überbewertet. Solange Sie sicherstellen, dass Sie die Cent-Rundung korrekt durchführen und über genügend signifikante Ziffern verfügen, um der durch zneak erklärten Nichtübereinstimmung der binär-dezimalen Darstellung entgegenzuwirken, gibt es kein Problem.

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

Natürlich muss man in der Vernunft bleiben; Beispiel: Ein einfacher Webshop würde wahrscheinlich nie Probleme mit Floats mit doppelter Genauigkeit haben. Wenn Sie jedoch z. B. Buchhaltung oder etwas anderes tun, bei dem eine große (uneingeschränkte) Anzahl von Zahlen hinzugefügt werden muss, möchten Sie Gleitkommazahlen nicht mit einem Meter berühren Pole.


3
Dies ist eigentlich eine ziemlich anständige Antwort. In den meisten Fällen ist es vollkommen in Ordnung, sie zu verwenden.
Vahid Amiri

2
Es ist zu beachten, dass die meisten Investmentbanken doppelt so viel verwenden wie die meisten C ++ - Programme. Einige verwenden lange, haben aber daher das eigene Problem der Skalierung.
Peter Lawrey

20

Zwar kann der Gleitkommatyp nur annähernd dezimale Daten darstellen, doch wenn man Zahlen vor der Darstellung mit der erforderlichen Genauigkeit rundet, erhält man das richtige Ergebnis. Meistens.

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

Es muss gesagt werden, dass Sie, selbst wenn Sie Festkomma-Arithmetik verwenden, immer noch Zahlen runden müssen, wenn BigInteger und BigDecimal keine Fehler liefern, wenn Sie periodische Dezimalzahlen erhalten. Auch hier gibt es eine Annäherung.

Zum Beispiel hat COBOL, das historisch für Finanzberechnungen verwendet wurde, eine maximale Genauigkeit von 18 Zahlen. Es gibt also oft eine implizite Rundung.

Zusammenfassend ist das Double meiner Meinung nach vor allem wegen seiner 16-stelligen Genauigkeit ungeeignet, was unzureichend sein kann, nicht weil es ungefähr ist.

Betrachten Sie die folgende Ausgabe des nachfolgenden Programms. Es zeigt, dass nach dem Runden von double das gleiche Ergebnis wie bei BigDecimal bis zur Genauigkeit 16 erzielt 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;
    }
}

15

Das Ergebnis der Gleitkommazahl ist nicht genau, was sie für eine finanzielle Berechnung ungeeignet macht, die ein genaues Ergebnis und keine Annäherung erfordert. float und double sind für technische und wissenschaftliche Berechnungen konzipiert und liefern oft kein genaues Ergebnis. Das Ergebnis der Gleitkommaberechnung kann von JVM zu JVM variieren. Schauen Sie sich das folgende Beispiel für BigDecimal und das Doppelprimitiv an, das zur Darstellung des Geldwerts verwendet wird. Es ist ziemlich klar, dass die Gleitkommaberechnung möglicherweise nicht genau ist und man BigDecimal für Finanzberechnungen verwenden 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

3
Versuchen wir etwas anderes als triviale Addition / Subtraktion und ganzzahlige Mutplicaiton. Wenn der Code die monatliche Rate eines 7% -Darlehens berechnet, müssten beide Typen keinen genauen Wert liefern und müssen auf 0,01 gerundet werden. Das Runden auf die niedrigste Währungseinheit ist Teil der Geldberechnung. Durch die Verwendung von Dezimaltypen wird diese Notwendigkeit durch Addition / Subtraktion vermieden - aber sonst nicht viel.
chux

@ chux-ReinstateMonica: Wenn die Zinsen monatlich zusammengesetzt werden sollen, berechnen Sie die Zinsen jeden Monat, indem Sie den täglichen Kontostand addieren, diesen mit 7 (dem Zinssatz) multiplizieren und durch Runden auf den nächsten Cent durch die Anzahl der Tage in dividieren das Jahr. Keine Rundung außer einmal im Monat beim allerletzten Schritt.
Superkatze

@supercat Mein Kommentar betont die Verwendung eines binären FP der kleinsten Währungseinheit oder eines dezimalen FP. Beide verursachen ähnliche Rundungsprobleme - wie in Ihrem Kommentar mit "und dividieren, auf den nächsten Cent runden". Die Verwendung eines Base 2- oder Base 10-FP bietet in Ihrem Szenario in beiden Fällen keinen Vorteil.
chux - Monica

@ chux-ReinstateMonica: Wenn sich im obigen Szenario herausstellt, dass die Zinsen genau gleich einer halben Anzahl von halben Cent sein sollten, muss ein korrektes Finanzprogramm auf genau festgelegte Weise gerundet werden. Wenn Gleitkommaberechnungen einen Zinswert von z. B. 1,23499941 USD ergeben, der mathematisch genaue Wert vor dem Runden jedoch 1,235 USD betragen sollte und die Rundung als "nächste Gerade" angegeben wird, führt die Verwendung solcher Gleitkommaberechnungen nicht zu einem Ergebnis um 0,000059 USD, aber um ganze 0,01 USD, was für Buchhaltungszwecke Just Plain Wrong ist.
Superkatze

@supercat Die Verwendung von binärem doubleFP für den Cent hätte keine Probleme bei der Berechnung auf 0,5 Cent, ebenso wenig wie dezimales FP. Wenn Gleitkommaberechnungen einen Zinswert von z. B. 123.499941 ¢ ergeben, entweder durch binäres FP oder dezimales FP, ist das Problem der doppelten Rundung dasselbe - in beiden Fällen kein Vorteil. Ihre Prämisse scheint davon auszugehen, dass der mathematisch genaue Wert und das Dezimal-FP gleich sind - etwas, das selbst das Dezimal-FP nicht garantiert. 0,5 / 7,0 * 7,0 ist ein Problem für binäre und deicmale FP. IAC, die meisten werden strittig sein, da ich davon ausgehe, dass die nächste Version von C dezimale FP liefert.
chux - Monica

11

Wie bereits erwähnt "Die Darstellung von Geld als Double oder Float wird wahrscheinlich zunächst gut aussehen, wenn die Software die winzigen Fehler abrundet. Wenn Sie jedoch mehr Additionen, Subtraktionen, Multiplikationen und Divisionen für ungenaue Zahlen durchführen, verlieren Sie immer mehr Präzision Dies macht Floats und Doubles für den Umgang mit Geld unzureichend, wo eine perfekte Genauigkeit für ein Vielfaches der Basis-10-Kräfte erforderlich ist. "

Endlich hat Java eine Standardmethode, um mit Währung und Geld zu arbeiten!

JSR 354: Geld- und Währungs-API

JSR 354 bietet eine API zum Darstellen, Transportieren und Durchführen umfassender Berechnungen mit Geld und Währung. Sie können es von diesem Link herunterladen:

JSR 354: Download der Geld- und Währungs-API

Die Spezifikation besteht aus folgenden Dingen:

  1. Eine API für den Umgang mit z. B. Geldbeträgen und Währungen
  2. APIs zur Unterstützung austauschbarer Implementierungen
  3. Fabriken zum Erstellen von Instanzen der Implementierungsklassen
  4. Funktionalität zur Berechnung, Umrechnung und Formatierung von Geldbeträgen
  5. Java-API für die Arbeit mit Geld und Währungen, die in Java 9 enthalten sein soll.
  6. Alle Spezifikationsklassen und Schnittstellen befinden sich im Paket javax.money. *.

Beispielbeispiele für JSR 354: Geld- und Währungs-API:

Ein Beispiel für das Erstellen eines MonetaryAmount und das Drucken auf der Konsole sieht folgendermaßen aus:

MonetaryAmountFactory<?> amountFactory = Monetary.getDefaultAmountFactory();
MonetaryAmount monetaryAmount = amountFactory.setCurrency(Monetary.getCurrency("EUR")).setNumber(12345.67).create();
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));

Bei Verwendung der Referenzimplementierungs-API ist der erforderliche Code viel einfacher:

MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));

Die API unterstützt auch Berechnungen mit MonetaryAmounts:

MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmount otherMonetaryAmount = monetaryAmount.divide(2).add(Money.of(5, "EUR"));

CurrencyUnit und MonetaryAmount

// getting CurrencyUnits by locale
CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN);
CurrencyUnit canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA);

MonetaryAmount verfügt über verschiedene Methoden, mit denen Sie auf die zugewiesene Währung, den numerischen Betrag, ihre Genauigkeit und vieles mehr zugreifen können:

MonetaryAmount monetaryAmount = Money.of(123.45, euro);
CurrencyUnit currency = monetaryAmount.getCurrency();
NumberValue numberValue = monetaryAmount.getNumber();

int intValue = numberValue.intValue(); // 123
double doubleValue = numberValue.doubleValue(); // 123.45
long fractionDenominator = numberValue.getAmountFractionDenominator(); // 100
long fractionNumerator = numberValue.getAmountFractionNumerator(); // 45
int precision = numberValue.getPrecision(); // 5

// NumberValue extends java.lang.Number. 
// So we assign numberValue to a variable of type Number
Number number = numberValue;

MonetaryAmounts können mit einem Rundungsoperator gerundet werden:

CurrencyUnit usd = MonetaryCurrencies.getCurrency("USD");
MonetaryAmount dollars = Money.of(12.34567, usd);
MonetaryOperator roundingOperator = MonetaryRoundings.getRounding(usd);
MonetaryAmount roundedDollars = dollars.with(roundingOperator); // USD 12.35

Bei der Arbeit mit Sammlungen von MonetaryAmounts stehen einige nützliche Dienstprogrammmethoden zum Filtern, Sortieren und Gruppieren zur Verfügung.

List<MonetaryAmount> amounts = new ArrayList<>();
amounts.add(Money.of(2, "EUR"));
amounts.add(Money.of(42, "USD"));
amounts.add(Money.of(7, "USD"));
amounts.add(Money.of(13.37, "JPY"));
amounts.add(Money.of(18, "USD"));

Benutzerdefinierte MonetaryAmount-Operationen

// A monetary operator that returns 10% of the input MonetaryAmount
// Implemented using Java 8 Lambdas
MonetaryOperator tenPercentOperator = (MonetaryAmount amount) -> {
  BigDecimal baseAmount = amount.getNumber().numberValue(BigDecimal.class);
  BigDecimal tenPercent = baseAmount.multiply(new BigDecimal("0.1"));
  return Money.of(tenPercent, amount.getCurrency());
};

MonetaryAmount dollars = Money.of(12.34567, "USD");

// apply tenPercentOperator to MonetaryAmount
MonetaryAmount tenPercentDollars = dollars.with(tenPercentOperator); // USD 1.234567

Ressourcen:

Umgang mit Geld und Währungen in Java mit JSR 354

Einblick in die Java 9 Money and Currency API (JSR 354)

Siehe auch: JSR 354 - Währung und Geld


5

Wenn Ihre Berechnung verschiedene Schritte umfasst, werden Sie durch Arithmetik mit beliebiger Genauigkeit nicht zu 100% abgedeckt.

Die einzige zuverlässige Möglichkeit, eine perfekte Darstellung der Ergebnisse zu verwenden (Verwenden Sie einen benutzerdefinierten Bruchdatentyp, der die Teilungsoperationen bis zum letzten Schritt stapelt) und erst im letzten Schritt in die Dezimalschreibweise zu konvertieren.

Beliebige Genauigkeit hilft nicht weiter, da es immer Zahlen mit so vielen Dezimalstellen oder einige Ergebnisse wie z 0.6666666 ... Keine willkürliche Darstellung wird das letzte Beispiel abdecken. Sie werden also in jedem Schritt kleine Fehler haben.

Diese Fehler summieren sich und können möglicherweise nicht mehr einfach ignoriert werden. Dies wird als Fehlerausbreitung bezeichnet .


4

Die meisten Antworten haben die Gründe hervorgehoben, warum man für Geld- und Währungsberechnungen kein Doppel verwenden sollte. Und ich stimme ihnen voll und ganz zu.

Dies bedeutet jedoch nicht, dass Doppelgänger niemals für diesen Zweck verwendet werden können.

Ich habe an einer Reihe von Projekten mit sehr geringen GC-Anforderungen gearbeitet, und BigDecimal-Objekte haben einen großen Beitrag zu diesem Overhead geleistet.

Es ist das mangelnde Verständnis für doppelte Repräsentation und die mangelnde Erfahrung im Umgang mit der Genauigkeit und Präzision, die diesen weisen Vorschlag hervorbringt.

Sie können es zum Laufen bringen, wenn Sie in der Lage sind, die Präzisions- und Genauigkeitsanforderungen Ihres Projekts zu erfüllen. Dies muss basierend auf dem Bereich der Doppelwerte erfolgen, mit dem Sie sich befassen.

Weitere Informationen finden Sie in der FuzzyCompare-Methode von Guave. Die Parametertoleranz ist der Schlüssel. Wir haben uns mit diesem Problem für eine Wertpapierhandelsanwendung befasst und ausführlich untersucht, welche Toleranzen für verschiedene Zahlenwerte in verschiedenen Bereichen zu verwenden sind.

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


2

Viele der Antworten auf diese Frage befassen sich mit IEEE und den Standards für Gleitkomma-Arithmetik.

Ich komme aus einem nicht-computerwissenschaftlichen Umfeld (Physik und Ingenieurwesen) und neige dazu, Probleme aus einer anderen Perspektive zu betrachten. Für mich ist der Grund, warum ich bei einer mathematischen Berechnung kein Double oder Float verwenden würde, dass ich zu viele Informationen verlieren würde.

Was sind die Alternativen? Es gibt viele (und viele weitere, von denen ich nichts weiß!).

BigDecimal in Java stammt ursprünglich aus der Java-Sprache. Apfloat ist eine weitere Bibliothek mit beliebiger Genauigkeit für Java.

Der dezimale Datentyp in C # ist die .NET-Alternative von Microsoft für 28 signifikante Zahlen.

SciPy (Scientific Python) kann wahrscheinlich auch finanzielle Berechnungen durchführen (ich habe es nicht versucht, aber ich vermute es).

Die GNU Multiple Precision Library (GMP) und die GNU MFPR Library sind zwei kostenlose Open-Source-Ressourcen für C und C ++.

Es gibt auch numerische Präzisionsbibliotheken für JavaScript (!) Und ich denke, PHP kann finanzielle Berechnungen durchführen.

Es gibt auch proprietäre (insbesondere für Fortran, glaube ich) und Open-Source-Lösungen für viele Computersprachen.

Ich bin kein ausgebildeter Informatiker. Ich tendiere jedoch dazu, entweder zu BigDecimal in Java oder zu Dezimal in C # zu tendieren. Ich habe die anderen Lösungen, die ich aufgelistet habe, nicht ausprobiert, aber sie sind wahrscheinlich auch sehr gut.

Ich mag BigDecimal wegen der Methoden, die es unterstützt. Die Dezimalstelle von C # ist sehr schön, aber ich hatte nicht die Möglichkeit, so viel damit zu arbeiten, wie ich möchte. In meiner Freizeit mache ich wissenschaftliche Berechnungen, die für mich von Interesse sind, und BigDecimal scheint sehr gut zu funktionieren, da ich die Genauigkeit meiner Gleitkommazahlen einstellen kann. Der Nachteil von BigDecimal? Es kann manchmal langsam sein, insbesondere wenn Sie die Divide-Methode verwenden.

Aus Gründen der Geschwindigkeit können Sie sich die kostenlosen und proprietären Bibliotheken in C, C ++ und Fortran ansehen.


1
In Bezug auf SciPy / Numpy wird die feste Genauigkeit (dh Pythons Dezimalzahl.Dezimalzahl) nicht unterstützt ( docs.scipy.org/doc/numpy-dev/user/basics.types.html ). Einige Funktionen funktionieren mit Decimal nicht richtig (z. B. isnan). Pandas basiert auf Numpy und wurde bei AQR, einem großen quantitativen Hedgefonds, initiiert. Sie haben also Ihre Antwort bezüglich finanzieller Berechnungen (nicht der Lebensmittelbuchhaltung).
Comte

2

Um frühere Antworten zu ergänzen , besteht neben BigDecimal auch die Möglichkeit, Joda-Money in Java zu implementieren , wenn das in der Frage behandelte Problem behandelt wird. Der Java-Modulname lautet org.joda.money.

Es erfordert Java SE 8 oder höher und hat keine Abhängigkeiten.

Genauer gesagt besteht eine Abhängigkeit zur Kompilierungszeit, die jedoch nicht erforderlich ist.

<dependency>
  <groupId>org.joda</groupId>
  <artifactId>joda-money</artifactId>
  <version>1.0.1</version>
</dependency>

Beispiele für die Verwendung von Joda Money:

  // create a monetary value
  Money money = Money.parse("USD 23.87");

  // add another amount with safe double conversion
  CurrencyUnit usd = CurrencyUnit.of("USD");
  money = money.plus(Money.of(usd, 12.43d));

  // subtracts an amount in dollars
  money = money.minusMajor(2);

  // multiplies by 3.5 with rounding
  money = money.multipliedBy(3.5d, RoundingMode.DOWN);

  // compare two amounts
  boolean bigAmount = money.isGreaterThan(dailyWage);

  // convert to GBP using a supplied rate
  BigDecimal conversionRate = ...;  // obtained from code outside Joda-Money
  Money moneyGBP = money.convertedTo(CurrencyUnit.GBP, conversionRate, RoundingMode.HALF_UP);

  // use a BigMoney for more complex calculations where scale matters
  BigMoney moneyCalc = money.toBigMoney();

Dokumentation: http://joda-money.sourceforge.net/apidocs/org/joda/money/Money.html

Implementierungsbeispiele: https://www.programcreek.com/java-api-examples/?api=org.joda.money.Money


0

Ein Beispiel ... dies funktioniert (funktioniert eigentlich nicht wie erwartet) in fast jeder Programmiersprache ... Ich habe es mit Delphi, VBScript, Visual Basic, JavaScript und jetzt mit Java / Android versucht:

    double total = 0.0;

    // do 10 adds of 10 cents
    for (int i = 0; i < 10; i++) {
        total += 0.1;  // adds 10 cents
    }

    Log.d("round problems?", "current total: " + total);

    // looks like total equals to 1.0, don't?

    // now, do reverse
    for (int i = 0; i < 10; i++) {
        total -= 0.1;  // removes 10 cents
    }

    // looks like total equals to 0.0, don't?
    Log.d("round problems?", "current total: " + total);
    if (total == 0.0) {
        Log.d("round problems?", "is total equal to ZERO? YES, of course!!");
    } else {
        Log.d("round problems?", "is total equal to ZERO? NO... thats why you should not use Double for some math!!!");
    }

AUSGABE:

round problems?: current total: 0.9999999999999999 round problems?: current total: 2.7755575615628914E-17 round problems?: is total equal to ZERO? NO... thats why you should not use Double for some math!!!


3
Das Problem ist nicht, dass ein Rundungsfehler auftritt, sondern dass Sie sich nicht damit befassen. Runden Sie das Ergebnis auf zwei Dezimalstellen (wenn Sie Cent möchten) und Sie sind fertig.
Maaartinus

0

Float ist eine binäre Form von Decimal mit unterschiedlichem Design. Das sind zwei verschiedene Dinge. Es gibt kleine Fehler zwischen zwei Typen, wenn sie ineinander konvertiert werden. Float ist auch so konzipiert, dass es unendlich viele Werte für wissenschaftliche Zwecke darstellt. Dies bedeutet, dass die Genauigkeit bei extrem kleinen und extrem großen Zahlen mit dieser festen Anzahl von Bytes verloren geht. Dezimal kann keine unendliche Anzahl von Werten darstellen, sondern beschränkt sich nur auf diese Anzahl von Dezimalstellen. Float und Decimal dienen also unterschiedlichen Zwecken.

Es gibt einige Möglichkeiten, den Fehler für den Währungswert zu verwalten:

  1. Verwenden Sie eine lange Ganzzahl und zählen Sie stattdessen in Cent.

  2. Verwenden Sie die doppelte Genauigkeit und halten Sie Ihre signifikanten Stellen nur auf 15, damit die Dezimalzahl genau simuliert werden kann. Runde vor der Präsentation von Werten; Bei Berechnungen oft rund.

  3. Verwenden Sie eine Dezimalbibliothek wie Java BigDecimal, damit Sie nicht double verwenden müssen, um Dezimalzahlen zu simulieren.

ps es ist interessant zu wissen, dass die meisten Marken von wissenschaftlichen Taschenrechnern mit Dezimalzahl statt mit Float arbeiten. Also beschwert sich niemand über Float-Konvertierungsfehler.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.