Cómo crear objetos de un tipo parametrizado en Java |
||
Pablo Sánchez |
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.
public class Foo<T> { public T newElemento() { return new T(); } // newElemento }// Foo<T> |
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).
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.
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> |
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
.
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)).
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.
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.
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 claseArray
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.
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().
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).
// 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.
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> |
[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 |