W3docs

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 compiler

Dos 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 runtime

El 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 String

El 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 ClassCastException simplemente deja de ocurrir.
  • Sin más casts. Leer de un Map<String, User> te da un User, no un Object que 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 aceptaba Object en todas partes, o bien incluía StringList, 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 Integer

El <> 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 — usa Pair<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() o instanceof T — Java borra los generics en tiempo de ejecución, por lo que el programa no tiene ningún T que 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.

java— editable, runs on the server

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 sobre T.
  • 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.

Práctica

Práctica
Un método declara `public static List getNames() { ... }` (sin parámetro de tipo en la lista). El llamador escribe `String first = getNames().get(0);`. ¿Por qué el compilador advierte — y cuál es el peligro si ignoras la advertencia?
Un método declara `public static List getNames() { ... }` (sin parámetro de tipo en la lista). El llamador escribe `String first = getNames().get(0);`. ¿Por qué el compilador advierte — y cuál es el peligro si ignoras la advertencia?
Was this page helpful?