web-dev-qa-db-fra.com

Java: Pourquoi devrions-nous utiliser BigDecimal au lieu de Double dans le monde réel?

Lorsqu’il s’agit de valeurs monétaires réelles, on me conseille d’utiliser BigDecimal au lieu de Double.Mais je n’ai pas d’explication convaincante, si ce n’est "C'est normalement fait ainsi".

Pouvez-vous s'il vous plaît éclaircir cette question?

50
Vinoth Kumar C M

C'est ce qu'on appelle la perte de précision et est très visible lorsque l'on travaille avec de très grands nombres ou de très petits nombres. La représentation binaire des nombres décimaux avec une base est dans de nombreux cas une approximation et non une valeur absolue. Pour comprendre pourquoi vous devez lire la représentation des nombres flottants en binaire. Voici un lien: http://en.wikipedia.org/wiki/IEEE_754-2008 . Voici une démonstration rapide: 
in bc (langage pour calculatrice à précision arbitraire) avec la précision = 10:

(1/3 + 1/12 + 1/8 + 1/30) = 0,6083333332
(1/3 + 1/12 + 1/8) = 0,541666666666666
(1/3 + 1/12) = 0,416666666666666

Java double:
0.6083333333333333
0.541666666666666666
0.4166666666666666663

Java float:

0.60833335
0.5416667
0.4166667


Si vous êtes une banque et êtes responsable de milliers de transactions chaque jour, même si elles ne sont pas vers et depuis un même compte (ou peut-être sont-elles), vous devez disposer de numéros fiables. Les flottants binaires ne sont pas fiables - à moins de comprendre leur fonctionnement et leurs limites.

Je pense que cela décrit la solution à votre problème: Java Traps: Big Decimal et le problème avec double ici

Du blog original qui semble être en panne maintenant.

pièges Java: double

De nombreux pièges sont devant l'apprenti programmeur alors qu'il se dirige vers le développement de logiciels. Cet article illustre, à travers une série d’exemples pratiques, les principaux pièges de l’utilisation des types simples de Java, double et float. Notez, cependant, que pour bien comprendre la précision des calculs numériques, vous devez disposer d’un manuel (ou deux) sur le sujet. Par conséquent, nous ne pouvons que gratter la surface du sujet. Cela étant dit, les connaissances transmises ici devraient vous fournir les connaissances fondamentales nécessaires pour détecter ou identifier les bogues dans votre code. Je pense que c'est un savoir que tout développeur de logiciel professionnel devrait connaître.

  1. Les nombres décimaux sont des approximations

    Bien que tous les nombres naturels compris entre 0 et 255 puissent être décrits avec précision à l'aide de 8 bits, la description de tous les nombres réels compris entre 0.0 et 255.0 nécessite un nombre infini de bits. Premièrement, il existe une infinité de nombres à décrire dans cette plage (même dans la plage de 0,0 à 0,1), et deuxièmement, certains nombres irrationnels ne peuvent absolument pas être décrits numériquement. Par exemple, e et π. En d'autres termes, les nombres 2 et 0,2 sont représentés de manière très différente dans l'ordinateur.

    Les entiers sont représentés par des bits représentant les valeurs 2n, où n est la position du bit. Ainsi, la valeur 6 est représentée par 23 * 0 + 22 * 1 + 21 * 1 + 20 * 0 correspondant à la séquence de bits 0110. Les décimales, par contre, sont décrites par des bits représentant 2-n, à savoir les fractions 1/2, 1/4, 1/8,... Le nombre 0,75 correspond à 2-1 * 1 + 2-2 * 1 + 2-3 * 0 + 2-4 * 0 donnant la séquence de bits 1100 (1/2 + 1/4).

    Forts de cette connaissance, nous pouvons formuler la règle empirique suivante: Tout nombre décimal est représenté par une valeur approximative.

    Examinons les conséquences pratiques de cette situation en effectuant une série de multiplications triviales.

    System.out.println( 0.2 + 0.2 + 0.2 + 0.2 + 0.2 );
    1.0
    

    1.0 est imprimé. Bien que cela soit effectivement correct, cela peut nous donner un faux sentiment de sécurité. Par coïncidence, 0,2 est l’une des rares valeurs que Java puisse représenter correctement. Défions encore Java avec un autre problème arithmétique trivial, ajoutant le nombre 0.1 dix fois.

    System.out.println( 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f );
    System.out.println( 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d );
    
    1.0000001
    0.9999999999999999
    

    Selon les diapositives du blog de Joseph D. Darcy, les sommes des deux calculs sont respectivement 0.100000001490116119384765625 et 0.1000000000000000055511151231.... Ces résultats sont corrects pour un ensemble limité de chiffres. les flottants ont une précision de 8 chiffres premiers, alors que double a une précision de 17 premiers chiffres. Maintenant, si le décalage conceptuel entre le résultat attendu 1.0 et les résultats imprimés sur les écrans ne suffisait pas pour déclencher les sonnettes d’alarme, remarquez comment les chiffres de mr. Les diapositives de Darcy ne semblent pas correspondre aux numéros imprimés! C'est un autre piège. Plus à ce sujet plus bas.

    Après avoir pris conscience de calculs erronés dans des scénarios apparemment simples, il est raisonnable de penser à la rapidité avec laquelle l'impression peut se produire. Simplifions le problème en ajoutant seulement trois nombres.

    System.out.println( 0.3 == 0.1d + 0.1d + 0.1d );
    false
    

    Étonnamment, l’imprécision s’impose déjà à trois ajouts!

  2. Double débordement

    Comme avec tout autre type simple en Java, un double est représenté par un ensemble fini de bits. Par conséquent, ajouter une valeur ou multiplier un double peut donner des résultats surprenants. Certes, les chiffres doivent être assez gros pour déborder, mais cela arrive. Essayons de multiplier puis de diviser un grand nombre. L'intuition mathématique dit que le résultat est le nombre original. En Java, nous pouvons obtenir un résultat différent.

    double big = 1.0e307 * 2000 / 2000;
    System.out.println( big == 1.0e307 );
    false
    

    Le problème ici est que grand est d'abord multiplié, débordant, puis le nombre débordé est divisé. Pire encore, aucune exception ou autre type d’avertissement n’est signalé au programmeur. Cela rend fondamentalement l'expression x * y complètement non fiable, aucune indication ou garantie n'étant fournie dans le cas général pour toutes les valeurs doubles représentées par x, y.

  3. Grand et petit ne sont pas amis!

    Laurel et Hardy étaient souvent en désaccord sur beaucoup de choses. De même en informatique, grands et petits ne sont pas amis. L'utilisation d'un nombre fixe de bits pour représenter des nombres a pour conséquence que le fait d'opérer sur des nombres très grands et très petits dans les mêmes calculs ne fonctionnera pas comme prévu. Essayons d'ajouter quelque chose de petit à quelque chose de grand.

    System.out.println( 1234.0d + 1.0e-13d == 1234.0d );
    true
    

    L'addition n'a aucun effet! Cela contredit toute intuition mathématique (saine) de l'addition, qui dit que, étant donné deux nombres, les nombres positifs d et f, puis d + f> d.

  4. Les nombres décimaux ne peuvent pas être comparés directement

    Ce que nous avons appris jusqu’à présent, c’est que nous devons jeter toute intuition que nous avons acquise en cours de mathématiques et en programmation avec des nombres entiers. Utilisez les nombres décimaux avec prudence. Par exemple, l'instruction for(double d = 0.1; d != 0.3; d += 0.1) est en réalité une boucle sans fin déguisée! L'erreur est de comparer les nombres décimaux directement les uns aux autres. Vous devez respecter les lignes directrices suivantes.

    Évitez les tests d'égalité entre deux nombres décimaux. Abstenez-vous de if(a == b) {..}, utilisez if(Math.abs(a-b) < tolerance) {..} où la tolérance pourrait être une constante définie par exemple. public static final double tolérance = 0.01 Envisagez plutôt d'utiliser les opérateurs <,> car ils peuvent décrire plus naturellement ce que vous voulez exprimer. Par exemple, je préfère la forme for(double d = 0; d <= 10.0; d+= 0.1) plutôt que la plus maladroite for(double d = 0; Math.abs(10.0-d) < tolerance; d+= 0.1) Les deux formes ont leurs avantages en fonction de la situation: lorsque je teste des unités, je préfère exprimer cette assertEquals(2.5, d, tolerance) plutôt que assertTrue(d > 2.5) non seulement la première forme est-elle mieux lue, mais c'est souvent le chèque que vous voulez faire (c.-à-d. Que d n'est pas trop grand).

  5. WYSINWYG - Ce que vous voyez n'est pas ce que vous obtenez

    WYSIWYG est une expression généralement utilisée dans les applications d'interface utilisateur graphique. Cela signifie "Ce que vous voyez est ce que vous obtenez" et est utilisé en informatique pour décrire un système dans lequel le contenu affiché lors de la modification est très similaire au résultat final, qui peut être un document imprimé, une page Web, etc. Cette phrase était à l’origine un slogan populaire créé par "Geraldine", le personnage de drag de Flip Wilson, qui disait souvent "Ce que vous voyez est ce que vous obtenez" pour excuser son comportement insolite (tiré de wikipedia).

    Un autre piège grave auquel se heurtent souvent les programmeurs, pense que les nombres décimaux sont WYSIWYG. Il est impératif de réaliser que, lors de l’impression ou de l’écriture d’un nombre décimal, ce n’est pas la valeur approximative qui doit être imprimée/écrite. Formulé différemment, Java fait beaucoup d'approximations en coulisse et essaie constamment de vous protéger de le savoir. Il y a juste un problème. Vous devez connaître ces approximations, sinon vous risquez de rencontrer toutes sortes de bugs mystérieux dans votre code.

    Avec un peu d'ingéniosité, cependant, nous pouvons enquêter sur ce qui se passe réellement dans les coulisses. Nous savons maintenant que le nombre 0.1 est représenté avec une approximation.

    System.out.println( 0.1d );
    0.1
    

    Nous savons que 0,1 n’est pas 0,1, pourtant 0,1 est imprimé à l’écran. Conclusion: Java est WYSINWYG!

    Par souci de variété, choisissons un autre chiffre innocent, par exemple 2.3. Comme 0.1, 2.3 est une valeur approximative. Sans surprise, lors de l'impression du nombre Java masque l'approximation.

    System.out.println( 2.3d );
    2.3
    

    Pour rechercher quelle peut être la valeur approximative interne de 2,3, nous pouvons comparer le nombre à d'autres nombres proches.

    double d1 = 2.2999999999999996d;
    double d2 = 2.2999999999999997d;
    System.out.println( d1 + " " + (2.3d == d1) );
    System.out.println( d2 + " " + (2.3d == d2) );
    2.2999999999999994 false
    2.3 true
    

    Donc, 2.2999999999999997 vaut tout autant 2,3 que la valeur 2,3! Notez également qu'en raison de l'approximation, le point de pivotement est à ..99997 et non à ..99995 où vous arrondissez normalement les arrondis en maths. Pour vous familiariser avec la valeur approximative, vous pouvez également faire appel aux services de BigDecimal.

    System.out.println( new BigDecimal(2.3d) );
    2.29999999999999982236431605997495353221893310546875
    

    Maintenant, ne vous reposez pas sur vos lauriers en pensant que vous pouvez simplement quitter le navire et utiliser uniquement BigDecimal. BigDecimal a sa propre collection de pièges documentée ici.

    Rien n'est facile, et rarement rien ne vient gratuitement. Et "naturellement", les flottants et les doubles donnent des résultats différents quand ils sont imprimés/écrits.

    System.out.println( Float.toString(0.1f) );
    System.out.println( Double.toString(0.1f) );
    System.out.println( Double.toString(0.1d) );
    0.1
    0.10000000149011612
    0.1
    

    Selon les diapositives du blog de Joseph D. Darcy, une approximation flottante a 24 bits significatifs, tandis qu'une approximation double contient 53 bits significatifs. Le moral est le suivant: pour préserver les valeurs, vous devez lire et écrire des nombres décimaux dans le même format.

  6. Division par 0

    De nombreux développeurs savent par expérience que diviser un nombre par zéro entraîne une interruption brutale de leurs applications. Un comportement similaire a été trouvé, Java lorsqu’il fonctionne sur des int, mais assez surprenant, pas lorsqu’il fonctionne sur des doubles. N'importe quel nombre, à l'exception de zéro, divisé par zéro, donne respectivement ou -∞. La division de zéro à zéro donne NaN spécial, la valeur Pas un nombre.

    System.out.println(22.0 / 0.0);
    System.out.println(-13.0 / 0.0);
    System.out.println(0.0 / 0.0);
    Infinity
    -Infinity
    NaN
    

    La division d'un nombre positif par un nombre négatif donne un résultat négatif, tandis que la division d'un nombre négatif par un nombre négatif donne un résultat positif. Puisque la division par zéro est possible, vous obtiendrez un résultat différent selon que vous divisez un nombre par 0.0 ou -0.0. Oui c'est vrai! Java a un zéro négatif! Ne vous y trompez pas, les deux valeurs zéro sont égales, comme indiqué ci-dessous.

    System.out.println(22.0 / 0.0);
    System.out.println(22.0 / -0.0);
    System.out.println(0.0 == -0.0);
    Infinity
    -Infinity
    true
    
  7. L'infini est étrange

    Dans le monde des mathématiques, l'infini était un concept que j'avais du mal à saisir. Par exemple, je n'ai jamais acquis d'intuition pour quand un infini était infiniment plus grand qu'un autre. Certes, Z> N, l’ensemble des nombres rationnels est infiniment plus grand que celui des nombres naturels, mais c’était à peu près la limite de mon intuition à cet égard!

    Heureusement, l'infini dans Java est à peu près aussi imprévisible que l'infini dans le monde mathématique. Vous pouvez effectuer les suspects habituels (+, -, *,/sur une valeur infinie, mais vous ne pouvez pas appliquer un infini à un infini.

    double infinity = 1.0 / 0.0;
    System.out.println(infinity + 1);
    System.out.println(infinity / 1e300);
    System.out.println(infinity / infinity);
    System.out.println(infinity - infinity);
    Infinity
    Infinity
    NaN
    NaN
    

    Le principal problème ici est que la valeur NaN est renvoyée sans aucun avertissement. Par conséquent, si vous cherchez bêtement si un double est pair ou impair, vous pouvez vraiment vous retrouver dans une situation délicate. Peut-être qu'une exception d'exécution aurait été plus appropriée?

    double d = 2.0, d2 = d - 2.0;
    System.out.println("even: " + (d % 2 == 0) + " odd: " + (d % 2 == 1));
    d = d / d2;
    System.out.println("even: " + (d % 2 == 0) + " odd: " + (d % 2 == 1));
    even: true odd: false
    even: false odd: false
    

    Du coup, votre variable n'est ni impair ni pair! NaN est encore plus étrange que l'infini. Une valeur infinie est différente de la valeur maximale d'un double et NaN est encore différente de la valeur infinie.

    double nan = 0.0 / 0.0, infinity = 1.0 / 0.0;
    System.out.println( Double.MAX_VALUE != infinity );
    System.out.println( Double.MAX_VALUE != nan );
    System.out.println( infinity         != nan );
    true
    true
    true
    

    Généralement, lorsqu'un double a acquis la valeur NaN, toute opération sur celle-ci donne un NaN.

    System.out.println( nan + 1.0 );
    NaN
    
  8. Conclusions

    1. Les nombres décimaux sont des approximations, pas la valeur que vous attribuez. Toute intuition acquise dans le monde mathématique ne s'applique plus. Attendez-vous à a+b = a et a != a/3 + a/3 + a/3
    2. Évitez d’utiliser le ==, comparez-le à une certaine tolérance ou utilisez les opérateurs> = ou <=
    3. Java c'est WYSINWYG! Ne croyez jamais que la valeur que vous imprimez/écrivez est une valeur approximative, par conséquent, lisez/écrivez toujours des nombres décimaux dans le même format.
    4. Veillez à ne pas déborder de votre double, ne pas mettre votre double dans un état de ± Infinity ou NaN. Dans les deux cas, vos calculs risquent de ne pas donner les résultats escomptés. Il peut être judicieux de toujours vérifier ces valeurs avant de renvoyer une valeur dans vos méthodes.
32
zengr

Bien que BigDecimal puisse stocker plus de précision que le double, cela n’est généralement pas requis. La vraie raison pour laquelle il a été utilisé est qu'il explique clairement comment l'arrondi est effectué, y compris différentes stratégies d'arrondi. Vous pouvez obtenir les mêmes résultats avec double dans la plupart des cas, mais si vous ne connaissez pas les techniques requises, BigDecimal est la solution. 

Un exemple courant, c'est l'argent. Même si l'argent ne sera pas assez important pour avoir besoin de la précision de BigDecimal dans 99% des cas d'utilisation, il est souvent considéré comme la meilleure pratique d'utiliser BigDecimal car le contrôle de l'arrondi se fait dans le logiciel, ce qui évite le risque que le développeur risque de faire. une erreur dans le traitement de l'arrondissement. Même si vous êtes sûr de pouvoir arrondir avec double, je vous suggère d'utiliser des méthodes auxiliaires pour effectuer l'arrondi que vous testez minutieusement.

29
Peter Lawrey

Ceci est principalement fait pour des raisons de précision. BigDecimal stocke les nombres en virgule flottante avec une précision illimitée. Vous pouvez jeter un oeil à cette page qui l'explique bien. http://blogs.Oracle.com/CoreJavaTechTips/entry/the_need_for_bigdecimal

1
Sai

Lorsque BigDecimal est utilisé, il peut stocker beaucoup plus de données que Double, ce qui le rend plus précis et constitue un meilleur choix pour le monde réel.

Bien que ce soit beaucoup plus lent et plus long, cela en vaut la peine.

Je parie que vous ne voudriez pas donner à votre patron des informations inexactes, hein?

0
Jason

Une autre idée: garder une trace du nombre de cents dans une long. Ceci est plus simple et évite la syntaxe fastidieuse et la lenteur des performances de BigDecimal.

La précision des calculs financiers est extrêmement importante car les gens sont très en colère lorsque leur argent disparaît à cause d’erreurs d’arrondi. C’est pourquoi double est un choix terrible en matière d’argent.

0
Jonathan Paulson