Clases Genéricas en Java
Aprende a escribir clases genéricas en Java con parámetros de tipo, múltiples parámetros, el operador diamante y un ejemplo funcional de Stack tipado.
Una clase genérica es una clase cuya declaración lleva uno o más parámetros de tipo — marcadores de posición que el llamador rellena al crear una instancia. El mismo cuerpo de clase describe entonces toda una familia de tipos: Box<String>, Box<Integer>, Box<User> son tipos distintos en tiempo de compilación que comparten un único código fuente. Esta es la forma más común que toman los genéricos, y es como están escritas todas las colecciones de java.util, cada Optional, cada Future y cada CompletableFuture.
La sintaxis
La lista de parámetros de tipo va entre el nombre de la clase y el cuerpo, entre corchetes angulares:
public class Box<T> {
private T value;
public Box(T value) { this.value = value; }
public T get() { return value; }
public void set(T value) { this.value = value; }
}Lee la declaración como "un Box parametrizado sobre algún tipo T." Dentro de la clase, T se comporta como cualquier otro tipo — puedes declarar campos de tipo T, métodos que devuelven T, parámetros de tipo T. El compilador lo trata como un tipo real y desconocido hasta que el llamador elige uno.
En el punto de llamada, se indica el tipo concreto:
Box<String> greeting = new Box<>("hello");
Box<Integer> answer = new Box<>(42);
String s = greeting.get(); // already a String — no cast
int i = answer.get(); // auto-unboxed from IntegerEl <> de la derecha es el operador diamante — el compilador infiere el argumento de tipo a partir de la declaración del lado izquierdo. Puedes escribir new Box<String>("hello") de forma explícita, pero casi nunca es necesario.
Múltiples parámetros de tipo
Una clase puede declarar más de un parámetro de tipo. El ejemplo clásico es un par clave/valor:
public class Entry<K, V> {
private final K key;
private final V value;
public Entry(K key, V value) {
this.key = key;
this.value = value;
}
public K key() { return key; }
public V value() { return value; }
}
Entry<String, Integer> score = new Entry<>("Ada", 100);
String name = score.key();
int n = score.value();La convención es usar nombres de una sola letra — K para clave, V para valor, E para elemento, R para retorno, T para "tipo genérico." Cuando se necesita más claridad (algo raro), se permiten nombres más largos: Map<KeyType, ValueType> es válido, aunque poco habitual.
Acotar el parámetro de tipo
Por defecto, un parámetro de tipo representa "cualquier tipo," por lo que dentro de la clase solo puedes llamar a los métodos que tiene todo objeto (equals, toString, hashCode). Si tu clase necesita hacer algo con los valores — compararlos, sumarlos, leer una propiedad — debes restringir T con un límite superior usando extends:
// T can be any type that is (or extends) Number, so .doubleValue() is callable.
public class NumberBox<T extends Number> {
private final T value;
public NumberBox(T value) { this.value = value; }
public double asDouble() { return value.doubleValue(); }
}
NumberBox<Integer> n = new NumberBox<>(42); // fine — Integer is a Number
// NumberBox<String> bad = ...; // ❌ String is not a Numberextends aquí significa "es un subtipo de," y funciona tanto para clases como para interfaces. Incluso puedes requerir varios límites a la vez — <T extends Number & Comparable<T>> — con el límite de clase (si existe) listado primero. El límite es también lo que hace que el tipo sea utilizable: sin extends Number, value.doubleValue() no compilaría.
Constructores genéricos
El parámetro de tipo está fijado por la instancia, por lo que cada constructor de una clase genérica ya tiene acceso a T:
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 Pair(T both) { this(both, both); }
}Los constructores en sí mismos también pueden ser genéricos con parámetros de tipo adicionales independientes de los de la clase — pero eso es poco común y corresponde al siguiente capítulo sobre métodos genéricos.
Las clases genéricas pueden heredar entre sí
Una subclase puede heredar de una clase genérica de tres formas. Cada una significa algo diferente:
// 1. Lock the parent's type parameter — concrete subclass for one element type.
public class StringList extends ArrayList<String> { ... }
// 2. Pass the type parameter through — the subclass is still generic.
public class MyList<E> extends ArrayList<E> { ... }
// 3. Add new type parameters of your own.
public class TaggedList<E, Tag> extends ArrayList<E> { ... }La forma intermedia es la más común — se propaga el parámetro del padre a los propios llamadores. La primera forma es la que se usa cuando la subclase es especializada: un árbol de nodos solo de strings.
Campos y el parámetro de tipo
Cada instancia de Box<...> lleva su propio T. El bytecode no — en tiempo de ejecución la JVM solo ve Box (esto es el borrado de tipos, tratado más adelante en esta parte). La consecuencia es que el parámetro de tipo pertenece a la instancia, no al objeto de clase:
Box<String> a = new Box<>("hi");
Box<Integer> b = new Box<>(5);
a.getClass() == b.getClass(); // true — both are class BoxEs un dato útil a tener en cuenta: Box<String> y Box<Integer> son tipos diferentes para el compilador pero la misma clase en tiempo de ejecución. Volveremos a esto en Java Type Erasure.
Los miembros estáticos no pueden ver el parámetro de tipo
Los campos y métodos estáticos pertenecen a la clase, no a ninguna instancia en particular — por lo que no pueden ver el T de la instancia. Esto es ilegal:
public class Box<T> {
private static T defaultValue; // ❌ won't compile — no T at the static level
public static T empty() { ... } // ❌ same problem
}Un método estático que necesita un parámetro de tipo debe declarar el suyo propio, independiente del de la clase. Ese es el tema del próximo capítulo.
Diseñando el tuyo propio: un pequeño stack tipado
Una clase funcional y completa para unirlo todo — un Stack genérico con push, pop, peek y size. Está parametrizado sobre E (elemento), respaldado internamente por un Object[] (debido a las restricciones de arrays genéricos), y el cast sin verificar en pop es el tipo de solución bien contenida que verás en código real.
Las anotaciones @SuppressWarnings("unchecked") están en las dos lecturas que deben hacer cast de Object a E. Esos casts son seguros — push solo almacena valores de tipo E — pero el compilador no puede verificarlo, porque el borrado ha eliminado E del bytecode. Suprimir la advertencia localmente, en el ámbito más pequeño posible, es lo correcto.
¿Qué sigue?
Has visto el parámetro a nivel de clase. A veces necesitas que un solo método sea genérico, con su propio parámetro de tipo independiente de la clase — útil para métodos de utilidad, helpers estáticos, y cualquier operación cuya relación de tipos vive solo dentro de ese método. Continúa con Java Generic Methods.