1. Introduzione
Sembrerebbe che, se il computer «intelligente», allora 0.1 + 0.2 debba essere semplicemente 0.3. Ma non è proprio così. Vediamo con un semplice esempio.
double x = 0.1;
double y = 0.2;
double sum = x + y;
System.out.println(sum); // Cosa stamperà il programma?
Ora provate a confrontare con 0.3:
System.out.println(sum == 0.3); // Qui sarà true o false?
Se vedete false, non stupitevi!
La ragione sta nella rappresentazione dei numeri in memoria
I computer lavorano con i numeri in sistema binario. Ma non tutte le frazioni decimali possono essere rappresentate come frazioni binarie finite, così come 1/3 non può essere scritto esattamente come frazione decimale (0.333...). Ad esempio, 0.1 in binario è una frazione infinita, e per memorizzarla bisogna «arrotondarla».
In parole semplici, double a volte «fa finta» di conservare il vostro numero esattamente, ma in realtà memorizza solo un valore approssimato molto vicino.
2. Quali «stranezze» si verificano con l’aritmetica su double?
Vediamo alcuni esempi pratici.
Esempio 1. La «magia» classica di 0.1 + 0.2
double a = 0.1;
double b = 0.2;
double sum = a + b;
System.out.println(sum); // 0.30000000000000004
System.out.println(sum == 0.3); // false
Il computer non ha stampato 0.3, ma 0.30000000000000004. La differenza è piccola, ma se vi occupate, ad esempio, di finanza — questo diventa già critico.
Esempio 2. Somma iterativa
double result = 0;
for (int i = 0; i < 10; i++)
{
result += 0.1;
}
System.out.println(result); // 0.9999999999999999
Volevate 1.0 — avete ottenuto un valore leggermente inferiore. Di nuovo, la causa è l’arrotondamento interno di double.
Perché questo è importante nei casi reali
Molti pensano: «Che differenza fa, è solo un piccolo errore, pazienza!» Consideriamo un esempio dal mondo dei pagamenti.
Supponiamo che il vostro internet banking sommi 100 transazioni da 0.1 euro. Se il vostro programma «perde» un centomillesimo di euro a ogni iterazione, su scala bancaria state già «perdendo» denaro reale. A questo punto il contabile verrà subito da voi e chiederà: «Dove sono i nostri soldi?!»
3. Come confrontare correttamente i numeri in virgola mobile
Poiché double spesso non può memorizzare esattamente il valore che vi aspettate, il confronto diretto con == può tradire. Invece è prassi confrontare il valore assoluto della differenza con un numero molto piccolo (epsilon).
Esempio di confronto con tolleranza
double a = 0.1 + 0.2;
double b = 0.3;
double epsilon = 0.000001;
if (Math.abs(a - b) < epsilon)
{
System.out.println("Quasi uguale!"); // Confrontare così è più sicuro
}
Qui diciamo: «Se la differenza tra i numeri è inferiore a un milionesimo, consideriamo i numeri uguali».
Nota: la funzione Math.abs(value) restituisce il valore assoluto (modulo) del numero passato.
4. Valori speciali di double: Infinity, NaN, -Infinity
Il tipo double memorizza non solo numeri, ma anche valori speciali. Questi si presentano in situazioni che un matematico proibirebbe tassativamente a uno studente.
Infinito (Infinity)
Cosa succede se si divide 1 per 0?
double result = 1.0 / 0.0;
System.out.println(result); // Infinity
In Java (e in molti linguaggi) la divisione per 0 per double non genera un’eccezione! Invece, il risultato diventa un valore speciale «infinito positivo».
Meno infinito (-Infinity)
Se si divide un numero negativo per 0, si ottiene l’infinito negativo:
double result = -1.0 / 0.0;
System.out.println(result); // -Infinity
«Non un numero» (NaN — Not a Number)
Se si fa qualcosa di davvero strano, ad esempio provare a calcolare la radice quadrata di un numero negativo:
double result = Math.sqrt(-1);
System.out.println(result); // NaN
Oppure il risultato di 0.0 / 0.0:
double result = 0.0 / 0.0;
System.out.println(result); // NaN
NaN è «qualcosa che non è un numero nel mondo reale».
Verifica dei valori speciali
In Java ci sono funzioni per verificare i valori speciali:
System.out.println(Double.isInfinite(result)); // true, se infinito
System.out.println(Double.isNaN(result)); // true, se NaN
Tabella: come double reagisce a operazioni insolite
| Operazione | Risultato | Cosa viene memorizzato in double |
|---|---|---|
|
Infinity | +∞ |
|
-Infinity | -∞ |
|
NaN | Non un numero |
|
NaN | Non un numero |
5. Errori tipici nel lavoro con i numeri in virgola mobile
Errore n. 1: confronto di numeri frazionari tramite ==
La trappola più comune: cercare di verificare se due numeri in virgola mobile calcolati sono uguali tramite un confronto normale. A causa dell’accumulo degli errori di arrotondamento, quasi sempre otterrete un risultato inatteso. Usate sempre un confronto con tolleranza (epsilon).
Errore n. 2: NaN e Infinity inattesi nei calcoli
Se non controllate la divisione per zero o le radici di numeri negativi, nel programma possono comparire NaN o Infinity e «contagiare» tutti i calcoli successivi. Non dimenticate di verificare i valori sospetti con Double.isNaN() e Double.isInfinite() — questo aiuta a evitare spiacevoli sorprese.
Errore n. 3: uso di NaN come «marcatore»
Alcuni principianti usano NaN come valore «speciale» per i controlli, ad esempio «se non trovato — restituiamo NaN». Ma ricordate: NaN è insidioso e il confronto con == non funzionerà! Verificate solo tramite i metodi speciali.
Errore n. 4: aspettarsi un’eccezione nella divisione per 0.0
A differenza della divisione tra interi, la divisione per 0.0 per double non genera un errore, ma restituisce Infinity o NaN. Questo può portare a bug silenziosi e difficili da individuare — un risultato c’è, ma non è quello che vi aspettavate.
GO TO FULL VERSION