Java: Perché dovremmo usare BigDecimal invece di Double nel mondo reale?

Quando si tratta di valori monetari del mondo reale, mi viene consigliato di utilizzare BigDecimal anziché Double. Ma non ho una spiegazione convincente, ad eccezione di “Normalmente è fatto in questo modo”.

Puoi per favore chiarire questa domanda?

Solutions Collecting From Web of "Java: Perché dovremmo usare BigDecimal invece di Double nel mondo reale?"

Si chiama perdita di precisione ed è molto evidente quando si lavora con numeri molto grandi o numeri molto piccoli. La rappresentazione binaria dei numeri decimali con una radice è in molti casi un’approssimazione e non un valore assoluto. Per capire perché è necessario leggere la rappresentazione di numeri mobili in binario. Ecco un link: http://en.wikipedia.org/wiki/IEEE_754-2008 . Ecco una rapida dimostrazione:
in bc (un linguaggio di calcolatrice a precisione arbitraria) con precisione = 10:

(1/3 + 1/12 + 1/8 + 1/30) = 0.6083333332
(1/3 + 1/12 + 1/8) = 0,541666666666666
(1/3 + 1/12) = 0,416666666666666

Java double:
,6083333333333333
,5416666666666666
,41666666666666663

Float Java:

0.60833335
0.5416667
0.4166667

Se sei una banca e sei responsabile di migliaia di transazioni ogni giorno, anche se non sono da e verso uno stesso account (o forse lo sono) devi avere numeri affidabili. I galleggianti binari non sono affidabili, a meno che non capiate come funzionano e i loro limiti.

Penso che questo descriva la soluzione al tuo problema: Java Trap: Big Decimal e il problema con il doppio qui

Dal blog originale che sembra essere giù adesso.

Trappole Java: doppio

Molte trappole giacciono davanti al programmatore dell’apprendista mentre percorre la strada dello sviluppo del software. Questo articolo illustra, attraverso una serie di esempi pratici, le principali trappole dell’uso dei tipi semplici di Java double e float. Si noti, tuttavia, che per abbracciare completamente la precisione nei calcoli numerici è necessario un libro di testo (o due) sull’argomento. Di conseguenza, possiamo solo scalfire la superficie dell’argomento. Detto questo, la conoscenza trasmessa qui, dovrebbe darti le conoscenze fondamentali necessarie per individuare o identificare i bug nel tuo codice. È una conoscenza che penso debba essere a conoscenza di qualsiasi sviluppatore di software professionale.

  1. I numeri decimali sono approssimazioni

    Mentre tutti i numeri naturali compresi tra 0 e 255 possono essere descritti con precisione usando 8 bit, la descrizione di tutti i numeri reali tra 0,0 – 255,0 richiede un numero infinito di bit. In primo luogo, esistono infiniti numeri da descrivere in quell’intervallo (anche nel range 0.0 – 0.1), e in secondo luogo, certi numeri irrazionali non possono essere descritti per niente numericamente. Ad esempio e e π. In altre parole, i numeri 2 e 0.2 sono rappresentati in modo molto diverso nel computer.

    Gli interi sono rappresentati da bit che rappresentano i valori 2n dove n è la posizione del bit. Quindi il valore 6 è rappresentato come 23 * 0 + 22 * 1 + 21 * 1 + 20 * 0 corrispondente alla sequenza di bit 0110. I decimali, d’altra parte, sono descritti da bit che rappresentano 2-n, cioè le frazioni 1/2, 1/4, 1/8,... Il numero 0,75 corrisponde a 2-1 * 1 + 2-2 * 1 + 2-3 * 0 + 2-4 * 0 ottenendo la sequenza di bit 1100 (1/2 + 1/4) .

    Dotato di questa conoscenza, possiamo formulare la seguente regola empirica: qualsiasi numero decimale è rappresentato da un valore approssimato.

    Cerchiamo di indagare le conseguenze pratiche di ciò eseguendo una serie di moltiplicazioni banali.

     System.out.println( 0.2 + 0.2 + 0.2 + 0.2 + 0.2 ); 1.0 

    1.0 è stampato. Anche se questo è davvero corretto, potrebbe darci un falso senso di sicurezza. Per coincidenza, 0.2 è uno dei pochi valori che Java è in grado di rappresentare correttamente. Sfidiamo ancora Java con un altro banale problema aritmetico, aggiungendo il numero 0.1 dieci volte.

     System.out.println( 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f ); System.out.println( 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d ); 1.0000001 0.9999999999999999 

    Secondo le diapositive del blog di Joseph D. Darcy, le somme dei due calcoli sono rispettivamente 0.100000001490116119384765625 e 0.1000000000000000055511151231... Questi risultati sono corretti per un numero limitato di cifre. float ha una precisione di 8 cifre iniziali, mentre il doppio ha una precisione di 17 cifre iniziali. Ora, se il disallineamento concettuale tra il risultato previsto 1.0 e i risultati stampati sugli schermi non fossero sufficienti per far funzionare il campanello d’allarme, allora notate come i numeri da mr. Le diapositive di Darcy non sembrano corrispondere ai numeri stampati! Questa è un’altra trappola. Altro su questo più in basso.

    Essendo stati messi a conoscenza di calcoli errati in apparentemente semplici scenari possibili, è ragionevole riflettere sulla rapidità con cui l’impressione può intervenire. Semplifichiamo il problema aggiungendo solo tre numeri.

     System.out.println( 0.3 == 0.1d + 0.1d + 0.1d ); false 

    Incredibilmente, l’imprecisione inizia già a tre aggiunte!

  2. Doppio overflow

    Come con qualsiasi altro tipo semplice in Java, un doppio è rappresentato da un insieme finito di bit. Di conseguenza, l’aggiunta di un valore o la moltiplicazione di una doppia può portare a risultati sorprendenti. Ammetto che i numeri devono essere piuttosto grandi per poter essere traboccati, ma succede. Proviamo a moltiplicare e poi a dividere un grande numero. L’intuizione matematica dice che il risultato è il numero originale. In Java potremmo ottenere un risultato diverso.

     double big = 1.0e307 * 2000 / 2000; System.out.println( big == 1.0e307 ); false 

    Il problema qui è che il grande viene dapprima moltiplicato, traboccante, e quindi il numero overflow è diviso. Peggio ancora, nessuna eccezione o altri tipi di avvertimenti vengono segnalati al programmatore. Fondamentalmente, questo rende l’espressione x * completamente inaffidabile in quanto non viene fornita alcuna indicazione o garanzia nel caso generale per tutti i doppi valori rappresentati da x, y.

  3. Grandi e piccoli non sono amici!

    Laurel e Hardy erano spesso in disaccordo su un sacco di cose. Allo stesso modo nell’informatica, grandi e piccoli non sono amici. Una conseguenza dell’uso di un numero fisso di bit per rappresentare i numeri è che il funzionamento su numeri veramente grandi e veramente piccoli negli stessi calcoli non funzionerà come previsto. Proviamo ad aggiungere qualcosa di piccolo a qualcosa di grande.

     System.out.println( 1234.0d + 1.0e-13d == 1234.0d ); true 

    L’aggiunta non ha alcun effetto! Questo contraddice qualsiasi intuizione matematica (addizionale) di addizione, che dice che dati due numeri positivi d ed f, quindi d + f> d.

  4. I numeri decimali non possono essere confrontati direttamente

    Ciò che abbiamo imparato fino ad ora, è che dobbiamo gettare via tutta l’intuizione che abbiamo acquisito in class matematica e programmare con interi. Utilizzare i numeri decimali con caucanvas. Ad esempio, l’istruzione for(double d = 0.1; d != 0.3; d += 0.1) è in effetti un loop mascherato che non finisce mai! L’errore è confrontare i numeri decimali direttamente tra loro. È necessario rispettare le seguenti linee guida.

    Evita test di uguaglianza tra due numeri decimali. Astenersi da if(a == b) {..} , utilizzare if(Math.abs(ab) < tolerance) {..} dove la tolleranza potrebbe essere una costante definita ad es. Doppia tolleranza finale statica pubblica = 0,01 Considerare come alternativa utilizzare gli operatori < ,> in quanto possono descrivere più chiaramente ciò che si desidera esprimere. Ad esempio, preferisco il modulo for(double d = 0; d < = 10.0; d+= 0.1) sul più maldestro for(double d = 0; Math.abs(10.0-d) < tolerance; d+= 0.1) Entrambi le forms hanno i loro meriti a seconda della situazione però: quando collaudo unità, preferisco esprimere quel valore assertEquals(2.5, d, tolerance) rispetto a dire assertTrue(d > 2.5) non solo la prima forma legge meglio, spesso è il controllo che voglio fare (cioè che d non è troppo grande).

  5. WYSINWYG - Quello che vedi non è ciò che ottieni

    WYSIWYG è un'espressione generalmente utilizzata nelle applicazioni di interfaccia utente grafica. Significa "Ciò che vedi è ciò che ottieni" e viene utilizzato nel calcolo per descrivere un sistema in cui il contenuto visualizzato durante la modifica appare molto simile all'output finale, che potrebbe essere un documento stampato, una pagina Web, ecc. frase era in origine una frase di cattura popolare originata dal personaggio drag di Flip Wilson "Geraldine", che spesso diceva "Quello che vedi è ciò che ottieni" per scusare il suo comportamento bizzarro (da wikipedia).

    Spesso si insidiano anche altri programmatori di trappole, pensa che i numeri decimali siano WYSIWYG. È fondamentale rendersi conto che quando si stampa o si scrive un numero decimale, non è il valore approssimato che viene stampato / scritto. In parole diverse, Java sta facendo molte approssimazioni dietro le quinte e cerca costantemente di proteggerti dal fatto di non averlo mai saputo. C'è solo un problema. Devi conoscere queste approssimazioni, altrimenti potresti dover affrontare ogni sorta di bug misterioso nel tuo codice.

    Con un po 'di ingenuità, tuttavia, possiamo indagare su cosa succede dietro le quinte. Ormai sappiamo che il numero 0.1 è rappresentato con una certa approssimazione.

     System.out.println( 0.1d ); 0.1 

    Sappiamo che 0.1 non è 0.1, tuttavia 0.1 è stampato sullo schermo. Conclusione: Java è WYSINWYG!

    Per motivi di varietà, prendiamo un altro numero dall'aspetto innocente, per esempio 2.3. Come 0.1, 2.3 è un valore approssimato. Non sorprende che quando si stampa il numero Java si nasconda l'approssimazione.

     System.out.println( 2.3d ); 2.3 

    Per indagare su quale potrebbe essere il valore approssimato interno di 2.3, possiamo confrontare il numero con altri numeri in un intervallo ravvicinato.

     double d1 = 2.2999999999999996d; double d2 = 2.2999999999999997d; System.out.println( d1 + " " + (2.3d == d1) ); System.out.println( d2 + " " + (2.3d == d2) ); 2.2999999999999994 false 2.3 true 

    Quindi 2.2999999999999997 è altrettanto 2.3 del valore 2.3! Si noti inoltre che, a causa dell'approssimazione, il punto di rotazione è a ..99997 e non a ..99995, in cui normalmente si arrotonda in matematica. Un altro modo per fare i conti con il valore approssimato è di chiamare i servizi di BigDecimal.

     System.out.println( new BigDecimal(2.3d) ); 2.29999999999999982236431605997495353221893310546875 

    Ora, non riposare sugli allori pensando di poter saltare la nave e utilizzare solo BigDecimal. BigDecimal ha la sua collezione di trappole documentata qui.

    Niente è facile, e raramente qualcosa viene gratis. E "naturalmente", fluttua e raddoppia i risultati diversi quando stampati / scritti.

     System.out.println( Float.toString(0.1f) ); System.out.println( Double.toString(0.1f) ); System.out.println( Double.toString(0.1d) ); 0.1 0.10000000149011612 0.1 

    Secondo le diapositive del blog di Joseph D. Darcy, un'approssimazione del float ha 24 bit significativi mentre una doppia approssimazione ha 53 bit significativi. Il morale è che al fine di preservare i valori, è necessario leggere e scrivere i numeri decimali nello stesso formato.

  6. Divisione per 0

    Molti sviluppatori sanno per esperienza che la divisione di un numero per rendimento zero interrompe bruscamente le loro applicazioni. Un comportamento simile si trova in Java quando funziona su int, ma piuttosto sorprendentemente, non quando si opera su double. Qualsiasi numero, con l'eccezione di zero, diviso per i rendimenti zero rispettivamente ∞ o -∞. Dividendo zero con zero risultati nel NaN speciale, il valore Not a Number.

     System.out.println(22.0 / 0.0); System.out.println(-13.0 / 0.0); System.out.println(0.0 / 0.0); Infinity -Infinity NaN 

    Dividere un numero positivo con un numero negativo produce un risultato negativo, mentre dividendo un numero negativo con un numero negativo si ottiene un risultato positivo. Poiché la divisione per zero è ansible, si otterranno risultati diversi a seconda che si divida un numero con 0.0 o -0.0. Si è vero! Java ha uno zero negativo! Non fatevi ingannare però, i due valori zero sono uguali come mostrato di seguito.

     System.out.println(22.0 / 0.0); System.out.println(22.0 / -0.0); System.out.println(0.0 == -0.0); Infinity -Infinity true 
  7. L'infinito è strano

    Nel mondo della matematica, l'infinito era un concetto che ho trovato difficile da comprendere. Per esempio, non ho mai acquisito un'intuizione per quando un infinito era infinitamente più grande di un altro. Sicuramente Z> N, l'insieme di tutti i numeri razionali è infinitamente più grande dell'insieme di numeri naturali, ma questo era il limite della mia intuizione in questo senso!

    Fortunatamente, l'infinito in Java è pressoché imprevedibile come l'infinito nel mondo matematico. È ansible eseguire i soliti sospetti (+, -, *, / su un valore infinito, ma non è ansible applicare un infinito a un infinito.

     double infinity = 1.0 / 0.0; System.out.println(infinity + 1); System.out.println(infinity / 1e300); System.out.println(infinity / infinity); System.out.println(infinity - infinity); Infinity Infinity NaN NaN 

    Il problema principale qui è che il valore NaN viene restituito senza alcun avviso. Quindi, se dovessi indagare scioccamente se un particolare doppio è pari o dispari, puoi davvero entrare in una situazione di peluria. Forse un'eccezione run-time sarebbe stata più appropriata?

     double d = 2.0, d2 = d - 2.0; System.out.println("even: " + (d % 2 == 0) + " odd: " + (d % 2 == 1)); d = d / d2; System.out.println("even: " + (d % 2 == 0) + " odd: " + (d % 2 == 1)); even: true odd: false even: false odd: false 

    Improvvisamente, la tua variabile non è né dispari né pari! NaN è ancora più strano dell'infinito Un valore infinito è diverso dal valore massimo di un doppio e NaN è di nuovo diverso dal valore infinito.

     double nan = 0.0 / 0.0, infinity = 1.0 / 0.0; System.out.println( Double.MAX_VALUE != infinity ); System.out.println( Double.MAX_VALUE != nan ); System.out.println( infinity != nan ); true true true 

    Generalmente, quando un doppio ha acquisito il valore NaN qualsiasi operazione su di esso risulta in un NaN.

     System.out.println( nan + 1.0 ); NaN 
  8. conclusioni

    1. I numeri decimali sono approssimazioni, non il valore che assegni. Qualsiasi intuizione acquisita nel mondo matematico non si applica più. Aspettatevi a+b = a e a != a/3 + a/3 + a/3
    2. Evita l'uso di ==, confronta con qualche tolleranza o usa gli operatori> = o < =
    3. Java è WYSINWYG! Non credere mai che il valore di stampa / scrittura sia un valore approssimato, quindi leggi sempre i numeri decimali di lettura / scrittura nello stesso formato.
    4. Fai attenzione a non traboccare il tuo doppio, non per ottenere il doppio in uno stato di ± Infinity o NaN. In entrambi i casi, i tuoi calcoli potrebbero non risultare come ti aspetteresti. Potresti trovare una buona idea controllare sempre questi valori prima di restituire un valore nei tuoi metodi.

Mentre BigDecimal può memorizzare più precisione del doppio, solitamente non è necessario. Il vero motivo per cui è stato utilizzato perché rende chiaro come viene eseguito l’arrotondamento, incluse diverse strategie di arrotondamento. Nella maggior parte dei casi puoi ottenere gli stessi risultati con il doppio, ma se non conosci le tecniche richieste, BigDecimal è la strada da seguire in questi casi.

Un esempio comune, sono i soldi. Anche se i soldi non saranno abbastanza grandi da richiedere la precisione di BigDecimal nel 99% dei casi d’uso, è spesso considerata la best practice usare BigDecimal perché il controllo dell’arrotondamento è nel software che evita il rischio che lo sviluppatore possa fare un errore nel gestire l’arrotondamento. Anche se sei sicuro di poter gestire l’arrotondamento con il double ti suggerisco di utilizzare metodi di supporto per eseguire l’arrotondamento che esegui test approfonditi.

Questo è principalmente fatto per ragioni di precisione. BigDecimal memorizza i numeri in virgola mobile con precisione illimitata. Puoi dare un’occhiata a questa pagina che lo spiega bene. http://blogs.oracle.com/CoreJavaTechTips/entry/the_need_for_bigdecimal

Quando viene utilizzato BigDecimal, può memorizzare molti più dati, quindi Double, che lo rende più preciso e solo una scelta migliore per il mondo reale.

Anche se è molto più lento e lungo, ne vale la pena.

Scommetto che non vorresti dare informazioni inesatte al tuo capo, eh?

Un’altra idea: tieni traccia del numero di centesimi in un long . Questo è più semplice ed evita la syntax ingombrante e le prestazioni lente di BigDecimal .

La precisione nei calcoli finanziari è molto importante perché le persone si arrabbiano molto quando i loro soldi scompaiono a causa degli errori di arrotondamento, motivo double cui il double è una scelta terribile per gestire i soldi.