Come evitare errori di precisione in virgola mobile con float o double in Java?

Ho un problema molto fastidioso con lunghe somme di float o doppie in Java. Fondamentalmente l’idea è che se eseguo:

for ( float value = 0.0f; value < 1.0f; value += 0.1f ) System.out.println( value ); 

Quello che ottengo è:

 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.70000005 0.8000001 0.9000001 

Capisco che c’è un accumulo dell’errore di precisione fluttuante, tuttavia, come sbarazzarsi di questo? Ho provato a usare il doppio a metà dell’errore, ma il risultato è sempre lo stesso.

Qualche idea?

Non esiste una rappresentazione esatta di 0,1 come float o double . A causa di questo errore di rappresentazione, i risultati sono leggermente diversi da quelli che ti aspettavi.

Un paio di approcci che puoi usare:

  • Quando si utilizza il double tipo, visualizza solo il numero di cifre necessario. Quando si controlla l’uguaglianza, si consente una piccola tolleranza in entrambi i casi.
  • In alternativa, usa un tipo che ti permette di memorizzare i numeri che stai cercando di rappresentare esattamente, per esempio BigDecimal può rappresentare esattamente 0.1.

Codice di esempio per BigDecimal :

 BigDecimal step = new BigDecimal("0.1"); for (BigDecimal value = BigDecimal.ZERO; value.compareTo(BigDecimal.ONE) < 0; value = value.add(step)) { System.out.println(value); } 

Guardalo online: ideone

È ansible evitare questo problema specifico utilizzando classi come BigDecimal . float e double , essendo IEEE 754 a virgola mobile, non sono progettati per essere perfettamente accurati, sono progettati per essere veloci. Nota però il punto di Jon qui sotto: BigDecimal non può rappresentare “un terzo” in modo preciso, non più del double può rappresentare “un decimo” con precisione. Ma per (dire) i calcoli finanziari, BigDecimal e le classi come tendono ad essere la strada da percorrere, perché possono rappresentare i numeri nel modo in cui noi umani tendiamo a pensarci.

Non utilizzare float / double in un iteratore poiché questo massimizza l’errore di arrotondamento. Se usi semplicemente quanto segue

 for (int i = 0; i < 10; i++) System.out.println(i / 10.0); 

stampa

 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 

So che BigDecimal è una scelta popolare, ma preferisco il doppio non perché è molto più veloce, ma di solito è molto più breve / più pulito da capire.

Se contate il numero di simboli come misura della complessità del codice

  • usando doppio => 11 simboli
  • usa BigDecimal (dall'esempio @Mark Byers) => 21 simboli

BTW: non usare float a meno che non ci sia una buona ragione per non usare il doppio.

Non è solo un errore accumulato (e non ha assolutamente nulla a che fare con Java). 1.0f , una volta tradotto nel codice reale, non ha il valore 0.1 – hai già un errore di arrotondamento.

Dalla guida a virgola mobile:

Cosa posso fare per evitare questo problema?

Dipende dal tipo di calcoli che stai facendo.

  • Se hai davvero bisogno che i tuoi risultati si sommino esattamente, soprattutto quando lavori con denaro: usa un tipo di dati decimale speciale.
  • Se non vuoi vedere tutte quelle cifre decimali extra: semplicemente formatta il risultato arrotondato a un numero fisso di posizioni decimali quando lo visualizzi.
  • Se non si dispone di un tipo di dati decimale disponibile, un’alternativa è quella di lavorare con numeri interi, ad esempio i calcoli monetari interamente in centesimi. Ma questo è più lavoro e ha alcuni inconvenienti.

Leggi il sito collegato per informazioni dettagliate.

Per completezza consiglio questo:

Shewchuck, “Predicati geometrici adattivi in ​​virgola mobile robusti adattivi”, se vuoi più esempi di come eseguire l’aritmetica esatta con virgola mobile o almeno l’accuratezza controllata che è l’intenzione originale dell’autore, http: //www.cs.berkeley. edu / ~ JRS / documenti / robustr.pdf

Un’altra soluzione è di rinunciare == e controllare se i due valori sono abbastanza vicini . (So ​​che questo non è quello che hai chiesto nel corpo ma sto rispondendo al titolo della domanda).

Avevo affrontato lo stesso problema, risolto lo stesso utilizzando BigDecimal. Di seguito è riportato lo snippet che mi ha aiutato.

 double[] array = {45.34d, 45000.24d, 15000.12d, 4534.89d, 3444.12d, 12000.00d, 4900.00d, 1800.01d}; double total = 0.00d; BigDecimal bTotal = new BigDecimal(0.0+""); for(int i = 0;i < array.length; i++) { total += (double)array[i]; bTotal = bTotal.add(new BigDecimal(array[i] +"")); } System.out.println(total); System.out.println(bTotal); 

Spero ti possa aiutare.

È necessario utilizzare un tipo di dati decimale, non mobile:

https://docs.oracle.com/javase/7/docs/api/java/math/BigDecimal.html

 package loopinamdar; import java.text.DecimalFormat; public class loopinam { static DecimalFormat valueFormat = new DecimalFormat("0.0"); public static void main(String[] args) { for (float value = 0.0f; value < 1.0f; value += 0.1f) System.out.println("" + valueFormat.format(value)); } } 

Per prima cosa rendilo doppio . Non utilizzare mai float o si avranno problemi nell’utilizzo delle utilità java.lang.Math .

Ora, se conosci in anticipo la precisione che desideri ed è uguale o inferiore a 15, diventa facile dire al tuo doppio di comportarsi. Controlla qui sotto:

 // the magic method: public final static double makePrecise(double value, int precision) { double pow = Math.pow(10, precision); long powValue = Math.round(pow * value); return powValue / pow; } 

Ora ogni volta che fai un’operazione, devi dire al tuo doppio risultato di comportarsi:

 for ( double value = 0.0d; value < 1.0d; value += 0.1d ) System.out.println( makePrecise(value, 1) + " => " + value ); 

Produzione:

 0.0 => 0.0 0.1 => 0.1 0.2 => 0.2 0.3 => 0.30000000000000004 0.4 => 0.4 0.5 => 0.5 0.6 => 0.6 0.7 => 0.7 0.8 => 0.7999999999999999 0.9 => 0.8999999999999999 1.0 => 0.9999999999999999 

Se hai bisogno di più di 15 precisione, non sei fortunato:

 for ( double value = 0.0d; value < 1.0d; value += 0.1d ) System.out.println( makePrecise(value, 16) + " => " + value ); 

Produzione:

 0.0 => 0.0 0.1 => 0.1 0.2 => 0.2 0.3000000000000001 => 0.30000000000000004 0.4 => 0.4 0.5 => 0.5 0.6 => 0.6 0.7 => 0.7 0.8 => 0.7999999999999999 0.9 => 0.8999999999999999 0.9999999999999998 => 0.9999999999999999 

NOTA 1: per prestazioni, è necessario memorizzare nella cache l’operazione Math.pow in una matrice. Non fatto qui per chiarezza.

NOTA2: Ecco perché non usiamo mai double s per i prezzi, ma long s dove l’ultimo N (cioè dove N <= 15, di solito 8) cifre sono le cifre decimali. Quindi puoi dimenticare ciò che ho scritto sopra :)

Se vuoi continuare a usare float ed evitare di accumulare errori aggiungendo ripetutamente 0.1f , prova qualcosa del genere:

 for (int count = 0; count < 10; count++) { float value = 0.1f * count; System.out.println(value); } 

Si noti tuttavia, come altri hanno già spiegato, che float non è un tipo di dati infinitamente preciso.

Devi solo essere consapevole della precisione richiesta nel tuo calcolo e della precisione con cui il tipo di dati scelto è capace e presentare le tue risposte di conseguenza.

Ad esempio, se si hanno a che fare con numeri con 3 cifre significative, l’uso del float (che fornisce una precisione di 7 cifre significative) è appropriato. Tuttavia, non puoi citare la tua risposta finale ad una precisione di 7 cifre significative se i tuoi valori iniziali hanno solo una precisione di 2 cifre significative.

 5.01 + 4.02 = 9.03 (to 3 significant figures) 

Nel tuo esempio stai eseguendo più aggiunte, e con ogni aggiunta c’è un conseguente impatto sulla precisione finale.