Mir wurde immer gesagt, ich solle niemals Geld mit double
oder float
Typen 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.
Mir wurde immer gesagt, ich solle niemals Geld mit double
oder float
Typen 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.
Antworten:
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 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, 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 BigDecimal
Klasse und C # den decimal
Typ.
1.0 / 10 * 10
kann nicht das gleiche wie 1.0.
Von Bloch, J., Effective Java, 2. Auflage, Punkt 48:
Die Typen
float
unddouble
sind für monetäre Berechnungen besonders ungeeignet, da es unmöglich ist, 0,1 (oder eine andere negative Zehnerpotenz) alsfloat
oderdouble
genau 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
,int
oderlong
für das Geld Berechnungen.
Es BigDecimal
gibt jedoch einige Einschränkungen (siehe derzeit akzeptierte Antwort).
long a = 104
und zählen in Cent anstelle von Dollar.
BigDecimal
.
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.
Math.round(0.49999999999999994)
1 zurückgegeben?
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
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.
float
, double
Und BigDecimal
sind vertreten genaue Werte. Die Konvertierung von Code in Objekt ist ebenso ungenau wie andere Vorgänge. Die Typen selbst sind nicht ungenau.
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.
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;
}
}
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
double
FP 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.
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:
- Eine API für den Umgang mit z. B. Geldbeträgen und Währungen
- APIs zur Unterstützung austauschbarer Implementierungen
- Fabriken zum Erstellen von Instanzen der Implementierungsklassen
- Funktionalität zur Berechnung, Umrechnung und Formatierung von Geldbeträgen
- Java-API für die Arbeit mit Geld und Währungen, die in Java 9 enthalten sein soll.
- 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
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 .
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.
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.
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
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!!!
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:
Verwenden Sie eine lange Ganzzahl und zählen Sie stattdessen in Cent.
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.
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.