Volver a Labs
Intermedio

Genéricos en Java

1–2 horas Actualizado Junio 2026
Java Genéricos Tipos Backend

Introducción

Imagina que necesitas una clase Caja que almacene un objeto. Sin genéricos, usarías Object como tipo — pero entonces cualquier código que saque el valor tiene que hacer un casteo manual y el compilador no puede detectar errores de tipo hasta que el programa ya está corriendo.

Los genéricos resuelven esto: permiten parametrizar clases, interfaces y métodos con un tipo que se concreta en el momento de uso. El compilador verifica la coherencia de tipos en tiempo de compilación, no en ejecución.

// Sin genéricos — inseguro
Object valor = "hola";
Integer numero = (Integer) valor; // ClassCastException en tiempo de ejecución

// Con genéricos — seguro
List<String> nombres = new ArrayList<>();
nombres.add(42); // Error de compilación — detectado inmediatamente

¿Qué vas a aprender en este lab?

  • Por qué existen los genéricos y qué problema resuelven
  • Crear clases genéricas (class Caja<T>)
  • Crear métodos genéricos (<T> T primero(List<T> lista))
  • Usar comodines (?, ? extends, ? super)
  • Entender las limitaciones: borrado de tipos (type erasure)

Requisitos previos: fundamentos de Java, POO (clases y herencia) y uso básico de colecciones (List, Map).


1. El problema sin genéricos

Antes de Java 5, las colecciones usaban Object:

// Lista sin tipo — cualquier cosa puede entrar
List lista = new ArrayList();
lista.add("hola");
lista.add(42);       // ¡También acepta enteros!
lista.add(new Persona("Julian"));

// Al sacar, hay que castear manualmente
String texto = (String) lista.get(0); // OK
String error = (String) lista.get(1); // ClassCastException en tiempo de ejecución

El compilador no puede ayudarte: acepta el código, y el error aparece cuando el usuario ejecuta el programa. Los genéricos hacen que este tipo de error sea imposible.


2. Clases genéricas

Una clase genérica acepta uno o más parámetros de tipo, convencionalmente nombrados T (Type), E (Element), K (Key), V (Value), R (Return).

public class Caja<T> {
    private T contenido;

    public void guardar(T contenido) {
        this.contenido = contenido;
    }

    public T abrir() {
        return contenido;
    }

    public boolean estaVacia() {
        return contenido == null;
    }
}

Ahora el tipo se especifica al crear la instancia:

Caja<String> cajaTexto = new Caja<>();
cajaTexto.guardar("¡Hola!");
String texto = cajaTexto.abrir(); // String — sin casteo
System.out.println(texto); // ¡Hola!

Caja<Integer> cajaNumero = new Caja<>();
cajaNumero.guardar(42);
int numero = cajaNumero.abrir(); // Integer — sin casteo
System.out.println(numero); // 42

// El compilador detecta el error inmediatamente:
cajaTexto.guardar(123); // Error de compilación: incompatible types

Múltiples parámetros de tipo

public class Par<K, V> {
    private K clave;
    private V valor;

    public Par(K clave, V valor) {
        this.clave = clave;
        this.valor = valor;
    }

    public K getClave() { return clave; }
    public V getValor() { return valor; }

    @Override
    public String toString() {
        return "(" + clave + ", " + valor + ")";
    }
}
Par<String, Integer> edad   = new Par<>("Julian", 25);
Par<String, Boolean> activo = new Par<>("admin", true);

System.out.println(edad);   // (Julian, 25)
System.out.println(activo); // (admin, true)

Ejercicio 1

Crea una clase genérica Pila<T> (stack) con los métodos:

  • push(T elemento) — agrega al tope
  • pop(): T — elimina y devuelve el tope (lanza EmptyStackException si está vacía)
  • peek(): T — devuelve el tope sin eliminarlo
  • isEmpty(): boolean
  • size(): int

Pruébala con Pila<String> y Pila<Integer>.


3. Métodos genéricos

Un método genérico declara su propio parámetro de tipo antes del tipo de retorno. Puede vivir en clases no genéricas.

public class Utilidades {

    // Devuelve el primero que cumpla una condición
    public static <T> T primero(List<T> lista) {
        if (lista.isEmpty()) throw new NoSuchElementException("Lista vacía");
        return lista.get(0);
    }

    // Intercambia dos elementos en una lista
    public static <T> void intercambiar(List<T> lista, int i, int j) {
        T temp = lista.get(i);
        lista.set(i, lista.get(j));
        lista.set(j, temp);
    }

    // Crea una lista con n copias de un elemento
    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;
    }
}
List<String> nombres = new ArrayList<>(List.of("Ana", "Carlos", "Diana"));

System.out.println(Utilidades.primero(nombres));         // Ana
Utilidades.intercambiar(nombres, 0, 2);
System.out.println(nombres);                             // [Diana, Carlos, Ana]
System.out.println(Utilidades.repetir("Java", 3));       // [Java, Java, Java]

Java infiere el tipo automáticamente del argumento — no necesitas escribir Utilidades.<String>primero(nombres).


4. Límites — extends y super

A veces necesitas restringir qué tipos acepta un parámetro genérico.

Límite superior (T extends Tipo)

Acepta Tipo y cualquiera de sus subclases:

// Solo acepta listas de Number o subclases (Integer, Double, Long...)
public static <T extends Number> double sumar(List<T> lista) {
    double total = 0;
    for (T elemento : lista) {
        total += elemento.doubleValue(); // puedes usar métodos de Number
    }
    return total;
}
System.out.println(sumar(List.of(1, 2, 3)));       // 6.0  — List<Integer>
System.out.println(sumar(List.of(1.5, 2.5)));       // 4.0  — List<Double>
// sumar(List.of("a", "b"));                        // Error de compilación

Múltiples límites

// T debe implementar Comparable Y extender Number
public static <T extends Number & Comparable<T>> T maximo(List<T> lista) {
    T max = lista.get(0);
    for (T elemento : lista) {
        if (elemento.compareTo(max) > 0) max = elemento;
    }
    return max;
}

5. Comodines (?)

El comodín ? representa un tipo desconocido. Se usa cuando quieres escribir código que funcione con distintas versiones parametrizadas de una clase, sin necesitar un parámetro propio.

Comodín sin límite

// Acepta List<String>, List<Integer>, List<Persona>... cualquier lista
public static void imprimirTodo(List<?> lista) {
    for (Object elemento : lista) {
        System.out.println(elemento);
    }
}

Comodín con límite superior (? extends T) — solo leer

// Acepta List<Integer>, List<Double>, List<Long> — cualquier lista de números
public static double calcularPromedio(List<? extends Number> numeros) {
    double suma = 0;
    for (Number n : numeros) suma += n.doubleValue();
    return suma / numeros.size();
}

Con ? extends T puedes leer elementos (como T), pero no puedes agregar (el compilador no sabe el tipo exacto).

Comodín con límite inferior (? super T) — solo escribir

// Acepta List<Number>, List<Object> — puede agregar Integers
public static void agregarEnteros(List<? super Integer> lista) {
    lista.add(10);
    lista.add(20);
    lista.add(30);
}

Con ? super T puedes agregar elementos de tipo T, pero leer solo devuelve Object.

Regla mnemotécnica (PECS)

Producer Extends, Consumer Super

  • Si la lista produce (lees de ella) → ? extends T
  • Si la lista consume (escribes en ella) → ? super T

6. Interfaces genéricas

Las interfaces también pueden ser genéricas:

public interface Repositorio<T, ID> {
    void guardar(T entidad);
    T buscarPorId(ID id);
    List<T> buscarTodos();
    void eliminar(ID id);
}
public class Persona {
    private int id;
    private String nombre;

    public Persona(int id, String nombre) {
        this.id = id;
        this.nombre = nombre;
    }

    public int getId()      { return id; }
    public String getNombre() { return nombre; }
    public String toString() { return id + ": " + nombre; }
}
public class PersonaRepositorio implements Repositorio<Persona, Integer> {
    private Map<Integer, Persona> almacen = new HashMap<>();

    @Override
    public void guardar(Persona p) {
        almacen.put(p.getId(), p);
    }

    @Override
    public Persona buscarPorId(Integer id) {
        return almacen.get(id);
    }

    @Override
    public List<Persona> buscarTodos() {
        return new ArrayList<>(almacen.values());
    }

    @Override
    public void eliminar(Integer id) {
        almacen.remove(id);
    }
}

Este patrón es la base de los repositorios en frameworks como Spring Data JPA.

Ejercicio 2

Implementa la interfaz Repositorio<T, ID> para una clase Producto. Agrega un método extra buscarPorNombre(String nombre): List<Producto> usando un Stream. Prueba con 5 productos distintos.


7. Type Erasure — el lado oscuro

Java implementa los genéricos mediante borrado de tipos: en tiempo de ejecución, la información del tipo genérico desaparece. List<String> y List<Integer> son el mismo tipo en bytecode: List.

List<String>  nombres  = new ArrayList<>();
List<Integer> numeros  = new ArrayList<>();

System.out.println(nombres.getClass() == numeros.getClass()); // true

// No puedes crear arrays de tipos genéricos
// T[] array = new T[10]; // Error de compilación

// No puedes usar instanceof con tipos genéricos
// if (lista instanceof List<String>) { }  // Error de compilación

// Sí puedes
if (lista instanceof List<?>) { } // OK

Por qué existe el type erasure

Es una decisión de compatibilidad: cuando se añadieron genéricos en Java 5, ya existían millones de líneas de código sin genéricos. El borrado de tipos permitió que el código antiguo y el nuevo funcionaran juntos sin cambiar la JVM.


8. Errores Comunes

Error 1 — Usar tipos primitivos como parámetro de tipo

// Incorrecto: los primitivos no son objetos
List<int> numeros = new ArrayList<>(); // Error de compilación

// Usa el tipo envuelto (wrapper)
List<Integer> numeros = new ArrayList<>();

Java hace auto-boxing automáticamente: numeros.add(42) convierte intInteger transparentemente.

Error 2 — Crear arrays de tipos genéricos

// No permitido por type erasure
T[] array = new T[10]; // Error de compilación

// Usa List<T> en su lugar
List<T> lista = new ArrayList<>();

Error 3 — Confundir herencia de clases con herencia de genéricos

// Integer extiende Number — esto es verdad
Number n = new Integer(5); // OK

// Pero List<Integer> NO es un subtipo de List<Number>
List<Number> numeros = new ArrayList<Integer>(); // Error de compilación

// Usa comodín si necesitas flexibilidad
List<? extends Number> numeros = new ArrayList<Integer>(); // OK

Resumen

Concepto Sintaxis Para qué
Clase genérica class Caja<T> Crear contenedores de tipo flexible
Método genérico <T> T metodo(T arg) Operaciones independientes del tipo
Límite superior <T extends Number> Restringir a subtipos
Comodín libre List<?> Aceptar cualquier lista (solo lectura)
PECS extends List<? extends T> Leer de la lista
PECS super List<? super T> Escribir en la lista

Próximos pasos

  • Patrones de diseño: Generic Factory, Generic Builder y Generic Repository son patrones que dependen directamente de los genéricos
  • Streams: la API de Streams usa genéricos extensamente (Stream<T>, Function<T, R>, Predicate<T>)
  • Spring: los repositorios de Spring Data usan JpaRepository<Entity, ID> — ahora entiendes por qué funcionan así