Tipos Genéricos en Java

¿Qué son tipos genéricos?

En el momento de escribir una clase se debe conocer con qué tipo de datos va a interactuar, sin embargo esto no siempre es conocido. Por lo tanto se debería poder definir una clase con la ayuda de un “contenedor” al cual nos referimos como si fuera el tipo sobre el que opera la clase.

La definición actual de la clase es creada una vez que declaramos un objeto en particular. Los tipos genéricos, también llamados tipos parametrizados, permiten definir un tipo sin especificar todos los tipos que éste usa. Estos serán suministrados como parámetros en punto de instanciación. Los cambios son realizados por tanto en tiempo de compilación.

Los tipos parametrizados se utilizan especialmente para implementar tipos abstractos de datos: pilas, colas, anillos, bolsas y otros que permiten almacenar distintos tipos de elementos según sean instanciados en tiempo de  compilación.

Los tipos genéricos han sido usados por otros lenguajes durante años, como por ejemplo la plantillas en C++, tipos genéricos en Ada, polimorfismo paramétrico en ML y Haskell, y ahora, debido a la demanda popular , los tipos genéricos han sido añadidos en Java.

Cómo se programan los tipos genéricos

Con los tipos de genéricos, los tipos que contienen datos, como por ejemplo las listas, no están definidas para operar sobre tipos de datos específicos sino que operan sobre un conjunto homogéneo donde el tipo del conjunto es definido en la declaración. Sin embargo sin los tipos genéricos es mucho más difícil lidiar con los tipos de datos que contienen otros tipos.

Para poder hacer esto es necesario declarar una lista que acepte objetos de tipo Object. Puesto que en Java todas las clases heredan de Object la clase Lista (List) puede almacenar cualquier tipo de elementos mediante una sustitución polimórfica.

Luego, al recuperar estos elementos de la lista, es necesario añadir los castings pertinentes dependiendo del tipo de dato que haya sido introducido en ella. Un ejemplo que orienta el manejo de tipos que contienen a otros tipos de datos podría ser el siguiente. En él se declara una lista de objetos de tipo Object en la que introducimos un número de enteros obteniendo lo que ha sido introducido posteriormente. Esto, como ya ha sido comentado, ha sido posible gracias a la sustitución polimórfica al heredar en Java todos los objetos de la clase Object.

List integerList = new LinkedList();

integerList.add( new Integer(1));

integerList.add( new Integer(2));

Iterator listIterator = integerList.iterator();

While(listIterator.hasNext())

{

Integer item = (Integer) listIterator.next();

}

Problemas de la forma de programar actual

En el método usado en la versión actual de Java se pueden observar algunos problemas:

  • El programador debe recordar que tipo de elemento hay almacenado en la lista y realizar el casting al tipo apropiado cuando lo extraiga de la lista. Extraer un elemento de una lista requiere por tanto dos castings.
  • No hay comprobación de tipos en tiempo de compilación. Para el compilador los elementos de todas las listas son de tipo Object por lo que no se pueden comprobar los tipos en tiempo de compilación.
  • Necesaria comprobación explicita de tipos en tiempo de ejecución. Al no diferenciar los tipos en tiempo de compilación se hace necesaria una comprobación de los mismos en tiempo de ejecución para poder detectar posibles errores de tipos. Esto significa que si el desarrollador confunde las dos listas y hace un casting ilegal en un elemento el error no será detectado hasta el tiempo de ejecución.
  • Aparición de excepciones (ClassCastException) en tiempo de ejecución. Al aparecer en tiempo de ejecución se hace mucho más difícil la eliminación de los errores de casting que los provocan que si aparecieran en tiempo de compilación.
  • Se permite la existencia de clases contenedoras con objetos heterogéneos lo que dará problemas al intentar recuperar los elementos de las listas.

Soluciones y ventajas derivadas del uso de tipos genéricos

Algunos de los beneficios de los tipos genéricos en Java son:

  • Comprobación estricta de tipos manteniendo la misma flexibilidad que el enlazado dinámico. Con tipos genéricos se puede alcanzar un polimorfismo similar al usado en el ejemplo anterior pero con una  comprobación estricta de tipos que permite detectar errores en tiempo de compilación. El compilador conoce que los tipos de listas son diferentes porque contienen distintos elementos. Los errores al ser detectados en tiempo de compilación son mucho más fáciles de detectar que los errores en tiempo de ejecución.
  • No es necesaria la comprobación de tipos en tiempo de ejecución , lo que redunda en un código con menos castings y por lo tanto más legible y menos propenso a errores. En lugar de confiar en la memoria del usuario los parámetros marcan el tipo de los elementos obtenidos de la lista.
  • Los tipos genéricos garantizan que las listas contienen solo un conjunto homogéneo de elementos eliminando los errores derivados de la aparición de listas heterogéneas.
  • Hace que el código sea menos ambiguo y más fácil de mantener.

Motivos para iniciar el desarrollo de tipos genéricos en Java

La ausencia de soporte para el uso de tipos genéricos fue una de las principales críticas que se realizaron al lenguaje Java desde sus primeras versiones. Esta característica estaba pensada originalmente para formar parte de la especificación del lenguaje, pero debido a la falta de tiempo y a la complejidad e inmadurez de la propuesta realizada por Gosling y Joy, autores de la especificación del lenguaje, la inclusión de genéricos no pudo ser posible.

Limitaciones a la implementación de tipos genéricos en Java

La principal restricción que se impuso al JSR14 a la hora de elaborar su especificación de tipos genéricos, era que el nuevo código genérico debería funcionar perfectamente con el viejo código que no soportaba todavía esta característica. Por ejemplo, código antiguo programado para usar las clases “collection” no genéricas, debería funcionar perfectamente con las nuevas clases “collection” con soporte para la genericidad, esto es posible mediante el uso de los llamados tipos crudos (“raw types”) como veremos más adelante, ya que la nueva implementación de la máquina virtual no necesita información extra sobre el tipo de los objetos. Así, una llamada al método getClass() sobre un contenedor genérico, no indicará el tipo que guarda el contenedor. Esta restricción nos permite sin embargo que el código antiguo sea compilable sin ninguna modificación.

Características

Las principales características de la especificación de los tipos genéricos en Java son:

  • Permite el polimorfismo restringido, es decir, se puede especificar por ejemplo, que un parámetro deba implementar una determinada interfaz para poder ser un parámetro válido.
  • Permite el polimorfismo f-restringido, es decir, se puede especificar que un parámetro deba implementar una interfaz que a su vez es también genérica.
  • No permite los “mix-ins”, un parámetro no se puede especificar como un supertipo. Permite el uso de métodos genéricos.
  • Mínimos cambios en la máquina virtual. Traducción homogénea. Se compila la clase genérica en un único archivo .class con extensiones sólo para el compilador. Esto tiene como ventaja que sólo es necesario un archivo en disco y una única instancia en tiempo de ejecución pero al precio de penalizaciones en la velocidad de ejecución, ya que no se puede optimizaciones de velocidad en el código y la necesidad de “castings”.
  • Soporte limitado a la reflexión como se ha visto anteriormente.
  • No se permite la instanciación de tipos genéricos con sustitución de parámetros por tipos primitivos.
  • No permite sustitución de parámetros por “no tipos”: constantes, “functors”…
  • Garantiza la compatibilidad con código antiguo no preparado para el uso con tipos genéricos.

Diferencias entre los tipos genéricos en Java y las plantillas de C++

Los tipos genéricos de Java no son plantillas, hay una serie de diferencias entre ambos enfoques:

En la declaración de un tipo genérico en Java existe comprobación de tipos no así en C++, en Java los tipos genéricos se compilan una sola vez, el código del tipo no se muestra al usuario del tipo.

En C++ se utiliza la “traducción textual” para instanciar los tipos, la declaración de la plantilla se utiliza como una macro, por lo tanto se necesita el código fuente de la plantilla para su instanciación, cada vez que se utiliza una plantilla esta se recompila.

Esta aproximación es “grande y rápida” ya que cada instanciación de la plantilla consume almacenamiento primario y secundario, sin embargo son posibles optimizaciones de velocidad en tiempo de ejecución. También permite un completo soporte para la reflexión. Por el contrario la aproximación usada en la especificación de tipos genéricos en Java se utiliza la “traducción homogénea”, esta es una aproximación “pequeña y lenta” sólo se requiere un archivo “bytecode” por tipo genérico, con el ahorro de memoria y además se oculta el código fuente del tipo, pero imposibilita las optimizaciones de velocidad y se limita el soporte a la reflexión.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *