Restricciones de los Generics en Java
Lo que no puedes hacer con los generics de Java: sin primitivos, sin parámetros de tipo estáticos, sin arrays genéricos y más.
Este capítulo es el catálogo de cosas que no puedes hacer con los generics de Java, con una breve explicación de por qué cada una está prohibida. Casi todas las restricciones se remontan a un único hecho que viste en el capítulo anterior: el parámetro de tipo se borra antes de emitir el bytecode, por lo que cualquier cosa que necesite que el parámetro exista en tiempo de ejecución no funcionará. Lee este capítulo como la tarjeta de referencia final de la parte — es la lista de momentos "lo intenté, el compilador se quejó" convertidos en una sola página.
1. Sin primitivos como argumentos de tipo
List<int> ints = new ArrayList<>(); // ❌
List<Integer> ints = new ArrayList<>(); // ✓La forma borrada de List<E> a nivel de bytecode almacena sus elementos como Object, y los primitivos no son Objects. La solución es la clase envolvente correspondiente — Integer, Long, Double, Boolean, Character, etc. El autoboxing luego cierra la brecha: ints.add(5) e int x = ints.get(0) ambos funcionan, con el costo de que cada elemento paga por un objeto Integer en el heap.
Project Valhalla es el esfuerzo continuo para hacer que List<int> funcione realmente, mediante tipos valor y generics especializados. A partir de Java 25, todavía no está disponible.
2. Sin new T()
public class Box<T> {
public T newInstance() { return new T(); } // ❌
}En tiempo de ejecución, no hay T — la JVM solo tiene Object (o el límite). No tiene un objeto de clase para llamar a un constructor, ni forma de saber qué constructor llamar. La solución estándar es tomar una fábrica como parámetro:
public class Box<T> {
public T newInstance(Supplier<T> factory) { return factory.get(); }
}
Box<String> b = new Box<>();
String fresh = b.newInstance(String::new);El Supplier<T> lleva la fábrica real en tiempo de ejecución, de una manera que el parámetro de tipo nunca podría.
3. Sin T.class ni instanceof T
public <T> boolean isIt(Object o) {
return o instanceof T; // ❌
}
public <T> Class<T> klass() {
return T.class; // ❌
}De nuevo, no hay T en tiempo de ejecución. La solución en ambos casos es pasar el token Class<T> como argumento:
public <T> boolean isIt(Object o, Class<T> type) {
return type.isInstance(o);
}Class.isInstance(Object) es la forma reflexiva de instanceof, y funciona con el objeto Class en tiempo de ejecución que pasaste. La biblioteca estándar hace esto en muchos lugares — Collections.checkedList(List<E>, Class<E>), EnumSet.noneOf(Class<E>), deserializadores JSON, etc.
4. Sin arrays de un tipo genérico
T[] arr = new T[10]; // ❌ — generic array creation
List<String>[] lists = new List<String>[10]; // ❌ — sameEste es más sutil. Los arrays en Java están reificados — un Integer[] sabe en tiempo de ejecución que es un Integer[], y las asignaciones se verifican. Pero los generics se borran — la JVM no puede distinguir List<String>[] de List<Integer>[]. Si no se aplicaran ambas restricciones, podrías corromper el heap con unas pocas líneas:
List<String>[] strs = new List<String>[1]; // pretend this is legal
Object[] objs = strs; // arrays are covariant
objs[0] = List.of(42); // stores an Integer list
String s = strs[0].get(0); // KABOOMEl compilador rechaza la creación de arrays genéricos en lugar de permitir que esto ocurra.
Soluciones alternativas:
- Usar
(T[]) new Object[n]con un@SuppressWarnings("unchecked")(lo viste en el Stack genérico anteriormente). Seguro si el array es interno y nunca lo dejas escapar comoT[]. - O simplemente usar una
List<T>en lugar de un array. En nueve de cada diez casos esta es la respuesta correcta.
5. Sin campos estáticos de un parámetro de tipo
public class Box<T> {
private static T defaultValue; // ❌
public static T empty() { ... } // ❌
}El parámetro de tipo pertenece a una instancia — cada Box<...> lleva su propio T. Los miembros estáticos pertenecen a la clase misma, que no tiene T. Los dos ámbitos no se conectan.
Si necesitas un método estático que sea polimórfico en un tipo, declara su propio parámetro de tipo (lo cubrimos en métodos genéricos):
public class Box<T> {
public static <U> Box<U> empty() { return new Box<>(null); }
}<U> es local al método — independiente de cualquier T a nivel de clase.
6. Sin tipos de excepción genéricos
public class MyException<T> extends Exception { ... } // ❌Las tablas de manejo de excepciones de la JVM buscan bloques catch por clase borrada. Si dos tipos de excepción genéricos diferentes se borraran a la misma clase, un catch (MyException<String> e) también capturaría un MyException<Integer> — lo que corrompería silenciosamente el sistema de tipos. En lugar de intentar que funcione, Java prohíbe la declaración directamente. Tampoco puedes tener un parámetro de tipo genérico en una cláusula catch:
try { ... } catch (T e) { ... } // ❌Si tu excepción genuinamente necesita llevar una carga útil tipada, almacena la carga útil como un campo genérico en una excepción no genérica:
public class TaggedException extends Exception {
public final Object payload;
public TaggedException(String message, Object payload) {
super(message);
this.payload = payload;
}
}O declara el sitio de lanzamiento de forma estrecha y reserva las cargas útiles tipadas para rutas de retorno normales.
7. Sin sobrecargas que difieran solo en parámetros genéricos
public void process(List<String> list) { ... }
public void process(List<Integer> list) { ... } // ❌ — both erase to process(List)Después del borrado, ambos métodos tienen la firma process(List). Java trata la resolución de sobrecarga por firmas borradas, por lo que no puede distinguir los dos. La solución es darles nombres diferentes — processStrings y processInts — o aceptar una List<Object> y verificar en tiempo de ejecución.
8. Los tipos genéricos son invariantes
Esta no es una regla de "el compilador rechaza esto", sino una de "el compilador rechaza lo que esperabas que fuera legal", y la cubrimos en detalle en Wildcards:
List<Integer> ints = ...;
List<Number> nums = ints; // ❌ — generic types are invariant
List<? extends Number> nums = ints; // ✓ — wildcard restores the flexibilityVale la pena conocerla porque es la restricción con la que más te toparás. Los wildcards son la válvula de alivio.
9. Sin tipos enum genéricos
public enum Box<T> { // ❌
EMPTY, FULL;
T value;
}Los enums se traducen a una única clase con un conjunto fijo de constantes — no hay forma de que las constantes compartan un T único y sensato. La solución generalmente es hacer que los métodos sean genéricos, no el enum en sí:
public enum Box {
EMPTY, FULL;
public <T> T orDefault(T fallback) { return this == FULL ? null : fallback; }
}O, si cada constante realmente quiere su propio tipo, usa una jerarquía de clases no enum y un Map<Name, Box>.
10. Llamar a un método genérico a través de un tipo raw
List rawList = new ArrayList();
rawList.add("hi"); // unchecked-warning, but allowed
List<String> typed = rawList; // unchecked-warning, dangerousMezclar tipos raw y genéricos deshabilita todas las verificaciones en tiempo de compilación de los generics para esa variable. El compilador advertirá (Unchecked call to add(E) as a member of raw type java.util.List), e ignorar la advertencia te devuelve la pistola de los tiempos anteriores a Java 5 — valores del tipo equivocado introducidos silenciosamente, que explotan en la próxima lectura.
Los tipos raw existen para compatibilidad hacia atrás, no como una característica. Trata la advertencia como un error en cualquier código nuevo.
Un ejemplo práctico: cada restricción, una al lado de la otra
El programa a continuación intenta hacer cada una de las cosas que las reglas prohíben (comentadas para que el archivo compile), luego muestra la solución canónica para cada una. Lee los comentarios — mapean uno a uno con las restricciones numeradas arriba.
Cada restricción numerada mapea a una solución de una línea en el programa. El patrón en todas ellas es el mismo: cualquier cosa que quiera el parámetro de tipo en tiempo de ejecución recibe la información explícitamente — un token Class<T>, un Supplier<T>, un parámetro de tipo a nivel de método, un wildcard. El borrado eliminó la forma implícita; tú la devuelves en el límite de la API.
Esto cierra la Parte 10
Los generics son la característica individual más profunda del lenguaje fuera de la JVM misma. Ahora tienes el vocabulario operativo: parámetros de tipo en clases, métodos e interfaces; límites; wildcards y PECS; borrado; y el catálogo de restricciones que el borrado impone al diseño. Toda API moderna de Java está moldeada por estas reglas, y leer código de biblioteca (o diseñar el tuyo propio) es mucho más fácil con el modelo en tu cabeza.
Qué sigue
Los generics no son un fin en sí mismos — existen porque Java necesitaba una forma de expresar "contenedor de T" sin copiar y pegar una clase por tipo de elemento. La siguiente parte del libro es el lugar para el que toda la maquinaria genérica de esta parte te estaba preparando en secreto: el Collections Framework. List, Set, Map, Queue y las decenas de implementaciones detrás de ellos — todos parametrizados, todos diseñados en torno a las reglas que acabas de aprender. Continúa en Introducción a las colecciones de Java.