Bienvenido a los Consejos Técnicos de la Conexión del Desarrollador Java (JDC), del 7 de agosto del 2001. Esta edición cubre:
- Realización de cálculos exactos con números de punto flotante.
- Uso de enumeraciones en Java
Estos consejos fueron desarrollados usando Java(tm) 2 SDK, Standard Edition v 1.3
Se puede ver esta edición (en su original en inglés) de los Consejos Técnicos en formato html en http://java.sun.com/jdc/JDCTechTips/2001/tt0807.html
Realización cálculos exactos con números de punto flotante
Suponga que esta desarrollando una aplicación en Java, y necesita hacer algunos cálculos financieros. Hay algunas monedas, y quiere sumar los valores de las monedas para encontrar el valor total. Digamos que usted usa dólares americanos, y asume las siguientes monedas y cantidades.
centavos(0.01) 1 cincos(0.05) 3 décimos (0.10) 7 cuartos (0.25) 3 cincuenta centavos (0.50) 4
El valor total de estas monedas es $3,61
Aquí esta un programa que suma los valores de las monedas
public class FpcDemo1 { // registro para pares de centavos y cantidad static class Rec { double cents; int count; Rec(double cents, int count) { this.cents = cents; this.count = count; } } // conjunto de registros static Rec values[] = { new Rec(0.01, 1), new Rec(0.05, 3), new Rec(0.10, 7), new Rec(0.25, 3), new Rec(0.50, 4) }; // calcular el error relativo y tomar su valor absoluto static double getRelativeError(double obs, double exp) { if (exp == 0.0) { throw new ArithmeticException(); } return Math.abs((obs - exp) / exp); } public static void main(String args[]) { double sum = 0.0; // sumando los valores de las monedas for (int i = 0; i < values.length; i++) { Rec r = values[i]; sum += r.cents * r.count; } // imprimir la suma y la diferencia con 3,61 System.out.println("sum = " + sum); System.out.println("sum - 3.61 = " + (sum - 3.61)); // comprobar si es igual a 3,61 if (sum == 3.61) { System.out.println("exactamente igual a 3,61"); } // calcular el error relativo double rerr = getRelativeError(sum, 3.61); System.out.println("error relativo = " + rerr); // comprobar si la suma es aproximadamente igual a 3,61 if (rerr <= 0.01) { System.out.println("aproximadamente igual a 3,61"); } } }
Este programa es simple y sencillo, pero desafortunadamente, no trabaja. Las primeras dos líneas de la salida son:
sum = 3.6100000000000003 sum - 3.61 = 4.440892098500626E-16
El problema es que algunos números decimales, como 0,1, no tienen una representación exacta como punto flotante.
Antes de examinar más este ejemplo, merece la pena mirar un poco más de cerca la idea de que algunos números como 0,1 no tienen un equivalente exacto en punto flotante. Aquí esta otro programa que muestra esto:
public class FpcDemo2 { public static void main(String args[]) { // calcular la representación en bits // de los valores 0,1 y 0,09375 long bits1 = Double.doubleToRawLongBits(0.1); long bits9375 = Double.doubleToRawLongBits(0.09375); // extrae y muestra los bits 51-0 de cada valor long mask = 0xfffffffffffffL; String s1 = Long.toBinaryString(bits1 & mask); String s9375 = Long.toBinaryString(bits9375 & mask); System.out.println(s1); System.out.println(s9375); // mostrar el resultado de multiplicar 0.1 by 56.0 System.out.println(0.1 * 56.0); } }
Este programa muestra los patrones en bruto de bit usados internamente para representar valores fraccionales de punto flotante. La salida es:
1001100110011001100110011001100110011001100110011010 1000000000000000000000000000000000000000000000000000 5.6000000000000005
La primera línea de salida es el patrón de bit para 0,1, y la segunda es el patrón para 0,09375. El primer patrón muestra una secuencia repetitiva, y de hecho el valor 1/10 es la suma de una serie infinita de potencias de dos:
1/10 = 1/16 + 1/32 + 1/256 + 1/512 + 1/4096 + 1/8192 + ...
En contraste, 0,09375 es 3/32, esto es:
3/32 = 1/16 + 1/32
En otras palabras, 0,09375 se representa con exactitud y 0,1 no. Se puede observar los efectos de esto al mirar en la tercera línea de la salida mostrada, la cuál es el producto de 56 y 0,1. El valor resultante está ligeramente fuera del valor esperado: 5,6.
Una forma de resolver el problema se ilustra en el ejemplo FpcDemo1, el cual usa un método de error relativo. Un error relativo se define como:
error relativo = (observado - esperado) / esperado
Por ejemplo, el valor esperado es de 3,62, pero el calor actual es ligeramente diferente. Al aplicar la fórmula, se toma la diferencia de los valores y se las divide entre 3,61. Tomando el valor absoluto. El resultado es un porcentaje que muestra cuan lejos está el valor actual del valor esperado. Esta técnica es generalmente útil en cualquier tipo de cálculos de punto flotante, porque hay muchas veces un problema con la obtención de valores exactos. Digamos, por ejemplo, que la suma calculada del valor de las monedas debe estar dentro del 1% de 3,61, entonces los valores son de hecho aproximadamente iguales de acuerdo con esta regla. Las dos últimas líneas de la salida de FpcDemo1 son:
Error relativo = 1.2301640162051597E-16 aproximadamente igual a 3.61
Calculando el error relativo y haciendo comparaciones aproximadas es bastante útil, pero aún hay algunos problemas con el ejemplo. Uno de ellos es la cuestión de la presentación. Si se espera un valor tal como 3,61 y en cambio se obtiene 3.6100000000000003, probablemente este resultado no sería una salida aceptable porque, por ejemplo, puede desbordar el ancho de un campo. Y usted puede, en realidad, requerir una respuesta exacta. Tal requerimiento sería común, por ejemplo, en cálculos que involucren transacciones de efectivo con un clientes al menudeo. Así veamos un par de soluciones alternativas a este problema.
Una solución es usar valores enteros, eso es, calcular la suma en centavos. El programa se vería como:
public class FpcDemo3 { // registro de pares para centavos/cantidad static class Rec { int cents; int count; Rec(int cents, int count) { this.cents = cents; this.count = count; } } // conjunto de registros static Rec values[] = { new Rec(1, 1), new Rec(5, 3), new Rec(10, 7), new Rec(25, 3), new Rec(50, 4) }; public static void main(String args[]) { int sum = 0; // sumando los valores de los registros for (int i = 0; i < values.length; i++) { Rec r = values[i]; sum += r.cents * r.count; } // mostrar la suma System.out.println("sum = " + sum); // mostrar las partes, entera y decimal, de la suma System.out.println((sum / 100) + "." + (sum % 100)); } }
Y la salida es:
sum = 3.61
Este ejemplo da valores tales como 0,1 al constructor BigDecimal como cadenas, no como valores double (los cuales también soporta). Haciendo las cosas así se consigue omitir el problema de no poder representar valores como 0,1. En otras palabras, la clase Bigdecimal tiene su propia representación de precisión arbitraria, diferente de la representación IEEE 754 usada para valores de punto flotante. Si usted pasa al constructor un valor como "0.1", como una cadena, entonces se preserva el valor exacto. Pero si se usa el constructor que toma un argumento double , el problema representación ocurrirá de nuevo.
BigDecimal rodea los problemas bosquejados anteriormente, pero haciéndolo a bajo costo. Se no se debe asumir que los cálculos hechos usando BigDecimal serán tan rápidos como los cálculos estándares de punto flotante, las cuales son normalmente realizadas en el hardware. Si planea usar BigDecimal, sería bueno evaluar los problemas de rendimiento.
Es importante entender las limitaciones de los valores de punto flotante in representaciones fraccionarias comunes como 0,1. También necesitará saber que técnicas están disponibles para evitar estas limitaciones.
Para mayor información acerca de la Clase BigDecimal, vea la descripción de la misma en http://java.sun.com/j2se/1.3.0/docs/api/java/math/BigDecimal.html
Uso de enumeraciones en Java
Una enumeración es un grupo pequeño de valores constantes (enumeradores o constantes de enumeración) que se están relacionadas una con otra. Por ejemplo , se puede tener un conjunto de colores como rojo, verde, y azul, y desear usarlos como constantes en un programa para especificar los colores de unos objetos grfico. Veamos un ejemplo simple del uso de las enumeraciones.
class EnumColor { // constructor privado, así la clase no puede ser instanciable private EnumColor() {} public static final int ROJO = 1; public static final int VERDE = 2; public static final int AZUL = 3; } public class EnumDemo1 { // imprimir el color basado en un argumento static void printColor(int color) { if (color == EnumColor.ROJO) { System.out.println("rojo"); } else if (color == EnumColor.VERDE) { System.out.println("verde"); } else { System.out.println("azul"); } } public static void main(String args[]) { printColor(EnumColor.VERDE); } }
Cuando se ejecute el programa la salida será:
verde
EnumColor es una clase usada como un paquete para un conjunto de constantes. Tiene un constructor privado para impedir a los usuarios crear objetos de la clase o extenderla. Los enumeradores se llaman por expresiones como "EnumColor.VERDE".
Esta forma de implementar enumeraciones es muy simple, trabaja muy bien, y es muy usada en Java. Pero hay algunos problemas al trabajar de esta forma. algunos de ellos aparecen el siguiente ejemplo.
import java.util.*; // enumeración de colores class EnumColor { private EnumColor() {} public static final int ROJO = 1; public static final int VERDE = 2; public static final int AZUL = 3; } // enumeración de booleanos class EnumBoolean { private EnumBoolean() {} public static final int TRUE = 1; public static final int FALSE = 2; } public class EnumDemo2 { static void printColor(int color) { if (color == EnumColor.ROJO) { System.out.println("rojo"); } else if (color == EnumColor.VERDE) { System.out.println("verde"); } else { System.out.println("azul"); } } public static void main(String args[]) { // asigna un valor equivocado a color // y entonces imprimer el color int color = 59; printColor(color); // asigna un valor de color de una enumeración diferente color = EnumBoolean.FALSE; printColor(color); // intenta agregar a una lista List list = new ArrayList(); //list.add(EnumColor.AZUL); } }
Al correr el programa la salida será:
azul verde
Este ejemplo muestra un conjunto de problemas. el primero es que el programa permite asignar el valor 59 a una variable que se supone representa un color. 59 no es valor permitido para cualquiera de los enumeradores en EnumColor. Entonces, cuando se llama a printColor, éste falla al diagnosticar el hecho que se le ha pasado como argumento un enumerador ilegal.
El programa entonces asigna un valor de una enumeración diferente a la variable color. Este error no esta siendo capturando. Finalmente, cuando el programa intenta agregar un enumerador a una lista, el resultado es un error de compilación (necesitará quitar el comentario la última línea en EnumDemo2 para poder ver este error).
Estos problemas tienen una causa: específicamente una enumeración en Java basada en valores enteros no establece un tipo distinto de enumeración. en otras palabras, si una enumeración consiste en un conjunto de constantes enteras, no hay nada que soporte la detección de valores ilegales que no son parte de la enumeración. No hay forma de forzar reglas, por ejemplo, las reglas usuales que dicen que no se puede asignar una referencia de un tipo de clase a una referencia de un tipo totalmente distinto.
Hay algunos problemas más con el uso de valores enteros para representar enumeraciones. Uno es que no hay un mecanismo "toString", esto es, no hay una forma fácil de asociar "2" con "verde". Se tiene que escribir un método printColor para hacer eso.
Otro problema es que los valores constantes están limitados dentro de un código cliente que esa esos valores. Se puede ver esto al decir:
javac EnumDemo2.java javap -c -classpath . EnumDemo2
Y examinando el método printColor. Por ejemplo, la secuencia:
1 iconst_1 2 if_icmpne 16
Compara los argumentos pasados al método printcolor con el valor constante 1 (EnumColor.ROJO). Este comportamiento puede llevarnos a problemas si el valor del enumerador cambia y una recompilación de todo las clases afectadas no estarán listas.
Si usted acostumbra un enfoque basado en enteros para las enumeraciones, usted puede hacer algo simple para mejorar la calidad del código al definir un método con la clase enumeración que revise si un enumerador dado es válido.
public static boolean isValidEnumerator(int e) { return e == ROJO || e == VERDE || e == AZUL; }
Entonces llame a este método como apropiado, para validar los valores del enumerador.
Hay otro método para implementar enumeraciones que tratan muchos de estos problemas. Esta técnica tiene el nombre de "typesafe enum", y se parece a los siguiente:
import java.util.*; class EnumColor { // nombre del enumerador private final String enum_name; // constructor privado, llamado solo desde la misma clase private EnumColor(String name) { enum_name = name; } // retorna el nombre del enumerador public String toString() { return enum_name; } // crea tres enumeradores public static final EnumColor ROJO = new EnumColor("rojo"); public static final EnumColor VERDE = new EnumColor("verde"); public static final EnumColor AZUL = new EnumColor("azul"); } class EnumBoolean { private final String enum_name; private EnumBoolean(String name) { enum_name = name; } public String toString() { return enum_name; } public static final EnumBoolean TRUE = new EnumBoolean("true"); public static final EnumBoolean FALSE = new EnumBoolean("false"); } class EnumDemo3 { public static void main(String args[]) { // asigna un enumerador e imprime el valor EnumColor color = EnumColor.VERDE; System.out.println(color); // intenta asignar un enumerador a una // variable de enumeración de tipo diferente //color = EnumBoolean.FALSE; // agregar un enumerador a una lista List list = new ArrayList(); list.add(EnumColor.AZUL); // comprueba si el color es azul color = EnumColor.AZUL; if (color == EnumColor.AZUL) { System.out.println("el color es azul"); } } }
Cuando se ejecute el programa la salida será:
verde el color es azul
la idea es tener una clase que represente un tipo de enumeración. dentro de la clase se define un conjunto de enumeradores como instancias de la clase, referenciados por campos de valores constantes estáticos. La clase especifica un constructor privado, significa que no hay forma para que los usuarios creen objetos de la clase o extiendan la clase. Así el conjunto de constantes estáticas dentro de la clase son sólo objetos de la clase existente.
Cuando se crea cada objeto estático, representando un enumerador, se pasa una cadena al constructor, especificando el nombre del enumerador. Así queda resuelto el problema mencionado del toString. Y porque cada clase que representa una enumeración es de un tipo distinto, el compilador automáticamente atrapa los problemas como asignar un enumerador a una referencia de un tipo distinto de enumeración.
Se resuelve el problema con las constante enteras dentro del código compilado. Eso es porque el compilador se refiere a los objetos estáticos de la enumeración en lugar de constantes enteras compiladas en el código cliente. y se puede agregar constantes de enumeración a colecciones como ArrayList, porque ellas son objetos en lugar de valores primitivos como int.
Si se usa una typesafe enum, se necesitará comprobar si una referencia a un objeto de tal tipo es nula antes de comprobar los valores específicos del enumerador.
void f(EnumColor e) { if (e == null) { throw new NullPointerException(); } }
Después de esta comprobación, se garantiza tener un valor de enumeración válido, uno de los elementos del conjunto de constantes establecidas con la clase enumeración.
¿Qué hay del rendimiento?, desde que los enumeradores son únicos, se puede usar el operador == para verificar la identidad de la referencia. Esto es muy rápido- No hay necesidad de usar equals() para comprobar la igualdad de las constantes de enumeración.
Hay unos aspectos que se dejan de lado con las typesafe enums. Diferentes enumeraciones basadas en enteros, no se puede usar enumeraciones basadas en objetos constantes como índices de arreglos, o como máscaras de bits para acceder un bit dentro de un conjunto de bits.
Las typesafe enums resuelven un conjunto de problemas importantes de programación, y son valiosas usadas en programas como una forma de mejorar la calidad del código y el mantenimiento.
Para mayor información consulte el artículo 21 "Replace enum constructs with classes" en "Effective Java Programming Languaje Guide" de Joshua Bloch, y la sección 13.4.8 "Final Fields and Constants" en "The Java Languaje Speceification Second Edition" de Gosling Joy, Steele y Bracha.
Copyright y notas de la traducción
Nota respecto a la traducción
El original en inglés de la presente edición de los JDC Tech Tips fue escrita por Glen McCluskey, la traducción no oficial fue hecha por Alan Ortiz, cualquier sugerencia o corrección hágala al correo [email protected] , sugerencia respecto a la edición original a mailto:[email protected]
Nota (Respecto a la edición via email)
Sun respeta su tiempo y su privacidad. La lista de correo de la Conexión del desarrollador Java se usa sólo para propósitos internos de Sun Microsystems(tm). Usted ha recibido este email porque se ha suscrito a la lista. Para desuscribirse vaya a la página de suscripciones, desmarque casilla apropiada y haga clic en el botón Update.
Suscripciones
Para suscribirse a la lista de correo de noticias de la JDC vaya a la página de suscripciones, elija los boletines a los que quiera suscribirse, y haga clic en Update.
Realimentación
¿Comentarios?, envie su sugerencias a los Consejos Técnicos de la JDC a mailto:[email protected]
Archivos
Usted encontrará las ediciones de los Consejos Técnicos de la JDC (en su original en inglés) en http://java.sun.com/jdc/TechTips/index.html
Copyright
Copyright 2001 Sun Microsystems, Inc. All rights reserved. 901 San Antonio Road, Palo Alto, California 94303 USA.
Este documento esta protegido por las leyes de autor. Para mayor información vea http://java.sun.com/jdc/copyright.html
Enlaces a sitios fuera de Sun
Los Consejos Técnicos de la JDC puede dar, p terceras pueden dar, enlaces a otros sitios y recursos. ya que Sun no tiene control sobre esos sitios o recursos usted reconoce y acepta que Sun no es responsable por la disponibilidad de tales sitios o recursos, y no se responsabiliza por cualquier contenido, anuncios , productos u otros materiales disponibles en tales sitios o recursos. Sun no será responsable, directa o indirectamente, por cualquier daño o pérdida causada o supuestamente causada por o en relación con el uso de o seguridad sobre cualquier tal contenido, bienes o servicios disponibles en o através de cualquier sitio o recurso.
El original en Ingles de esta edición de los Consejos técnicos fue escrita por Glen McCluskey.
JDC Tech Tips August 7, 2001
Sun, Sun Microsystems, Java y Java Developer Connection (JDC) son marcas registradas de Sun Microsystems Incs. en los Estados Unidos y cualquier otro país.