Parámetros de tipo acotados en Java
Aprende a restringir tipos genéricos en Java con parámetros acotados usando extends — con límites simples y múltiples.
Un parámetro de tipo acotado es un parámetro de tipo con una cláusula extends que restringe qué tipos pueden ocupar su lugar. Sin un límite, T se trata como Object — el compilador no puede saber si T tiene un compareTo, un intValue u otro método. Con un límite, haces una promesa al compilador sobre lo que T es, y a cambio el compilador te permite llamar a los métodos que esa promesa implica. Casi todo método genérico interesante tiene al menos un límite en algún lugar de su firma.
La forma básica: <T extends Bound>
La sintaxis es extends entre el parámetro y su límite — la misma palabra clave tanto si el límite es una clase como una interfaz:
public static <T extends Number> double sum(List<T> values) {
double total = 0;
for (T n : values) total += n.doubleValue(); // legal — T is-a Number
return total;
}Lee <T extends Number> como "cualquier T que sea un Number o una subclase de él". Dentro del cuerpo, puedes tratar cualquier valor T como un Number — llamar a doubleValue(), intValue() o cualquier método de la API pública de Number.
Úsalo de la misma manera que cualquier otro método genérico:
sum(List.of(1, 2, 3)); // T = Integer — fine
sum(List.of(1.5, 2.5)); // T = Double — fine
sum(List.of(1L, 2L, 3L)); // T = Long — fine
sum(List.of("a", "b")); // ❌ String is not a NumberSin el límite, sum no podría ni intentar llamar a doubleValue() — T se borraría a Object, y Object no tiene ese método. El límite es lo que hace que el cuerpo sea válido.
extends también funciona para interfaces
En los genéricos de Java, extends está sobrecargado — significa "es un subtipo de", ya sea que el límite sea una clase o una interfaz. No existe una palabra clave implements separada en una lista de parámetros de tipo:
public static <T extends Comparable<T>> T max(List<T> list) {
T best = list.get(0);
for (T candidate : list) {
if (candidate.compareTo(best) > 0) best = candidate;
}
return best;
}
max(List.of(3, 1, 4, 1, 5, 9)); // T = Integer
max(List.of("Ada", "Grace", "Linus")); // T = StringComparable<T> es una interfaz, pero la sintaxis sigue siendo extends. Lee <T extends Comparable<T>> como "cualquier T que sea comparable consigo mismo" — la restricción es lo que te permite llamar a candidate.compareTo(best) dentro del bucle.
El pequeño detalle aquí es el <T> dentro del límite — es la forma autorreferencial que vimos en interfaces genéricas. Garantiza que compareTo acepte un T, no simplemente cualquier Comparable. Sin esto, podrías comparar Integer con String y el sistema de tipos no lo detectaría.
Límites múltiples
Un parámetro de tipo puede tener más de un límite, unidos por &:
public static <T extends Number & Comparable<T>> T maxNumber(List<T> list) {
T best = list.get(0);
for (T n : list) {
if (n.compareTo(best) > 0) best = n;
}
return best;
}Ahora T debe ser tanto un Number como un Comparable<T>. Dentro del cuerpo puedes llamar a cualquier método de ambos. Integer, Long, Double, BigDecimal satisfacen ambos — puedes pasar cualquiera de ellos.
Algunas reglas:
- El límite de clase (si lo hay) debe ir primero.
<T extends Number & Comparable<T>>es válido;<T extends Comparable<T> & Number>no lo es. - Como máximo un límite de clase. Java tiene herencia simple para clases, por lo que esto se deriva de las reglas existentes del lenguaje.
- Cualquier número de límites de interfaz. Apila tantos como necesites de verdad; en la práctica, dos es el máximo habitual antes de que el diseño necesite replantearse.
La regla de la clase primero refleja el bytecode: el borrado reemplaza T con su límite más a la izquierda, por lo que la posición más a la izquierda es especial. Volveremos a eso en Type Erasure.
Límites en parámetros de tipo a nivel de clase
Los límites no son exclusivos de los métodos. Una clase o interfaz genérica puede acotar sus parámetros de tipo en la declaración:
public class SortedBag<T extends Comparable<T>> {
private final List<T> items = new ArrayList<>();
public void add(T item) {
items.add(item);
Collections.sort(items);
}
public T smallest() { return items.get(0); }
}
SortedBag<Integer> bag = new SortedBag<>(); // OK — Integer is Comparable<Integer>
// SortedBag<Object> bag2 = new SortedBag<>(); // ❌ Object is not Comparable<Object>Este es el lugar correcto para un límite que aplica a toda la clase. Cada método, cada campo, cada operación predeterminada tiene acceso a la restricción.
Los límites inferiores pertenecen a los comodines, no a los parámetros de tipo
Una confusión habitual: los parámetros de tipo pueden tener límites superiores (extends) pero no inferiores (super). Esto es válido:
public static <T extends Number> void foo(T x) { ... }Esto no lo es:
public static <T super Integer> void bar(T x) { ... } // ❌ no such syntaxLos límites inferiores sí existen en los genéricos de Java — pero solo viven en los comodines, el token ?, no en parámetros de tipo con nombre. Cubriremos la historia completa de ? extends / ? super en el capítulo de Comodines; por ahora, basta con saber que super funciona con ? y no con T.
Cuándo añadir un límite
Lo predeterminado debería ser "sin límite" — cuanto más flexible sea la restricción, más tipos acepta tu método. Añade uno cuando, y solo cuando, el cuerpo necesite llamar a un método específico sobre T:
- "Necesito comparar valores
T" →<T extends Comparable<T>>. - "Necesito sumar valores
Tcomo números" →<T extends Number>. - "Necesito leer campos de
Tcomo si fuera unUser" →<T extends User>. - "Necesito que
Tsea auto-cerrable para usarlo en try-with-resources" →<T extends AutoCloseable>.
Si no estás llamando a un método sobre T, no necesitas un límite — y añadir uno de todos modos solo restringe tu API sin razón.
Un ejemplo elaborado: un between acotado y un top ordenado
Dos métodos, cada uno con un estilo diferente de límite. between usa Comparable<T> para poder limitar valores; topN usa Number & Comparable<T> para poder tanto comparar como reportar una suma numérica.
between acepta cualquier Comparable — incluyendo Character, que es Comparable<Character> y no tiene nada que ver con los números. topN es más restrictivo — necesita tanto ordenamiento (para ordenar) como valores numéricos (para sumar), por eso apila los dos límites con &. Cada método solicita exactamente la capacidad que usa, nada más.
Qué sigue
Un límite en un parámetro de tipo fija un único tipo en el punto de llamada — List<Integer> significa "esta lista, este método, este tipo". A veces quieres ser menos específico: "una lista de algún Number — podría ser Integer, podría ser Double, no importa." Para eso están los comodines, y resuelven una de las mayores fuentes de confusión en el sistema de tipos de Java. Continúa con Comodines genéricos de Java.