Introducción a los Generics de Java
Por qué existen los generics en Java: seguridad de tipos, reutilización de código y eliminación de casts en colecciones y APIs.
Los Generics son la característica que permite que una clase, interfaz o método opere sobre un tipo no especificado, para que el compilador fije ese tipo en el lugar donde se usa. Un List<String> es una lista de cadenas, el compilador lo sabe, y cualquier intento de insertar un Date en ella se rechaza antes de que el programa se ejecute. Antes de que los generics llegaran en Java 5, esa misma lista era un List de Object, y cada lectura de ella requería un cast escrito a mano que podía o no tener éxito en tiempo de ejecución. Los generics convirtieron esa apuesta en tiempo de ejecución en una verificación en tiempo de compilación, y casi todas las APIs modernas de Java están moldeadas por ellos.
El problema que resuelven los generics
Para entender por qué existen los generics, imagina un Java sin ellos. Un contenedor que almacena elementos arbitrarios debe declarar su contenido como Object:
// Pre-Java-5 style — what the standard library actually looked like.
List names = new ArrayList();
names.add("Ada");
names.add("Linus");
String first = (String) names.get(0); // cast required, never checked by the compilerDos problemas. Primero, el cast es ruido — cada lectura del contenedor necesita uno. Segundo, y peor, nada impide que alguien inserte un Date en esa misma lista:
names.add(new java.util.Date()); // compiler is fine with this
String oops = (String) names.get(2); // ClassCastException at runtimeEl error aparece en la lectura, lejos de la escritura. El cast miente — dice "esto es un String," y la JVM lo descubre solo cuando ya es demasiado tarde para darte un stack frame útil cerca del fallo real.
Los generics resuelven ambos problemas:
List<String> names = new ArrayList<>();
names.add("Ada");
names.add("Linus");
names.add(new Date()); // ❌ compile error — won't even build
String first = names.get(0); // no cast — the compiler already knows it's a StringEl corchete angular <String> es el parámetro de tipo. Le dice al compilador "esta lista contiene Strings," y desde ese momento cada add y get se verifica contra esa promesa.
Tres cosas que obtienes gratis
Los generics te ofrecen tres beneficios concretos, y son la razón por la que cada colección, stream y optional en el JDK moderno es genérico:
- Verificaciones más estrictas en tiempo de compilación. La inserción del tipo incorrecto mostrada arriba se detecta en tiempo de compilación, no en producción. Una clase de
ClassCastExceptionsimplemente deja de ocurrir. - Sin más casts. Leer de un
Map<String, User>te da unUser, no unObjectque tienes que castear. Menos ruido sintáctico, menos que leer, menos que mantener. - Reutilización de código sin copiar y pegar. Una sola clase
List<E>funciona para cualquier tipo de elemento. Antes de los generics, la biblioteca estándar o bien aceptabaObjecten todas partes, o bien incluíaStringList,IntList,DateList, y así sucesivamente. Ahora escribes una clase y dejas que el llamador la parametrice.
Ese último punto es la mayor ventaja arquitectónica. Los generics son la forma en que escribes un contenedor, un algoritmo o una forma de callback una vez y lo aplicas a cualquier tipo que el llamador pueda pasar.
Una primera clase genérica
La convención es nombrar un parámetro de tipo con una sola letra mayúscula — T para un "tipo" genérico, E para "elemento" de una colección, K/V para "clave" y "valor" de un mapa, R para "retorno". Aquí está la clase genérica más sencilla posible — un par de dos elementos del mismo tipo:
public class Pair<T> {
private final T first;
private final T second;
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T first() { return first; }
public T second() { return second; }
}El <T> después del nombre de la clase introduce el parámetro de tipo. A partir de ahí, T se puede usar dentro de la clase en cualquier lugar donde podría ir un tipo normal. El llamador elige T al crear el objeto:
Pair<String> names = new Pair<>("Ada", "Grace");
Pair<Integer> scores = new Pair<>(100, 87);
String n1 = names.first(); // already a String, no cast
int s1 = scores.first(); // auto-unboxed from IntegerEl <> vacío a la derecha (el operador diamante, Java 7+) le dice al compilador que infiera el tipo a partir de la declaración del lado izquierdo — casi nunca tienes que repetir el argumento de tipo.
Qué se puede parametrizar y qué no
Un parámetro de tipo puede sustituir a:
- El tipo de un campo (
private T value;) - Un parámetro o tipo de retorno de un método (
public T get() { ... },void put(T value)) - El tipo de elemento de un array de ese tipo (
T[] items— con algunas advertencias)
Un parámetro de tipo no puede sustituir a:
- Un primitivo (
Pair<int>es ilegal — usaPair<Integer>y deja que el autoboxing haga el trabajo) - El parámetro de tipo de un campo estático o método estático (el parámetro pertenece a una instancia, no a la clase en sí)
- El objetivo de
new T()oinstanceof T— Java borra los generics en tiempo de ejecución, por lo que el programa no tiene ningúnTque construir o contra el que verificar
La lista completa de "cosas que no puedes hacer" tiene su propio capítulo al final de esta parte — Java Generics Restrictions — una vez que hayamos cubierto suficiente maquinaria para que las reglas tengan sentido.
Un ejemplo práctico: seguridad de tipos vs. tipos raw, lado a lado
El programa siguiente construye el mismo contenedor dos veces — una como un List raw (la forma anterior a los generics) y otra como un List<String>. Ambos compilan; solo el parametrizado es seguro.
La versión raw falla a mitad de la iteración porque el bucle confiaba en un cast que no tenía razón de confiar. La versión genérica hizo que el mismo error fuera irrepresentable — el add(42) incorrecto no compilará en primer lugar. Ese cambio de tiempo de ejecución a tiempo de compilación es la razón completa por la que existen los generics.
Qué cubre esta parte del libro
Los capítulos restantes de esta parte desglosan los generics pieza a pieza:
- Clases genéricas — el parámetro de tipo a nivel de clase que acabas de ver, con más profundidad.
- Métodos genéricos — métodos que introducen su propio parámetro de tipo, independiente de la clase.
- Interfaces genéricas — diseño de contratos de API que se parametrizan sobre un tipo.
- Parámetros de tipo acotados — decir "T debe extender
Number" para poder llamar métodos sobreT. - Wildcards —
? extends T,? super T, y la regla PECS que decide cuándo usar cada uno. - Borrado de tipos — cómo la JVM implementa los generics internamente, y por qué algunas cosas que esperarías que funcionaran no lo hacen.
- Restricciones — el catálogo de cosas que el lenguaje se niega a dejarte hacer, con las razones detrás de cada una.
Léelos en orden — cada capítulo asume los anteriores.
Qué sigue
Comienza con la forma más común — una clase cuyos campos y métodos están parametrizados sobre un tipo que el llamador elige. Continúa en Java Generic Classes.