Generics

Parámetros de tipo, bounds y wildcards

Por qué existen los generics

Antes de Java 5, las colecciones almacenaban Object y requerían cast explícito al recuperar elementos — con riesgo de ClassCastException en tiempo de ejecución. Los generics mueven esa verificación al tiempo de compilación, eliminando la necesidad de casts y haciendo el código más seguro y legible.

// Sin generics (pre-Java 5): todo era Object, con riesgo de ClassCastException
List lista = new ArrayList();
lista.add("hola");
lista.add(42);           // se permite en tiempo de compilación
String s = (String) lista.get(1); // ClassCastException en ejecución

// Con generics: el compilador verifica los tipos
List<String> nombres = new ArrayList<>();
nombres.add("hola");
// nombres.add(42); // Error de compilación: tipo incorrecto
String nombre = nombres.get(0); // sin cast necesario

Clases genéricas

Los parámetros de tipo se declaran entre <> después del nombre de la clase. Por convención: T para tipo genérico, E para elemento de colección, K/V para clave/valor de mapa, R para resultado.

// Clase genérica propia
public class Par<A, B> {
    private final A primero;
    private final B segundo;

    public Par(A primero, B segundo) {
        this.primero = primero;
        this.segundo = segundo;
    }

    public A getPrimero() { return primero; }
    public B getSegundo() { return segundo; }

    @Override
    public String toString() {
        return "(" + primero + ", " + segundo + ")";
    }
}

// Uso:
Par<String, Integer> par = new Par<>("hola", 42);
String a = par.getPrimero(); // String, sin cast
Integer b = par.getSegundo();

Métodos genéricos

Un método puede tener su propio parámetro de tipo, independiente de la clase. El compilador infiere el tipo a partir de los argumentos — rara vez hay que especificarlo explícitamente.

// Método genérico: el parámetro de tipo se declara antes del tipo de retorno
public static <T> List<T> repetir(T elemento, int veces) {
    List<T> resultado = new ArrayList<>();
    for (int i = 0; i < veces; i++) resultado.add(elemento);
    return resultado;
}

// Uso:
List<String> tres = repetir("hola", 3); // ["hola", "hola", "hola"]
List<Integer> cinco = repetir(0, 5);    // [0, 0, 0, 0, 0]
// El tipo T se infiere del argumento

Bounds (límites de tipo)

Los bounds permiten restringir los tipos que acepta un parámetro genérico. Con extends se puede usar la API del tipo límite dentro del método genérico.

// Bounded type parameters: restringir qué tipos acepta el parámetro

// Upper bound (T extends): T debe ser subclase de Number
public static <T extends Number> double sumar(List<T> numeros) {
    double total = 0;
    for (T n : numeros) total += n.doubleValue(); // se puede usar la API de Number
    return total;
}

sumar(List.of(1, 2, 3));        // List<Integer>
sumar(List.of(1.5, 2.5, 3.0));  // List<Double>

// Múltiples bounds: T extends Comparable<T> & Cloneable
public static <T extends Comparable<T>> T maximo(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}

Wildcards y regla PECS

// Wildcards: para cuando el tipo exacto no importa

// ? (unbounded wildcard): cualquier tipo
void imprimirLista(List<?> lista) {
    for (Object o : lista) System.out.println(o);
    // no se puede añadir nada (excepto null)
}

// ? extends T (upper bounded): subtipos de T — para LEER
double calcularMedia(List<? extends Number> numeros) {
    return numeros.stream().mapToDouble(Number::doubleValue).average().orElse(0);
}
// Acepta List<Integer>, List<Double>, List<Float>...

// ? super T (lower bounded): supertipos de T — para ESCRIBIR
void rellenarConCeros(List<? super Integer> lista, int cantidad) {
    for (int i = 0; i < cantidad; i++) lista.add(0);
}
// Acepta List<Integer>, List<Number>, List<Object>

// Regla PECS (Producer Extends, Consumer Super):
// Si la colección PRODUCE (se lee de ella): usa extends
// Si la colección CONSUME (se escribe en ella): usa super
Type erasure: los generics son una característica en tiempo de compilación. En tiempo de ejecución, List<String> y List<Integer> son el mismo tipo List. No se puede hacer new T[] ni instanceof List<String>. Esto es relevante para interoperar con APIs de reflexión o serialización.

Siguiente apartado → Enums en Java

Índice de la sección

Índice del curso