JavaRush /Java Blog /Random-IT /Il dispositivo dei numeri reali

Il dispositivo dei numeri reali

Pubblicato nel gruppo Random-IT
Ciao! Nella lezione di oggi parleremo dei numeri in Java, e nello specifico dei numeri reali. Il dispositivo dei numeri reali - 1Niente panico! :) Non ci saranno difficoltà matematiche nella lezione. Parleremo di numeri reali esclusivamente dal nostro punto di vista “programmatore”. Allora, cosa sono i “numeri reali”? I numeri reali sono numeri che hanno una parte frazionaria (che può essere zero). Possono essere positivi o negativi. Ecco alcuni esempi: 15 56.22 0.0 1242342343445246 -232336.11 Come funziona un numero reale? Abbastanza semplice: è composto da una parte intera, una parte frazionaria e un segno. Per i numeri positivi il segno solitamente non viene indicato esplicitamente, ma per i numeri negativi viene indicato. In precedenza, abbiamo esaminato in dettaglio quali operazioni sui numeri possono essere eseguite in Java. Tra questi c'erano molte operazioni matematiche standard: addizione, sottrazione, ecc. Ce n'erano anche alcune nuove per te: ad esempio il resto della divisione. Ma come funziona esattamente il lavoro con i numeri all’interno di un computer? In che forma vengono archiviati nella memoria?

Memorizzazione di numeri reali in memoria

Penso che non sarà una scoperta per te che i numeri possano essere grandi e piccoli :) Possono essere confrontati tra loro. Ad esempio, il numero 100 è inferiore al numero 423324. Ciò influisce sul funzionamento del computer e del nostro programma? Attualmente si . Ogni numero è rappresentato in Java da uno specifico intervallo di valori :
Tipo Dimensioni della memoria (bit) Intervallo di valori
byte 8 bit da -128 a 127
short 16 bit Da -32768 a 32767
char 16 bit intero senza segno che rappresenta un carattere UTF-16 (lettere e numeri)
int 32 bit da -2147483648 a 2147483647
long 64 bit da -9223372036854775808 a 9223372036854775807
float 32 bit da 2 -149 a (2-2 -23 )*2 127
double 64 bit da 2 -1074 a (2-2 -52 )*2 1023
Oggi parleremo degli ultimi due tipi: floate double. Entrambi svolgono lo stesso compito: rappresentare i numeri frazionari. Molto spesso vengono anche chiamati “ numeri in virgola mobile” . Ricorda questo termine per il futuro :) Ad esempio, il numero 2.3333 o 134.1212121212. Piuttosto strano. Dopotutto, si scopre che non c'è differenza tra questi due tipi, dal momento che svolgono lo stesso compito? Ma c'è una differenza. Presta attenzione alla colonna "dimensione in memoria" nella tabella sopra. Tutti i numeri (e non solo i numeri, tutte le informazioni in generale) sono archiviati nella memoria del computer sotto forma di bit. Un bit è la più piccola unità di informazione. È piuttosto semplice. Qualsiasi bit è uguale a 0 o 1. E la parola " bit " stessa deriva dall'inglese " binary digit " - un numero binario. Penso che probabilmente tu abbia sentito parlare dell'esistenza del sistema di numeri binari in matematica. Qualsiasi numero decimale con cui abbiamo familiarità può essere rappresentato come un insieme di uno e zeri. Ad esempio, il numero 584.32 in binario sarebbe simile a questo: 100100100001010001111 . Ciascuno uno e zero in questo numero è un bit separato. Ora dovresti essere più chiaro sulla differenza tra i tipi di dati. Se ad esempio creiamo un numero di tipo float, avremo a disposizione solo 32 bit. Quando si crea un numero, floatquesto è esattamente lo spazio che gli verrà assegnato nella memoria del computer. Se vogliamo creare il numero 123456789.65656565656565, in binario sarà simile a questo: 11101011011110011010001010110101000000 . È composto da 38 uno e zero, ovvero sono necessari 38 bit per memorizzarlo in memoria. floatQuesto numero semplicemente non “si adatta” al tipo ! Pertanto, il numero 123456789 può essere rappresentato come un tipo double. Per memorizzarlo vengono allocati ben 64 bit: questo ci va bene! Naturalmente anche l'intervallo di valori sarà adatto. Per comodità, puoi pensare a un numero come a una piccola scatola con celle. Se ci sono abbastanza celle per memorizzare ogni bit, il tipo di dati viene scelto correttamente :) Il dispositivo dei numeri reali - 2Naturalmente, diverse quantità di memoria allocata influiscono anche sul numero stesso. Tieni presente che i tipi floathanno doubleintervalli di valori diversi. Cosa significa in pratica? Un numero doublepuò esprimere una precisione maggiore di un numero float. I numeri in virgola mobile a 32 bit (in Java questo è esattamente il tipo float) hanno una precisione di circa 24 bit, cioè circa 7 cifre decimali. E i numeri a 64 bit (in Java questo è il tipo double) hanno una precisione di circa 53 bit, cioè circa 16 cifre decimali. Ecco un esempio che dimostra bene questa differenza:
public class Main {

   public static void main(String[] args)  {

       float f = 0.0f;
       for (int i=1; i <= 7; i++) {
           f += 0.1111111111111111;
       }

       System.out.println(f);
   }
}
Cosa dovremmo ottenere qui di conseguenza? Sembrerebbe che tutto sia abbastanza semplice. Abbiamo il numero 0.0 e gli aggiungiamo 0.1111111111111111 7 volte di seguito. Il risultato dovrebbe essere 0,7777777777777777. Ma abbiamo creato un numero float. La sua dimensione è limitata a 32 bit e, come abbiamo detto prima, è in grado di visualizzare un numero fino alla settima cifra decimale. Pertanto, alla fine, il risultato che otterremo sulla console sarà diverso da quello che ci aspettavamo:

0.7777778
Il numero sembrava essere "tagliato". Sai già come vengono archiviati i dati in memoria, sotto forma di bit, quindi questo non dovrebbe sorprenderti. È chiaro il motivo per cui ciò è accaduto: il risultato 0,7777777777777777 semplicemente non rientrava nei 32 bit assegnati a noi, quindi è stato troncato per adattarsi a una variabile di tipo float:) Possiamo cambiare il tipo della variabile in doublenel nostro esempio, e quindi il risultato finale il risultato non verrà troncato:
public class Main {

   public static void main(String[] args)  {

       double f = 0.0;
       for (int i=1; i <= 7; i++) {
           f += 0.1111111111111111;
       }

       System.out.println(f);
   }
}

0.7777777777777779
Ci sono già 16 cifre decimali, il risultato "si adatta" a 64 bit. A proposito, forse hai notato che in entrambi i casi i risultati non erano del tutto corretti? Il calcolo è stato effettuato con piccoli errori. Ne parleremo di seguito :) Ora diciamo alcune parole su come confrontare i numeri tra loro.

Confronto di numeri reali

Abbiamo in parte già toccato questo tema nella lezione precedente, quando abbiamo parlato delle operazioni di confronto. Non rianalizzeremo operazioni come >, <, >=. <=Vediamo invece un esempio più interessante:
public class Main {

   public static void main(String[] args)  {

       double f = 0.0;
       for (int i=1; i <= 10; i++) {
           f += 0.1;
       }

       System.out.println(f);
   }
}
Quale numero pensi che verrà visualizzato sullo schermo? La risposta logica sarebbe la risposta: il numero 1. Iniziamo a contare dal numero 0,0 e successivamente vi aggiungiamo 0,1 dieci volte di seguito. Tutto sembra essere corretto, dovrebbe essere uno. Prova a eseguire questo codice e la risposta ti sorprenderà molto :) Output della console:

0.9999999999999999
Ma perché si è verificato un errore in un esempio così semplice? O_o Qui anche un bambino di quinta elementare potrebbe facilmente rispondere correttamente, ma il programma Java ha prodotto un risultato impreciso. In questo caso “impreciso” è una parola migliore di “errato”. Abbiamo comunque ottenuto un numero molto vicino a uno, e non solo un valore casuale :) Si differenzia da quello corretto letteralmente di un millimetro. Ma perché? Forse questo è solo un errore occasionale. Forse il computer si è bloccato? Proviamo a scrivere un altro esempio.
public class Main {

   public static void main(String[] args)  {

       //add 0.1 to zero eleven times in a row
       double f1 = 0.0;
       for (int i = 1; i <= 11; i++) {
           f1 += .1;
       }

       // Multiply 0.1 by 11
       double f2 = 0.1 * 11;

       //should be the same - 1.1 in both cases
       System.out.println("f1 = " + f1);
       System.out.println("f2 = " + f2);

       // Let's check!
       if (f1 == f2)
           System.out.println("f1 and f2 are equal!");
       else
           System.out.println("f1 and f2 are not equal!");
   }
}
Uscita console:

f1 = 1.0999999999999999
f2 = 1.1
f1 и f2 не равны!
Quindi, chiaramente non è una questione di problemi del computer :) Cosa sta succedendo? Errori come questi sono legati al modo in cui i numeri sono rappresentati in forma binaria nella memoria del computer. Il fatto è che nel sistema binario è impossibile rappresentare con precisione il numero 0.1 . A proposito, anche il sistema decimale ha un problema simile: è impossibile rappresentare correttamente le frazioni (e invece di ⅓ otteniamo 0,33333333333333..., che non è nemmeno il risultato corretto). Sembrerebbe una sciocchezza: con tali calcoli la differenza può essere della centomillesima parte (0,00001) o anche meno. Ma cosa succederebbe se l’intero risultato del tuo Very Serious Program dipendesse da questo confronto?
if (f1 == f2)
   System.out.println("Rocket flies into space");
else
   System.out.println("The launch is canceled, everyone goes home");
Ci aspettavamo chiaramente che i due numeri fossero uguali, ma a causa della progettazione della memoria interna, abbiamo annullato il lancio del razzo. Il dispositivo dei numeri reali - 3Se è così, dobbiamo decidere come confrontare due numeri in virgola mobile in modo che il risultato del confronto sia più... ummm... prevedibile. Quindi, abbiamo già imparato la regola n. 1 quando si confrontano numeri reali: non utilizzare mai ==numeri in virgola mobile quando si confrontano numeri reali. Ok, penso che bastano cattivi esempi :) Diamo un'occhiata a un buon esempio!
public class Main {

   public static void main(String[] args)  {

       final double threshold = 0.0001;

       //add 0.1 to zero eleven times in a row
       double f1 = .0;
       for (int i = 1; i <= 11; i++) {
           f1 += .1;
       }

       // Multiply 0.1 by 11
       double f2 = .1 * 11;

       System.out.println("f1 = " + f1);
       System.out.println("f2 = " + f2);

       if (Math.abs(f1 - f2) < threshold)
           System.out.println("f1 and f2 are equal");
       else
           System.out.println("f1 and f2 are not equal");
   }
}
Qui stiamo essenzialmente facendo la stessa cosa, ma cambiando il modo in cui confrontiamo i numeri. Abbiamo un numero di "soglia" speciale: 0,0001, un decimillesimo. Potrebbe essere diverso. Dipende dalla precisione del confronto necessaria in un caso particolare. Puoi renderlo più grande o più piccolo. Usando il metodo, Math.abs()otteniamo il modulo di un numero. Il modulo è il valore di un numero indipendentemente dal segno. Ad esempio, i numeri -5 e 5 avranno lo stesso modulo e saranno uguali a 5. Sottraiamo il secondo numero dal primo e se il risultato risultante, indipendentemente dal segno, è inferiore alla soglia che abbiamo impostato, allora i nostri numeri sono uguali. In ogni caso sono pari al grado di precisione che abbiamo stabilito utilizzando il nostro “numero soglia” , cioè come minimo sono pari fino a un decimillesimo. Questo metodo di confronto ti salverà dal comportamento inaspettato che abbiamo visto nel caso di ==. Un altro buon modo per confrontare i numeri reali è utilizzare una classe speciale BigDecimal. Questa classe è stata creata appositamente per memorizzare numeri molto grandi con una parte frazionaria. A differenza di doublee float, quando si utilizza BigDecimall'addizione, la sottrazione e altre operazioni matematiche vengono eseguite non utilizzando operatori ( +-, ecc.), ma utilizzando metodi. Ecco come apparirà nel nostro caso:
import java.math.BigDecimal;

public class Main {

   public static void main(String[] args)  {

       /*Create two BigDecimal objects - zero and 0.1.
       We do the same thing as before - add 0.1 to zero 11 times in a row
       In the BigDecimal class, addition is done using the add () method */
       BigDecimal f1 = new BigDecimal(0.0);
       BigDecimal pointOne = new BigDecimal(0.1);
       for (int i = 1; i <= 11; i++) {
           f1 = f1.add(pointOne);
       }

       /*Nothing has changed here either: create two BigDecimal objects
       and multiply 0.1 by 11
       In the BigDecimal class, multiplication is done using the multiply() method*/
       BigDecimal f2 = new BigDecimal(0.1);
       BigDecimal eleven = new BigDecimal(11);
       f2 = f2.multiply(eleven);

       System.out.println("f1 = " + f1);
       System.out.println("f2 = " + f2);

       /*Another feature of BigDecimal is that number objects need to be compared with each other
       using the special compareTo() method*/
       if (f1.compareTo(f2) == 0)
           System.out.println("f1 and f2 are equal");
       else
           System.out.println("f1 and f2 are not equal");
   }
}
Che tipo di output su console otterremo?

f1 = 1.1000000000000000610622663543836097232997417449951171875
f2 = 1.1000000000000000610622663543836097232997417449951171875
f1 и f2 равны
Abbiamo ottenuto esattamente il risultato che ci aspettavamo. E presta attenzione a quanto sono accurati i nostri numeri e a quante cifre decimali rientrano in essi! Molto più che in floate anche in double! Ricorda il corso BigDecimalper il futuro, ne avrai sicuramente bisogno :) Uff! La lezione è stata piuttosto lunga, ma ce l'hai fatta: complimenti! :) Ci vediamo alla prossima lezione, futuro programmatore!
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION