JavaRush /Blog Java /Random-FR /L'appareil des nombres réels

L'appareil des nombres réels

Publié dans le groupe Random-FR
Bonjour! Dans la conférence d'aujourd'hui, nous parlerons des nombres en Java, et plus particulièrement des nombres réels. L'appareil des nombres réels - 1Ne pas paniquer! :) Il n'y aura aucune difficulté mathématique dans le cours. Nous parlerons des nombres réels exclusivement de notre point de vue « programmeur ». Alors, que sont les « chiffres réels » ? Les nombres réels sont des nombres qui ont une partie fractionnaire (qui peut être nulle). Ils peuvent être positifs ou négatifs. Voici quelques exemples : 15 56,22 0,0 1242342343445246 -232336,11 Comment fonctionne un nombre réel ? Assez simple : il est constitué d'une partie entière, d'une partie fractionnaire et d'un signe. Pour les nombres positifs, le signe n'est généralement pas indiqué explicitement, mais pour les nombres négatifs, il l'est. Auparavant, nous avons examiné en détail quelles opérations sur les nombres peuvent être effectuées en Java. Parmi elles se trouvaient de nombreuses opérations mathématiques standards - addition, soustraction, etc. Il y en avait aussi de nouvelles pour vous : par exemple, le reste de la division. Mais comment fonctionne exactement le travail avec les chiffres dans un ordinateur ? Sous quelle forme sont-ils stockés en mémoire ?

Stockage de nombres réels en mémoire

Je pense que ce ne sera pas une découverte pour vous que les nombres puissent être grands et petits :) Ils peuvent être comparés les uns aux autres. Par exemple, le nombre 100 est inférieur au nombre 423324. Cela affecte-t-il le fonctionnement de l'ordinateur et de notre programme ? En fait, oui . Chaque nombre est représenté en Java par une plage de valeurs spécifique :
Taper Taille de la mémoire (bits) Plage de valeurs
byte 8 bits -128 à 127
short 16 bits -32768 à 32767
char 16 bits entier non signé qui représente un caractère UTF-16 (lettres et chiffres)
int 32 bits du -2147483648 au 2147483647
long 64 bits de -9223372036854775808 à 9223372036854775807
float 32 bits de 2 -149 à (2-2 -23 )*2 127
double 64 bits de 2 -1074 à (2-2 -52 )*2 1023
Aujourd'hui, nous allons parler des deux derniers types - floatet double. Les deux effectuent la même tâche : représenter des nombres fractionnaires. Ils sont aussi très souvent appelés « nombres à virgule flottante » . Souvenez-vous de ce terme pour le futur :) Par exemple, le numéro 2.3333 ou 134.1212121212. Assez étrange. Après tout, il s'avère qu'il n'y a pas de différence entre ces deux types, puisqu'ils effectuent la même tâche ? Mais il y a une différence. Faites attention à la colonne « taille en mémoire » dans le tableau ci-dessus. Tous les nombres (et pas seulement les nombres - toutes les informations en général) sont stockés dans la mémoire de l'ordinateur sous forme de bits. Un bit est la plus petite unité d'information. C'est assez simple. Tout bit est égal à 0 ou à 1. Et le mot « bit » lui-même vient de l'anglais « binaire digit » - un nombre binaire. Je pense que vous avez probablement entendu parler de l'existence du système de nombres binaires en mathématiques. Tout nombre décimal que nous connaissons peut être représenté par un ensemble de uns et de zéros. Par exemple, le nombre 584,32 en binaire ressemblerait à ceci : 100100100001010001111 . Chaque un et zéro de ce nombre est un bit distinct. Vous devriez maintenant être plus clair sur la différence entre les types de données. Par exemple, si nous créons un nombre de type float, nous ne disposons que de 32 bits. Lors de la création d'un numéro, floatc'est exactement la quantité d'espace qui lui sera allouée dans la mémoire de l'ordinateur. Si nous voulons créer le nombre 123456789.65656565656565, en binaire cela ressemblera à ceci : 11101011011110011010001010110101000000 . Il se compose de 38 uns et zéros, c'est-à-dire que 38 bits sont nécessaires pour le stocker en mémoire. floatCe numéro ne « rentre » tout simplement pas dans le type ! Par conséquent, le numéro 123456789 peut être représenté comme un type double. Jusqu'à 64 bits sont alloués pour le stocker : cela nous arrange ! Bien entendu, la plage de valeurs sera également adaptée. Pour plus de commodité, vous pouvez considérer un nombre comme une petite boîte avec des cellules. S'il y a suffisamment de cellules pour stocker chaque bit, alors le type de données est choisi correctement :) L'appareil des nombres réels - 2Bien entendu, différentes quantités de mémoire allouée affectent également le nombre lui-même. Veuillez noter que les types floatont doubledifférentes plages de valeurs. Qu’est-ce que cela signifie en pratique ? Un nombre doublepeut exprimer une plus grande précision qu'un nombre float. Les nombres à virgule flottante de 32 bits (en Java, c'est exactement le type float) ont une précision d'environ 24 bits, soit environ 7 décimales. Et les nombres 64 bits (en Java c'est le type double) ont une précision d'environ 53 bits, soit environ 16 décimales. Voici un exemple qui démontre bien cette différence :
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);
   }
}
Que devrions-nous obtenir ici en conséquence ? Il semblerait que tout soit assez simple. Nous avons le nombre 0,0 et nous y ajoutons 0,1111111111111111 7 fois de suite. Le résultat devrait être 0,7777777777777777. Mais nous avons créé un numéro float. Sa taille est limitée à 32 bits et, comme nous l'avons dit plus tôt, il est capable d'afficher un nombre jusqu'à environ la 7ème décimale. Par conséquent, au final, le résultat que nous obtiendrons dans la console sera différent de ce à quoi nous nous attendions :

0.7777778
Le numéro semblait « coupé ». Vous savez déjà comment les données sont stockées en mémoire – sous forme de bits, cela ne devrait donc pas vous surprendre. La raison pour laquelle cela s'est produit est claire : le résultat 0,77777777777777777 ne rentrait tout simplement pas dans les 32 bits qui nous étaient alloués, il a donc été tronqué pour tenir dans une variable de type float:) Nous pouvons changer le type de la variable en doubledans notre exemple, puis le final le résultat ne sera pas tronqué :
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
Il y a déjà 16 décimales, le résultat « tient » sur 64 bits. Au fait, peut-être avez-vous remarqué que dans les deux cas, les résultats n'étaient pas tout à fait corrects ? Le calcul a été effectué avec des erreurs mineures. Nous en parlerons ci-dessous :) Disons maintenant quelques mots sur la façon dont vous pouvez comparer les nombres entre eux.

Comparaison de nombres réels

Nous avons déjà partiellement abordé cette question dans le cours précédent, lorsque nous avons parlé des opérations de comparaison. Nous ne réanalyserons pas les opérations telles que >, <, >=. <=Regardons plutôt un exemple plus intéressant :
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);
   }
}
Selon vous, quel numéro sera affiché à l’écran ? La réponse logique serait la réponse : le nombre 1. Nous commençons à compter à partir du nombre 0,0 et y ajoutons successivement 0,1 dix fois de suite. Tout semble correct, ça devrait en être un. Essayez d'exécuter ce code et la réponse vous surprendra grandement :) Sortie de la console :

0.9999999999999999
Mais pourquoi une erreur s’est-elle produite dans un exemple aussi simple ? O_o Ici, même un élève de cinquième année pourrait facilement répondre correctement, mais le programme Java a produit un résultat inexact. « Inexact » est ici un meilleur mot que « incorrect ». Nous avons toujours un nombre très proche de un, et pas seulement une valeur aléatoire :) Il diffère littéralement d'un millimètre du bon. Mais pourquoi? Ce n’est peut-être qu’une erreur ponctuelle. Peut-être que l'ordinateur est tombé en panne ? Essayons d'écrire un autre exemple.
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!");
   }
}
Sortie de la console :

f1 = 1.0999999999999999
f2 = 1.1
f1 и f2 не равны!
Il ne s'agit donc clairement pas de problèmes informatiques :) Que se passe-t-il ? De telles erreurs sont liées à la manière dont les nombres sont représentés sous forme binaire dans la mémoire de l'ordinateur. Le fait est que dans le système binaire, il est impossible de représenter avec précision le nombre 0,1 . À propos, le système décimal a également un problème similaire : il est impossible de représenter correctement les fractions (et au lieu de ⅓, nous obtenons 0,33333333333333..., ce qui n'est pas non plus tout à fait le résultat correct). Cela semblerait une bagatelle : avec de tels calculs, la différence peut être d'un cent millième (0,00001) ou même moins. Et si tout le résultat de votre Programme Très Sérieux dépendait de cette comparaison ?
if (f1 == f2)
   System.out.println("Rocket flies into space");
else
   System.out.println("The launch is canceled, everyone goes home");
Nous nous attendions clairement à ce que les deux chiffres soient égaux, mais en raison de la conception de la mémoire interne, nous avons annulé le lancement de la fusée. L'appareil des nombres réels - 3Si tel est le cas, nous devons décider comment comparer deux nombres à virgule flottante afin que le résultat de la comparaison soit plus... euh... prévisible. Ainsi, nous avons déjà appris la règle n°1 lors de la comparaison de nombres réels : n'utilisez jamais ==de nombres à virgule flottante lorsque vous comparez des nombres réels. Ok, je pense que ça suffit de mauvais exemples :) Regardons un bon exemple !
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");
   }
}
Ici, nous faisons essentiellement la même chose, mais en changeant la façon dont nous comparons les chiffres. Nous avons un nombre de « seuil » spécial - 0,0001, un dix millième. Cela peut être différent. Cela dépend de la précision de la comparaison dont vous avez besoin dans un cas particulier. Vous pouvez l'agrandir ou le réduire. En utilisant la méthode, Math.abs()nous obtenons le module d'un nombre. Le module est la valeur d'un nombre quel que soit son signe. Par exemple, les nombres -5 et 5 auront le même module et seront égaux à 5. On soustrait le deuxième nombre du premier, et si le résultat obtenu, quel que soit le signe, est inférieur au seuil que l'on fixe, alors nos nombres sont égaux. Dans tous les cas, ils sont égaux au degré de précision que nous avons établi à l’aide de notre « nombre seuil » , c’est-à-dire qu’ils sont au minimum égaux jusqu’au dix millième. Cette méthode de comparaison vous évitera le comportement inattendu que nous avons constaté dans le cas de ==. Un autre bon moyen de comparer des nombres réels consiste à utiliser une classe spéciale BigDecimal. Cette classe a été spécialement créée pour stocker de très grands nombres avec une partie fractionnaire. Contrairement à doubleet float, lors de l'utilisation BigDecimalde l'addition, la soustraction et d'autres opérations mathématiques sont effectuées non pas à l'aide d'opérateurs ( +-, etc.), mais à l'aide de méthodes. Voici à quoi cela ressemblera dans notre cas :
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");
   }
}
Quel type de sortie console obtiendrons-nous ?

f1 = 1.1000000000000000610622663543836097232997417449951171875
f2 = 1.1000000000000000610622663543836097232997417449951171875
f1 и f2 равны
Nous avons obtenu exactement le résultat que nous attendions. Et faites attention à la précision de nos chiffres et au nombre de décimales qu'ils contiennent ! Bien plus que dedans floatet même dedans double! N'oubliez pas le cours BigDecimalpour le futur, vous en aurez certainement besoin :) Ouf ! La conférence a été assez longue, mais vous l'avez fait : bravo ! :) Rendez-vous dans la prochaine leçon, futur programmeur !
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION