Saltar al contenido

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

Este team de especialistas luego de varios días de trabajo y de recopilar de información, obtuvieron la solución, nuestro deseo es que te resulte útil en tu trabajo.

Solución:

Se llama pérdida de precisión y es muy notable cuando se trabaja con números muy grandes o con números muy pequeños. La representación binaria de números decimales con una base es en muchos casos una aproximación y no un valor absoluto. Para comprender por qué necesita leer sobre 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/15) = 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 hacia y desde una misma cuenta (o tal vez lo sean), debe tener números confiables. Los flotadores binarios no son confiables, a menos que comprenda 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 caído ahora.

Trampas Java: doble

Muchas trampas se encuentran ante el aprendiz de programador 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 double y float de Java. Sin embargo, tenga en cuenta que para adoptar por completo la precisión en los cálculos numéricos, se requiere un libro de texto (o dos) sobre el tema. En consecuencia, solo podemos arañar la superficie del tema. Dicho esto, el conocimiento que se transmite aquí debería brindarle el conocimiento fundamental necesario para detectar o identificar errores en su código. Es un conocimiento que creo que cualquier desarrollador de software profesional debería conocer.

  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, la descripción de todos los números reales entre 0,0 y 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 ey π. En otras palabras, los números 2 y 0.2 están representados de manera muy diferente en la computadora.

    Los enteros están representados por bits que representan valores 2n donde n es la posición del bit. Por 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 produciendo la secuencia de bits 1100 (1/2 + 1/4).

    Equipados con este conocimiento, podemos formular la siguiente regla empírica: Cualquier número decimal está representado por un valor aproximado.

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

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

    Se imprime 1.0. Si bien esto es correcto, puede darnos una false sensación de seguridad. Casualmente, 0.2 es uno de los pocos valores que Java puede representar correctamente. Retemos de nuevo a Java con otro problema aritmético trivial, sumando 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
    

    Según las diapositivas del blog de Joseph D. Darcy, las sumas 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 iniciales, mientras que el doble tiene una precisión de 17 dígitos iniciales. Ahora, si la falta de coincidencia conceptual entre el resultado esperado 1.0 y los resultados impresos en las pantallas no fue suficiente para activar las alarmas, observe cómo los números del sr. ¡Las diapositivas de Darcy no parecen corresponder a los números impresos! Esa es otra trampa. Más sobre esto más abajo.

    Habiendo sido consciente de los errores de cálculo en los escenarios posibles aparentemente más simples, es razonable contemplar qué tan rápido puede aparecer la impresión. Simplifiquemos el problema a la suma de sólo tres números.

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

    ¡Sorprendentemente, la imprecisión ya se activa en tres adiciones!

  2. Doble desbordamiento

    Al igual que con cualquier otro tipo simple en Java, un doble se representa mediante un conjunto finito de bits. En consecuencia, agregar un valor o multiplicar un doble puede arrojar resultados sorprendentes. Es cierto que los números tienen que ser bastante grandes para desbordar, pero sucede. Intentemos multiplicar y luego dividir un número grande. 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 lo grande, se desborda, y luego se divide el número desbordado. Peor aún, no se presentan excepciones u otros tipos de advertencias al programador. Básicamente, esto hace que la expresión x * y sea completamente poco 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. De manera similar, en informática, grandes y pequeños no son amigos. Una consecuencia de usar un número fijo de bits para representar números es que operar con números realmente grandes y realmente pequeños en los mismos cálculos no funcionará como se esperaba. Intentemos agregar algo pequeño a algo grande.

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

    ¡La adición no tiene ningún efecto! Esto contradice cualquier intuición matemática (sana) de la suma, 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 desechar toda la intuición que hemos adquirido en la clase de matemáticas y la programación con números enteros. Utilice los números decimales con precaución. Por ejemplo, la declaración for(double d = 0.1; d != 0.3; d += 0.1) es en efecto un bucle sin fin disfrazado! El error es comparar números decimales directamente entre sí. Debe adherirse a las siguientes pautas.

    Evite las pruebas de igualdad entre dos números decimales. Abstenerse de if(a == b) .., usar if(Math.abs(a-b) < tolerance) .. donde la tolerancia podría ser una constante definida como, por ejemplo, público static doble tolerancia final = 0.01 Considere como una alternativa para usar los operadores <, > ya que pueden describir de forma más natural lo que desea expresar. Por ejemplo, prefiero la forma
    for(double d = 0; d <= 10.0; d+= 0.1) sobre los mas torpes
    for(double d = 0; Math.abs(10.0-d) < tolerance; d+= 0.1)
    Sin embargo, ambas formas tienen sus méritos dependiendo de la situación: cuando las pruebas unitarias, prefiero expresar que assertEquals(2.5, d, tolerance) sobre decir assertTrue(d > 2.5) no sólo se lee mejor el primer formulario, a menudo es la comprobación que desea realizar (es decir, que d no es demasiado grande).

  5. WYSINWYG - Lo que ves no es lo que obtienes

    WYSIWYG es una expresión que se utiliza normalmente en aplicaciones de interfaz gráfica de usuario. Significa "Lo que ves es lo que obtienes" y se utiliza en informática para describir un sistema en el que el contenido que se muestra durante la edición parece muy similar al resultado final, que puede ser un documento impreso, una página web, etc. La frase era originalmente un eslogan popular originado por la persona drag de Flip Wilson "Geraldine", quien solía decir "Lo que ves es lo que obtienes" para excusar su comportamiento peculiar (de wikipedia).

    Otra trampa seria en la que suelen caer los programadores es pensar que los números decimales son WYSIWYG. Es imperativo darse cuenta de que al imprimir o escribir un número decimal, no es el valor aproximado el que se imprime / escribe. Expresado de manera diferente, Java está haciendo muchas aproximaciones detrás de escena y persistentemente trata de protegerlo para que no lo sepa. 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 lo que realmente sucede detrás de escena. A estas alturas 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, pero 0,1 está impreso en la pantalla. Conclusión: ¡Java es WYSINWYG!

    En aras de la variedad, escojamos otro número de apariencia 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 puede ser 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 tanto 2,3 como el valor 2,3! También observe que debido a la aproximación, el punto de pivote está en ..99997 y no en ..99995, donde normalmente se redondea 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 duerma en los laureles pensando que puede simplemente abandonar el barco y usar únicamente BigDecimal. BigDecimal tiene su propia colección de trampas documentadas aquí.

    Nada es fácil y rara vez todo es gratis. Y "naturalmente", los flotadores y los 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
    

    Según las diapositivas del blog de Joseph D. Darcy, una aproximación flotante tiene 24 bits significativos, mientras que una aproximación doble tiene 53 bits significativos. La moral es que para preservar 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 la terminación abrupta de sus aplicaciones. Se encuentra un comportamiento similar en Java cuando se opera en int, pero sorprendentemente, no cuando se opera en dobles. Cualquier número, con la excepción de cero, dividido por cero produce, 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 produce un resultado negativo, mientras que dividir un número negativo con un número negativo produce un resultado positivo. Dado que es posible la división por cero, obtendrá un resultado diferente dependiendo de si divide un número con 0.0 o -0.0. Sí, es true! ¡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. El infinito es raro

    En el mundo de las matemáticas, el infinito era un concepto que me costaba comprender. Por ejemplo, nunca adquirí la intuición de cuándo 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 números naturales, ¡pero eso fue casi el límite de mi intuición en este sentido!

    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 investiga tontamente si un doble en particular es par o impar, realmente puede meterse en una situación difícil. ¿Quizás una excepción en 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, su variable no es ni par ni impar! NaN es incluso más extraño que Infinity Un valor infinito es diferente del valor máximo de un doble y NaN es diferente nuevamente del valor 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 sobre é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 adquirida en el mundo de las matemáticas ya no se aplica. Suponer a+b = a y a != a/3 + a/3 + a/3
    2. Evite usar ==, compare con alguna 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, de no hacer que su doble entre en un estado de ± Infinito o NaN. En cualquier caso, es posible que sus cálculos no sean los esperados. 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 de redondeo diferentes. Puede lograr los mismos resultados con el doble en la mayoría de los casos, pero a menos que conozca las técnicas necesarias, 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 una mejor práctica usar BigDecimal porque el control del redondeo está en el software, lo que evita el riesgo que correrá el desarrollador. un error en el manejo del redondeo. Incluso si está seguro de que puede manejar el redondeo con double Le sugiero que utilice métodos auxiliares para realizar el redondeo que pruebe a fondo.

Calificaciones y comentarios

Si te ha resultado de utilidad este artículo, te agradeceríamos que lo compartas con más desarrolladores y nos ayudes a difundir este contenido.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *