Precisión decimal en Java

Ésta, es una de esas cosas que cuando creas aplicaciones para ti, puedes ignorarlas o ni darte cuenta, pero cuando estás creando aplicaciones para grandes empresas pueden fastidiarte muy mucho.

En mi caso, el problema surgió cuando empecé a trabajar con dinero y en determinadas operaciones se perdían céntimos. ¿Cómo? Pues sí, aquí es donde empezó la búsqueda de un porqué para que


Double x = new Double(8192.55);
System.out.println("->" + (x * 100));

tenga como resultado por pantalla


819254.9999999999

Navegando por Internet, me encontré con un bonito artículo, que a su vez es la traducción de otro, en el que se explica el uso de BigDecimal para trabajar con cantidades que necesiten precisión decimal, como puede ser el caso de trabajar con dinero. El artículo viene a explicar que si trabajas con números en coma flotante se puede trabajar sin problemas con datos de tipo primitivo como double, pero si sobre esos números se tuvieran que realizar operaciones hay que tener mucho cuidado porque puede que no salgan los resultados correctos.

¿Por qué? Pues porque los ordenadores trabajan en binario y a la hora de trabajar con variables de tipo float o double cometen errores de precisión al trabajar con valores que no pueden representar.

Es en este momento es cuando alguno se lleva las manos a la cabeza diciendo, ¿CÓMO? TÚ ESTÁS MAL DE LA CABEZA. Pero no, porque nosotros también tenemos esos problemas de precisión en nuestro sistema decimal. Por ejemplo, si queremos reprentar 1/3, el resultado es 0.3333333333333… y así hasta el infinito. No podemos dar un valor exacto a esa operación. Y a los ordenadores les pasa exactamente lo mismo, si quieren representar por ejemplo 1/100, el resultado sería 1100110011001100110011001100110011… y así hasta el infinito. Ahí está nuestro problema.

Como no podía ser de otra forma, Java ya había pensado en esto, y por eso nos proporciona BigDecimal, una clase pensada para resolver este tipo de problemas. Pero aún nos queda una mala sorpresa reservada.

Lo primero a por lo que uno tira al utilizar BigDecimal es el constructor BigDecimal(double), y esto nos lleva al mismo problema. Este constructor lo que hace es guardar exactamente la misma representación que si trabajara con un dato de tipo double. La solución es trabajar SIEMPRE con el constructor BigDecimal(String). La propia documentación de BigDecimal así lo recomienda.


BigDecimal bd1 = new BigDecimal("8192.55");
BigDecimal bd2 = new BigDecimal("100");
bd1 = bd1.multiply(bd2);
System.out.println("->" + bd1.toString());

Para obtener, ahora sí, 819255.00

En los proyectos que ya teníamos y no se podía modificar la base de datos, recurrimos a un simple


BigDecimal bigDecimal = new BigDecimal(Double.toString(amount));

 

EDITO:

En cuanto al trato en base de datos, como bien ha comentado ignorante, lo mejor es guardar las cantidades como DECIMAL (en el caso de MySQL) y con el ResultSet de la conexión a base de datos, obtener la cifra directamente con el método getBigDecimal(). Funciona perfectamente y no pierdes nada de preción.

Anuncios
  1. Todas las bases tienen tipos de datos numéricos decimales. En Oracle se llama NUMBER y en otras bases de datos es DECIMAL o a veces también number.

    Guardarlos como cadena es ineficiente ya que tienes que hacer siempre las conversiones y no pueden comparar directamente así:

    select * from empleado where salario > 1000

    Tienes que hacer

    select * from empleado where to_number(salario) > 1000

    Además al usar tipos numéricos en la base se puede especificar la precisión y la cantidad de decimales cosa que con cadenas no es posible. Además de que si decides usar la coma o el punto como separador decimal, la aplicación no es internacionalizable en forma sencilla.

      • dieghada
      • 28/03/11

      Totalmente de acuerdo. De hecho, siempre guardaba las cantidades como DECIMAL en MySQL. El problema es que a la hora de tratarlo en Java si te traes el dato como DECIMAL y guardarlo en un variable de tipo float, tienes un problema. Si te lo traes como DECIMAL y decides guardarlo en una variable de tipo BigDecimal, tienes que recurrir al constructor BigDecimal(float), y tienes otro problema.

      En cuanto a lo de la internacionalización, la idea es trabajar en base de datos como cadena, pero a la hora de tratar los datos en Java hacerlo sobre BigDecimal con lo que no hay problema con la internacionalización. Aunque entiendo lo que quieres decir.

      Desde luego, si necesito traerme los datos ya filtrados desde base de datos, la consulta siempre será más ineficiente (aunque no se cuanto, de echo haré pruebas y consultaré los tiempos), pero hasta ahora no he tenido problemas, la verdad.

  2. Cuando traes un decimal de la base no lo guardas en una variable float, sino en una variable BigDecial. Así:

    ResultSet rs = …
    BigDecimal monto = rs.getBigDecimal(“monto”);

    Nunca usas ni float ni double.

      • dieghada
      • 28/03/11

      WoW! Muy grande. Mañana a primera hora lo pruebo.

      Me encantan los blogs por esto mismo, no paras de aprender 😉

      • dieghada
      • 29/03/11

      Gracias ignorante, funciona perfectamente y es mucho más eficiente 😉

      Un saludo.

  1. No trackbacks yet.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s