UKOnline

Nombre à précision arbitraire

Comme on l'a vu au premier chapitre, les entiers et les réels sont représentés sur un nombre fini de bits et ne peuvent donc pas prendre toutes les valeurs possibles. Ceci peut parfois poser problème car on aimerait manipuler des nombres en dehors des limites imposées par les types primitifs numériques, c'est-à-dire utiliser des nombres à précision arbitraire. Avant de voir la solution à ce problème, voyons quelques problèmes qui surviennent avec les types primitifs numériques.

Type primitif numérique

Le premier problème qui apparait avec les types primitifs numériques est le dépassement de capacité (ou overflow en anglais). Ceci arrive lorsque vous tentez de stocker dans une variable de type primitif un nombre qui est trop grand ou trop petit. Vous pouvez le faire et il n'y aura aucune erreur de compilation ou d'exécution; néanmoins, ceci peut mener à des erreurs logiques comme en témoigne l'exemple suivant :

On déclare une variable counter qui représente par exemple le nombre de visites qui ont eu lieu sur votre site. Ce compteur est initialisé à la plus grande valeur entière possible de type int. Ensuite, un nouveau visiteur arrive sur votre site et le compteur est incrémenté de 1. Voici ce qui va s'afficher à l'écran :

2147483647
-2147483648

Voilà que votre site compte soudainement un nombre négatif de visiteurs. Il y a eu un dépassement de capacité : on avait déjà le plus grand entier possible dans la variable counter et donc, quand on a fait +1, ça n'a pas fait +1. Imaginez que ceci se produise dans une application qui calcule ce que le fisc doit vous rembourser; tout d'un coup, vous leur devriez de l'argent ! Bien entendu ce problème arrive également dans l'autre sens : si on initialise la variable counter à Integer.MIN_VALUE, puis qu'on fait counter--, on se retrouve également avec un dépassement de capacité.

Avec les flottants, lorsqu'un calcul fait que la capacité est dépassée, le résultat est tout autre; on se retrouve avec une valeur infinie. Voici un exemple :

L'exécution de ce programme affiche ceci à l'écran :

1.7976931348623157E308
Infinity

Vous avez donc au départ une certaine somme d'argent stockée dans la variable money. Vous touchez 1% d'intérêts qu'on ajoute sur votre compte et vous vous retrouvez avec une infinité d'argent. Très intéressant me direz-vous, mais pas forcément pour votre banque.

Un autre problème survient avec les flottants, celui de la représentation. En effet, il n'est pas possible de représenter tous les nombres réels qui existent avec les types primitifs flottants. Il n'est pas exemple pas possible de représenter les puissances négatives de 10 (comme 0,1; 0,01...). Prenons un exemple qui va très certainement vous surprendre :

Que va afficher ce programme lors de son exécution ? Vous souhaiteriez sans doute qu'il affiche 0.1, 0.2, 0.3... et s'arrête avec 0.9 ? Il s'agit en fait d'une boucle infinie !

0.0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999
1.0999999999999999
1.2
1.3
1.4000000000000001
1.5000000000000002
...

Ce problème survient car il n'est pas possible de représenter tous les réels avec les flottants primitifs et donc, les calculs effectués sur des nombres flottants ne seront pas toujours précis. C'est ce qui se passe avec la condition d != 1 qu'on a utilisée, qui ne sera jamais fausse. Pour éviter des problèmes, on n'utilise d'ailleurs que très rarement les opérateurs d'égalité == et != avec les flottants. Si on avait mis d < 1 comme condition, on aurait déjà évité la boucle infinie, mais le problème de précision n'aurait pas été résolu.

Classes BigInteger et BigDecimal

Pour résoudre ces problèmes, on va utiliser deux classes de la librairie standard Java : BigInteger et BigDecimal, qui se trouvent dans le package java.math. Ces classes représentent respectivement des entiers et des flottants à précision arbitraire.

Avec ces classes, on peut représenter potentiellement tous les entiers et tous les réels existants (la seule limitation est imposée par la mémoire disponible pour la machine virtuelle Java). Dans cette section, on va apprendre à utiliser ces classes et pour en savoir plus, on vous renvoie à nouveau vers la documentation de l'API Standard Java.

Création

Il s'agit d'objets et il faut donc commencer par les créer. Il y a plusieurs manières de faire : si on veut un objet représentant 0, 1 ou 10, on peut utiliser les constantes de classe ZERO, ONE et TEN définies dans les deux classes. Sinon, on peut passer par le constructeur qui prend une chaine de caractères en paramètre. Enfin, on peut également passer par la méthode de classe valueOf qui prend une donnée primitive en paramètre. L'exemple suivant illustre les différentes manières de création :

Opération

On manipule des objets et donc, pour faire les opérations arithmétiques comme l'addition, la soustraction... il faut passer par des méthodes. Il y a logiquement les méthodes suivantes : add (+), subtract (-), multiply (*), divide (/) et remainder (%). Reprenons l'exemple qui provoquait un dépassement de capacité mais écrit avec des objets BigInteger :

Le problème de dépassement de capacité n'apparait plus cette fois-ci. Le calcul s'est très bien déroulé et le résultat obtenu est celui attendu :

2147483647
2147483648

Néanmoins, comme vous avez pu le constater, le code est beaucoup moins lisible. On pourrait déjà largement améliorer la lisibilité en utilisant des imports statiques. Mais il est vraiment dommage qu'on ne puisse utiliser les opérateurs arithmétiques +, -, *, / et % avec les objets BigInteger (il y a des langages de programmation comme C++, C#, Python, etc. qui autorisent la surcharge des opérateurs et l'utilisation avec des objets). Revoyons maintenant le second exemple qui bouclait infiniment à cause d'un problème de précision en utilisant un objet BigDecimal :

On commence donc par créer deux objets BigDecimal : le premier est l'incrément qu'on va ajouter à chaque boucle et le second est l'objectif qu'on doit atteindre et qui sera utilisé dans la partie condition de la boucle. Cette fois-ci, la boucle s'arrête bien et affiche exactement ce qu'on souhaitait :

0
0.1
0.2
0.3
0.4
0.5
0.6
0.7
0.8
0.9

Vous vous demandez peut-être pourquoi on a créé l'objet end avec 1.0 et pas avec 1 tout court. Pour la méthode equals, deux objets de type BigDecimal sont considérés égaux s'ils ont exactement la même partie entière et la même partie décimale, et donc 1 est différent de 1.0, de 1.00... Pour comparer deux objets de type BigDecimal sans que ce problème n'intervienne, il faut utiliser la méthode compareTo qu'on verra au chapitre 5.

Il y a beaucoup d'autre méthodes dans les classes BigInteger et BigDecimal permettant de calculer des puissances, de changer le signe, de calculer la valeur absolue, de faire des opérations sur les bits, etc.

Conversion

Il n'est pas possible de faire des opérations entre un objet de type BigInteger et un de type BigDecimal. Il faut tout d'abord convertir les objets en passant par la méthode toBigInteger pour convertir un réel vers un entier et en passant par un constructeur de la classe BigDecimal pour convertir un entier en réel. L'exemple suivant illustre ces deux conversions :

Il est également possible de convertir des objets BigInteger et BigDecimal vers les types primitifs int ou long. Deux méthodes sont disponibles pour ce faire : les méthodes intValue et longValue fonctionnent comme des conversions explicites et peuvent donc entrainer des pertes d'information.

Les méthodes intValueExact et longValueExact, seulement disponibles pour les objets de type BigDecimal, génère une erreur de type ArithmeticException si la valeur ne peut être convertie. Enfin, les deux classes proposent les méthodes floatValue et doubleValue qui convertissent le nombre respectivement en float et double. Si le nombre est trop grand ou trop petit, la conversion produit des valeurs infinies.

Type primitif ou BigInteger et BigDecimal ?

Que choisir ? Type primitif ou objets BigInteger et BigDecimal ? Il n'y a pas de réponse absolue à cette question. Lorsqu'on travaille avec les types primitifs, il faut savoir qu'il y a des problèmes de dépassement et de précision qui peuvent survenir. Si vous êtes sûr que ça n'arrivera jamais ou si ce n'est pas critique pour votre application, alors autant prendre les types primitifs, les calculs seront plus rapide et le code sera plus lisible.

Par contre, si vous faites des applications critiques pour lesquelles la précision est importante, comme des applications financières par exemple, alors vous devez utiliser les objets BigInteger et BigDecimal, même si ce sera plus lent à l'exécution et que le code sera moins lisible.