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 compileAunque Integer extends Number, List<Integer> no extiende List<Number>. Los tipos genéricos son invariantes — List<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 DoubleEl 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> — OKDentro 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> — OKList<? 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 tooAquí 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 nombreT— útil si quieres devolverT, aceptar otroList<T>como segundo parámetro, o escribirT 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 comoNumber. 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.
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.