Héctor Luaces

Genéricos para recién llegados a Java

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 muuucho tiempo, 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 de Genéricos para recién llegados a Java, pues espero que pueda ayudarte a 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: “¿qué es ese «diamante» 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 añadir una comprobación de tipado estricto a los objetos requeridos por clases, interfaces o métodos. Esto te permitirá crear código mucho más legible y robusto.

Supón las siguientes dos clases:

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

Imagina que quieres crear una colección que guarde instancias de la clase A. Podrías hacerlo así:

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

Este código funciona, pero no te garantiza que la colección solo tenga elementos de ese tipo. Podrías añadir instancias de otras clases sin problema…

ArrayList c = new ArrayList();

// Añadimos instancias de la clase A
c.add(new A());
c.add(new A());
c.add(new A());

// Pero nada nos impide añadir instancias de la clase B
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;

// Podemos añadir elementos de cualquier clase
c.add(new A());
c.add(new B()); 

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

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<A> 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 estando así seguros de que no vamos a distribuir código que pueda tener errores como los que se daban cuando no usábamos 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: <T>.

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

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

El operador diamante después del texto «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.

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

Como se puede ver, usamos el operador diamante con la clase «String» de la siguiente forma: <String>. 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<K,V>

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.

Los detalles de la implementación, evidentemente, dependerán 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<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

Ejemplo de clase que usa dos genéricos:

HashMap <Integer, String> h = new HashMap<>();
h.put(new Integer(1), "uno");
h.put(new Integer(2), "dos");

// Esto dará un error, ya que "tres",3 no se corresponde con la declaración que hemos hecho (String e Integer)
h.put("tres", 3);

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 intentamos crear una relación llave -> valor que no sea «Integer -> String«, tendremos un error de compilación, como se ve en la línea 6.

¿Qué significan <K>, y <V>?, ¿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:

  • E – Para nombrar “elementos” de una colección
  • K – Para nombrar identificadores en una relación asociativa
  • N – Para nombrar genéricos que, esperamos, han de ser numéricos
  • V – Para nombrar valores en una relación asociativa
  • Etc…

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<E>

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<E>
{
    private ArrayList<E> lista;

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

    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<String> h = new Almacen<>();

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

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.

20 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.

  11. Marco A. Hernandez

    Gracias por brindarnos tan útil información, Profesor. Solo me atrevo a comentarle que las cajas de los ejemplos tienen algún bug que ocultan el nombres de las clases entre las corcheas de diamante, provocando que sean interpretadas como etiquetas html y por ello, no muestran el ejemplo correctamente. Saludos.

Deja una respuesta