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 topepop(): T— elimina y devuelve el tope (lanzaEmptyStackExceptionsi está vacía)peek(): T— devuelve el tope sin eliminarloisEmpty(): booleansize(): 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 int → Integer 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í