Invariance: List<String> no es List<Object>
En Java, los tipos genéricos son invariantes por defecto: aunque String herede de Object, List<String> no es subtipo de List<Object>.
List<String> strings = new ArrayList<>();
List<Object> objetos = strings; // ERROR de compilación
¿Por qué? Supón que fuera legal:
List<String> strings = new ArrayList<>();
List<Object> objetos = strings;
objetos.add(42); // añadir Integer a lo que en runtime es List<String>
String s = strings.get(0); // explota en runtime con ClassCastException
Java impide esto bloqueándolo en compilación. Para aceptar colecciones de distintos tipos relacionados, existen los wildcards.
Tres wildcards
<?>: unbounded wildcard: acepta cualquier tipo. Solo puedes leer como Object y añadir null.
List<?> lista; // puede ser List<String>, List<Integer>, List<Persona>...
for (Object o : lista) { ... } // leer como Object
lista.add("algo"); // ERROR: no se puede añadir
<? extends T>: upper-bounded wildcard: acepta T o cualquier subtipo. Solo lectura segura.
List<? extends Number> numeros; // acepta List<Integer>, List<Double>, List<Number>
Number n = numeros.get(0); // OK: lo que haya, se lee como Number
numeros.add(42); // ERROR: no sabemos qué tipo exacto es
<? super T>: lower-bounded wildcard: acepta T o cualquier supertipo. Solo escritura segura de T.
List<? super Integer> numeros; // acepta List<Integer>, List<Number>, List<Object>
numeros.add(42); // OK: Integer cabe en cualquier supertipo
numeros.add(3.14); // ERROR: Double no es garantizadamente Integer ni supertipo
Object o = numeros.get(0); // solo Object se puede leer seguro
El principio PECS (Producer Extends, Consumer Super)
Joshua Bloch acuñó la regla mnemotécnica PECS: Producer Extends, Consumer Super.
- Si una colección produce valores (vas a leer de ella), usa
<? extends T>. - Si una colección consume valores (vas a añadirle cosas), usa
<? super T>.
Ejemplo PECS clásico
Un método que copia elementos de una lista fuente a una destino:
public static <T> void copiar(List<? extends T> fuente, List<? super T> destino) {
for (T elemento : fuente) {
destino.add(elemento);
}
}
fuentees un producer deT(leemos de ella) a? extends T.destinoes un consumer deT(escribimos en ella) a? super T.
Gracias a PECS, este método admite usos flexibles:
List<Integer> enteros = List.of(1, 2, 3);
List<Number> numeros = new ArrayList<>();
List<Object> objetos = new ArrayList<>();
copiar(enteros, numeros); // T=Number: Integer extends Number, Number super Number
copiar(enteros, objetos); // T=Integer: Integer extends Integer, Object super Integer
Sin wildcards, habría que escribir muchas sobrecargas o renunciar a la seguridad.
Casos reales en el JDK
El JDK aplica PECS constantemente:
// Collections.copy (simplificado)
public static <T> void copy(List<? super T> dest, List<? extends T> src) { ... }
// Collections.addAll (consumer)
public static <T> boolean addAll(Collection<? super T> c, T... elements) { ... }
// Stream.flatMap (producer)
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
// Comparator.comparing
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(...);
Esto último (Comparable<? super U>) es un ejemplo delicado de PECS: el comparador necesita que U sea comparable con él mismo o algún supertipo, lo que es correcto: si Empleado extends Persona y Persona implements Comparable<Persona>, quiero poder ordenar Empleado aunque compareTo esté en Persona.
Métodos genéricos con bounds
Un método genérico puede restringir T a subtipos de una clase o que implementen una interfaz:
public static <T extends Comparable<T>> T maximo(List<T> lista) {
T max = lista.get(0);
for (T x : lista) {
if (x.compareTo(max) > 0) max = x;
}
return max;
}
// Uso
maximo(List.of(3, 1, 4, 1, 5, 9)); // T=Integer
maximo(List.of("b", "a", "c")); // T=String
El bound <T extends Comparable<T>> es más restrictivo que necesario. Con PECS mejorado:
public static <T extends Comparable<? super T>> T maximo(List<? extends T> lista) { ... }
Ahora acepta listas cuyos elementos implementen Comparable en alguna clase superior.
Multiple bounds
Un parámetro puede tener múltiples bounds con &:
public static <T extends Comparable<T> & Cloneable> T clonarMaximo(List<T> lista) { ... }
Solo el primer bound puede ser clase; el resto deben ser interfaces.
Type erasure: lo que no puedes hacer
Los generics en Java se implementan con type erasure: el compilador verifica tipos pero elimina la información en el bytecode. Por eso:
List<String> strings = new ArrayList<>();
List<Integer> enteros = new ArrayList<>();
strings.getClass() == enteros.getClass(); // true: ambos son ArrayList.class
if (obj instanceof List<String>) { } // ERROR: no se puede comprobar en runtime
if (obj instanceof List<?>) { } // OK: unbounded wildcard
Consecuencias prácticas:
- No puedes crear arrays genéricos directamente:
new T[10]es ilegal. instanceofsolo funciona con wildcards:instanceof List<?>.- No puedes sobrecargar métodos por tipo genérico (mismo bytecode).
- No hay
T.class: usaClass<T> clasecomo parámetro.
Workaround: Class<T> para obtener tipo runtime
public static <T> T cargarDesdeJSON(String json, Class<T> clase) {
// mapper.readValue(json, clase)
return null;
}
// Uso
Persona p = cargarDesdeJSON("{...}", Persona.class);
Wildcards vs métodos genéricos
Cuando una API puede expresarse con wildcards o con método genérico, los wildcards son preferibles para el usuario:
// Con wildcard (preferido en APIs públicas)
public static void intercambiarPrimero(List<?> lista) { ... }
// Con método genérico (necesario si referenciamos T internamente varias veces)
public static <T> void intercambiar(List<T> lista, int i, int j) {
T tmp = lista.get(i);
lista.set(i, lista.get(j));
lista.set(j, tmp);
}
Regla: si vas a leer y escribir referenciando el tipo, necesitas un método genérico. Para operaciones que solo consumen de forma agnóstica, wildcards.
Buenas prácticas
- Usa PECS siempre que diseñes APIs que acepten colecciones.
- Prefiere wildcards a tipos específicos cuando el método no necesita conocer el tipo exacto.
- Evita
List<Object>como parámetro "para todo": rompe type safety y pierde flexibilidad. - Documenta (
@param) el contrato de wildcards cuando no sea obvio. - No uses wildcards sin motivo en tipos de retorno (complica al llamante).
Errores comunes
// MAL: wildcards innecesarios en return
public List<? extends Number> obtener() { return numeros; }
// El llamante no puede añadir al resultado; mejor devolver List<Number> o tipo exacto.
// MAL: asumir invariance relajada
List<Integer> ints = List.of(1, 2);
List<Number> nums = ints; // NO compila
// BIEN: usar wildcard al recibir
void consumir(List<? extends Number> numeros) { ... }
consumir(ints); // OK
Los generics con wildcards son de las piezas más complejas del sistema de tipos Java. Dominarlos: especialmente PECS: es un salto cualitativo en el diseño de APIs y bibliotecas reutilizables. Un desarrollador Java avanzado debe razonar con ellos de forma natural.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Java es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Java
Explora más contenido relacionado con Java y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Distinguir List<Object>, List<?>, List<? extends T> y List<? super T>. Aplicar el principio PECS en diseño de APIs. Usar wildcards para aceptar jerarquías de tipos. Comprender por qué List<String> no es List<Object> (invariance). Reconocer limitaciones por type erasure. Declarar métodos genéricos con bounds (<T extends Comparable<T>>).