1. Invariancia de los genéricos: List<Number> ≠ List<Integer>
En Java, los genéricos son estrictos: son invariantes. Esto significa que, aunque un tipo sea subtipo de otro (por ejemplo, Integer es un subtipo de Number), las colecciones con esos tipos no están relacionadas entre sí.
Imagina que tienes una caja con manzanas (List<Integer>) y alguien dice que en realidad es «una caja con frutas» (List<Number>). Parece lógico: las manzanas son frutas. Pero entonces se podría meter un plátano (Double) y todo se rompería — la caja en realidad es «de manzanas».
Ejemplo de código:
List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // Error de compilación!
El compilador es deliberadamente estricto aquí: no permite convertir una lista de «manzanas» en una lista de «frutas». De lo contrario podríamos añadirle cualquier cosa, y el programa fallaría ya en tiempo de ejecución.
Por eso List<Number> y List<Integer> son dos tipos completamente distintos, aunque Integer en sí mismo sea un subtipo de Number.
A diferencia de los arrays:
Integer[] intArr = new Integer[10];
Number[] numArr = intArr; // Permitido (los arrays son covariantes)
numArr[0] = 3.14; // Error en tiempo de ejecución (ArrayStoreException)
Conclusión: los genéricos son invariantes para garantizar la seguridad de tipos en tiempo de compilación.
2. Límites de los parámetros de tipo
A veces es necesario acotar con qué tipos puede trabajar una clase o método genérico. Para ello se usan límites (bounds).
Límite superior (extends)
class Stats<T extends Number> {
private T[] nums;
// ...
}
Ahora Stats solo puede trabajar con tipos que sean subclases de Number (Integer, Double, Float, etc.). Intentar crear Stats<String> provocará un error de compilación.
Límite con interfaz
class Sorter<T extends Comparable<T>> {
void sort(List<T> list) { /* ... */ }
}
Ahora Sorter solo puede trabajar con tipos que implementen la interfaz Comparable.
Múltiples límites
Se pueden indicar varios límites a la vez usando &:
class MyClass<T extends Number & Comparable<T>> { /* ... */ }
T debe ser una subclase de Number y implementar Comparable<T>.
El orden importa: primero la clase, luego las interfaces.
3. Introducción al comodín: ?
Un wildcard es un tipo «comodín» que dice: «Aquí puede haber algún tipo, pero no sé cuál exactamente».
Ejemplos:
- List<?> — lista de cualquier cosa.
- List<? extends Number> — lista de cualquier tipo que sea subclase de Number (por ejemplo, Integer, Double).
- List<? super Integer> — lista de cualquier tipo que sea supertipo de Integer (por ejemplo, Integer, Number, Object).
¿Para qué sirven los wildcards?
Permiten escribir métodos que funcionen con colecciones de tipos diferentes pero relacionados.
Ejemplo: solo lectura (productor)
void printNumbers(List<? extends Number> list) {
for (Number n : list) {
System.out.println(n);
}
// list.add(123); // ¡Error! No se pueden añadir elementos
}
- Se pueden leer los elementos como Number.
- No se pueden añadir elementos (excepto null).
Ejemplo: solo escritura (consumidor)
void addIntegers(List<? super Integer> list) {
list.add(42); // OK
// Integer x = list.get(0); // Error: no sabemos qué tipo se devolverá
}
- Se pueden añadir elementos de tipo Integer (o de sus subtipos).
- No es seguro leer los elementos como Integer (solo se puede como Object).
Regla PECS
PECS — «Producer Extends, Consumer Super»:
- Productor — Extends: si la colección solo produce datos (productor), usa ? extends T.
- Consumidor — Super: si la colección solo consume datos (consumidor), usa ? super T.
Fácil de recordar: extends — para leer (productor), super — para escribir (consumidor).
Comparativa: los arrays son covariantes, los genéricos son invariantes
- Los arrays son covariantes: se puede asignar un Integer[] a una variable de tipo Number[].
- Los genéricos son invariantes: no se puede asignar un List<Integer> a una variable de tipo List<Number>.
Los wildcards permiten «suavizar» parcialmente la invariancia de los genéricos.
5. Métodos genéricos e inferencia de tipos; limitaciones (type erasure)
Métodos genéricos
Se pueden declarar métodos genéricos que funcionen con cualquier tipo:
public static <T> void printList(List<T> list) {
for (T elem : list) {
System.out.println(elem);
}
}
Inferencia de tipos (type inference)
Java puede «inferir» el tipo del parámetro:
List<String> strings = List.of("a", "b");
printList(strings); // T = String
Limitaciones de los genéricos: borrado de tipos
- En Java, los genéricos se implementan mediante el borrado de tipos: la información sobre los parámetros de tipo se elimina tras la compilación.
- En el bytecode no hay diferencia entre List<String> y List<Integer>.
- No se pueden crear arrays de tipos genéricos: new List<String>[10] — error.
- No se puede usar instanceof con parámetros de tipo: obj instanceof List<String> — error.
6. Práctica con colecciones y la Stream API
Copiar elementos entre listas
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T item : src) {
dest.add(item);
}
}
- src — productor (? extends T)
- dest — consumidor (? super T)
Uso:
List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new ArrayList<>();
copy(nums, ints); // OK: Number es supertipo de Integer
Stream API y wildcards
List<Integer> ints = List.of(1, 2, 3);
List<? extends Number> numbers = ints;
numbers.stream()
.map(Number::doubleValue)
.forEach(System.out::println);
Filtrado con wildcard
public static void printAll(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
7. Errores frecuentes
Error n.º 1: raw types (tipos sin parametrizar).
El uso de tipos sin parametrizar desactiva la comprobación de tipos y conduce a errores en tiempo de ejecución.
List list = new ArrayList(); // raw type — mala práctica
list.add("cadena");
list.add(123); // Se puede añadir cualquier cosa
String s = (String) list.get(1); // ClassCastException!
Nunca utilices raw types. Indica siempre los parámetros de tipo: List<String>, List<Integer>.
Error n.º 2: conversiones inseguras con extends.
Intentar añadir elementos a una colección con ? extends ... provocará un error de compilación.
List<? extends Number> nums = new ArrayList<Integer>();
nums.add(3.14); // Error de compilación!
Error n.º 3: «borrado» de tipo en la sobrecarga.
No se pueden sobrecargar métodos solo por sus parámetros genéricos: tras el borrado de tipos, las firmas coinciden.
public void process(List<String> list) { /* ... */ }
public void process(List<Integer> list) { /* ... */ } // Error de compilación!
Error n.º 4: arrays de tipos genéricos.
No se pueden crear arrays de tipos parametrizados debido al borrado de tipos.
List<String>[] arr = new List<String>[10]; // Error de compilación!
GO TO FULL VERSION