Mantieni la precisione con il doppio in Java


Risposte:


151

Come altri hanno già detto, probabilmente vorrai usare la BigDecimalclasse, se vuoi avere una rappresentazione esatta di 11.4.

Ora, una piccola spiegazione del perché questo sta accadendo:

I floate doubleprimitivi tipi in Java sono virgola mobile numeri, in cui il numero è memorizzato come una rappresentazione binaria di una frazione e un esponente.

Più specificamente, un valore in virgola mobile a precisione doppia come il doubletipo è un valore a 64 bit, dove:

  • 1 bit indica il segno (positivo o negativo).
  • 11 bit per l'esponente.
  • 52 bit per le cifre significative (la parte frazionaria come binaria).

Queste parti sono combinate per produrre una doublerappresentazione di un valore.

(Fonte: Wikipedia: doppia precisione )

Per una descrizione dettagliata di come vengono gestiti i valori in virgola mobile in Java, consultare la Sezione 4.2.3: Tipi, formati e valori in virgola mobile della specifica del linguaggio Java.

I byte, char, int, longtipi sono punti fissi numeri, che sono representions esatte di numeri. A differenza dei numeri in virgola fissa, alcune volte i numeri in virgola mobile (si può presumere "il più delle volte") non saranno in grado di restituire una rappresentazione esatta di un numero. Questo è il motivo per cui si finisce con 11.399999999999il risultato di 5.6 + 5.8.

Quando si richiede un valore esatto, ad esempio 1.5 o 150.1005, è necessario utilizzare uno dei tipi a virgola fissa, che sarà in grado di rappresentare esattamente il numero.

Come già accennato più volte, Java ha una BigDecimalclasse che gestirà numeri molto grandi e numeri molto piccoli.

Dal riferimento API Java per la BigDecimalclasse:

Numeri decimali immutabili, con precisione arbitraria. Un BigDecimal è costituito da un valore intero non scalato di precisione arbitraria e da una scala di numeri interi a 32 bit. Se zero o positivo, la scala è il numero di cifre a destra del punto decimale. Se negativo, il valore non scalato del numero viene moltiplicato per dieci per la potenza della negazione della scala. Il valore del numero rappresentato da BigDecimal è quindi (unscaledValue × 10 ^ -scale).

Ci sono state molte domande su Stack Overflow relative alla questione dei numeri in virgola mobile e alla sua precisione. Ecco un elenco di domande correlate che potrebbero essere di interesse:

Se vuoi davvero scendere ai dettagli grintosi dei numeri in virgola mobile, dai un'occhiata a Ciò che ogni scienziato informatico dovrebbe sapere sull'aritmetica in virgola mobile .


3
Infatti, di solito ci sono 53 bit significativi perché 1 prima del punto "decimale" è implicito per tutti i valori tranne quelli denormalizzati, dando un ulteriore bit di precisione. ad es. 3 viene memorizzato come (1.) 1000 ... x 2 ^ 1 mentre 0,5 viene memorizzato come (1.) 0000 ... x 2 ^ -1 Quando il valore viene denormalizzato (tutti i bit esponenti sono zero), è possibile, e di solito lo saranno, meno cifre significative, ad es. 1 x 2 ^ -1030 viene memorizzata come (0.) 00000001 x 2 ^ -1022, quindi sette cifre significative sono state sacrificate per ridimensionare.
Sarah Phillips,

1
Va notato che mentre BigDecimalè molto più lento che doublein questo caso non è necessario poiché il doppio ha 15 posizioni decimali di precisione, è sufficiente arrotondare.
Peter Lawrey,

2
@PeterLawrey Ha 15 cifre decimali di precisione, se sono tutte prima del punto decimale. Tutto può succedere dopo il punto decimale, a causa dell'incommensurabilità delle frazioni decimali e binarie.
Marchese di Lorne,

@EJP Hai ragione, ha circa 15 cifre significative di precisione. Può essere 16, ma è più sicuro supporre che sia 15 o forse 14.
Peter Lawrey

@PeterLawrey La correzione di EJP era dovuta alla mia domanda: stackoverflow.com/questions/36344758/… potresti ampliare il motivo per cui non è esattamente 15 e quali situazioni può essere 16 o 14?
Shivam Sinha,

103

Quando inserisci un doppio numero, ad esempio, 33.33333333333333il valore che ottieni è in realtà il valore di doppia precisione rappresentabile più vicino, che è esattamente:

33.3333333333333285963817615993320941925048828125

Dividendo che per 100 si ottiene:

0.333333333333333285963817615993320941925048828125

che inoltre non è rappresentabile come un numero a doppia precisione, quindi viene nuovamente arrotondato al valore rappresentabile più vicino, che è esattamente:

0.3333333333333332593184650249895639717578887939453125

Quando si stampa questo valore, viene nuovamente arrotondato a 17 cifre decimali, dando:

0.33333333333333326

114
A chiunque stia leggendo questo in futuro ed è perplesso sul perché la risposta non ha nulla a che fare con la domanda: un moderatore ha deciso di unire la domanda a cui io (e altri) abbiamo risposto con questa, piuttosto diversa, domanda.
Stephen Canon,

Come conosci il doppio valore esatto?
Michael Yaworski,

@mikeyaworski en.wikipedia.org/wiki/Double-precision_floating-point_format Vedi esempi di doppia precisione
Jaydee,

23

Se vuoi solo elaborare valori come frazioni, puoi creare una classe di frazioni che contiene un campo numeratore e denominatore.

Scrivi metodi per aggiungere, sottrarre, moltiplicare e dividere, nonché un metodo toDouble. In questo modo è possibile evitare i galleggianti durante i calcoli.

EDIT: implementazione rapida,

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
Sicuramente numeratore denominatordovrebbe essere ints? Perché vorresti la precisione in virgola mobile?
Samir Talwar,

Immagino che non sia davvero necessario, ma evita il casting nella funzione toDouble in modo che il codice sia migliore.
Viral Shah,

5
ViralShah: potenzialmente introduce anche errori in virgola mobile quando si ha a che fare con operazioni matematiche. Dato che il punto di questo esercizio è quello di evitarlo esattamente, sembra prudente modificarlo.
Samir Talwar,

Modificato per usare ints anziché doppi, per i motivi menzionati sopra da Samir Talwar.
Viral Shah,

3
Questa implementazione delle frazioni ha problemi in quanto non li riduce ad una forma più semplice. 2/3 * 1/2 dai 2/6 dove vuoi davvero che la risposta sia 1/3. Idealmente nel costruttore vuoi trovare il gcd di numeratore e divisore e dividere entrambi per quello.
Salix alba

15

Tieni presente che avresti lo stesso problema se utilizzassi l'aritmetica decimale a precisione limitata e desideri occuparti di 1/3: 0.333333333 * 3 è 0.999999999, non 1.00000000.

Sfortunatamente, 5.6, 5.8 e 11.4 non sono numeri rotondi in binario, perché coinvolgono le quinte. Quindi la loro rappresentazione float non è esatta, proprio come 0.3333 non è esattamente 1/3.

Se tutti i numeri che usi sono decimali non ricorrenti e desideri risultati esatti, usa BigDecimal. O come altri hanno già detto, se i tuoi valori sono come denaro nel senso che sono tutti un multiplo di 0,01 o 0,001 o qualcosa del genere, allora moltiplica tutto per una potenza fissa di 10 e usa int o long (addizione e sottrazione sono banale: attenzione alla moltiplicazione).

Tuttavia, se sei soddisfatto del binario per il calcolo, ma vuoi solo stampare le cose in un formato leggermente più amichevole, prova java.util.Formattero String.format. Nella stringa del formato specificare una precisione inferiore alla precisione completa di un doppio. A 10 cifre significative, diciamo, 11.399999999999 è 11.4, quindi il risultato sarà quasi altrettanto accurato e più leggibile dall'uomo nei casi in cui il risultato binario è molto vicino a un valore che richiede solo poche cifre decimali.

La precisione da specificare dipende un po 'da quanti calcoli hai fatto con i tuoi numeri: in generale più fai, più si accumulerà l'errore, ma alcuni algoritmi lo accumulano molto più velocemente di altri (sono chiamati "instabili" come al contrario di "stabile" rispetto agli errori di arrotondamento). Se tutto ciò che stai facendo è aggiungere alcuni valori, indovinerei che l'eliminazione di un solo decimale di precisione risolverà le cose. Sperimentare.


3
No, non usare il doppio con i valori monetari! Hai bisogno di precisione con i soldi, usa invece BigDecimal. Altrimenti, la tua risposta è buona. Qualunque cosa tu abbia bisogno di precisione, usa BigDecimal, se la precisione non è così importante, puoi usare float o double.
MetroidFan2002,

1
La domanda non afferma più o implica che sia coinvolto il denaro. In particolare dico di usare BigDecimal o numeri interi per soldi. Qual è il problema?
Steve Jessop,

1
E uguale a "non usare il doppio per soldi" è "non usare BigDecimal o doppio per i terzi". Ma a volte un problema comporta la divisione, nel qual caso tutte le basi non divisibili per tutti i fattori primi di tutti i denominatori sono ugualmente cattive.
Steve Jessop,

1
.9999 = 1 se la tua precisione è inferiore a 4 cifre significative
Brian Leahy

9

Potresti voler esaminare l'uso della classe java.math.BigDecimal di java se hai davvero bisogno di matematica di precisione. Ecco un buon articolo di Oracle / Sun sul caso di BigDecimal . Mentre non puoi mai rappresentare 1/3 come qualcuno ha menzionato, puoi avere il potere di decidere esattamente quanto vuoi che sia il risultato. setScale () è tuo amico .. :)

Ok, perché al momento ho troppo tempo a disposizione, ecco un esempio di codice relativo alla tua domanda:

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


    }
}

e per collegare la mia nuova lingua preferita, Groovy, ecco un esempio più preciso della stessa cosa:

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

Abbastanza sicuro che avresti potuto trasformarlo in un esempio di tre righe. :)

Se vuoi la precisione esatta, usa BigDecimal. Altrimenti, puoi usare ints moltiplicati per 10 ^ qualunque precisione tu voglia.


5

Come altri hanno notato, non tutti i valori decimali possono essere rappresentati come binari poiché i decimali si basano su potenze di 10 e i binari si basano su potenze di due.

Se la precisione conta, usa BigDecimal, ma se vuoi solo un output amichevole:

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

Ti darà:

11.40


5

Non puoi, perché 7.3 non ha una rappresentazione finita in binario. Il più vicino che puoi ottenere è 2054767329987789/2 ** 48 = 7.3 + 1/1407374883553280.

Dai un'occhiata a http://docs.python.org/tutorial/floatingpoint.html per un'ulteriore spiegazione. (È sul sito Web di Python, ma Java e C ++ hanno lo stesso "problema".)

La soluzione dipende da quale sia esattamente il tuo problema:

  • Se è che non ti piace vedere tutte quelle cifre del rumore, correggi la formattazione della stringa. Non visualizzare più di 15 cifre significative (o 7 per float).
  • Se è che l'inesattezza dei tuoi numeri sta rompendo cose come le dichiarazioni "if", allora dovresti scrivere if (abs (x - 7.3) <TOLLERANZA) invece di if (x == 7.3).
  • Se lavori con i soldi, probabilmente quello che vuoi davvero è un punto fisso decimale. Memorizza un numero intero di centesimi o qualunque sia l'unità più piccola della tua valuta.
  • (MOLTO NON PIACEVOLE) Se hai bisogno di più di 53 bit significativi (15-16 cifre significative) di precisione, usa un tipo a virgola mobile ad alta precisione, come BigDecimal.

7.3 potrebbe non avere una rappresentazione finita in binario, ma di sicuro ottengo -7.3 quando provo la stessa cosa in C ++
wrongusername

2
wrongusername: No, non lo fai. Viene visualizzato solo in questo modo. Usa il formato "% .17g" (o meglio, "% .51g") per vedere la risposta reale.
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

Usa java.math.BigDecimal

I doppi sono frazioni binarie internamente, quindi a volte non possono rappresentare le frazioni decimali con il decimale esatto.


1
-1 per consigliare ciecamente BigDecimal. Se in realtà non hai bisogno dell'aritmetica decimale (cioè, se stai facendo calcoli con denaro), allora BigDecimal non ti aiuta. Non risolve tutti i tuoi errori in virgola mobile: devi comunque gestire 1/3 * 3 = 0.99999999999999999999999999999999 e sqrt (2) ** 2 = 1.99999999999999999999999999999. Inoltre, BigDecimal comporta un'enorme penalità di velocità. Peggio ancora, a causa della mancanza di sovraccarico da parte dell'operatore Java, è necessario riscrivere tutto il codice.
dan04

2
@ dan04 - Se si esegue il calcolo con denaro, perché utilizzare la rappresentazione fluttuante conoscendo l'errore intrinseco in esso .... Dato che non ci sono frazioni di centesimi, è possibile utilizzare i decimali e calcolare i centesimi invece di usare il dollaro approssimativo, si ha un importo esatto di centesimi. Se vuoi davvero la frazione di centesimo usa un long e calcola migliaia di centesimi. Inoltre, l'OP non ha fatto menzione di numeri irrazionali, tutto ciò di cui era preoccupato era l'aggiunta. Leggi attentamente il post e comprendi il problema prima di rispondere, potresti risparmiarti un po 'di imbarazzo.
Newtopiano,

3
@Newtopian: non ho nulla di cui essere imbarazzato. Il PO fatto nessuna menzione di denaro, né alcuna indicazione che il problema ha alcun decimalness intrinseca.
dan04,

@ dan04 - No, l'OP non ... L'hai fatto e ciecamente offerto dall'opinione contestuale a quella che molto probabilmente era una risposta perfettamente accettabile data la scarsa quantità di dettagli forniti
Newtopian

2

Moltiplica tutto per 100 e conservalo in centesimi.


2
@Draemon - guarda il post prima dell'ultima modifica - tutta quella roba "shoppingTotal" e "calcGST" e "calcPST" mi sembrano soldi.
Paul Tomblin,

2

I computer memorizzano i numeri in binario e in realtà non possono rappresentare esattamente numeri come 33.333333333 o 100.0. Questa è una delle cose difficili dell'uso dei doppi. Dovrai semplicemente arrotondare la risposta prima di mostrarla a un utente. Fortunatamente nella maggior parte delle applicazioni, in ogni caso non sono necessari molti decimali.


Sto facendo alcuni calcoli delle probabilità che preferirei avere la massima precisione possibile. Ma capisco che ci sono dei limiti
Aly,

2

I numeri in virgola mobile differiscono dai numeri reali in quanto per ogni dato numero in virgola mobile c'è un successivo numero in virgola mobile più alto. Come gli interi. Non c'è un numero intero compreso tra 1 e 2.

Non c'è modo di rappresentare 1/3 come float. C'è un galleggiante sotto di esso e c'è un galleggiante sopra di esso, e c'è una certa distanza tra di loro. E 1/3 è in quello spazio.

Apfloat per Java afferma di funzionare con numeri a virgola mobile di precisione arbitraria, ma non l'ho mai usato. Probabilmente vale la pena dare un'occhiata. http://www.apfloat.org/apfloat_java/

Una domanda simile è stata posta qui prima della libreria Java ad alta precisione in virgola mobile


1

I doppi sono approssimazioni dei numeri decimali nella tua sorgente Java. Stai vedendo la conseguenza della mancata corrispondenza tra il doppio (che è un valore con codice binario) e la tua fonte (che è con codice decimale).

Java sta producendo l'approssimazione binaria più vicina. È possibile utilizzare java.text.DecimalFormat per visualizzare un valore decimale più bello.


1

Usa un BigDecimal. Ti consente persino di specificare le regole di arrotondamento (come ROUND_HALF_EVEN, che minimizzerà l'errore statistico arrotondando al vicino pari se entrambi hanno la stessa distanza; cioè sia 1,5 che 2,5 arrotondano a 2).


1

Risposta breve: usa sempre BigDecimal e assicurati di utilizzare il costruttore con argomento String , non quello doppio.

Tornando al tuo esempio, il seguente codice stamperà 11.4, come desideri.

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

Dai un'occhiata a BigDecimal, gestisce i problemi relativi all'aritmetica in virgola mobile del genere.

La nuova chiamata sarebbe simile a questa:

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

Utilizzare setScale () per impostare il numero di precisione del decimale da utilizzare.


0

Perché non usare il metodo round () della classe 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

Se non hai altra scelta che utilizzare i doppi valori, puoi utilizzare il codice seguente.

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

Non sprecare il tuo efford usando BigDecimal. Nel 99,99999% dei casi non è necessario. java double type è di tipo approssimativo, ma in quasi tutti i casi è sufficientemente preciso. Tieni presente che hai un errore alla 14a cifra significativa. Questo è davvero trascurabile!

Per ottenere un buon output usare:

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

2
Penso che sia preoccupato dall'output, non dalla precisione numerica. e BigDecimal non sarebbe d'aiuto se es. dividere per tre. Può anche peggiorare le cose ...
Maciek D.

Non dovresti mai usare mai il virgola mobile per soldi. Ho visto importanti rilavorazioni forzate su un appaltatore che ha infranto questa regola nonostante fosse stato istruito.
Marchese di Lorne,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.