Además del polimorfismo, una característica que permite a la plataforma Java tratar homogéneamente objetos heterogéneos, de los que habitualmente no se conoce su tipo concreto, el lenguaje Java cuenta con otro mecanismo con el mismo fin: los tipos genéricos.
Al definir una clase genérica, o bien una interfaz, en realidad estamos definiendo un meta-tipo, una especie de plantilla a partir de la cual se crearán posteriormente clases/interfaces que actuarán como tipos concretos.
El objetivo principal de los tipos genéricos es evitar que tengamos que utilizar la clase Object
como tipo para ciertos atributos, parámetros o valores de retorno, como se hacía tradicionalmente en las primeras versiones de Java, al ser Object
el ascendiente común de todos los tipos por referencia y, por tanto, un mínimo común denominador. En el siguiente vídeo, nuestro tutor Francisco Charte te ayuda a conocer los aspectos esenciales sobre la definición de clases/interfaces con tipos genéricos, así como la posterior instanciación de tipos concretos. También comprobarás la utilidad de esta funcionalidad de Java:
Transcripción del vídeo
En este vídeo vamos a conocer la utilidad de los tipos genéricos de Java definiendo una clase de tipo genérico y viendo cómo podemos utilizarla con diferentes tipos concretos.
Los tipos genéricos de Java son similares, en cuanto a funcionalidad se refiere, a las plantillas de C++ y lenguajes similares. El objetivo es definir una clase, una interfaz o un método cuyos tipos de datos son parametrizables. Los tipos genéricos están estrechamente vinculados a las estructuras de datos clásicas como pueden ser las colas y las listas, etc.
En lugar de definir una clase "Pila" para trabajar con números enteros, otra para trabajar con números en punto flotante, una tercera para trabajar con cadenas y así sucesivamente, es mucho más sencillo definir una clase que sea capaz de tratar con datos de diferentes tipos. En Java esto inicialmente implicaba utilizar el tipo Object
, pero esto conlleva una serie de problemas.
Al definir los atributos de una clase como Object
es necesario realizar posteriormente conversiones a los tipos concretos que el usuario desea utilizar. Asimismo, es un peligro, puesto que sería potencialmente posible introducir datos de diferentes tipos dentro de esa estructura de datos.
Con los tipos genéricos estos inconvenientes se evitan, puesto que el tipo concreto de dato con el que va a trabajar es un parámetro de entrada más.
Vamos a utilizar, como es habitual, Netbeans y vamos a crear un nuevo proyecto que contará inicialmente con una clase "Plano". El objetivo de esta clase será representar planos cuyas coordenadas podrán ser de diferentes tipos. Podrán ser números enteros de diferentes tamaños o números en punto flotante.
Lo primero que vamos a hacer en esta clase es agregar detrás del nombre de la clase el parámetro T
que es el que va a actuar como tipo de dato parametrizable. En lugar de T
podríamos asignarle cualquier otro nombre (otro identificador), pero es habitual utilizar esta notación. Completamos la documentación asociada a la clase y, dentro de la definición de la clase, ya podemos utilizar este tipo parametrizable T. Por ejemplo, para definir los atributos con los que va a contar cada uno de los planos, vamos a tener un mínimo y un máximo para el eje X, y un mínimo y un máximo para el eje Y. Observa que el tipo de estos cuatro atributos, de estas variables, es T
. No conocemos en este momento cuál va a ser el tipo concreto.
De igual forma, al definir el constructor de esta clase, definimos la lista de parámetros utilizando este mismo tipo y guardamos los datos recibidos en los correspondientes atributos.
Análogamente podemos implementar el habitual método toString()
para mostrar el plano, las coordenadas del plano, por la consola y también implementamos los getters y setters, que todos ellos utilizarían el tipo T
como parámetro.
Vamos a comprobar cómo podríamos crear objetos de esta clase plano especificando un tipo concreto para este parámetro T.
Nos vamos a nuestra clase principal donde tenemos el habitual método en el que vamos a introducir el código.
Vamos a comenzar declarando una variable Plano<Integer>
.
Fíjate que al especificar el tipo ya entre los símbolos menor y mayor, especificamos cuál será el tipo concreto.
Este ha de ser necesariamente un tipo por referencia.
No podemos utilizar tipos primitivos de Java.
Una vez hemos declarado la variable podemos crear un objeto de esa clase y especificar los parámetros.
El objeto podemos mostrarlo por consola.
Si ejecutamos la aplicación aquí podemos comprobar que tenemos un plano con coordenadas enteras.
Análogamente podríamos definir un segundo plano cuyas coordenadas sean de un tipo numérico en punto flotante: Plano<Double>
Aquí especificamos cuáles son sus coordenadas.
Suponemos que es un plano virtual con centro en (0,0) y que va de -1 a +1 en los dos ejes.
Mostramos también que este plano por consola, y aquí tenemos las coordenadas.
Partiendo de una única definición para la clase Plano
de tipo genérico, estamos creando objetos de una clase Plano
que trabaja con enteros y de una clase Plano
que trabaja con números en punto flotante.
De hecho, este sistema es tan flexible que podríamos llegar a introducir cualquier tipo de dato como coordenada.
Por ejemplo, vamos a crear una variable de la clase Plano
especificando como tipo concreto TiposGenericos
.
Observa qué TiposGenericos
es esta misma clase.
No es un tipo numérico que podamos utilizar para establecer las coordenadas de un plano pero esto, la clase que hemos definido, Plano, no lo sabe, con lo cual nos permite declarar la variable y a la hora de crear el objeto observa que en este punto no he especificado el tipo.
Podría introducirlo pero no es necesario.
La notación "diamond" que así se denomina en Java, infiere el tipo a partir del tipo de la variable.
Como decía, aquí creamos una variable de tipo Plano<TiposGenericos>
y puesto que el constructor espera cuatro parámetros de este tipo TiposGenericos
lo que hacemos es crear cuatro instancias de esta clase completa y las facilitamos como parámetro.
No hay ningún problema.
Si ejecutamos el programa veremos cómo funciona.
Lo que ocurre es que las coordenadas de este hipotético plano son objetos que no tienen un sentido para esta funcionalidad concreta.
Para evitar este problema, lo que podemos hacer es establecer una restricción en la definición de la clase Plano
, del tipo genérico Plano, especificando que este tipo que hemos indicado aquí, T
, ha de ser necesariamente un subtipo de la clase Number
, que es la clase que actúa como superclase de todos los tipos numéricos por referencia de Java, incluyendo Integer, Long y Double.
Una vez que hemos introducido esta modificación, si volvemos al código del proyecto podemos comprobar cómo esta sentencia directamente ya genera un error.
No podemos ejecutar el programa.
Vemos que ese error lo impide de tal forma que la única manera de crear variables de tipo Plano sea especificando alguno de los tipos numéricos que ya conocemos.
En resumen, lo que nos permiten los tipos genéricos es facilitar una sola definición de una funcionalidad que posteriormente se aplicaría a diferentes tipos concretos, lo cual en definitiva nos ahorra mucho trabajo de codificación.