cerosYunos

Cómo crear objetos de un tipo parametrizado en Java

computadora
Pablo Sánchez
p.sanchez (arroba) unican (punto) es
Dpto. Matemáticas, Estadística y Computación
Facultad de Ciencias, Universidad de Cantabria



1. Introducción

Se asume que el lector conoce lo que son los genéricos, y sabe como hacer clases genéricas en Java

El problema la creación de objetos de un tipo parametrizado en Java es el siguiente. Dado el esquema de la Figura 1, el problema es que la sentencia de la línea 04 da un error de compilación porque el compilador no puede asegurar que exista un constructor vacío para el tipo T, cualquiera que éste sea. En lo que resta de documento analizaremos diversas soluciones, viendo si son factibles, y que ventajas e inconvenientes plantea cada una.

Fig. 1 - Descripción del problema
public class Foo<T> { 
 
   public T newElemento() {
	 return new T();
   } // newElemento
	
}// Foo<T>

1. Intento (fallido) 1: Exigir que haya un constructor por defecto usando la restricción extends

Sabemos que cuando queremos obligar a un tipo parametrizado a ser capaz de responder a ciertas operaciones, la técnica que se usa en Java es obligar a que el tipo parametrizado sea un subtipo de un cierto tipo, normalmente especificado por medio de una interfaz. Siguiendo esta técnica, intentamos crear la interfaz de la Figura 2 (izq) y luego obligamos a que el tipo parametrizado herede de dicha interfaz, tal como se muestra en la Figura 2 (dch).

Fig. 2 - Intentamos que el tipo T tenga un constructor vacío usando una interfaz
public interface ICreable {   

	// Constructor vacío
	public ICreable() { // Error
	}	

} // ICreable
public class Foo<T extends ICreable> {
	
   public T newElement() {
      return new T();
   } // newElemento
   
}// Foo<T>

El problema, por todos conocido, es que las interfaces en Java no nos permiten declarar constructores. Por tanto, la solución de la Figura 2 no nos valdría porque sencillamente no compila.

Una alternativa al problema de arriba es convertir la interfaz en una clase abstracta tal como se muestra en la Figura 3 (izquierda). Una interfaz es en cierto modo equivalente a una clase abstracta. El primer problema que nos encontramos (ver Figura 3 (centro)) es con el sistema de tipos. Con la técnica usada podemos crear objetos de la clase padre, es decir de Creable, pero Creable no es del tipo T (es al revés, es T quien es del tipo Creable). Por tanto, Creable no puede aparecer donde se requiera algo del tipo T y el compilador genera el correspondiente error. Además, es imposible crear objetos de clases abstractas, así que aunque pudiésemos sortear este error, el problema permanecería.

Fig. 3 - Intentamos que el tipo T tenga un constructor vacío usando una clase abstracta
public abstract class Creable {
	
	public Creable() {
	  // Se pueden crear constructores
	  // de  clases abstractas pero 
	  // no se pueden invocar
	} // Creable()

} // Creable
public class Foo<T extends Creable> {
	
   public T newElement() {
	  // Problemas con los tipos
	  // Creable no es del tipo T
      return new Creable(); 
	} // newElement
   
}// Foo<T>

2. Intento 2: Reflexión

Reflexión es una técnica por la cual un programa puede acceder a su estructura interna para obtener información sobre sí mismo y/o modificarse. Por ejemplo, el método getClass() es un método reflexivo que poseen todos los objetos de Java por heredar de la clase Object. getClass() devuelve un objeto de tipo Class donde Class es una clase que representa la clase de todas las clase Java. Por tanto, todas las clases de las que se compone una aplicación son instancias u objetos de la clase Class.

Fig. 4 - Creando una instancia de una clase usando reflexión.
Person p = new Person(); 
				
Class<? extends Person> clase = p.getClass();

try {
	Person p =  clase.newInstance();
} catch (InstantiationException e) {
	System.out.println(p.getName() + " no puede ser instanciada");
	System.out.println("Puede ser que " + p.getName() + " sea una clase abstracta," +
		" una interfaz, un vector (array), a primitive type, or void;");		
	e.printStackTrace();
} catch (IllegalAccessException e) {
	System.out.println("La clase " + p.getName() + "no tiene un constructor " + 
		"vacío y público");
		e.printStackTrace();
} // try

Usando reflexión podemos crear objetos ''a mano'', es decir, tal como lo haría la máquina virtual de Java internamente. Para ello se usaría la secuencia de instrucciones que aparece en la Figura 4 . En primer lugar obtendríamos la clase de un cierto objeto (NOTA: Este objeto debe de existir, es decir, no puede ser null) mediante el uso del método getClass() (línea 03). Este método está declarado en la clase Object y por tanto está disponible en todos los objetos de Java. Este método devuelve un objeto del tipo Class<T> (Sí, la clase Class es genérica). A continuación, sobre un objeto de tipo Class podemos invocar el método newInstance() (línea 06). Este método invoca al constructor vacío (y público) de la clase (si éste existiese) y devuelve un objeto del tipo representado por Class<T>. En nuestro caso, un objeto de tipo Person. Adviértase que por el uso de genéricos en la clase Class no se precisa del uso de ningún tipo de casting.

El método newInstance() puede generar diversas excepciones. En el ejemplo de la Figura 4, se controlan dos de ellas. La primera InstantiationException informa de que la clase no puede ser instanciada por ser, por ejemplo, una clase abstracta o una interfaz. La segunda de ellas, IllegalAccessException informa de que no es posible invocar al constructor vacío para dicha clase, bien porque dicho constructor simplemente no existe o porque en caso de existir no es público. Por tanto, si usamos esta técnica reflexiva deberíamos establecer como precondición al tipo parametrizado que tenga un constructor público y vacío para evitar que se produzcan estas excepciones.

Una desventaja de esta solución es que necesitamos tener una instancia de la clase de la cual queremos crear un objeto. Si tenemos, por algún azar del destino, una instancia distinta de null del tipo parametrizado T accesible desde donde queremos crear un objeto del tipo T, esta solución funcionaría sin mayores problemas (ver Figura 5 (izquierda)). Sino, es la pescadilla que se muerde la cola, porque lo que necesitaríamos para resolver el problema de crear un objeto del tipo parametrizado sería precisamente crear una instancia del tipo parametrizado (ver Figura 5 (derecha)).

Fig. 5 - Aplicando reflexión al problema de la Figura 1.
public class Foo<T > {
	
   public T newElement() {
	  
	  T result;
	  
	  // Hay un objeto objT accesible y 
	  // del tipo T
	  try { 		
		result = objT.getClass().newInstance(); 
	  } catch(...) {...}
	  
	  return result;
	} // newElement
   
}// Foo<T>
public class Foo<T> {
	
   public T newElement() {
	  
	  // Intento crear un objeto de tipo T 
	  T objT = new T(); 
	  // Error, no se pueden instancias
	  // tipos patrametrizados
	  try { 		
		result = objT.getClass().newInstance(); 
	  } catch(...) {...}
	  
	  return result;
	} // newElement
   
}// Foo<T>

En realidad queremos simplemente un objeto del tipo parametrizado para poder invocar el método getClass y obtener su clase. Por tanto, el problema planteado en la Figura 5 (derecha) se solventaría simplemente modificando la cabecera del método newElement para que acepte como parámetro un objeto del tipo Class<T>, tal se muestra en la Figura 6. El único problema con este enfoque, cuando queremos crear objetos simples, es que resulta ''molesto'' tener que estar pasando la clase del tipo parametrizado cuando queremos invocar métodos que necesiten crear objetos de dicho tipo parametrizado. Es decir, es simplemente un problema de elegancia al programar.

Fig. 6 - Creando objetos reflexivamente usando un objeto que representa la clase del objeto a crear.
public class Foo<T> {
	
   public T newElement(Class<T> cT) {
	  
	  T result = null;
	  
	  try { 		
		result = cT..newInstance(); 
	  } catch(...) {...}
	  
	  return result;
	} // newElement
   
}// Foo<T>

Un problema más serio es que con esta solución podemos crear objetos del tipo parametrizado, pero no podemos crear vectores (arrays) del tipo parametrizado. Es decir, el código de la Figura 7 no funcionaría porque al invocar el método newInstance() (línea 06), dicho método generaría una excepción del tipo InstantiationException, ya que no se pueden crear vectores (arrays) usando newInstance().

La solución sería utilizar el método estático Array.newInstance(Class<?> c, int length), que crea un array de length objetos de la clase Class<?>, tal como se ilustra en la Figura 7.

Fig. 7 - Creación un vector (array) de objetos usando reflexion.
Person p = new Person(); 
				
Class<? extends Person> clase = p.getClass();

Person [] vPerson = (Person []) Array.newInstance(clase, 10);

Adviértase que en este caso necesitamos saber también, como es obvio por otra parte, el tamaño del vector.

Además, la clase Array no es genérica, por lo que deberemos de hacer un casting para convertir el valor devuelto por el método Array.newInstance(..) al tipo deseado (NOTA: Si lo hacemos de forma consciente, este casting será seguro).

El principal problema de la reflexión, aparte de que necesitamos un objeto del tipo T o del tipo Class<T> para que funcione, es que, aunque potente, es también bastante peligrosa, y requiere además de cierta capacidad de abstracción por parte del programador, propiedad que por desgracia no se da con la frecuencia deseada. Además, hemos asumido la existencia de un constructor público y vacío. Cuando dicho constuctor no exista, aún es posible crear objetos reflexivamente, pero ello implica buscar y llamar de forma reflexiva el constructor deseado, los cual es un proceso más largo, complejo y tedioso. En estos caso, se recomienda simplemente aplicar el patrón Factoría [1], tal como se explica en la siguiente sección.

3. Solución 3: Patrón Factoría

Una clase factoría es simplemente una clase que sirve para crear objetos de otra clase. La Figura 8 muestra el ejemplo de una factoría simple que devuelve objetos del tipo Person cuando se invoca al método newPerson().

Fig. 8 - Ejemplo de una clase factoria para el tipo Person.
public class FactoryPerson  {

	public Person newPerson() {
		return new Person();
	}

} // FactoryPerson

Por tanto, se trataría de usar una factoría cuando queramos crear un objeto de un tipo parametrizado. Pero, ¿qué factoría?. Pues, desgraciadamente la factoría dependerá del tipo parametrizado. No podemos parametrizar el código de la Figura 8 con respecto a Person, que es lo que desearíamos, porque si sustituimos Person por un parámetro volveríamos al problema original, que es que no podemos crear objetos de un tipo parmetrizado. Por tanto la solución es obligar a que junto con cada tipo parametrizado, se proporcione la factoría que lo crea. Desgraciadamente esta factoría no es conocida hasta que se instancia el tipo parametrizado. La solución es crear una interfaz IFactory<T> que contenga los métodos para crear objetos del tipo T que toda factoría debería tener de acuerdo con las necesidades de nuestra aplicación. Por tanto, toda factoría concreta para un tipo específico deberá implementar la interfaz IFactory<T>. Un ejemplo de tal interfaz se muestra en la Figura 9 (izquierda).

El siguiente paso es que la clase parametrizada pueda invocar a la factoría para los tipos concretos. Si intentamos declarar una variable del tipo IFactoria<T> sin conocer el tipo concreto de la factoría que implementa dicha interfaz, nos será imposible crear un objeto para dicha variable. Recordemos que el tipo concreto de la factoría a utilizar sólo se conoce cuand se instancia el tipo parametrizado.

Ello significa que debemos pasarle la factoría concreta a la clase parametrizada una vez que la clase parametrizada ha sido instaciada y creada. Podríamos crear un método setter que le asigne a la cada instancia de la clase parametrizada su correspondiente factoría. O, simplemente, podemos modificar los métodos de la clase parametrizada que necesiten crear objetos del tipo parametrizado para que acepten como parámetro un objeto que implemente (es decir, que sea del tipo) IFactory<T>, donde T es el tipo parametrizado. Esta solución se ilustra en la Figura 9 (derecha).

Fig. 9 - Aplicando el Patrón Factoria a nuestro problema.
// Declaramos una interfaz con un 
// método cuyo único propósito es 
// crear objetos del tipo T
public interface IFactory<T> {

  public T newObject();
  
} // IFactory<T>
public FactoryPerson implements  IFactory<Person>  {

  public Person newObject() {
	
	return new Person(); 
  } // newObject

} // FactoryPerson
public class Foo<T> { 
 
   public T newElemento(IFactory<T> f) {
	  
	  return f.newObject();
   } // newElemento
	
}// Foo<T>

Es lógico preguntarse que ventaja reporta este esquema con respecto al que usa reflexión. En primer lugar, hemos de controlar menos excepciones. En segundo lugar, a la mayoría de los programadores les resulta más rápido e intuitivo crear factorías que manejar reflexivamente un programa. El problema de tener que seguir pasando argumentos a los métodos que requieren crear objetos del tipo parametrizado persiste. Es lícito en este punto preguntarse, ¿qué ventaja real aporta el uso de factorías?. Las factorías premiten crear de manera más sencilla objetos cuyos constructores precisan parámetros, siempre y cuando sepamos de antemano que parámetros son.

Supongamos que todos los tipos parametrizados para nuestra clase deben tener un constructor público con un argumento que acepte un parámetro de tipo int, pero no tienen constructor público. En tal caso modificaríamos él ejemplo de la Figura 9 tal como se muestra en la Figura 10. Este mismo problema, usando reflexión, nos obligaría a buscar el constructor adecuado en la lista de constructores para la clase parametrizada y a continuación invocar el constructor deseado de forma reflexiva, lo cual es un proceso complejo, largo, tedioso y propenso a errores.

Fig. 10 - Aplicando el Patrón Factoria a constructores parametrizados.
public interface IFactory<T> {

  public T newObject(int i);

  
  
  
  
} // FactoryPerson
public FactoryPerson implements Factory<Person>  {

  public Person newObject(int i) {
	
	return new Person(i); 		
  
  } // newObject

} // FactoryPerson
public class Foo<T> { 
 
   int sizeT;
 
   public T newElemento(IFactory<T> f) {
	  return f.newObject(sizeT);
   } // newElemento
	
}// Foo<T>

4. Referencias

[1] E. Gamma, R. Helm, R. Johnson, J. M. Vlissides."Design Patterns: Elements of Reusable Object-Oriented Software".Addison-Wesley Professional (November, 1994)


Last update: 29/10/2010