Java: Pourquoi utiliser BigDecimal au lieu du Double dans le monde réel? [dupliquer]

cette question a déjà une réponse ici:

lorsqu'il s'agit de valeurs monétaires du monde réel, on me conseille D'utiliser le BigDecimal au lieu du Double.Mais je n'ai pas d'explication convaincante sauf, "il est normalement fait de cette façon".

pouvez-vous nous éclairer sur cette question?

49
demandé sur Peter Lawrey 2011-06-12 08:52:01

6 réponses

c'est appelé perte de précision et est très visible quand on travaille avec des nombres très grands ou très petits. La représentation binaire des nombres décimaux avec un radix est dans de nombreux cas, une approximation et non une valeur absolue. Pour comprendre pourquoi vous avez besoin de lire sur la représentation des nombres flottants en binaire. Voici un lien: http://en.wikipedia.org/wiki/IEEE_754-2008 . Voici une démonstration rapide:

en colombie-britannique (arbitraire langage de calcul de précision) avec 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,608333333333333333

0,5416666666666666

0.41666666666666663

Java flotteur:

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 à et à partir d'un seul et même compte (ou peut-être ils sont) vous devez avoir des numéros fiables. Flotteurs binaires ne sont pas fiables-pas à moins que vous comprenez comment ils travaillent et leurs limites.

36
répondu Ярослав Рахматуллин 2011-06-12 06:54:11

je pense que cela décrit la solution à votre problème: pièges Java: grande décimale et le problème avec double ici

du blog original qui semble être en baisse maintenant.

Java Pièges: double

de nombreux pièges se dressent devant l'apprenti programmeur alors qu'il s'engage sur la voie du développement logiciel. Cet article illustre, à travers série d'exemples pratiques, les pièges principaux de L'utilisation des types simples de Java double et float. Notez, cependant, que pour embrasser pleinement la précision dans les calculs numériques vous un livre de texte (ou deux) sur le sujet est nécessaire. Par conséquent, nous ne pouvons qu'effleurer le sujet. Ceci étant dit, les connaissances transmises ici, devraient vous donner les connaissances fondamentales nécessaires pour repérer ou identifier les bogues dans votre code. C'est la connaissance que je pense que n'importe quel développeur de logiciel professionnel devrait être au courant.

  1. les nombres décimaux sont des approximations

    alors que tous les nombres naturels entre 0 - 255 peuvent être décrits avec précision en utilisant 8 bits, la description de tous les nombres réels entre 0.0 - 255.0 nécessite un nombre infini de bits. Premièrement, il existe un nombre infini de nombres à décrire dans cette gamme (même dans la gamme 0.0-0.1), et deuxièmement, certains nombres irrationnels ne peuvent pas être décrits numériquement à tout. Par exemple e et π. En d'autres termes, les nombres 2 et 0.2 sont représentés très différemment dans l'ordinateur.

    Les entiers

    sont représentés par des bits représentant des 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, d'autre part, sont décrites par des bits représentant 2-n, c'est-à-dire 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 les bits séquence 1100 (1/2 + 1/4) .

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

    examinons les conséquences pratiques de ceci 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 vrai, cela peut nous donner un faux sentiment de sécurité. Par coïncidence, 0,2 est l'un des rares valeurs Java est capable de représenter correctement. Lançons un nouveau défi à Java avec un autre problème arithmétique insignifiant, en 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 0.100000001490116119384765625 et 0.1000000000000000055511151231... respectivement. Ces résultats sont corrects pour un ensemble limité de chiffres. les floats ont une précision de 8 chiffres avancés, tandis que le double a 17 chiffres avancés. Maintenant, si le conceptuel discordance entre le résultat attendu 1.0 et les résultats imprimés sur les écrans n'étaient pas assez pour obtenir votre cloches d'alarme allez, remarquez comment les nombres de monsieur. Darcy: diapositives, ne semble pas correspondre aux numéros imprimés! C'est un autre piège. Plus sur cela plus bas.

    ayant été mis au courant des erreurs de calcul dans les scénarios apparemment simples possibles, il est raisonnable de penser à la vitesse à laquelle l'impression peut entrer en jeu. Laissez-nous simplifier la problème pour ajouter seulement trois nombres.

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

    de manière choquante, l'imprécision fait déjà trois additions!

  2. double overflow

    comme pour tout autre type simple en Java, un double est représenté par un ensemble fini de bits. Par conséquent, l'ajout d'une valeur ou la multiplication d'un double peut donner des résultats surprenants. Avouez-le, les nombres doivent être assez grands pour que à débordement, mais ça arrive. Essayons de multiplier et de diviser un grand nombre. L'intuition mathématique dit que le résultat est le nombre d'origine. 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, aucune exception ou autre sorte d'avertissements ne sont envoyés au programmeur. Fondamentalement, cela rend l'expression x * y complètement non fiable en tant que non indication ou garantie est faite dans le cas général pour toutes les valeurs doubles représentées par x, Y.

  3. Grands et petits ne sont pas des 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. Une conséquence de l'utilisation d'un nombre fixe de bits pour représenter les nombres d'exploitation sur de très vastes et très petit nombre dans la même les calculs ne fonctionneront 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'ajout n'a aucun effet! Ceci contredit n'importe quelle (saine) intuition mathématique d'addition, qui dit que donné deux nombres positifs nombres 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 toutes les intuitions que nous avons acquises en classe de mathématiques et la programmation avec des entiers. Utilisez les nombres décimaux avec prudence. Par exemple, l'énoncé for(double d = 0.1; d != 0.3; d += 0.1) est en fait une boucle sans fin! L'erreur est de comparer directement les nombres décimaux entre eux. Vous devez respecter les directives suivantes.

    Éviter les tests d'égalité entre deux nombres décimaux. S'abstenir de if(a == b) {..} , utiliser if(Math.abs(a-b) < tolerance) {..} où la tolérance pourrait être une constante définie par exemple, la double tolérance finale statique publique = 0,01 Considérez comme une alternative 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) sur les plus maladroits for(double d = 0; Math.abs(10.0-d) < tolerance; d+= 0.1) Les deux formulaires ont leurs mérites en fonction de la situation cependant: lorsque l'essai à l'Unité, je préfère exprimer que assertEquals(2.5, d, tolerance) au lieu de dire assertTrue(d > 2.5) non seulement le premier formulaire se lit mieux, il est souvent la vérification 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 typiquement utilisée dans les applications d'interface utilisateur graphique. Cela signifie, "ce que vous voyez est ce que vous obtenez", et est utilisé dans le calcul pour décrire un système dans lequel le contenu affiché pendant l'édition apparaît très similaire à la sortie finale, qui pourrait être un document imprimé, une page web, etc. La phrase a été à l'origine, une phrase d'accroche populaire a été créée par la drag persona de Flip Wilson "Geraldine", qui dirait souvent "ce que vous voyez est ce que vous obtenez" pour excuser son comportement bizarre (de wikipedia).

    un autre piège sérieux dans lequel les programmeurs tombent souvent, pense que les nombres décimaux sont WYSIWYG. Il est impératif de réaliser, que lors de l'impression ou l'écriture d'un nombre décimal, il n'est pas la valeur approximative qui est imprimé/écrit. Autrement dit, Java fait beaucoup de des approximations dans les coulisses, et essaie de façon persistante de vous protéger de le savoir. Il y a juste un problème. Vous devez savoir à propos de ces approximations, sinon vous pourriez faire face à toutes sortes de bogues mystérieux dans votre code.

    Avec un peu d'ingéniosité, cependant, nous pouvons enquêter sur ce qui se passe vraiment derrière la scène. Nous savons maintenant que le nombre 0.1 est représenté avec une certaine approximation.

    System.out.println( 0.1d );
    0.1
    

    nous savons que 0,1 n'est pas 0,1, pourtant 0.1 est imprimé sur l'écran. Conclusion: Java is WYSINWYG!

    pour le bien de la variété, choisissons un autre chiffre innocent, disons 2.3. Comme de 0,1, 2,3 est une valeur approximative. Sans surprise lors de l'impression du nombre Java cache l'approximation.

    System.out.println( 2.3d );
    2.3
    

    pour étudier ce que peut être la valeur interne approximative de 2.3, nous pouvons comparer le nombre à d'autres nombres dans une gamme étroite.

    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
    

    So 2.29999999999997 est autant 2.3 que la valeur 2.3! Notez également qu'en raison de l'approximation, le point de pivot est ..99997 et non ..99995 où vous ordinairement ronde ronde en mathématiques. Une autre façon de s'attaquer à la valeur approximative est de 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 seulement BigDecimal. BigDecimal a sa propre collection de pièges documentés ici.

    Rien n'est facile, et rarement rien n'est gratuit. Et" naturellement", les flotteurs et les doubles donnent des résultats différents lorsqu'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 de flotteur a 24 bits significatifs tandis qu'une double approximation a 53 bits significatifs. Le moral est que pour préserver les valeurs, vous devez lire et écrire des nombres décimaux dans le même format.

  6. Division par 0

    beaucoup de développeurs savent par expérience que diviser un nombre par zéro donne une fin abrupte de leurs applications. On trouve un comportement similaire en Java lorsqu'on opère sur int, mais assez étonnamment, pas lorsqu'on opère sur double. Tout nombre, à l'exception de zéro, divisé par zéro rendement, respectivement × ou×. En divisant zéro avec zéro, on obtient la NaN spéciale, la valeur non 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 avec 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 bizarre

    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. Sûrement Z > N, l'ensemble de tous les nombres rationnels est infiniment plus grand que l'ensemble des nombres naturels, mais c'était à propos de la limite de mon intuition à ce sujet!

    heureusement, l'infini en Java est d'environ comme imprévisible comme 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 à l'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 problème principal ici est que la valeur NaN est retournée sans aucun avertissement. Par conséquent, si vous enquêtez bêtement si un double en particulier est pair ou impair, vous pouvez vraiment entrer dans une situation chevelue. Peut-être une exception d'exécution aurait été plus approprié?

    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
    

    tout à coup, votre variable n'est ni étrange ni égale! NaN est encore plus étrange que L'infini Une valeur infinie est différente de la valeur maximale d'un lit 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
    

    en général, lorsqu'un double a acquis la valeur NaN, toute opération sur celle-ci entraîne une NaN.

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

    1. les nombres décimaux sont des approximations, pas la valeur que vous assignez. Toute intuition acquise dans le monde des maths ne s'applique plus. Expect a+b = a et a != a/3 + a/3 + a/3
    2. éviter D'utiliser le==, comparer avec une certaine tolérance ou utiliser le > = ou < = opérateurs
    3. Java is WYSINWYG! Ne croyez jamais que la valeur que vous imprimez/écrivez est une valeur approximative, donc toujours lire/écrire des nombres décimaux dans le même format.
    4. Be attention de ne pas déborder votre double, pour ne pas obtenir votre double dans un État d'infini ou NaN. Dans les deux cas, vos calculs peuvent ne pas se révéler comme vous l'attendez. Vous pouvez trouver une bonne idée de toujours vérifier contre ces valeurs avant de retourner une valeur dans vos méthodes.
31
répondu zengr 2014-08-26 18:02:01

alors que BigDecimal peut stocker plus de précision que double, ce n'est généralement pas nécessaire. La véritable raison pour laquelle il a été utilisé parce qu'il indique clairement comment l'arrondissement est effectuée, y compris un certain nombre de différentes stratégies d'arrondissement. Vous pouvez obtenir les mêmes résultats avec double dans la plupart des cas, mais à moins que vous ne connaissiez les techniques requises, BigDecimal est la voie à suivre dans ce cas.

un exemple courant, c'est l'argent. Même si l'argent ne sera pas assez grand pour avoir besoin de la précision du BigDecimal dans 99% des cas d'utilisation, il est souvent considéré comme la meilleure pratique D'utiliser le BigDecimal parce que le contrôle de l'arrondissement est dans le logiciel qui évite le risque que le développeur fera une erreur dans la manipulation de l'arrondissement. Même si vous êtes sûr que vous pouvez gérer l'arrondissement avec double je vous suggère d'utiliser des méthodes d'aide pour effectuer l'arrondissement que vous testez à fond.

28
répondu Peter Lawrey 2011-06-12 09:27:52

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

1
répondu Sai 2011-06-12 04:55:58

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

bien qu'il soit beaucoup plus lent et plus long, il en vaut la peine.

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

0
répondu Jason 2011-06-12 04:55:32

une Autre idée: garder une trace du nombre de centimes dans un long . Ceci est plus simple, et évite la syntaxe encombrante et les performances lentes de BigDecimal .

la précision dans les calculs financiers est très importante parce que les gens deviennent très fâchés quand leur argent disparaît en raison d'erreurs d'arrondissement, ce qui est pourquoi double est un choix terrible pour traiter avec l'argent.

0
répondu Jonathan Paulson 2012-10-03 15:02:11