Java: ¿Por qué deberíamos usar BigDecimal en lugar de Double en el mundo real?

Cuando se trata de valores monetarios del mundo real, se me aconseja utilizar BigDecimal en lugar de Double. Pero no tengo una explicación convincente excepto que “normalmente se hace de esa manera”.

¿Puedes por favor arrojar luz sobre esta pregunta?

Se llama pérdida de precisión y es muy notable cuando se trabaja con números muy grandes o muy pequeños. La representación binaria de números decimales con una raíz es en muchos casos una aproximación y no un valor absoluto. Para entender por qué necesita leer la representación de números flotantes en binario. Aquí hay un enlace: http://en.wikipedia.org/wiki/IEEE_754-2008 . Aquí hay una demostración rápida:
en bc (Un lenguaje de calculadora de precisión arbitraria) con precisión = 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 doble:
0.6083333333333333
0.5416666666666666
0.41666666666666663

Flotador de Java:

0.60833335
0.5416667
0.4166667

Si usted es un banco y es responsable de miles de transacciones todos los días, aunque no sean de y para una misma cuenta (o tal vez lo sean), debe tener números confiables. Los flotadores binarios no son confiables, a menos que usted entienda cómo funcionan y sus limitaciones.

Creo que esto describe la solución a su problema: Java Traps: Big Decimal y el problema con el doble aquí

Del blog original que parece estar abajo ahora.

Trampas Java: doble

Muchas trampas se encuentran ante el aprendiz de progtwigdor mientras recorre el camino del desarrollo de software. Este artículo ilustra, a través de una serie de ejemplos prácticos, las principales trampas del uso de los tipos simples de Java double y float. Sin embargo, tenga en cuenta que para abarcar completamente la precisión en los cálculos numéricos, se necesita un libro de texto (o dos) sobre el tema. En consecuencia, solo podemos arañar la superficie del tema. Una vez dicho esto, el conocimiento transmitido aquí, debería proporcionarle los conocimientos fundamentales necesarios para detectar o identificar errores en su código. Es un conocimiento que creo que cualquier desarrollador profesional de software debe tener en cuenta.

  1. Los números decimales son aproximaciones

    Si bien todos los números naturales entre 0 y 255 se pueden describir con precisión utilizando 8 bits, describir todos los números reales entre 0.0 – 255.0 requiere un número infinito de bits. En primer lugar, existen infinitos números para describir en ese rango (incluso en el rango de 0.0 – 0.1), y en segundo lugar, ciertos números irracionales no pueden describirse numéricamente en absoluto. Por ejemplo, e y π. En otras palabras, los números 2 y 0.2 están enormemente representados en la computadora.

    Los enteros están representados por bits que representan valores 2n donde n es la posición del bit. Por lo tanto, el valor 6 se representa como 23 * 0 + 22 * 1 + 21 * 1 + 20 * 0 correspondiente a la secuencia de bits 0110. Los decimales, por otro lado, se describen mediante bits que representan 2-n, es decir, las fracciones 1/2, 1/4, 1/8,... El número 0.75 corresponde a 2-1 * 1 + 2-2 * 1 + 2-3 * 0 + 2-4 * 0 produce la secuencia de bits 1100 (1/2 + 1/4) .

    Equipado con este conocimiento, podemos formular la siguiente regla general: cualquier número decimal se representa con un valor aproximado.

    Investiguemos las consecuencias prácticas de esto al realizar una serie de multiplicaciones triviales.

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

    1.0 está impreso. Si bien esto es cierto, puede darnos una falsa sensación de seguridad. Casualmente, 0.2 es uno de los pocos valores que Java puede representar correctamente. Vamos a desafiar a Java nuevamente con otro problema aritmético trivial, agregando el número 0.1 diez veces.

     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 

    De acuerdo con las diapositivas del blog de Joseph D. Darcy, las sums de los dos cálculos son 0.100000001490116119384765625 y 0.1000000000000000055511151231... respectivamente. Estos resultados son correctos para un conjunto limitado de dígitos. los flotadores tienen una precisión de 8 dígitos principales, mientras que el doble tiene 17 dígitos principales de precisión. Ahora, si la falta de correspondencia conceptual entre el resultado esperado 1.0 y los resultados impresos en las pantallas no fue suficiente para encender las alarmas, entonces observe cómo los números de mr. ¡Las diapositivas de Darcy no parecen corresponder a los números impresos! Esa es otra trampa. Más sobre esto más abajo.

    Habiendo tenido conocimiento de cálculos erróneos en escenarios aparentemente simples, es razonable contemplar qué tan rápido puede aparecer la impresión. Permítanos simplificar el problema para agregar solo tres números.

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

    Sorprendentemente, la imprecisión ya comienza con tres adiciones.

  2. Doble desbordamiento

    Como con cualquier otro tipo simple en Java, un doble está representado por un conjunto finito de bits. En consecuencia, agregar un valor o multiplicar un doble puede arrojar resultados sorprendentes. Admitidamente, los números tienen que ser bastante grandes para desbordarse, pero sucede. Probemos multiplicando y luego dividiendo un gran número. La intuición matemática dice que el resultado es el número original. En Java podemos obtener un resultado diferente.

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

    El problema aquí es que primero se multiplica primero, se desborda y luego se divide el número desbordado. Peor aún, no se exponen excepciones u otros tipos de advertencias al progtwigdor. Básicamente, esto hace que la expresión x * y no sea completamente confiable ya que no se hace ninguna indicación o garantía en el caso general para todos los valores dobles representados por x, y.

  3. Grandes y pequeños no son amigos!

    Laurel y Hardy a menudo estaban en desacuerdo sobre muchas cosas. Del mismo modo, en informática, grandes y pequeños no son amigos. Una consecuencia del uso de un número fijo de bits para representar números es que operar en números realmente grandes y realmente pequeños en los mismos cálculos no funcionará como se esperaba. Tratemos de agregar algo pequeño a algo grande.

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

    ¡La adición no tiene efecto! Esto contradice cualquier (lógica) intuición matemática de la sum, que dice que dados dos números números positivos d y f, entonces d + f> d.

  4. Los números decimales no se pueden comparar directamente

    Lo que hemos aprendido hasta ahora es que debemos deshacernos de toda la intuición que hemos adquirido en la clase de matemáticas y la progtwigción con números enteros. Use números decimales con precaución. Por ejemplo, el enunciado for(double d = 0.1; d != 0.3; d += 0.1) es en efecto un bucle interminable disfrazado. El error es comparar números decimales directamente entre sí. Debe cumplir con las siguientes pautas.

    Evite pruebas de igualdad entre dos números decimales. Abstenerse de if(a == b) {..} , use if(Math.abs(ab) < tolerance) {..} donde la tolerancia podría ser una constante definida como p. Ej if(a == b) {..} , if(Math.abs(ab) < tolerance) {..} pública doble tolerancia final = 0.01 Considere como alternativa para utilizar los operadores <,> ya que pueden describir más naturalmente lo que desea express. Por ejemplo, prefiero la forma for(double d = 0; d <= 10.0; d+= 0.1) sobre la más torpe for(double d = 0; Math.abs(10.0-d) < tolerance; d+= 0.1) Ambos sin embargo, las formas tienen sus méritos dependiendo de la situación: cuando realizo la prueba unitaria, prefiero express que assertEquals(2.5, d, tolerance) afirmar assertTrue(d > 2.5) no solo lee mejor la primera forma, a menudo es el cheque que usted querer estar haciendo (es decir, que d no es demasiado grande).

  5. WYSINWYG: lo que ves no es lo que obtienes

    WYSIWYG es una expresión típicamente utilizada en aplicaciones de interfaz gráfica de usuario. Significa "Lo que ves es lo que obtienes", y se usa en informática para describir un sistema en el que el contenido que se muestra durante la edición es muy similar al resultado final, que puede ser un documento impreso, una página web, etc. la frase fue originalmente una frase de captura popular originada por la personalidad de arrastre de Flip Wilson "Geraldine", que a menudo decía "Lo que ves es lo que obtienes" para excusar su comportamiento peculiar (de wikipedia).

    Otro serio progtwigdor de trampas a menudo se cae, está pensando que los números decimales son WYSIWYG. Es imprescindible darse cuenta de que, al imprimir o escribir un número decimal, no es el valor aproximado el que se imprime / escribe. Dicho de otra manera, Java está haciendo muchas aproximaciones detrás de la escena, e intenta persistentemente evitar que lo sepas. Solo hay un problema Necesita conocer estas aproximaciones, de lo contrario, puede enfrentar todo tipo de errores misteriosos en su código.

    Sin embargo, con un poco de ingenio, podemos investigar qué sucede realmente detrás de la escena. Ahora sabemos que el número 0.1 está representado con alguna aproximación.

     System.out.println( 0.1d ); 0.1 

    Sabemos que 0.1 no es 0.1, sin embargo 0.1 está impreso en la pantalla. Conclusión: ¡Java es WYSINWYG!

    Por el bien de la variedad, escojamos otro número inocente, digamos 2.3. Como 0.1, 2.3 es un valor aproximado. Como era de esperar, al imprimir el número, Java oculta la aproximación.

     System.out.println( 2.3d ); 2.3 

    Para investigar cuál es el valor interno aproximado de 2.3, podemos comparar el número con otros números en un rango cercano.

     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 

    ¡Así que 2.2999999999999997 es tan 2.3 como el valor 2.3! También tenga en cuenta que, debido a la aproximación, el punto pivotante está en ..99997 y no en ...99995, donde normalmente redondea el redondeo en matemáticas. Otra forma de familiarizarse con el valor aproximado es recurrir a los servicios de BigDecimal.

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

    Ahora, no se recueste en sus laureles pensando que puede simplemente abandonar el barco y solo usar BigDecimal. BigDecimal tiene su propia colección de trampas documentada aquí.

    Nada es fácil, y rara vez todo es gratis. Y "naturalmente", flotantes y dobles producen resultados diferentes cuando se imprimen / escriben.

     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 

    De acuerdo con las diapositivas del blog de Joseph D. Darcy, una aproximación flotante tiene 24 bits significativos, mientras que una doble aproximación tiene 53 bits significativos. La moral es que Para conservar los valores, debe leer y escribir números decimales en el mismo formato.

  6. División por 0

    Muchos desarrolladores saben por experiencia que dividir un número por cero produce una terminación abrupta de sus aplicaciones. Un comportamiento similar se encuentra en Java cuando se opera en int, pero sorprendentemente, no cuando se trabaja con dobles. Cualquier número, con la excepción de cero, dividido por rendimientos de cero respectivamente ∞ o -∞. Dividir cero con cero da como resultado el NaN especial, el valor No es un número.

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

    Dividir un número positivo con un número negativo arroja un resultado negativo, mientras que dividir un número negativo con un número negativo arroja un resultado positivo. Dado que la división por cero es posible, obtendrá un resultado diferente dependiendo de si divide un número con 0.0 o -0.0. ¡Sí, es verdad! ¡Java tiene un cero negativo! Sin embargo, no se deje engañar, los dos valores cero son iguales como se muestra a continuación.

     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. Infinity es raro

    En el mundo de las matemáticas, el infinito fue un concepto que me resultó difícil de comprender. Por ejemplo, nunca adquirí una intuición para cuando un infinito era infinitamente más grande que otro. Seguramente Z> N, el conjunto de todos los números racionales es infinitamente más grande que el conjunto de los números naturales, ¡pero ese era el límite de mi intuición en este aspecto!

    Afortunadamente, el infinito en Java es tan impredecible como el infinito en el mundo matemático. Puede realizar los sospechosos habituales (+, -, *, / en un valor infinito, pero no puede aplicar un infinito a un infinito.

     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 

    El principal problema aquí es que el valor de NaN se devuelve sin advertencias. Por lo tanto, si investigas tontamente si un doble en particular es par o impar, realmente puedes ponerte en una situación peliaguda. Tal vez una excepción de tiempo de ejecución hubiera sido más apropiada?

     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 

    ¡De repente, tu variable no es impar ni par! NaN es incluso más extraño que Infinity. Un valor infinito es diferente del valor máximo de un double y NaN es diferente nuevamente del infinito.

     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 

    Generalmente, cuando un doble ha adquirido el valor NaN, cualquier operación en él da como resultado un NaN.

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

    1. Los números decimales son aproximaciones, no el valor que asigna. Cualquier intuición obtenida en el mundo de las matemáticas ya no se aplica. Esperar a+b = a y a != a/3 + a/3 + a/3
    2. Evite usar el ==, compare con cierta tolerancia o use los operadores> = o <=
    3. Java es WYSINWYG! Nunca crea que el valor que imprime / escribe es un valor aproximado, por lo tanto, siempre lea / escriba números decimales en el mismo formato.
    4. Tenga cuidado de no desbordar su doble, para no poner su doble en un estado de ± Infinito o NaN. En cualquier caso, sus cálculos pueden no resultar como era de esperar. Puede que le resulte una buena idea comprobar siempre esos valores antes de devolver un valor en sus métodos.

Si bien BigDecimal puede almacenar más precisión que el doble, generalmente no es necesario. La verdadera razón por la que se utilizó porque deja en claro cómo se realiza el redondeo, incluidas varias estrategias diferentes de redondeo. Puede lograr los mismos resultados con el doble en la mayoría de los casos, pero a menos que conozca las técnicas requeridas, BigDecimal es el camino a seguir en estos casos.

Un ejemplo común es el dinero. Aunque el dinero no será lo suficientemente grande como para necesitar la precisión de BigDecimal en el 99% de los casos de uso, a menudo se considera la mejor práctica usar BigDecimal porque el control del redondeo está en el software que evita el riesgo de que el desarrollador haga un error al manejar el redondeo. Incluso si está seguro de que puede manejar el redondeo con double le sugiero que use métodos de ayuda para realizar el redondeo que prueba exhaustivamente.

Esto se hace principalmente por razones de precisión. BigDecimal almacena números en coma flotante con una precisión ilimitada. Puede echar un vistazo a esta página que lo explica bien. http://blogs.oracle.com/CoreJavaTechTips/entry/the_need_for_bigdecimal

Cuando se utiliza BigDecimal, puede almacenar muchos más datos que Double, lo que lo hace más preciso y una mejor opción para el mundo real.

Aunque es mucho más lento y más largo, vale la pena.

Apuesto a que no le gustaría dar información inexacta a su jefe, ¿eh?

Otra idea: realizar un seguimiento de la cantidad de centavos en un long . Esto es más simple y evita la syntax engorrosa y el bajo rendimiento de BigDecimal .

La precisión en los cálculos financieros es muy importante porque las personas se ponen muy furiosas cuando su dinero desaparece debido a errores de redondeo, por lo que el double es una opción terrible para lidiar con el dinero.