Generics avanzados: wildcards y PECS

Avanzado
Java
Java
Actualizado: 18/04/2026

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);
    }
}
  • fuente es un producer de T (leemos de ella) a ? extends T.
  • destino es un consumer de T (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.
  • instanceof solo funciona con wildcards: instanceof List<?>.
  • No puedes sobrecargar métodos por tipo genérico (mismo bytecode).
  • No hay T.class: usa Class<T> clase como 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 - Autor del tutorial

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