Menú de navegaciónMenú
Categorías

La mejor forma de Aprender Programación online y en español www.campusmvp.es

?id=1a1c161e-009e-4bf1-b29c-6f5f83c6017d

Java: comodines para tipos genéricos - PECS. Diferencias entre <? extends T> y <? super T>

Foto ornamental. Muestra un comodín de una baraja fluorescente con el texto Java Genéricos. A partir de imagen de Tudor Baciu, CC0 en Unsplash

Hace unas semanas, Francisco Charte nos explicaba en un interesante vídeo introductorio qué son lo tipos genéricos, para qué sirven y cómo se utilizan.

En esta ocasión nos vamos a centrar en un tema relacionado directamente con los tipos genéricos en Java: los comodines para genéricos o PECS.

PECS es el acrónimo de Producer extends and Consumer super, que quiere decir en español que el productor usa extends y el consumidor usa super. Esta terminología tan extraña, acuñada por Joshua Bloch, cobra sentido cuando entiendes el funcionamiento y el propósito de estos comodines, cosa que veremos enseguida.

El problema de los tipos incompatibles

Supongamos que tenemos unas clases en nuestro programa, en este caso muy simples, que sirven para representar a unas entidades, en este ejemplo seres vivos:

class SerVivo {
  public String nombre;

  public SerVivo(String n) {
    nombre = n;
  }

  @Override
  public String toString() {
    return "Ser vivo: " + nombre;
  }
}

class Animal extends SerVivo {
  public Animal(String n) {
    super(n);
  }

  @Override
  public String toString() {
    return "Animal: " + nombre;
  }
}

class Gato extends Animal {
  public Gato(String n) {
    super(n);
  }

  @Override
  public String toString() {
    return "Gato: " + nombre;
  }
}

En este ejemplo tenemos tres clases: una "genérica" llamada SerVivo que tiene un constructor para poder pasarle el nombre de la criatura en cuestión, y que ha sobrescrito el método toString() para mostrar ese nombre por consola. Además, hay unas clases Animal y Gato que heredan de SerVivo y de Animal respectivamente (o sea, un Gato es también un Animal y un animal es un SerVivo). No tiene misterio alguno.

Ahora supongamos que creo sendas listas de animales y gatos, añadiendo a cada una de ellas algunos objetos de estas clases:

List<Animal> listaAnimales = new ArrayList<Animal>();
//Llenamos la lista de animales
listaAnimales.add(new Animal("Jirafa"));
//Se pueden añadir Gatos a listaAnimales por ser una subclase de Animal
listaAnimales.add(new Gato("Patucas"));
listaAnimales.add(new Animal("Halcón"));

Fíjate en que, en la lista de animales hemos podido añadir no sólo objetos de tipo Animal, sino también gatos, ya que estos heredan de Animal y son, por tanto, animales.

Por ello, podríamos añadir incluso tan solo gatos a la lista y funcionaría perfectamente. Supón ahora que creamos una segunda lista de gatos e intentamos asignar el contenido de la primera a esta segunda:

List<Gato> listaGatos = new ArrayList<Gato>();
listaGatos = listaAnimales;

Entonces se produciría un error de tipos incompatibles. Pero, ¿por qué? Al fin y al cabo todo lo que hemos metido en la primera lista de animales son gatos, así que debería funcionar.

Correcto, pero el problema es que en tu lista de animales puede haber de todo, y en tu lista de gatos sólo puede haber gatos, así que nadie te asegura que no se haya "colado" otro animal diferente en la primera, por lo que el compilador no puede permitirlo.

Pero al revés ocurre lo mismo:

listaAnimales = listaGatos;

produce también un error, a pesar de que todos los gatos son animales y por lo tanto deberían poder convertirse sin problemas.

Vale, de acuerdo, pero... ¿qué tiene que ver este problema con los genéricos?

Trabajo con colecciones genéricas

El problema viene a la hora de trabajar con colecciones de objetos para hacer algo con ellos: bien introducir objetos en una colección o bien procesar colecciones de objetos. Esto ya empieza a parecerse a lo que decía al principio de PECS...

Imagina que tenemos un método para procesar animales (objetos de la clase Animal), por ejemplo, uno que simplemente los muestre por consola, como este:

public static void muestraElementos01(List<Animal> lista) {
    for(Animal elemento : lista) {
        System.out.println(elemento);
    }
}

//lo usamos con nuestras colecciones
muestraElementos01(listaAnimales);
muestraElementos01(listaGatos); //¡¡Falla!!

Cabría pensar que como los objetos de tipo Gato son también animales (un subtipo de la clase Animal), la última línea funcionaría. Pero no es el caso: falla con exactamente el mismo mensaje del compilador que en el ejemplo anterior: tipos incompatibles. Java no permite pasarle una lista genérica de Gato (List<Gato>) a un método que espera una lista concreta de tipo List<Animal>, y por eso rompe.

Siempre podríamos utilizar una lista de Object de modo que le pudiéramos pasar cualquier cosa, pero entonces, por esa regla de tres, ¿para que querríamos los tipos genéricos? 😉 No, evidentemente lo que necesitamos es algún mecanismo del lenguaje para poder gestionar este tipo de situaciones. Este mecanismo son los comodines para tipos genéricos.

Comodines para tipos genéricos en Java

Los comodines para tipos genéricos son la solución específica para un problemas muy concreto: leer y escribir en colecciones genéricas, justo lo que acabamos de ver.

Existen 3 comodines para tipos genéricos que podemos aplicar y que vamos a ver a continuación:

Comodines para tipo desconocido: <?>

Son el tipo más simple de comodines genéricos, y también el más limitado, paradójicamente por su falta de límites. Nos permiten indicar al compilador que no sabemos el tipo exacto que se nos va a pasar para procesar en nuestro tipo genérico. Dado que no tenemos ni idea ni tampoco se lo podemos indicar al compilador. Su sintaxis sería como la siguiente, en la que modificamos el genérico con nuestro comodín:

public static void muestraElementos02(List<?> lista) {
	for(Object elemento : lista) {
      System.out.println(elemento);
    }
}

En el fondo es casi como si estuviésemos usando la solución tradicional con Object. De hecho, los elementos de una colección de este tipo los tendríamos que tratar como objetos básicos, Object, como se ve en el fragmento anterior. Así que le podríamos pasar cualquier cosa, no sólo las dos clases de ejemplo que hemos creado, sino listas genéricas de cadenas de texto, de números o de cualquier otro tipo que quisiésemos.

Mala solución, pero tiene sus usos, así que conviene conocerla. Vamos a conocer a los comodines "buenos"...

Comodín para tipos derivados: <? extends T>

Este comodín quiere decir que se admiten todos los objetos que hereden de la clase especificada. En nuestro ejemplo podríamos escribir esto:

public static void muestraElementos03(List<? extends Animal> lista) {
    for(Animal elemento : lista) {
        System.out.println(elemento);
    }
}

Fíjate en que, dado que cualquier clase que herede de Animal se puede convertir ("castear") a Animal, entonces podemos recibir listas genéricas con ellas y convertir cada elemento a Animal antes de utilizarlo. Es por esto que como variable del bucle se puede usar un elemento de tipo Animal. Y es por esto que podemos pasarle sin problemas nuestra lista de gatos, que se imprimirá al igual que cualquier otra lista genérica de animales.

Es decir, el comodín para tipos derivados actúa como límite superior en la jerarquía de clases que admite la lista genérica, ya que cualquier clase que descienda de Animal, a cualquier nivel, se admitirá sin problemas.

Comodín para supertipos: <? super T>

Volvamos ahora al problema del principio. Como hemos visto, si tenemos dos listas genéricas con tipos compatibles, aún así no podemos asignar una variable de una en la otra, o sea, no podemos por ejemplo asignar una referencia a una lista de gatos en una variable que sea de tipo lista de animales, a pesar de que los gatos son animales (heredan de la clase Animal).

¿Cómo resolvemos el problema de crear una función genérica que permita añadir tan solo objetos de un tipo a cualquier colección genérica de objetos de nuestra jerarquía? O sea, en nuestro ejemplo, cómo creamos una función genérica que nos deje usar una colección de cualquier tipo de Animal para arriba, pero añadir tan sólo elementos de tipo Animal.

Por ejemplo, tenemos una colección de seres vivos, así:

List<SerVivo> organismos = new ArrayList<SerVivo>();
organismos.add(new SerVivo("Protozoo"));
organismos.add(new SerVivo("Árbol"));
organismos.add(new Gato("Calcetines"));

Y ahora, por el motivo que sea, necesitamos crear una función genérica que reciba como argumento una colección de cualquier tipo de la jerarquía que sea Animal o superior (o ArrayList<SerVivo> o ArrayList<Animal>), y que sólo deje añadir elementos de ese determinado nivel de la jerarquía (en este caso Animal, o sea, sólo animales o gatos, que están por debajo).

Podríamos intentar esto:

public static void cargaAnimales(List<SerVivo> lista) {
    lista.add(new Gato("Marramiau"));
    lista.add(new Animal("Pulpo"));
    lista.add(new SerVivo("Ameba"));  //Rompe
}

cargaAnimales(organismos);
cargaAnimales(listaAnimales);  //¡¡Falla!

Y cumpliría con este caso concreto, pero dejaría añadir cualquier tipo de ser vivo, y no solo animales como necesitamos. Además si intentásemos llamarlo con una lista de objetos de alguna subclase de SerVivo, como en la última línea, obtendríamos un error de tipos incompatibles como los que vimos al principio.

Así que podríamos intentar esto otro:

public static void cargaAnimales(List<Animal> lista) {
    lista.add(new Gato("Marramiau"));
    lista.add(new Animal("Pulpo"));
}

cargaAnimales(organismos);  //¡¡Falla! :-(

pero ni siquiera podríamos llamarla porque rompería en tiempo de ejecución por el problema que vimos al principio: tipos incompatibles en la última línea (espera un List<Animal> y le estamos pasando un List<SerVivo>.

O sea, le pongamos lo que le pongamos en el tipo de lista, no podemos cumplir con las dos condiciones expuestas.

Para este caso en concreto existe precisamente el comodín para supertipos: <? super T>. Si definimos la función así:

public static void cargaAnimales(List<? super Animal> lista) {
    lista.add(new Gato("Marramiau"));
    lista.add(new Animal("Pulpo"));
    //lista.add(new SerVivo("Ameba"));  //Rompe
}

cargaAnimales(organismos);  // ¡¡Funciona!!

se están cumpliendo las dos condiciones: le podemos pasar como argumento una lista de objetos de cualquier tipo de la jerarquía que esté por encima de Animal (en este caso de SerVivo), pero sólo le podremos añadir elementos de la clase indicada (o, por supuesto, de sus subclases: Gato en nuestro ejemplo).

El comodín de supertipos sirve para indicar que una clase genérica permitirá recibir una asignación de cualquier elemento que sea del tipo indicado o de sus supertipos (o sea, de las clases de las que hereda). Esto es para hacer la asignación de una referencia, pero el funcionamiento de la clase genérica luego se ciñe a dicho tipo (y por definición a sus subtipos).

Como la jerarquía de nuestra clase Animal es la siguiente:

Object
  |_ SerVivo
        |_ Animal
             |_ Gato

En nuestro ejemplo al indicar List<? super Animal> como argumento de la función, estamos indicando que podemos pasarle cualquier lista de las clases por encima de Animal (incluyendo de Object que es la clase base de todas), y que la lista sólo admitirá objetos del tipo Animal.

Es decir, en la práctica significa que nos deja asignar a esta variable/argumento genérico, objetos que pertenezcan a una determinada jerarquía pero con un límite por debajo, que es la clase que estamos indicando.

O sea, esta sintaxis establece un límite por arriba de la jerarquía y la anterior un límite por debajo.

Buff, es algo complicado de digerir, pero como ves tiene sus aplicaciones. Y esto nos lleva a...

PECS

¿Recuerdas que al principio mencionábamos PECS: Producer extends and Consumer super?

Bien, pues PECS es la regla mnemotécnica para saber cuándo usar estos comodines con genéricos, incluso aunque no entendamos bien lo anterior:

  • Usa el comodín para tipos derivados <? extends T> cuando debas obtener elementos de tipo T (o sus descendientes) en una colección (Producer extends, ya que la función produce algo con ellos, los utiliza).
  • Usa el comodín para supertipos <? super T> cuando debas añadir elementos del tipo T (o sus descendientes) a una colección (Consumer super, ya que la función consume los elementos).

Si seguimos estas reglas no tendremos problemas a la hora de ejecutar el código que use genéricos.

Te he dejado un Repl.it con el código de ejemplo para que puedas ir viendo cada cosa por separado y te ayude a digerirlo todo. Si haces un fork (copia) de este Repl.it puedes retocarlo y hacer los cambios y pruebas que quieras. Dale a ejecutar y verás los resultados enseguida.

¡Espero que te resulte útil!

Fecha de publicación:
José Manuel Alarcón Fundador de campusMVP, es ingeniero industrial y especialista en consultoría de empresa. Ha escrito diversos libros, habiendo publicado hasta la fecha cientos de artículos sobre informática e ingeniería en publicaciones especializadas. Microsoft lo ha reconocido como MVP (Most Valuable Professional) en desarrollo web desde el año 2004 hasta la actualidad. Puedes seguirlo en Twitter en @jm_alarcon o leer sus blog técnico o personal. Ver todos los posts de José Manuel Alarcón
Archivado en: Lenguajes y plataformas

Boletín campusMVP.es

Solo cosas útiles. Una vez al mes.

🚀 Únete a miles de desarrolladores

DATE DE ALTA

x No me interesa | x Ya soy suscriptor

La mejor formación online para desarrolladores como tú

Agregar comentario

Los datos anteriores se utilizarán exclusivamente para permitirte hacer el comentario y, si lo seleccionas, notificarte de nuevos comentarios en este artículo, pero no se procesarán ni se utilizarán para ningún otro propósito. Lee nuestra política de privacidad.