Héctor Luaces

Genéricos para recién llegados a Java

p>Recientemente he empezado a preparar una certificación de Java. He programado en POO desde años atrás gracias a administrar y programar en el juego online Reinos de Leyenda durante muchos años, por lo que empezar a “pensar en Java” me costó poco, aunque he de reconocer que algunas cosas me tuvieron en vilo más que otras.

En el curso me di cuenta de que la idea de los genéricos en Java es algo que muchos programadores no entienden. Precisamente por eso me apeteció escribir este artículo Genéricos para recién llegados a Java; porque espero que pueda ayudar a alguien que tenga problemas para entender estos conceptos. Probablemente la primera pregunta relacionada con los genéricos que se hace un programador que recién ha llegado a java es: “¿que es esa que veo en muchas declaraciones de clases, interfaces o métodos?”. Pues bien: eso es un genérico.

¿Para qué sirven los genéricos en Java?

Los genéricos sirven para permitir que métodos o clases puedan operar con distintos tipos de objetos proporcionando seguridad en tiempo de compilación. Esto nos permite crear código más legible y mucho más robusto.

Supongamos las siguientes dos clases:

class A()
{
}
class B()
{
}

Imaginemos que queremos crear una colección que guarde instancias de la clase A. Podríamos hacerlo así:

ArrayList c = new ArrayList();
c.add(new A());
c.add(new A());
c.add(new A());

Sin embargo, esto no nos garantiza que la colección solo tenga elementos de ese tipo. Podemos añadir instancias de otras clases sin problema…

ArrayList c = new ArrayList();
c.add(new A());
c.add(new A());
c.add(new A());
c.add(new B());
c.add(new B());

Esto podría solucionarse añadiendo otra capa de abstracción que se encargue de controlar los elementos que vamos añadiendo, pero eso no garantiza necesariamente un 100% de fiabilidad. El programador, a la hora de recuperar un objeto de una colección, no puede saber con seguridad a que clase pertenece.

Fijémonos en este pequeño snippet:

ArrayList c = new ArrayList();
A clase_a;

c.add(new A());
c.add(new B()); // Fijémonos que podemos añadir elementos de cualquier clase a la colección

clase_a = (A) c.get(1); // El programador intenta recuperar elementos de la colección en la que cree que solo hay instancias de la clase (A)

El código superior es erróneo. Intentamos asignar a la variable clase_a una instancia de la clase B. El compilador no nos dará ningún problema, pero seguramente el cliente o usuario del código si nos de un par de mareos de cabeza cuando la ejecución del código llegue ahí, ya que eso provocará una excepción en tiempo de ejecución. Basta decir, que esto es lo que un programador ha de evitar a toda costa: queremos estar seguros de que nuestro código es sólido, no podemos permitirnos tener errores descontrolados como este.

La diferencia entre un error en tiempo de ejecución y un error en tiempo de compilación es enorme. Un error en tiempo de ejecución puede permitir que distribuyamos un aplicativo con errores. Un error en tiempo de compilación, no nos lo permitirá. Sabiendo esto, creo que estaremos de acuerdo en que cuanto antes veamos el error, mejor. No queremos una llamada de un cliente a última hora explicándonos un problema por culpa de un error en tiempo de ejecución que no hayamos visto. Esto solo trae disgustos y horas extra.

¿Qué tienen que ver los genéricos en esto?

Los genéricos nos permitirán especificar que clase de elementos han de estar dentro de una colección. Java no permitirá añadir instancias de otra clase que no sea la declarada mediante el genérico.

Imaginemos el mismo código, esta vez, usando genéricos (después explicaré la sintaxis de los genéricos, ahora quiero que os quedéis con la diferencia):

ArrayList c = new ArrayList(); 
c.add(new A()); 
c.add(new A()); 
c.add(new A()); 
c.add(new B());

El código solo se diferencia en la adición del genérico en la inicialización y declaración de la variable c. Fijaos en el operador “diamond” (<>) que alberga en su interior el nombre de una clase, eso significa que la colección que precede a ese operador solo podrá contener instancias de esa clase. Si intentamos añadir una instancia de otra clase, se producirá un error en tiempo de compilación.

Al lanzar el error en tiempo de compilación estamos ganando dos cosas: fiabilidad y simplicidad.

Al lanzar el error el tiempo de compilación, estamos seguros de que no vamos a distribuir código que pueda tener errores como los que se daban cuando no usabamos genéricos. El código anterior no compilará, por lo que sabremos donde está el error sin tener que esperar a la llamada de un cliente molesto. En proyectos grandes donde trabajan varios programadores, esto dará mucha fiabilidad a la hora de compilar una nueva versión de nuestro aplicativo.

También ganamos en simplicidad. El concepto de “colección de instancias de una clase determinada” es algo que tarde o temprano termina apareciendo en los análisis funcionales de muchos desarrollos. No es raro encontrar soluciones a medida que añadan una capa de abstracción para asegurarse de que una colección, realmente, solo guarda el tipo de datos que queremos. Con los genéricos, no necesitamos ningún desarrollo: el propio Java nos da esta funcionalidad.

¿Como usamos los genéricos?

Reconoceremos el uso de genéricos en una clase, interfaz o método cuando veamos un solo carácter en mayúsculas encerrado en el operador “diamond”. Por ejemplo: .

Como ejemplo, veamos la declaración de la clase ArrayList:

public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, Serializable

El operador después de ArrayList significa que la clase está implementando genéricos. Si nos fijamos en sus herencias e implementaciones, veremos que AbstractList y List también usan genéricos.

Para usar una clase que implemente genéricos, solo deberemos inicializar y declarar esa clase usando el operador diamante con la clase deseada. ¿Confuso?, veamos un ejemplo.

ArrayList str = new ArrayList(); // Un ArrayList que solo permite Strings

Como podéis ver, usamos el operador diamante con la clase String de la siguiente forma:. De esta forma, le decimos a Java que queremos un ArrayList que solo acepte instancias de la clase , si intentamos añadir otra cosa, tendremos un bonito error en tiempo de compilación.

Una vez entendida la funcionalidad de los genéricos, podemos decir que su sintaxis es similar (ignorando el operador diamante) a la de la declaración de parámetros de cualquier método, ya que una misma clase o método puede usar todos los genéricos que necesite. Veamos la declaración de la interfaz Map:

public interface Map

Como veis, utiliza dos genéricos. Esto es que, a la hora de inicializarlo o declararlo deberemos usar dos nombres de Clase en el interior del operador diamante. La funcionalidad, evidentemente, dependerá de la clase. Para que nos entendamos mediante la práctica, voy a poner un ejemplo de que clase que impelementa la interfaz Map y, por tanto, usa dos genéricos. Esta clase es la clase HashMap.

Declaración de la clase, para que sepamos que hacemos:

public class HashMap extends AbstractMap implements Cloneable, Serializable

Ejemplo de clase que usa dos genéricos:

HashMap  h = new HashMap();
h.put(new Integer(1), "uno");
h.put(new Integer(2), "dos");
h.put("tres", 3); // Esto dará un error, ya que "tres",3 no se corresponde con la declaración  que hemos hecho

En el ejemplo se crea un HashMap (un array asociativo) que relaciona un objeto Integer con un String. En concreto, relaciono un número con su representación lexicográfica. Si intentamods crear una relación llave -> valor que no sea Integer -> String, tendremos un error de compilación, como se ve en la línea 4.

¿Qué significan , y ?, ¿hay diferencias?

Esta es una duda que tiene mucha gente y la respuesta es simple: no. Los genéricos pueden nombrarse como queramos, como si se tratasen de parámetros. Los genéricos tienen nomenclaturas distintas debido a la convención de nombres que Oracle, amigablemente, nos pide que usemos. Si tenéis curiosidad, os muestro cuando se usan unos y otros:

Si queréis saber más, podéis leer la convención de Oracle. Pero recordad que no tiene ningún impacto en el código y esto solo se usa para facilitar la documentación. Un genérico podría llamarse o , pero a estas alturas ya sabréis que los programadores de Java se toman muy en serio las convenciones.

¿Cómo implemento mis propias clases con genéricos?

Para usar un genérico (o más, claro) en tus clases, solo deberás añadir el operador diamante y la lista de genéricos a usar después de la generación de la clase.

Imaginemos una clase cuya funcionalidad es la de almacenar un array de clases.

public class Almacen

Como veis, solo tenemos que añadir el genérico en su declaración para poder usarlo. ¿Que hacemos con él?, pues en el interior de la clase podremos usar para referirnos al tipo de clase usado a la hora de instanciar el objeto. ¿Confuso?, ejemplo rápido:

public class Almacen
{
    private ArrayList lista;

    public Almacen()
    {
        this.lista = new ArrayList();
    }

    public void add(E item)
    {
        this.lista.add(item);
    }
}

En el ejemplo podéis ver como usamos para referirnos a la clase que se usó como genérico durante la instanciación o inicialización. De esta forma, creamos un ArrayList igual (haciendo referencia a) y, además, mostramos un ejemplo en el que usamos E para hacer referencia al tipo de parámetro usado un método. Veamos un ejemplo de uso de la clase:

Almacen h = new Almacen();

h.add("hola");
h.add(1); // Esto generará un error, porque la clase Almacen() se inicializó con el genérico

Notas finales

Hay mucho más que hablar de genéricos, pero lo haré más adelante, porque creo que explicar algunas cosas a mayores podría entorpercer el proceso de asimilación de toda esta información. Pronto nos veremos en otra entrega donde hablaré de herencia, comodines y métodos que usan genéricos. Como último apunte, os dejo unos cuantos enlaces donde podéis obtener más información sobre genéricos.

18 Comentarios

  1. Muy buen post lo has explicado de maravilla, no he perdido el tiempo leyendolo a merecido la pena gracias Héctor

  2. gracias, por fin entendi el atado de las letras mayusculas tipo T, N y similares.
    saludos

  3. No se ven los ejemplos bien, vaya cagada.

  4. Hola buen dia…
    Estoy apenas entendiendo sobre genericos y ahora tengo un detalle, por ejemplo si creo un list que es lo que deberia ir en mi claseprueba?, aun me resulta algo confuso

  5. Hola muchas gracias logre entender muy bien esto. Además de los arreglos que más aplicaciones y ventajas podemos obtener de los genéricos, además que relación tiene con el polimorfismo para-métrico. Muchas gracias

    • Hola Julián,

      Creo que en este hilo se habla bastante de las ventajas, pero supongo que estás en algún trabajo académico y quieres desglosarlos un poco, pues ahí va:

      • Consiguen que podamos eliminar el casting de nuestro código, eliminando código innecesario y haciendo todo mucho más claro.
      • Añaden chequeos de lógica en tiempo de compilación, haciendo que el código sea mucho más robusto, ya que al forzar a ciertos métodos a admitir solo determinados tipos de objetos, estamos indirectamente eliminando casos en los que se pueda usar el método con un parámetro incorrecto.
      • El código es mucho más legible y abstracto, pudiendo crear algoritmos completamente reutilizables que usen genéricos para fortalecer todo aún más.
      • Podemos crear colecciones de genéricos, sabiendo que todos los elementos de la colección serán de un tipo dado.

      Respecto al polimorfismo paramétrico, los genéricos son la implementación de Java de este concepto de POO, por lo que no puede estar más relacionado. Si entiendes el concepto de polimorfismo paramétrico, verás que los genéricos son realmente eso.

      Espero haberte solucionado alguna duda.

      Un abrazo, Julián.

  6. Gracias por tu aporte, me ayudó a entenderlo mejor.

  7. Esta es por mucho la MEJOR explicacion sobre generics .. felicidades 🙂

  8. Buenos dias, estoy intentando crear una lista dentro de otra lista genérica, pero no se como hacer para guardar los elementos; esto es lo que me mandaron y no se como hacer

    Cree una clase genérica que permita administrar un listado de

    Registros de vuelos en un aereopuerto.
    Campos de los registros (Id, Prioridad -del 1 al 3- , Origen, Destino)

    Registros de Autos en un estacionamiento.
    Campos de los registros (Id, Piso -del 1 al 3- , Dueño, Placa)

    Cada vuelo representará un registro de la lista, por lo que usted deberá crear una matriz.
    Ejemplo de
    Listado de Vuelos(
    (1, 2 , Aruba , Miami)
    (2, 1 , Portugal , España)
    (3, 1 , Margarita , Curacao )
    )

    Listado de Autos(
    (3, 2 , Pedro , smb835)
    (2, 2 , Maria , nyk230)
    (3, 1 , Angela , uyd497)
    )

    Garantice que la Clase genérica permita:
    Agregar un registro a la lista.

    • Hola,

      Tendrás que crear una clase que represente una entrada de ‘vuelo’ o de ‘autos’ y posteriormente crear una lista genérica con esa clase.

      Por ejemplo, una entrada del registro de vuelo puede ser una clase como la que enlazo aquí.

      Tras eso solo tendrías que crear la lista para esos objetos, por ejemplo:

      ArrayList listadoVuelos;
      
      listadoVuelos = new ArrayList<>();
      listadoVuelos.add(new RegistroVuelo(1, 2, "Aruba", "Miami"));
      listadoVuelos.add(new RegistroVuelo(2, 1, "Portugal", "España"));
      

      Un saludo,
      Héctor

      • tengo una duda!!! ok yo creo mis clases de registro tanto para autos y para los vuelos. ero como garantizo que mi clase generica me permita ingresar vuelos, ingresar autos.

        esto es lo que me piden.
        Garantice que la Clase genérica permita:
        Agregar un registro a la lista.
        Eliminar un registro de la lista. Según la placa de un carro, o según el id del vuelo.
        Buscar un registro si existe filtrando por el campo destino, o placa de un carro, con la finalidad de mostrar el registro completo. En caso de que no exista debe indicarlo.
        Mostrar el listado de registros de vuelos y de autos.
        Garantizar que los ID de los registros no se repitan.
        Lo qu no entiendo es de que manera la ralizo generica.

        Muchas gracias disculpe la molestia soy nuevo en java

  9. lo creo que es no estoy seguro!!

    generico
    ArrayListgenerico= new ArrayList();

    y en el main
    ArrayList listado= new ArrayList();

    De esta manera llamo a todo lo que esta en la clase ResitroVuelo? de esta manera mi lista es generica?
    Solo me quedaria realizar los metodos para ingresar buscar y elimanar.

  10. Excelente entrada y muy explicativa.

Deja un comentario