W3docs

Comodines Genéricos en Java

Usa comodines acotados superiores, inferiores y sin acotar en generics de Java, y aprende la regla PECS.

Un comodín es el token ? que aparece en los tipos genéricos en lugar de un argumento de tipo concreto — List<?>, List<? extends Number>, List<? super Integer>. Es la respuesta a un problema que encuentras casi de inmediato cuando empiezas a escribir código genérico: List<Integer> no es un subtipo de List<Number>, aunque Integer sí es un subtipo de Number. Los comodines son la forma de describir "una lista de algún Number" sin comprometerse con un tipo de elemento específico — y son, con bastante diferencia, la parte más confusa del sistema de tipos de Java.

El punto de partida contraintuitivo

Este es el hecho que hace necesarios los comodines:

List<Integer> ints  = List.of(1, 2, 3);
List<Number>  nums  = ints;            // ❌ does not compile

Aunque Integer extends Number, List<Integer> no extiende List<Number>. Los tipos genéricos son invariantesList<Sub> y List<Super> no tienen relación, sin importar qué sean Sub y Super.

La razón es sólida, aunque sorprendente. Si List<Integer> fuera un List<Number>, podrías hacer esto:

List<Number> nums = ints;     // pretend this is legal
nums.add(3.14);               // legal — 3.14 is a Number
int x = ints.get(3);          // KABOOM at runtime — it's a Double

El cast en la última línea explotar. Para evitar eso, el compilador rechaza el primer paso: List<Integer> no es un List<Number>. Punto final.

Los comodines son la forma de recuperar la flexibilidad de manera segura.

El comodín sin acotar: List<?>

El comodín más simple es el solitario ? — "una lista de algún tipo desconocido":

public static void printAll(List<?> list) {
  for (Object o : list) System.out.println(o);
}

printAll(List.of(1, 2, 3));          // List<Integer> — OK
printAll(List.of("a", "b"));         // List<String>  — OK
printAll(new ArrayList<>());         // List<Object>  — OK

Dentro del cuerpo, lo único que puedes hacer con los elementos de un List<?> es leerlos como Object — porque el compilador no sabe qué es ?. No puedes agregar nada a un List<?> (con la única excepción de null):

public static void corrupt(List<?> list) {
  list.add("hello");   // ❌ does not compile — ? is unknown
  list.add(null);      // ✓ — null is a value of every reference type
}

List<?> es lo que escribes cuando quieres expresar "acepto cualquier lista y solo voy a leerla como Object."

Comodín acotado superior: ? extends T

Cuando necesitas leer los elementos como un tipo específico — por ejemplo, tratarlos todos como Number — usa un comodín acotado superiormente:

public static double sum(List<? extends Number> list) {
  double total = 0;
  for (Number n : list) total += n.doubleValue();   // legal — every element IS-A Number
  return total;
}

sum(List.of(1, 2, 3));          // List<Integer> — OK, Integer extends Number
sum(List.of(1.5, 2.5));         // List<Double>  — OK
sum(List.of(1L, 2L, 3L));       // List<Long>    — OK

List<? extends Number> se lee como "una lista de algún tipo específico que es Number o un subtipo de Number." Puedes leer de ella como Number. No puedes agregar nada a ella, nuevamente con la excepción de null — porque el compilador no sabe cuál subtipo de Number contiene realmente la lista. Agregar un Integer a un List<? extends Number> que en secreto es un List<Double> lo corrompería; en lugar de intentar averiguar cuál es el subtipo, el compilador simplemente rechaza todo add.

Comodín acotado inferior: ? super T

La imagen especular. ? super T significa "el tipo de elemento de la lista es T o algún supertipo de T":

public static void addOneTwoThree(List<? super Integer> list) {
  list.add(1);
  list.add(2);
  list.add(3);
}

List<Integer> ints   = new ArrayList<>();   addOneTwoThree(ints);   // ✓
List<Number>  nums   = new ArrayList<>();   addOneTwoThree(nums);   // ✓ — Number is a supertype of Integer
List<Object>  objs   = new ArrayList<>();   addOneTwoThree(objs);   // ✓ — Object is too

Aquí puedes agregar cualquier Integer (o subtipo) de forma segura — el tipo de elemento de la lista tiene garantía de ser Integer o algún ancestro suyo, por lo que un Integer encaja. Lo que no puedes hacer es leer un tipo específico — lo máximo que puedes decir sobre un elemento es que es un Object, porque la lista real podría ser List<Object>.

La regla PECS

Hay un nemotécnico que todo desarrollador Java acaba memorizando:

PECS — Producer Extends, Consumer Super.

Es la regla general para saber qué comodín usar:

  • Si el parámetro produce valores (lees de él): usa ? extends T.
  • Si el parámetro consume valores (escribes en él): usa ? super T.

La firma canónica que produce es Collections.copy:

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
  for (int i = 0; i < src.size(); i++) {
    dest.set(i, src.get(i));
  }
}

src se lee (produce Ts) — ? extends T. dest se escribe (consume Ts) — ? super T. Esa es la razón completa de la asimetría: la misma firma funciona si src es un List<Integer> y dest es un List<Number>, o al revés, siempre que T sea un punto de encuentro entre los dos.

Si no recuerdas nada más de este capítulo, recuerda PECS.

Cuándo no usar un comodín

Si un parámetro se lee y se escribe en el mismo método, ni ? extends T ni ? super T funcionan — ninguno permite hacer ambas cosas. En ese caso, simplemente usa un parámetro de tipo normal:

public static <T> void swap(List<T> list, int i, int j) {
  T tmp = list.get(i);              // read
  list.set(i, list.get(j));         // write
  list.set(j, tmp);                 // write
}

Un comodín es la herramienta correcta cuando un lado de la relación es "solo leo" o "solo escribo." Un parámetro de tipo es la herramienta correcta cuando necesitas hablar de un tipo de elemento específico en ambos lados.

Comodines vs. parámetros de tipo acotados

Compara:

public static <T extends Number> double sumNamed(List<T> list)         { ... }
public static            double sumWildcard(List<? extends Number> list) { ... }

Funcionalmente ambos aceptan el mismo conjunto de argumentos. La diferencia está en lo que el cuerpo puede decir:

  • La forma con nombre (<T extends Number>) te da un nombre T — útil si quieres devolver T, aceptar otro List<T> como segundo parámetro, o escribir T tmp = list.get(0) para preservar el tipo de elemento preciso.
  • La forma con comodín (? extends Number) no te da un nombre — solo puedes referirte a los elementos como Number. Es más compacta en la API (ningún nombre se filtra a la firma) pero menos expresiva en el cuerpo.

Regla general: si solo necesitas los elementos como Number, el comodín es la opción más pequeña y limpia. Si el cuerpo necesita hablar de un T específico, nómbralo.

Un ejemplo práctico: PECS en acción

El programa copia elementos de una lista a otra y calcula la suma acumulada — ambas operaciones parametrizadas al estilo PECS. Observa los sitios de llamada: copyOf(intList, numberList) mezcla tipos de elementos porque los comodines permiten que un destino Number acepte valores Integer.

java— editable, runs on the server

sum acepta un List<Integer> y un List<Double> porque el comodín dice "algún subtipo de Number." fillWithSquares agrega valores Integer en un List<Number> porque el comodín dice "cualquier lista que pueda contener Integer o uno de sus ancestros." copyTo usa ambos — la fuente es un productor, el destino es un consumidor, y T es el tipo de elemento compartido que el compilador infiere a partir del acuerdo entre los dos lados.

Qué sigue

Has visto las cuatro formas en que los generics aparecen en el código fuente — clases, métodos, interfaces y comodines. Ahora miramos una capa más abajo en cómo la JVM implementa realmente todo esto. La respuesta — el borrado de tipos — explica algunas restricciones sorprendentes (sin new T(), sin instanceof T, sin arreglos genéricos) y es la única pieza de comprensión que hace que la historia de los generics en Java tenga sentido. Continúa con Borrado de Tipos en Java.

Práctica

Práctica
Estás escribiendo `addAll(Collection<? extends T> src, Collection<? super T> dest)`. ¿Por qué exactamente esta combinación de comodines?
Estás escribiendo `addAll(Collection<? extends T> src, Collection<? super T> dest)`. ¿Por qué exactamente esta combinación de comodines?
Was this page helpful?