Wrapper Classes y Boxing en Java

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 exception
      x x x x x x
      parseXxx (con radix)
      static, NFE exception
      x x x x
      valueOf
      static, NFE exception
      s x x x x x x
      valueOf (con radix)
      static, NFE exception
      x x x x
      toString x x x x x x x x
      toString (primitive)
      static
      x x x x x x x x
      toString (primitive, radix)
      static
      x 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!!!