Java Warum ist BigDecimal genauer?

Hendoul

Commander
Registriert
Apr. 2008
Beiträge
2.162
Hi :)

Ich verstehe warum Rechenoperationen mit double zu Ungenauigkeiten führen können und man BigDecimal nutzten sollte für z.B. Währungen. Aber warum ist eigentlich BigDecimal genauer?

Das Problem beim Double ist ja, dass z.B. 0.1 im Binärsystem nicht genau dargestellt werden kann und sich immer wiederholt. Und dann wird nach einer gewissen Anzahl Stellen halt einfach abgeschnitten weil es sonst 'überläuft'.

Aber wie ist das denn beim BigDecimal anders? Hät dieser einfach eine noch viel höhere Präzision oder speichert der die Zahlen irgendwie anders intern?
 
BigDecimal ist eine Klasse, double ein primitiver Datentyp. In der Klasse kann dann eine viel genauere verrechnung implementiert werden, als bei primitiven Datentypen :)

Das Objekt einer BigDecimal kann daher theoretisch viel viel mehr Speicher im RAM belegen als einfach eine Ziffernfolge mit 64 Bit die als Double interpretiert wird.
 
  • Gefällt mir
Reaktionen: BeBur
Hendoul schrieb:
oder speichert der die Zahlen irgendwie anders intern?
Genau das. 0.1 ist für ein decimal eine 1 mit einer Skalierung von 1.
Bei 0.11 wäre das 11 mit einer Skalierung von 2, etc.
Wie als wenn du eine Währung als int in Cent speicherst und dann für Euro eben noch um 2 Stellen verschieben musst.
 
BigDecimal ist jetzt nicht das große Wunder. Probleme macht die Division, also braucht man für Genauigkeit eine Fraction Klasse, die eine gebrochene Zahl als Paar (Zähler, Nenner) implementiert und Operationen dafür bereitstellt.

BigDecimal hat aber eine flexible Genauigkeit bis 32 Stellen. Float hat 7 und double 15. Sprich, so Dinge wie 2/5 können mit BigDecimal exakter dargestellt werden.

Richtig exakt geht es aber auch nicht. Dazu braucht man die “Bruch”klasse, ggfs selbst zu implementieren.
 
Toms schrieb:
BigDecimal ist eine Klasse, double ein primitiver Datentyp. In der Klasse kann dann eine viel genauere verrechnung implementiert werden, als bei primitiven Datentypen
Nicht grundsätzlich, das ganze hängt auch an der Antwort auf die Frage "Warum gibt es überhaupt so einen ungenauen Floating-Point Datentyp?"
Antwort:
1. Sehr oft reichen gerundete Ergebnisse
2. floats können sehr schnell auf der CPU (oder einer speziellen Unit) berechnet werden
 
  • Gefällt mir
Reaktionen: LencoX2
Hendoul schrieb:
Aber wie ist das denn beim BigDecimal anders? Hät dieser einfach eine noch viel höhere Präzision oder speichert der die Zahlen irgendwie anders intern?
Kurze Antwort: Ja und Ja

Lange Antwort:
BigDecimal rechnet intern im Dezimalsystem, daher wird intern die berühmte Zahl "0.1" auch exakt als 0.1 dargestellt. Siehe auch "A BigDecimal consists of an arbitrary precision integer unscaled value and a 32-bit integer scale." (Zitat von https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html)

Die meisten anderen Implementierungen benutzen IEEE 754 welches systembedingt einige kleine Schwächen hat, eben daß es z.B. die Zahl "0.1" intern nicht exakt darstellen kann, da es intern im Binärsystem (mit endlicher Genauigkeit) arbeitet.

Die (dezimale) Zahl "0.1" in binär ungerechnet lautet:

0.00011001100110011001100110011001100110011001100110011.....

oder exakt

0.0 {0011}Periode (Ich hoffe man kann erkennen was gemeint ist)

Das ist genauso wie im dezimalen Fall mit 1/3= 0.333333333333... bzw. 0.3 Periode.

Aus dieses Umwandlung von dezimal nach binär (und evtl. auch bei der Umwandlung zurück von binär nach dezimal) ergeben sich Rundungsfehler (insbesondere bei periodischen Zahlen), die evtl. zu Problemen führen.

Wie die Zahl "0.1" im IEEE754-Format aussieht kannst Du Dir hier ansehen.

Auf der Seite https://www.exploringbinary.com/why-0-point-1-does-not-exist-in-floating-point/ gibt es eine sehr schöne Erklärung des Phänomens in englisch.

Mehrere Ausprägungen des Problems sind auch schön auf https://www.exploringbinary.com/floating-point-questions-are-endless-on-stackoverflow-com/ dargestellt, exemplarisch sei von meiner Seite das Beispiel

Code:
// from https://www.exploringbinary.com/floating-point-questions-are-endless-on-stackoverflow-com/

public class DecimalTest1
{
    public static void main(String args[])
    {
        double check= 0.615 * 255 -0.515 * 255 -0.100 * 255;
        System.out.println(check);
    }
}
herausgegriffen, das als Ergebnis

-2.8421709430404007E-14

liefert, obwohl eigentlich "0.0" herauskommen sollte.

Ich habe mir noch als kleine Spielerei ein Programm gebastelt, welches den exakten Zahlenwert, der sich bei der Umwandlung von dezimal "0.1" in "IEEE754 0.1" ergibt berechnet:

Code:
// see also https://www.exploringbinary.com/why-0-point-1-does-not-exist-in-floating-point/

import java.math.BigInteger;
import java.math.BigDecimal;


public class ZeroPointOne
{
    public static void main(String args[])
    {
        // 0.0001100110011001100110011001100110011001100110011001101
        BigInteger zp1= new BigInteger("0001100110011001100110011001100110011001100110011001101", 2);
        System.out.println("zp1 [10]= " + zp1.toString() );
        System.out.println("zp1 [16]= 0x" + zp1.toString(16) );
        
        BigDecimal zeroPointOne= new BigDecimal(zp1).divide(new BigDecimal(2).pow(55));
        System.out.println("zeroPointOne= " + zeroPointOne.toString() );
    }
}
Das Ergebnis lautet (und zwar exakt, keine Näherung!):

0.1000000000000000055511151231257827021181583404541015625


RalphS schrieb:
Probleme macht die Division, also braucht man für Genauigkeit eine Fraction Klasse, die eine gebrochene Zahl als Paar (Zähler, Nenner) implementiert und Operationen dafür bereitstellt.
MÖÖÖÖÖÖÖP! Falsch!
Die Division kann zwar evtl. Probleme bereiten, das trifft aber in diesem Fall nicht zu!

RalphS schrieb:
BigDecimal hat aber eine flexible Genauigkeit bis 32 Stellen.
MÖÖÖÖÖÖÖP! KOMPLETT FALSCH!

Siehe weiter oben mein Zitat von https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html:
"A BigDecimal consists of an arbitrary precision integer unscaled value and a 32-bit integer scale."
BigDecimal kann problemlos mit 1 Million Stellen rechnen und zwar exakt!
BigDecimal kann sogar mit "beliebiger" ("arbitrary precision") Genauigkeit rechnen, in der Praxis wird diese durch den Arbeitsspeicher begrenzt!

RalphS schrieb:
Sprich, so Dinge wie 2/5 können mit BigDecimal exakter dargestellt werden.

Richtig exakt geht es aber auch nicht.
MÖÖÖÖÖÖÖP! Schon wieder falsch!

2/5 ist 0,4 und diese Zahl wird intern in BigDecimal als "4*10 hoch -1" exakt dargestellt!


BeBur schrieb:
Nicht grundsätzlich, das ganze hängt auch an der Antwort auf die Frage "Warum gibt es überhaupt so einen ungenauen Floating-Point Datentyp?"
Antwort:
1. Sehr oft reichen gerundete Ergebnisse
2. floats können sehr schnell auf der CPU (oder einer speziellen Unit) berechnet werden
Richtig!
Und ich würde noch als Drittens ergänzen:
3. Gemessen an der kompakten internen Darstellung (4 bzw. 8 Bytes) liefert die Umsetzung (nahezu) das bestmögliche Ergebnis!



HTH

BigNum
 
  • Gefällt mir
Reaktionen: LencoX2 und Raijin
Woher die Aufregung in deinem Beitrag?

BigNum schrieb:
2/5 ist 0,4 und diese Zahl wird intern in BigDecimal als "4*10 hoch -1" exakt dargestellt!
Wenn ich RalphS richtig verstanden habe meinte er im Dezimalsystem periodische Zahlen wie z.B. 1/3, welche laut Google bei BigDecimal eine Exception werfen. Falls man exakte Ergebnisse für solche Brüche erhalten will sollte man mit BigRational o.ä. arbeiten.


btw. diese floating point Geschichte ist ganz klassischer Teil des ersten Semesters in Informatik.
 
Kurz gesagt: bei den Grundtypen float und Double stehen nur 32 und 64 Bit zur Darstellung von Dezimalzahlen mit fliessenden Denkmalpunkt und daher beschränkter Anzahl Stellen zur Verfügung.
BigDecimal hat intern ein bytearray theoretisch beliebiger Länge und damit beliebiger Anzahl Stellen zur Verfügung. Ist im Endeffekt nur durch den RAM der Maschine beschränkt. Das ist der wesentliche Unterschied.
 
Vielen Dank für die zahlreichen Antworten.

Wenn ich das jetzt richtig verstanden habe ist für die Addition von z.B. 0.1 + 0.2 nicht die Anzahl Stellen entscheidend, sondern wie die Zahlen intern gespeichert werden. Auch wenn double unendlich viele Stellen zur Verfügung hätte, das Ergebnis wäre trotzdem falsch, weil die 0.1 in binär bzw. IEEE754 einfach nur ungenau abgebildet werden kann?

Was ich noch nicht ganz verstehe ist die Aussage "BigDecimal rechnet intern im Dezimalsystem". Aber schlussendlich läuft doch alles auf binär raus? Und das was macht BigDecimal mit 1/3 + 1/3 + 1/3 ?

Java:
public class MyBigDecimalTest {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal(1);
        BigDecimal b = new BigDecimal(3);

        BigDecimal result = a.divide(b).add(a.divide(b)).add(a.divide(b));

        System.out.println(result);
    }
}

Führt dann ja einfach zu:
Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
 
Das würde mich ja echt mal interessieren, wie man dem PC mit binären Komponenten das 10fingersystem beibringt. Ein PC kann nicht mit 10^x rechnen, dafür fehlen ihm die Register.

Meh, geschenkt. Wenn nicht klar ist, warum konkret die Division ein Problem sein sollte, dann sei an der Stelle Lektüre zum Thema Numerik ans Herz gelegt.
 
Hendoul schrieb:
Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
Das sagt doch exakt alles aus, was man über das Problem wissen muss. Das Ergebnis der Division ist unendlich lang. Was passiert wohl, wenn man versuchen würde eine unendlich lange Zahl im Speicher abzulegen? Ganz genau: Der Speicher reicht nicht aus, weil er nicht unendlich groß ist.

Es gibt schlaue Verfahren, mit denen man das Problem umgehen kann. Das basiert dann aber eher weniger auf ausrechnen, sondern auf Vereinfachung von Formeln. Wird z.B. von https://www.wolframalpha.com/ verwendet. Das Tool gibt dir - soweit es geht - eine Vereinfachung deiner Formel aus. Das ist die einzige exakte Repräsentation, die ohne unendlich großen Speicher auskommt.
D.h. aus 1/3 * 1/3 wird die exakte vereinfachte Repräsentation 1/9
 
Hendoul schrieb:
Auch wenn double unendlich viele Stellen zur Verfügung hätte, das Ergebnis wäre trotzdem falsch, weil die 0.1 in binär bzw. IEEE754
Das war weiter oben leicht missverständlich formuliert. IEEE 754 hat damit nichts zu tun.
Du hast sowohl im binär- als auch dezimal System periodische Zahlen.
1/10 ist in binär periodisch.
1/3 ist in dezimal periodisch.

Deine Hardware rechnet intern prinzipiell binär aber das Problem umgeht BigDecimal indem es eine Repräsentation des Wertes wählt die auf ganzen Zahlen basiert so dass das Rundungs problem erstmal nicht Auftritt. Denn in IEER 754 werden ggf auch nicht-periodische Zahlen gerundet. Aber auch so kannst du 1/3 nicht genau darstellen.
Die Frage ist dann: was tun? floats werden gerundet, weil das so Standard ist. BigDecimal will dir aber die Sicherheit geben dass alles was du rein tust Nicht gerundet wird. Deswegen wirft es lieber eine Fehlermeldung.

Wirklich umgehen kannst du das wie über mir schon gesagt wurde ausschließlich indem du symbolisch rechnest, also z.b. 1/3 wirklich als Bruch und nicht als Kommazahl speicherst. Die nicht triviale Rechnungen ist das höchst schwierig, aber es wurde viel Gehirn Schweiß da rein investiert, dass Wolframalpha z.b. alles lösen kann, was sich ein nicht-Mathematiker/Physiker ausdenken kann - und noch viel mehr.

Ein Augenmerk sei vielleicht noch darauf gelegt, dass so eine exception nie auftreten kann, wenn du 'normale' dezimale Koma Zahlen in BigDecimal rein wirfst. Denn die sind ja Prinzip bedingt bereits endlich darstellbar. Ein Problem kannst du kriegen wenn du binär dargestellte Komma Zahlen rein wirfst oder eben Brüche


Hendoul schrieb:
Was ich noch nicht ganz verstehe ist die Aussage "BigDecimal rechnet intern im Dezimalsystem". Aber schlussendlich läuft doch alles auf binär raus? Und das was macht BigDecimal mit 1/3 + 1/3 + 1/3 ?
Überleg dir einfach folgendes: Von 0 bis 1 gibt es wie viele Komma Zahlen? Antwort: Unendlich viele. Die Anzahl der Komma Zahlen, die du als String also z.b. "0.789" schreiben wirst/kannst ist jedoch endlich. Deswegen kannst du quasi beliebig große Zahlen speichern wenn du die als '0789*10^-3' speicherst, bzw. als tupel (0789,-3).
Warum machen wir das nicht immer und überall? Das ist uns meist zu langsam und meistens reichen 7 stellen nach dem Komma
 
Zuletzt bearbeitet:
Deine Hardware rechnet intern prinzipiell binär aber das Problem umgeht BigDecimal indem es eine Repräsentation des Wertes wählt die auf ganzen Zahlen basiert so dass das Rundungs problem erstmal nicht Auftritt.
Und welche Repräsentation wäre denn das? Hier blicke ich nicht durch.
 
Ich habe das schon in #3 veranschaulicht und BigNum hat in #6 noch einen Auszug der Doku gepostet.
 
Zurück
Oben