On m'a toujours dit de ne JAMAIS faire cela, et cette fois je vous pose la question : pourquoi ? Je suis sûr qu'il y a une très bonne raison, mais je ne la connais pas. :-P
Réponses
Trop de publicités?Parce que les flottants et les doubles ne peuvent pas représenter avec précision les multiples en base 10 que nous utilisons pour l'argent. Ce problème ne concerne pas uniquement Java, mais tous les langages de programmation qui utilisent des types natifs à virgule flottante, car il découle de la manière dont les ordinateurs traitent les nombres à virgule flottante par défaut.
C'est ainsi qu'un IEEE-754 Un nombre à virgule flottante fonctionne : il dédie un bit au signe, quelques bits pour stocker un exposant pour la base, et le reste pour un multiple de cette base élevée. Cela conduit à ce que des nombres comme 10,25 soient représentés sous une forme similaire à celle qui suit 1025 * 10^-2
sauf qu'au lieu que la base soit 10, pour float
et double
s, c'est deux (donc ce serait 164 * 2^-4
).
Même en base 10, cette notation ne peut pas représenter avec précision la plupart des fractions simples. Par exemple, vous ne pouvez pas représenter 1/3 comme un multiple d'une puissance de 10 : il vous faudrait stocker une quantité infinie de 3 et un exposant négatif infiniment grand, ce qui est tout simplement impossible. Cependant, pour les besoins de l'argent, dans la plupart des scénarios, tout ce dont vous avez besoin est de pouvoir stocker des multiples de 10. -2 donc ce n'est pas si mal.
De même que certaines fractions ne peuvent pas être représentées exactement comme des multiples d'une puissance de dix, certaines d'entre elles ne peuvent pas non plus être représentées exactement comme des multiples d'une puissance de deux. En fait, les seules fractions de cent entre 0/100 et 100/100 (qui sont significatives lorsqu'il s'agit d'argent car ce sont des centimes entiers) qui peuvent être représentées exactement comme un nombre binaire à virgule flottante IEEE-754 sont 0, 0,25, 0,5, 0,75 et 1. Toutes les autres sont faussées par une petite quantité.
Représenter l'argent comme un double
ou float
aura probablement l'air bon au début, car le logiciel arrondit les petites erreurs, mais au fur et à mesure que vous effectuez des additions, des soustractions, des multiplications et des divisions sur des nombres inexacts, vous perdrez de plus en plus de précision, car les erreurs s'accumulent. Les flottants et les doubles sont donc inadaptés au traitement de l'argent, où une précision parfaite pour les multiples des puissances en base 10 est requise.
Une solution qui fonctionne dans presque toutes les langues consiste à utiliser des nombres entiers à la place et à compter les centimes. Par exemple, 1025 correspondrait à 10,25 $. Plusieurs langages ont également des types intégrés pour traiter l'argent. Entre autres, Java dispose du type BigDecimal
et le C# dispose de la classe decimal
type.
De Bloch, J., Effective Java, 2nd ed, Item 48 :
Le site
float
etdouble
types sont particulièrement mal adaptés aux calculs monétaires car il est impossible de représenter 0,1 (ou toute autre puissance négative de dix) comme unefloat
oudouble
exactement.Par exemple, supposons que vous ayez 1,03 $ et que vous dépensez 42c. Combien d'argent vous reste-t-il ?
System.out.println(1.03 - .42);
imprime
0.6100000000000001
.La bonne façon de résoudre ce problème est d'utiliser
BigDecimal
,int
oulong
pour les calculs monétaires.
Ce n'est ni une question d'exactitude, ni une question de précision. Il s'agit de répondre aux attentes des humains qui utilisent la base 10 pour les calculs au lieu de la base 2. Par exemple, l'utilisation de doubles pour les calculs financiers ne produit pas de réponses "fausses" dans un sens mathématique, mais elle peut produire des réponses qui ne correspondent pas aux attentes dans un sens financier.
Même si vous arrondissez vos résultats à la dernière minute avant la sortie, vous pouvez toujours obtenir occasionnellement un résultat en utilisant des doubles qui ne correspond pas aux attentes.
En utilisant une calculatrice, ou en calculant les résultats à la main, 1,40 * 165 = 231 exactement. Cependant, en utilisant des doubles en interne, sur mon compilateur / environnement système d'exploitation, il est stocké comme un nombre binaire proche de 230.99999... donc si vous tronquez le nombre, vous obtenez 230 au lieu de 231. Vous pouvez penser que l'arrondi au lieu de la troncature aurait donné le résultat souhaité de 231. C'est vrai, mais l'arrondi implique toujours une troncature. Quelle que soit la technique d'arrondi que vous utilisez, il existe toujours des conditions limites comme celle-ci qui arrondissent à la baisse alors que vous vous attendez à ce qu'elles arrondissent à la hausse. Elles sont suffisamment rares pour ne pas être découvertes par des tests ou des observations occasionnels. Vous devrez peut-être écrire du code pour rechercher des exemples illustrant des résultats qui ne se comportent pas comme prévu.
Supposons que vous vouliez arrondir quelque chose au centime le plus proche. Vous prenez donc votre résultat final, le multipliez par 100, ajoutez 0,5, tronquez, puis divisez le résultat par 100 pour revenir aux centimes. Si le nombre interne que vous avez stocké était 3,46499999.... au lieu de 3,465, vous allez obtenir 3,46 au lieu de 3,47 lorsque vous arrondissez le nombre au centime le plus proche. Mais vos calculs en base 10 ont peut-être indiqué que la réponse devait être 3,465 exactement, ce qui devrait clairement être arrondi à 3,47 et non à 3,46. Ce genre de choses se produit occasionnellement dans la vie réelle lorsque vous utilisez des doubles pour des calculs financiers. C'est rare, et le problème passe souvent inaperçu, mais cela arrive.
Si vous utilisez la base 10 pour vos calculs internes au lieu des doubles, les réponses sont toujours exactement ce qui est attendu par les humains, en supposant qu'il n'y ait pas d'autres bugs dans votre code.
Je suis troublé par certaines de ces réponses. Je pense que les doubles et les flottants ont leur place dans les calculs financiers. Certes, lors de l'addition et de la soustraction de montants monétaires non fractionnaires, il n'y aura pas de perte de précision en utilisant les classes integer ou BigDecimal. Mais lorsque vous effectuez des opérations plus complexes, vous obtenez souvent des résultats qui dépassent plusieurs ou plusieurs décimales, quelle que soit la manière dont vous stockez les chiffres. Le problème est de savoir comment présenter le résultat.
Si votre résultat est à la limite entre l'arrondi vers le haut et l'arrondi vers le bas, et que le dernier centime compte vraiment, vous devriez probablement indiquer au spectateur que la réponse se situe presque au milieu - en affichant davantage de décimales.
Le problème avec les doubles, et plus encore avec les flottants, est qu'ils sont utilisés pour combiner de grands nombres et de petits nombres. En java,
System.out.println(1000000.0f + 1.2f - 1000000.0f);
résulte en
1.1875
Les flottants et les doubles sont approximatifs. Si vous créez un BigDecimal et que vous passez un flottant dans le constructeur, vous verrez ce que le flottant représente réellement :
groovy:000> new BigDecimal(1.0F)
===> 1
groovy:000> new BigDecimal(1.01F)
===> 1.0099999904632568359375
ce n'est probablement pas ainsi que vous voulez représenter 1,01 $.
Le problème est que la spécification IEEE ne permet pas de représenter exactement toutes les fractions, certaines d'entre elles finissent par être des fractions répétées, ce qui entraîne des erreurs d'approximation. Étant donné que les comptables aiment que les résultats soient exacts au centime près et que les clients seront mécontents s'ils paient leur facture et qu'après le traitement du paiement, ils doivent 0,01 et se voient facturer des frais ou ne peuvent pas clôturer leur compte, il est préférable d'utiliser des types exacts comme decimal (en C#) ou java.math.BigDecimal en Java.
Ce n'est pas que l'erreur n'est pas contrôlable si vous arrondissez : voir cet article de Peter Lawrey . Il est tout simplement plus facile de ne pas avoir à arrondir en premier lieu. La plupart des applications qui manipulent de l'argent ne font pas appel à beaucoup de mathématiques, les opérations consistent à ajouter des choses ou à allouer des montants à différents compartiments. L'introduction de la virgule flottante et des arrondis ne fait que compliquer les choses.