Zachowaj precyzję z podwójną precyzją w Javie


Odpowiedzi:


151

Jak wspominali inni, prawdopodobnie będziesz chciał użyć tej BigDecimalklasy, jeśli chcesz mieć dokładną reprezentację 11.4.

Teraz krótkie wyjaśnienie, dlaczego tak się dzieje:

Te floati doubleprymitywne typy w Javie są zmiennoprzecinkowych liczb, których liczba jest przechowywana jako binarnej reprezentacji ułamek i wykładnik.

Mówiąc dokładniej, wartość zmiennoprzecinkowa podwójnej precyzji, taka jak doubletyp, jest wartością 64-bitową, gdzie:

  • 1 bit oznacza znak (dodatni lub ujemny).
  • 11 bitów na wykładnik.
  • 52 bity dla cyfr znaczących (część ułamkowa jako binarna).

Te części są łączone w celu uzyskania doublereprezentacji wartości.

(Źródło: Wikipedia: Podwójna precyzja )

Szczegółowy opis sposobu obsługi wartości zmiennoprzecinkowych w języku Java znajduje się w Sekcji 4.2.3: Typy, formaty i wartości zmiennoprzecinkowe specyfikacji języka Java.

Na byte, char, int, longtypy stałoprzecinkowych numery, które są dokładne representions liczb. W przeciwieństwie do liczb stałoprzecinkowych, liczby zmiennoprzecinkowe czasami (można bezpiecznie założyć „przez większość czasu”) nie będą w stanie zwrócić dokładnej reprezentacji liczby. To jest powód, dla którego kończysz 11.399999999999jako wynik 5.6 + 5.8.

Wymagając dokładnej wartości, takiej jak 1.5 lub 150.1005, będziesz chciał użyć jednego z typów stałoprzecinkowych, które będą w stanie dokładnie reprezentować liczbę.

Jak już kilkakrotnie wspominano, Java ma BigDecimalklasę, która obsługuje bardzo duże liczby i bardzo małe liczby.

Z dokumentacji Java API dla BigDecimalklasy:

Niezmienne liczby dziesiętne ze znakiem o dowolnej precyzji. BigDecimal składa się z nieskalowanej wartości całkowitej o dowolnej dokładności i 32-bitowej skali całkowitej. Jeśli wartość wynosi zero lub jest dodatnia, skala to liczba cyfr po prawej stronie przecinka dziesiętnego. Jeśli jest ujemna, nieskalowana wartość liczby jest mnożona przez dziesięć do potęgi negacji skali. Wartość liczby reprezentowanej przez BigDecimal jest zatem (nieskalowanaValue × 10 ^ -scale).

Pojawiło się wiele pytań dotyczących przepełnienia stosu związanych z liczbami zmiennoprzecinkowymi i ich dokładnością. Oto lista powiązanych pytań, które mogą być interesujące:

Jeśli naprawdę chcesz przejść do drobiazgowych szczegółów liczb zmiennoprzecinkowych, zapoznaj się z artykułem Co każdy informatyk powinien wiedzieć o arytmetyce zmiennoprzecinkowej .


3
W rzeczywistości zazwyczaj są 53 bity znaczące, ponieważ 1 przed kropką „dziesiętną” jest implikowana dla wszystkich wartości poza zdenormalizowanymi, co daje dodatkowy bit precyzji. np. 3 jest przechowywane jako (1.) 1000 ... x 2 ^ 1, podczas gdy 0,5 jest przechowywane jako (1.) 0000 ... x 2 ^ -1 Gdy wartość jest zdenormalizowana (wszystkie bity wykładnika mają wartość zero), można, i zwykle będzie mniej cyfr znaczących, np. 1 x 2 ^ -1030 jest przechowywany jako (0) 00000001 x 2 ^ -1022, więc siedem cyfr znaczących zostało poświęconych na rzecz skali.
Sarah Phillips,

1
Należy zauważyć, że chociaż BigDecimaljest znacznie wolniejsze niż doublew tym przypadku, nie jest potrzebne, ponieważ liczba podwójna ma 15 miejsc po przecinku, wystarczy ją zaokrąglić.
Peter Lawrey

2
@PeterLawrey Ma 15 cyfr dziesiętnych dokładności, jeśli wszystkie są przed przecinkiem dziesiętnym. Wszystko może się zdarzyć po przecinku z powodu niewspółmierności ułamków dziesiętnych i binarnych.
Markiz Lorne

@EJP Masz rację, ma około 15 znaczących cyfr precyzji. Może to być 16, ale bezpieczniej jest założyć, że jest to 15, a może 14
Peter Lawrey

@PeterLawrey EJP poprawił wynik z mojego pytania: stackoverflow.com/questions/36344758/ ... czy mógłbyś wyjaśnić, dlaczego nie jest to dokładnie 15 i jakie sytuacje może to być 16 lub 14?
Shivam Sinha

103

Na przykład, gdy wprowadzasz podwójną liczbę, 33.33333333333333otrzymana wartość jest w rzeczywistości najbliższą reprezentowalną wartością podwójnej precyzji, która jest dokładnie:

33.3333333333333285963817615993320941925048828125

Dzieląc to przez 100, otrzymujemy:

0.333333333333333285963817615993320941925048828125

która również nie może być reprezentowana jako liczba podwójnej precyzji, więc ponownie jest zaokrąglana do najbliższej możliwej do przedstawienia wartości, czyli dokładnie:

0.3333333333333332593184650249895639717578887939453125

Kiedy drukujesz tę wartość, jest ona ponownie zaokrąglana do 17 cyfr dziesiętnych, dając:

0.33333333333333326

114
Do każdego, kto czyta to w przyszłości i jest zdziwiony, dlaczego odpowiedź nie ma nic wspólnego z pytaniem: jakiś moderator postanowił połączyć pytanie, na które odpowiedziałem (i inni) z tym, raczej innym, pytaniem.
Stephen Canon

Skąd znasz dokładną podwójną wartość?
Michael Yaworski

@mikeyaworski en.wikipedia.org/wiki/Double-precision_floating-point_format Zobacz przykłady podwójnej precyzji
Jaydee

23

Jeśli chcesz przetwarzać wartości jako ułamki, możesz utworzyć klasę Fraction, która zawiera pole licznika i mianownika.

Napisz metody dodawania, odejmowania, mnożenia i dzielenia, a także metodę toDouble. W ten sposób można uniknąć pływaków podczas obliczeń.

EDYCJA: Szybka realizacja,

public class Fraction {

private int numerator;
private int denominator;

public Fraction(int n, int d){
    numerator = n;
    denominator = d;
}

public double toDouble(){
    return ((double)numerator)/((double)denominator);
}


public static Fraction add(Fraction a, Fraction b){
    if(a.denominator != b.denominator){
        double aTop = b.denominator * a.numerator;
        double bTop = a.denominator * b.numerator;
        return new Fraction(aTop + bTop, a.denominator * b.denominator);
    }
    else{
        return new Fraction(a.numerator + b.numerator, a.denominator);
    }
}

public static Fraction divide(Fraction a, Fraction b){
    return new Fraction(a.numerator * b.denominator, a.denominator * b.numerator);
}

public static Fraction multiply(Fraction a, Fraction b){
    return new Fraction(a.numerator * b.numerator, a.denominator * b.denominator);
}

public static Fraction subtract(Fraction a, Fraction b){
    if(a.denominator != b.denominator){
        double aTop = b.denominator * a.numerator;
        double bTop = a.denominator * b.numerator;
        return new Fraction(aTop-bTop, a.denominator*b.denominator);
    }
    else{
        return new Fraction(a.numerator - b.numerator, a.denominator);
    }
}

}

1
Z pewnością numeratori denominatorpowinno być int? Dlaczego miałbyś chcieć precyzji zmiennoprzecinkowej?
Samir Talwar

Chyba nie jest to naprawdę konieczne, ale unika rzutowania w funkcji toDouble, dzięki czemu kod lepiej się czyta.
Viral Shah

5
ViralShah: Potencjalnie wprowadza również błąd zmiennoprzecinkowy podczas wykonywania operacji matematycznych. Biorąc pod uwagę, że celem tego ćwiczenia jest właśnie uniknięcie tego, wydaje się rozsądne, aby to zmienić.
Samir Talwar

Zredagowano tak, aby używały ints zamiast double, z powodów wymienionych powyżej przez Samira Talwara.
Viral Shah

3
Taka implementacja ułamków stwarza problemy, ponieważ nie ogranicza ich do najprostszej postaci. 2/3 * 1/2 podaj 2/6 tam, gdzie naprawdę chcesz, aby odpowiedź wynosiła 1/3. Idealnie w konstruktorze chcesz znaleźć gcd licznika i dzielnika i podzielić oba przez to.
Salix alba

15

Zauważ, że miałbyś ten sam problem, gdybyś użył arytmetyki dziesiętnej o ograniczonej precyzji i chciałbyś sobie poradzić z 1/3: 0,333333333 * 3 to 0,999999999, a nie 1,00000000.

Niestety, 5,6, 5,8 i 11,4 po prostu nie są liczbami okrągłymi w systemie binarnym, ponieważ obejmują one piąte. Więc ich reprezentacja zmiennoprzecinkowa nie jest dokładna, tak jak 0,3333 nie jest dokładnie 1/3.

Jeśli wszystkie używane liczby nie są liczbami dziesiętnymi, a chcesz uzyskać dokładne wyniki, użyj funkcji BigDecimal. Lub, jak powiedzieli inni, jeśli twoje wartości są jak pieniądze w tym sensie, że wszystkie są wielokrotnością 0,01 lub 0,001, lub coś podobnego, pomnóż wszystko przez stałą potęgę 10 i użyj int lub long (dodawanie i odejmowanie to trywialne: uważaj na mnożenie).

Jeśli jednak jesteś zadowolony z binarnego do obliczeń, ale chcesz po prostu wydrukować rzeczy w nieco bardziej przyjaznym formacie, wypróbuj java.util.Formatterlub String.format. W ciągu formatu określ precyzję mniejszą niż pełna precyzja double. Do 10 cyfr znaczących, powiedzmy, 11,399999999999 to 11,4, więc wynik będzie prawie tak samo dokładny i bardziej czytelny dla człowieka w przypadkach, gdy wynik binarny jest bardzo zbliżony do wartości wymagającej tylko kilku miejsc po przecinku.

Precyzja do określenia zależy trochę od tego, ile matematyki wykonałeś na swoich liczbach - ogólnie im więcej zrobisz, tym więcej błędów będzie się kumulować, ale niektóre algorytmy kumulują go znacznie szybciej niż inne (nazywane są „niestabilnymi”, w przeciwieństwie do „stabilnego” w odniesieniu do błędów zaokrąglania). Jeśli wszystko, co robisz, to dodawanie kilku wartości, to domyślam się, że upuszczenie tylko jednego miejsca po przecinku precyzji rozwiąże problem. Eksperyment.


3
Nie, nie używaj podwójnych z wartościami pieniężnymi! Potrzebujesz precyzji z pieniędzmi, zamiast tego użyj BigDecimal. W przeciwnym razie twoja odpowiedź jest dobra. Cokolwiek potrzebujesz precyzji, użyj BigDecimal, jeśli precyzja nie jest tak ważna, możesz użyć float lub double.
MetroidFan2002

1
Pytanie nie stwierdza już ani nie sugeruje, że w grę wchodzą pieniądze. Mówię konkretnie, aby używać BigDecimal lub liczb całkowitych dla pieniędzy. Jaki jest problem?
Steve Jessop

1
A równe „nie używaj podwójnego dla pieniędzy” to „nie używaj BigDecimal ani podwójnego dla trzecich”. Czasami jednak problemem jest podział, w którym to przypadku wszystkie zasady niepodzielne przez wszystkie czynniki pierwsze wszystkich mianowników są mniej więcej równie złe.
Steve Jessop

1
.9999 = 1, jeśli Twoja precyzja jest mniejsza niż 4 cyfry znaczące
Brian Leahy

9

Jeśli naprawdę potrzebujesz precyzyjnej matematyki, możesz zajrzeć do klasy java.math.BigDecimal w języku Java. Oto dobry artykuł firmy Oracle / Sun na temat przypadku BigDecimal . Chociaż nigdy nie można reprezentować 1/3 jak ktoś wspomniał, to może mieć uprawnienia do decydowania, jak dokładnie chcesz dokładny wynik będzie. setScale () jest Twoim przyjacielem .. :)

Ok, ponieważ w tej chwili mam zbyt dużo czasu, oto przykład kodu, który odnosi się do twojego pytania:

import java.math.BigDecimal;
/**
 * Created by a wonderful programmer known as:
 * Vincent Stoessel
 * xaymaca@gmail.com
 * on Mar 17, 2010 at  11:05:16 PM
 */
public class BigUp {

    public static void main(String[] args) {
        BigDecimal first, second, result ;
        first = new BigDecimal("33.33333333333333")  ;
        second = new BigDecimal("100") ;
        result = first.divide(second);
        System.out.println("result is " + result);
       //will print : result is 0.3333333333333333


    }
}

i aby podłączyć mój nowy ulubiony język, Groovy, oto ładniejszy przykład tego samego:

import java.math.BigDecimal

def  first =   new BigDecimal("33.33333333333333")
def second = new BigDecimal("100")


println "result is " + first/second   // will print: result is 0.33333333333333

5

Całkiem pewny, że mogłeś to przekształcić w przykład z trzema wierszami. :)

Jeśli potrzebujesz dokładnej precyzji, użyj BigDecimal. W przeciwnym razie możesz użyć liczb całkowitych pomnożonych przez 10 ^ z dowolną dokładnością.


5

Jak zauważyli inni, nie wszystkie wartości dziesiętne można przedstawić jako dwójkowe, ponieważ dziesiętne są oparte na potęgach 10, a dwójkowe na potęgach dwóch.

Jeśli liczy się precyzja, użyj BigDecimal, ale jeśli potrzebujesz tylko przyjaznego wyniku:

System.out.printf("%.2f\n", total);

Da tobie:

11.40


5

Nie możesz, ponieważ 7.3 nie ma skończonej reprezentacji w systemie binarnym. Najbliższy możliwy do uzyskania to 2054767329987789/2 ** 48 = 7,3 + 1/1407374883553280.

Spójrz na http://docs.python.org/tutorial/floatingpoint.html aby uzyskać dalsze wyjaśnienia. (Jest na stronie Pythona, ale Java i C ++ mają ten sam „problem”).

Rozwiązanie zależy od tego, na czym dokładnie polega Twój problem:

  • Jeśli po prostu nie lubisz widzieć tych wszystkich cyfr szumu, napraw formatowanie ciągu. Nie wyświetlaj więcej niż 15 cyfr znaczących (lub 7 dla liczb zmiennoprzecinkowych).
  • Jeśli chodzi o to, że niedokładność twoich liczb łamie takie rzeczy, jak instrukcje "if", powinieneś napisać if (abs (x - 7,3) <TOLERANCJA) zamiast if (x == 7,3).
  • Jeśli pracujesz z pieniędzmi, to prawdopodobnie naprawdę potrzebujesz stałego przecinka dziesiętnego. Przechowuj całkowitą liczbę centów lub jakąkolwiek najmniejszą jednostkę waluty.
  • (BARDZO NIEPRAWDOPODOBNE) Jeśli potrzebujesz więcej niż 53 bitów znaczących (15-16 cyfr znaczących) precyzji, użyj typu zmiennoprzecinkowego o wysokiej precyzji, takiego jak BigDecimal.

7.3 może nie mieć skończonej reprezentacji w systemie binarnym, ale na pewno otrzymam -7.3, gdy spróbuję tego samego w C ++
błędna nazwa użytkownika

2
błędna nazwa użytkownika: Nie, nie masz. Po prostu wyświetla się w ten sposób. Użyj formatu „% .17g” (lub jeszcze lepiej „% .51g”), aby zobaczyć prawdziwą odpowiedź.
dan04

4
private void getRound() {
    // this is very simple and interesting 
    double a = 5, b = 3, c;
    c = a / b;
    System.out.println(" round  val is " + c);

    //  round  val is  :  1.6666666666666667
    // if you want to only two precision point with double we 
            //  can use formate option in String 
           // which takes 2 parameters one is formte specifier which 
           // shows dicimal places another double value 
    String s = String.format("%.2f", c);
    double val = Double.parseDouble(s);
    System.out.println(" val is :" + val);
    // now out put will be : val is :1.67
}

3

Użyj java.math.BigDecimal

Podwójne są wewnętrznie ułamkami binarnymi, więc czasami nie mogą reprezentować ułamków dziesiętnych do dokładnego dziesiętnego.


1
-1 do ślepego polecania BigDecimal. Jeśli w rzeczywistości nie potrzebujesz arytmetyki dziesiętnej (np. Jeśli wykonujesz obliczenia z pieniędzmi), to BigDecimal ci nie pomoże. Nie rozwiązuje wszystkich błędów zmiennoprzecinkowych: nadal musisz radzić sobie z 1/3 * 3 = 0.999999999999999999999999999999 i sqrt (2) ** 2 = 1,999999999999999999999999999. Ponadto BigDecimal niesie ze sobą ogromną karę za prędkość. Co gorsza, z powodu braku przeciążenia operatorów w Javie, musisz przepisać cały kod.
dan04

2
@ dan04 - Jeśli wykonujesz obliczenia na pieniądzach, po co używać reprezentacji zmiennoprzecinkowej, znając ich nieodłączny błąd ... Ponieważ nie ma ułamka procentowego, możesz użyć dziesiętnego i obliczyć centy zamiast przybliżonego dolara, masz dokładną kwotę w centach. Jeśli naprawdę chcesz ułamek centa, użyj aa long i oblicz tysiące centów. Ponadto PO nie wspomniał o liczbach nieracjonalnych, martwił się tylko o dodawanie. Przeczytaj uważnie post i zrozum problem, zanim odpowiesz, może zaoszczędzić trochę zakłopotania.
Newtopian

3
@Newtopian: Nie mam się czego wstydzić. OP NIE wspomniał o pieniądzach ani nie wskazał, że jego problem ma wrodzoną liczbę dziesiętną.
dan04

@ dan04 - Nie, OP nie ... ZROBIŁEŚ I na ślepo zaoferowałeś poza kontekstem opinię, która najprawdopodobniej była całkowicie akceptowalną odpowiedzią, biorąc pod uwagę niewielką ilość podanych szczegółów
Newtopian

2

Pomnóż wszystko przez 100 i zapisz w długości centów.


2
@Draemon - spójrz na post przed ostatnią edycją - wszystkie te "shoppingTotal", "calcGST" i "calcPST" wyglądają dla mnie jak pieniądze.
Paul Tomblin

2

Komputery przechowują liczby w postaci binarnej i nie mogą w rzeczywistości reprezentować dokładnie takich liczb, jak 33,333333333 lub 100,0. To jedna z najtrudniejszych rzeczy w używaniu podwójnych. Będziesz musiał po prostu zaokrąglić odpowiedź, zanim pokażesz ją użytkownikowi. Na szczęście w większości aplikacji i tak nie potrzebujesz tylu miejsc po przecinku.


Wykonuję obliczenia kursów, które wolę mieć jak największą precyzję. Ale rozumiem, że są ograniczenia
Aly

2

Liczby zmiennoprzecinkowe różnią się od liczb rzeczywistych tym, że dla dowolnej liczby zmiennoprzecinkowej istnieje następna wyższa liczba zmiennoprzecinkowa. To samo co liczby całkowite. Nie ma liczby całkowitej od 1 do 2.

Nie ma możliwości przedstawienia 1/3 jako liczby zmiennoprzecinkowej. Pod nim jest pływak, a nad nim pływak, a między nimi jest pewna odległość. A 1/3 jest w tej przestrzeni.

Apfloat for Java twierdzi, że działa z liczbami zmiennoprzecinkowymi o dowolnej precyzji, ale nigdy go nie używałem. Chyba warto zajrzeć. http://www.apfloat.org/apfloat_java/

Podobne pytanie zadano tutaj przed biblioteką zmiennoprzecinkową Java o wysokiej precyzji


1

Podwojenia to przybliżenia liczb dziesiętnych w źródle Java. Widzisz konsekwencje niedopasowania podwójnej (która jest wartością zakodowaną binarnie) i źródła (które jest zakodowane dziesiętnie).

Java tworzy najbliższe przybliżenie binarne. Możesz użyć java.text.DecimalFormat, aby wyświetlić lepiej wyglądającą wartość dziesiętną.


1

Użyj BigDecimal. Pozwala nawet określić reguły zaokrąglania (takie jak ROUND_HALF_EVEN, które zminimalizują błąd statystyczny poprzez zaokrąglenie do równego sąsiada, jeśli oba są tej samej odległości, tj. Zarówno 1,5, jak i 2,5 zaokrąglenia do 2).


1

Krótka odpowiedź: zawsze używaj BigDecimal i upewnij się, że używasz konstruktora z String argumentem , a nie podwójnym.

Wracając do przykładu, poniższy kod wydrukuje 11.4, jak chcesz.

public class doublePrecision {
    public static void main(String[] args) {
      BigDecimal total = new BigDecimal("0");
      total = total.add(new BigDecimal("5.6"));
      total = total.add(new BigDecimal("5.8"));
      System.out.println(total);
    }
}

0

Sprawdź BigDecimal, rozwiązuje problemy z arytmetyką zmiennoprzecinkową w ten sposób.

Nowe wezwanie wyglądałoby tak:

term[number].coefficient.add(co);

Użyj setScale (), aby ustawić liczbę dokładności miejsca dziesiętnego, która ma być używana.


0

Dlaczego nie użyć metody round () z klasy Math?

// The number of 0s determines how many digits you want after the floating point
// (here one digit)
total = (double)Math.round(total * 10) / 10;
System.out.println(total); // prints 11.4

0

Jeśli nie masz innego wyboru niż używanie podwójnych wartości, możesz użyć poniższego kodu.

public static double sumDouble(double value1, double value2) {
    double sum = 0.0;
    String value1Str = Double.toString(value1);
    int decimalIndex = value1Str.indexOf(".");
    int value1Precision = 0;
    if (decimalIndex != -1) {
        value1Precision = (value1Str.length() - 1) - decimalIndex;
    }

    String value2Str = Double.toString(value2);
    decimalIndex = value2Str.indexOf(".");
    int value2Precision = 0;
    if (decimalIndex != -1) {
        value2Precision = (value2Str.length() - 1) - decimalIndex;
    }

    int maxPrecision = value1Precision > value2Precision ? value1Precision : value2Precision;
    sum = value1 + value2;
    String s = String.format("%." + maxPrecision + "f", sum);
    sum = Double.parseDouble(s);
    return sum;
}

-1

Nie marnuj wysiłku przy użyciu BigDecimal. W 99,99999% przypadków nie jest to potrzebne. java double type jest oczywiście przybliżony, ale w prawie wszystkich przypadkach jest wystarczająco dokładny. Pamiętaj, że masz błąd na czternastej cyfrze znaczącej.To jest naprawdę nieistotne!

Aby uzyskać ładny wynik, użyj:

System.out.printf("%.2f\n", total);

2
Myślę, że martwi go wynik, a nie dokładność numeryczna. i BigDecimal nie pomogłyby, gdybyś np. podziel przez trzy. To może nawet pogorszyć sprawę ...
Maciek D.

Nigdy nie powinieneś nigdy używać zmiennoprzecinkowych dla pieniędzy. Widziałem poważne poprawki na wykonawcy, który złamał tę zasadę, mimo że otrzymał takie polecenie.
Markiz Lorne
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.