BigDecimal VS Flottant (float, double)

De jerome_meillant dans Menu mobile

24 sept 2012

Java nous trompe lorsqu’il s’agit d’afficher la valeur d’un double ou d’un float.
double d1 = 0.1; System.out.println(« d1 =>  » + d1); float f1 = 0.1f; System.out.println(« f1 =>  » + f1);
d1 => 0.1
f1 => 0.1

La console affiche 0.1 alors qu’en représentation flottante la valeur 0.1 ne peut pas être représentée de manière exacte.


Pour afficher la valeur exacte représentée en mémoire on peut utiliser la classe BigDecimal:
BigDecimal b1 = new BigDecimal(d1); System.out.println(« b1 =>  » + b1); BigDecimal b2 = new BigDecimal(f1); System.out.println(« b2 =>  » + b2);
b1 => 0.1000000000000000055511151231257827021181583404541015625
b2 => 0.100000001490116119384765625

Dans le cas d’un double la valeur s’approchant le plus de 0.1 est 0.1000000000000000055511151231257827021181583404541015625
C’est pourquoi :
0.1 == 0.1000000000000000055511151231257827021181583404541015625
renvoie true
Les approximations étant différentes en fonction du type de flottant (double ou float) utilisé
System.out.println(0.1d == 0.1f);
Renvoie false car
System.out.println(0.1000000000000000055511151231257827021181583404541015625f == 0.100000001490116119384765625d)
Renvoie false

Du fait de ces approximations il ne faut jamais écrire d’algorithme strictement basé sur l’égalité des flottants. Ou alors il faut introduire une marge d’erreur. Comme par exemple
System.out.println(0.1d – 0.1f < 0.000001);
Qui renvoie true

Du fait encore de ses approximations un enchainement de calculs entrainera forcément une accumulation d’erreurs d’approximation.
double d2 = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1; System.out.println(« d2 =>  » + d2);
d2 => 0.9999999999999999
et du coup
System.out.println(0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 == 1);
renvoie false

Encore un autre exemple qui démontre que les float (et double) sont à proscrire dans les calculs financiers :
float sum = 1000000F + 0.05F; System.out.println(« sum =>  » + sum);
sum => 1000000.06
1 centime d’erreur a été introduit à cause de l’imprécision du float codé sur 32bit
Il existe par contre certaines valeurs qui peuvent être représentées de manière exacte. Comme par exemple: 0.5 = 1 / 21
0.25 = 1 / 2^2
0.125 = 1 / 2^3
0.0625 = 1 / 2^4
0.03125 = 1 / 2^5
0.015625 = 1 / 2^6
0.0078125 = 1 / 2^7
… ainsi que toute combinaison d’addition/soustraction des éléments de la suite introduite ci dessus. Mais la proportion de valeurs qui ne peuvent pas être représentées est infiniment plus grande.

Dans des cas d’utilisations ou la précision du calcul est importante notamment dans les calculs financiers, il est indispensable d’utiliser une structure de donnée qui est capable de mettre en cache la valeur réelle d’un nombre décimal sous la forme d’un String par exemple comme c’est le cas pour la class java.math.BigDecimal.
Les instances de BigDecimal sont immutables tout comme Integer, Long … Les opérations effectués renvoient donc à chaque fois une nouvelle instance immutable et ne change pas l’état interne de l’instance sur laquelle l’opération a été appelée.

BigDecimal Best Practices

Dans la mesure du possible : toujours initialiser un BigDecimal avec une chaîne de caractère.
Exemple:
BigDecimal b3 = new BigDecimal(« 0.1″); System.out.println(« b3 =>  » + b3);
b3 => 0.1

Il est possible d’initialiser un BigDecimal avec un flottant mais dans ce cas on retombe dans le travers des approximations :
BigDecimal b4 = new BigDecimal(0.1); System.out.println(« b4 =>  » + b4); BigDecimal b5 = new BigDecimal(0.1F); System.out.println(« b5 =>  » + b5);
b4 => 0.1000000000000000055511151231257827021181583404541015625
b5 => 0.100000001490116119384765625

Donc si vous avez comme entrée des flottants traduisez les en String avant de construire un BigDecimal.
BigDecimal b6 = new BigDecimal(0.1 + «  »); System.out.println(« b6 =>  » + b6); BigDecimal b7 = new BigDecimal(0.1F + «  »); System.out.println(« b7 =>  » + b7);
b6 => 0.1
b7 => 0.1

Ne pas écrire :
BigDecimal b8 = new BigDecimal(0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + «  »); System.out.println(« b8 =>  » + b8);
b8 => 0.9999999999999999
Mais
BigDecimal b9 = new BigDecimal(0.1 + «  »).add(new BigDecimal(0.1 + «  »)) .add(new BigDecimal(0.1 + «  »)).add(new BigDecimal(0.1 + «  »)).add(new BigDecimal(0.1 + «  »)) .add(new BigDecimal(0.1 + «  »)).add(new BigDecimal(0.1 + «  »)).add(new BigDecimal(0.1 + «  »)) .add(new BigDecimal(0.1 + «  »)).add(new BigDecimal(0.1 + «  »)); System.out.println(« b9 =>  » + b9);
b9 => 1.0

La classe BigDecimal permet une gestion plus poussée au niveau des arrondis et de l’affichage des valeurs.
BigDecimal b10 = new BigDecimal(« 9.994″).setScale(2, RoundingMode.HALF_UP); System.out.println(« b10 =>  » + b10);
b10 => 9.99
BigDecimal b11 = new BigDecimal(« 9.995″).setScale(2, RoundingMode.HALF_UP); System.out.println(« b11 =>  » + b11);
b11 => 10.00
Ici on voit bien que le formattage est adéquat pour un contexte monétaire.
Faire attention : setScale doit être appelé au bon moment en fonction du cas d’utilisation

Il reste que l’objet BigDecimal ne résout pas les problèmes arithmétiques classiques tel que, par exemple, la somme des arrondis n’est pas tout le temps égale à l’arrondi de la somme :
BigDecimal b12 = new BigDecimal(« 9.99″).setScale(2, RoundingMode.HALF_UP); BigDecimal percent = new BigDecimal(« 0.05″); BigDecimal result1 = BigDecimal.ZERO; for (int i = 0; i  » + result1.setScale(2, RoundingMode.HALF_UP)); result1 = BigDecimal.ZERO; for (int i = 0; i  » + result1);
arrondi de la somme => 49.95
somme des arrondis => 50.00

Il faudra alors s’adapter en fonction des cas d’utilisations pour faire les arrondis aux endroits propices

3 Réponses pour BigDecimal VS Flottant (float, double)

Avatar

julien

octobre 3rd, 2012 à 9 h 11 min

Pourquoi faire simple quand on peut le faire en Java…

Qu’est-ce qui fait que b1 = 0.1000000000000000055511151231257827021181583404541015625 ?

Le processeur (un certain Pentium était buggé sur ce genre de calculs) ou l’implémentation dans Java ?

Avatar

rouleau

octobre 4th, 2012 à 14 h 45 min

la passage binaire à décimal.

Avatar

jerome_meillant

octobre 8th, 2012 à 17 h 22 min

Tout à fait, rien à voir avec Java, le résultat sera le même quel que soit le langage.
Le problème est dû à la façon dont les nombres flottants sont stockés en mémoire :
double = 64 bits [1 bit de signe, 11 bits pour l’exposant et 52 bits pour la mantisse]
0.1 = 0 x 0.5 + 0 x 0.25 + 0 x 0.125 + 1 x 0.0625 + 1 x 0.03125 + … = approximation 0.1000000000000000055511151231257827021181583404541015625

La classe Java BigDecimal donne un contrôle total sur le comportement de l’arrondi ; si aucun arrondi n’est indiqué et que le résultat exacte ne peut être représenté une exception est levée.
Voir java doc : http://docs.oracle.com/javase/1.5.0/docs/api/java/math/BigDecimal.html

Commentaire

+ deux = 3

iMDEO recrute !

REJOIGNEZ-NOUS

A la recherche de nouveaux talents (développeurs web et mobile, chefs de projet,...)

Voir les annonces