Buenas tardes, en esta entrada veremos bastate información sobre las Wrapper Classes (O clases contenedora) en Java y su Boxing.
Usando Wrapper Classes y Boxing
Las wrapper classes en la API de Java sirven para 2 propósitos primarios:
- Proveen un mecanismo de “envolver” valores primitivos en un objeto para que estos primitivos puedan ser incluidos en actividades que estén reservadas para objetos, como ser añadidas a Collectiones, o ser retornadas desde un método con un valor de retorno que sea un objeto.
- Proveer un surtido de funciones de utilidad a los primitivos. La mayoría de estas funciones están relacionadas con varias conversiones: convertirprimitivos desde o de un objeto String, y convertir primitivos y objetos String a una base diferente, como binario, octal y hexadecimal.
-
Resumen de las Wrapper Classes
Hay una clase contenedora para cada primitivo en Java. Por ejemplo, la clase contenedora para un int es Integer, la clase para un float es Float, y así con el resto. Recordemos que los nombres primitivos están siempre en minúscula, excepto para int, que es Integer, y char, que es Character. En la siguiente tabla podemos ver las clases contenedoras en la API de Java.
Primitivos Clase Contenedora Argumentos del Constructor boolean Boolean boolean o String byte Byte byte o String char Character char double Double double o String float Float float, double o String int Integer int o String long Long long o String short Short short o String -
Creando Objetos Contenedores
Los enfoques principales en estas clases contenedora es usar una representación de un String de un primitivo como argumento. Aquellos que cogen un String lanzan un NumberFormatException si el String que se ha pasado no puede ser convertido en su primitivo apropiado. Por ejemplo, “tw” no puede ser analizado en “2”. Los objetos contenedor son inmutables. Una vez que se les ha dado un valor, ese valor no puede ser cambiado. Hablaremos mas sobre la inmutabilidad cuando discutamos el boxing.
-
Los Constructores de las clases Wrapper
Todas las clases wrapper excepto Character proveen de 2 constructores: uno que coge un primitivo del tipo que está siendo construido, y otro que recoge un String como representación del tipo que está siendo construido:
Integer i1 = new Integer(42); Integer i2 = new Integer ("42");
o:
Float f1 = new Float(3.14f); Float f2 = new Float ("3.14f");
Los constructores para las clase Boolean coge o un valor booleano, ya sea true o false, o una String. Si la String (sin importar mayusculas o minusculas) es “true” el Boolean será true, y cualquier otro valor será false:
Boolean b = new Boolean ("false"); if (b) // No compilará si usamos Java 1.4 o anterior
Como en Java 5, un objeto Boolean puede ser usado en un test boolean, porque el compilador automáticamente hará un “unbox” del Boolean en un boolean.
-
Los Métodos valueOf()
Los 2 (normalmente son 2) métodos static valueOf() proveídos en la mayoría de las clases wrapper nos dán otro enfoque para podre crear objetos wrapper. Ambos métodos cogen una representación String del tipo apropiado de primitivo como primer argumento, y el segundo método (cuando se provee) coge un argumento adicional, int radix, el cual indica en que base será representado el primer argumento:
Integer i2 = Integer.valueOf("101011", 2); // Convierte el 101011 a 43 y le asigna el valor de 43al objeto Integer i2
o:
Float f2 = Float.valueOf("3.14f"); // Asigna 3.14 al objeto Float f2
-
-
Usando las Utilidades de Conversión de Wrapper
Como dijimos antes, una de las segundas funciones mas importantes es la de convertir. Los siguientes métodos son los mas usados comúnmente.
-
xxxValue()
Cuando necesitamos convertir un valor de un contenedor numérico a un primivito, usamos uno de los métodos xxxValue(). Todos estos métodos en esta familia son métodos que no necesitan argumentos. Como podemos ver en una tabla mas tarde, hay 36 métodos xxxValue(). Cada una de las clases contenedoras numéricas tiene 6 métodos, por lo que cualquier objeto contenedor numérico puede ser convertiro a un tipo numérico primitivo:
Integer i2 = new Integer(42); // Crea un nuevo objeto wrapper byte b = i2.byteValue(); // Convierte el valor de i2 a un primitivo byte short s = i2.shortValue(); // Otro de los métodos de Integer double d = i2.doubleValue(); // Otro de los métodos xxxValue de Integer
o:
Float f2 = new Float(3.14f); // Crea un nuevo objeto wrapper short s = f2.shortValue(); // Convierte el valor de f2 en un primitivo short System.out.println(s); // El resultado es 3 (truncado, no redondeado)
-
parseXxx() y valueOf()
Los seis métodos parseXxx() (uno para cada tipo wrapper numérico) están relacionados con el método valueOf() que existen en todas las clases wrapper numéricas. Ambos, parseXxx() y valueOf() cogen un String como argumento, lanzan un NumberFormatException si el argumento String no está apropiadamente formado, y pueden convertir objetos String de diferentes bases, cuando el tipo primitivo es alguno de los 4 tipos enteros. La diferencia entre estos 2 métodos es:
- parseXxx() retorna el primitivo.
- valueOf() retorna un objeto wrapper creado del tipo que ha invocado el método.
Aquí tenemos algunos ejemplo de estos métodos en acción:
double d4 = Double.parseDouble("3.14"); // Convierte un String a primitivo System.out.println("d4 = " + d4); // El resultado será d4 = 3.14 Double d5 = Double.valueOf("3.14"); // Crea un objeto Double System.out.println(d5 instanceof Double); // El resultado es "true"
Los siguientes ejemplos usan argumentos radix (en este caso binario):
long L2 = Long.parseLong("101010", 2); // un String binario a primitivo System.out.println("L2 = " + L2); // El resultado es L2 42 Long L3 = Long.valueOf("101010", 2); // String en binario a un objeto Long System.out.println("Valor de L3 = " + L3); // El resultado es: Valor de L3 = 42
-
toString()
La Clase Objeto, la clase alpha, tiene un método toString(). Ya que sabemos que todas las otras clases en Java heredan de la clase Object, también sabemos que todas las clases tienen el método toString(). La idea del método toString() es permitir que obtengamos una representación significativa del objeto dado. Por ejemplo, si tenemos una Colección de varios tipos d eobjetos, podemos hacer un bucle en la Colección y sacar por pantallaalguna representación significativa de cada objeto usando el método toString(), el cual está en todas las clases de manera garantizada. Todas las clases wrapper tienen una versión de toString(), sina rgumentos y no static. Este método retorna un String con el valor del primitivo que se enceuntra dentro del objeto contenedor:
Double d = new Double("3.14"); System.out.println("d = " + d.toString() ); // El resultado es d = 3.14
Todas las clases contenedor numéricas proveen un método sobrecargado, el método static toString() que obtiene un primitivo numérico del tipo apropiado (Double.toString() coge un double, Long.toString() coge un long, y así) y por supuesto, retorna un String:
String d = Double.toString(3.14); // d = "3.14"
Finalmente, Integer y Long proveen un tercer método toString(). Es static, su primer argumento es el primitivo, y el segundo argumento es el radix. El radix le dice al método de coger el primer argumento, el cual es radix 10 (base 10) por defecto, y lo convierte en el radix que le damos, entonces retorna el resultado como un String:
String s = "hex = " + Long.toString(254, 16); // s = "hex = fe"
-
toXxxString() (Binario, Hexadecimal, Octal)
Las clases contenedoras Integer y Long nos permiten convertir números en base 10 a otras bases. Estos métodos de conversión, toXxxString(), cogen un int o un long, y retornan una representación String del número convertido, por ejemplo:
String s3 = Integer.toHexString(254); // Convierte 254 a hex System.out.println("254 es " + s3); // Resultado: "254 es fe" String s4 = Long.toOctalString(254); // Convierte 254 a octal System.out.println("254(oct) = " + s4); // Resultado: "254(oct) = 376"
Estudiar la siguiente tabla es la mejor manera de preparar esta parte. Si podemos saber la diferencia entre xxxValue(), parseXxx() y valueOf(), no tendremos problemas en aplicar esto:
Método Boolean Byte Character Double Float Integer Long Short byteValue x x x x x x doubleValue x x x x x x floatValue x x x x x x intValue x x x x x x longValue x x x x x x shortValue x x x x x x parseXxx
static, NFE exceptionx x x x x x parseXxx (con radix)
static, NFE exceptionx x x x valueOf
static, NFE exceptions x x x x x x valueOf (con radix)
static, NFE exceptionx x x x toString x x x x x x x x toString (primitive)
staticx x x x x x x x toString (primitive, radix)
staticx x Para resummir, los nombres de los métodos esenciales para las conversiones son:
- primitive xxxValue() – Para convertir de Wrapper a primitive
- primitive parseXxx(String) – Para convertir un String en primitive
- Wrapper valueOf(String) – Para convertir String en Wrapper
-
-
Autoboxing
En Java 5 se añadió una nueva característica conocída variadamente como: autoboxing, auto-unboxing, boxing y unboxing. Vamos a seguir con los términos boxing y unboxing. Boxing y unboxing hacen el uso de las clases contenedoras mas convenientes. En tiempos anteriores, antes de Java 5, si queríamos hacer una clase contenedora, desenvolverla, usarla, y luego envolverla de nuevo, teníamos que hacer algo como lo siguiente:
Integer y = new Integer(567); // Crea el objeto int x = y.intValue(); // Lo desenvuelve x++; // Lo usa y = new Integer(x); // lo vuelve a envolver System.out.println("y = " + y); //lo imprime
Ahora, con lo nuevo incluido en Java 5 podemos hacer lo siguiente:
Integer y = new Integer(567); //Crea el objeto y++; // Lo desenvuelve, lo incrementa y lo vuelve a envolver System.out.pritln("y = " + y); // Lo imprime
Las salidas de ambos ejemplos serán la misma:
y = 568
Y sí, estamos leyendo correctamente. El código está usando el operador de postincremento en un variable de referencia de un objeto. Tras las escenas, el compilador hace todo el unboxing y la reasignación por nosotros. Antes comentabamos que los objetos wrapper eran inmutables….aunque este ejemplo parece que contradice esto. Aparentemente parece que el valor de y ha cambiado de 567 a 568. Lo que actualmente ha pasado, es que se ha creado un segundo objeto wrapper y su valor fué establecido en 568. Si solo pudiéramos tener acceso al primer objeto wrapper creado, podríamos probarlo. Probemos esto:
Integer y = 567; // Hace un wrapper Integer x = y; // Le asigna una segunda variable de referencia System.out.println(y == x); // Verificamos que se refieren al mismo objeto y++; // Lo desenvuelve, lo usa,y lo vuelve a envolver System.out.println(x + " " + y); // Imprime los valores System.out.println(y == x); // verifica que se refieren a diferentes objetos
Esto produce la siguiente salida:
true 567 568 false
Por lo que cuando el compilador llega ala línea y++, tiene que hacer algo para sustituirlo:
int x2 = y.intValue(); x2++; y = new Integer(x2);
Como sospechabamos, debe haber una llamada a new en algún lugar.
-
Boxing, ==, y equals()
Hemos usado == para hacer una pequeña exploración de las envolturas. Vamos a ver mas sobre como las envolturas trabajan con ==, !=, y equals(). Por ahora todo lo que sabemos es que la intención del método equals() es determinar cuando 2 instancias de una clase dada son “significativamente equivalentes”. Esta definición es intencionadamente subjetiva; le toca al creador de la clase determinar significa “equivalente” para objetos de la clase en cuestión. Los desarrolladores de la API decidieron que para todas las clases wrapper, 2 obejetos son iguales si son del mismo tipo y tienen el mismo valor. No nos debería sorprender que:
Integer i1 = 1000; Integer i2 = 1000; if (i1 != i2) System.out.println("Objetos Diferentes"); if (i1.equals(i2)) System.out.println("Significativamente iguales");
Produce la salida:
Objetos Diferentes Significativamente iguales
Son solo 2 objetos wrapper que tienen el mismo valor. Debido a que tienen el mismo valor int, el método equals() considera que son “significativamente equivalentes”, y entonces retorna true. Que tal esta:
Integer i3 = 10; Integer i4 = 10; if (i3 == i4) System.out.println("mismo objeto"); if (i3.equals(i4)) System.out.println("Iguales significativamente");
Este ejemplo produce lo siguiente:
mismo objeto Iguales significativamente
El método equals() parece que está funcionando, ¿Pero que ha pasado con el == y el !=?¿Por qué != nos está contando que i1 y i2 son objetos diferentes, cuando == nos dice que i3 y i4 son el mismo objeto? Con el fin de ahorrar memoria, 2 instancias de los siguientes objetos wrapper, siempre serán == cuando sus valores primitivos seran el mismo:
- Boolean
- Byte
- Character desde u0000 hasta u007f
- Short e Integer desde -128 hasta 127
-
Donde puede ser usado el Boxing
Como discutimos anteriormente, es muy común usar wrappers en conjunción con las colecciones. Siempre que queramos que nustra colección almacene objetos y primitivos, querremos usar los wrappers para que esos primitivos sean compatibles con las colecciones. La regla general es que el boxing y unboxing funcionan dondequiera que podamos usar un primitivo o un objeto wrapper. El siguiente código demuestra algunas maneras legales de usar el boxing:
class UseBoxing { public static void main (String[] args){ UseBoxing u = new UseBoxing(); u.go(5); } boolean go(Integer i){ // Envuelve el int que ha sido pasado Boolean ifSo = true; // Envuelve el literal Short s = 300; // Envuelve el primitivo if (ifSo){ //Unboxing System.out.println(++s); // unboxes, incremente, reboxes } return !ifSo; // unboxes, retorna el inverso } }
-
Hasta aquí un poco todo lo relacionado a como podemos usar estas clases de envoltura de primitivos, o Wrapper Classes.
Sin mas, cualquier aporte o corrección es bienvenido!!.
Saludos!!!