Métodos Genéricos en Java
Define métodos con sus propios parámetros de tipo en Java, independientes de la clase que los contiene.
Un método genérico es un método que introduce su propio parámetro de tipo en su firma, independientemente de cualquier parámetro a nivel de clase. Esta es la herramienta adecuada cuando la relación de tipos pertenece a un solo método: una utilidad que intercambia dos elementos de un array, una fábrica que devuelve una lista de lo que el llamante pase, un helper estático que no tiene una instancia a la que adjuntar un T. Los métodos genéricos son la forma en que está escrita casi toda utilidad estática de java.util.Collections y java.util.Arrays.
Dónde va el parámetro de tipo
El parámetro de tipo se declara antes del tipo de retorno, entre los modificadores y el retorno:
public static <T> T identity(T value) {
return value;
}Leyendo de izquierda a derecha: "public, static, declara un parámetro de tipo T, retorna un T, llamado identity, recibe un T." El <T> es lo que hace que esto sea un método genérico en lugar de un método que por casualidad usa un T a nivel de clase.
Llámalo como un método normal — el compilador infiere el argumento de tipo a partir de los argumentos que pasas:
String s = identity("hello"); // T inferred as String
Integer n = identity(42); // T inferred as IntegerSi la inferencia falla o deseas sobrescribirla, puedes suministrar el argumento de tipo explícitamente con la sintaxis de testigo de tipo, después del punto:
String s = MyUtil.<String>identity("hello"); // rarely neededEn diez años de Java, escribirás esa forma explícita quizás una docena de veces.
Por qué un parámetro a nivel de método en lugar de uno a nivel de clase
Un parámetro a nivel de clase dice "toda esta clase trata sobre un tipo." Un parámetro a nivel de método dice "esta operación es polimórfica en un tipo que no necesita sobrevivir a la llamada." No son sustitutos — responden preguntas diferentes:
// Method-level: the class isn't generic; the method is.
public class Arrays {
public static <T> void swap(T[] arr, int i, int j) { ... }
}
// Class-level: the class is parameterised; methods share that T.
public class Box<T> {
public T get() { ... }
public void set(T value) { ... }
}Usa un parámetro a nivel de método cuando:
- El método es
static(no tiene instancia, por lo tanto no puede tomar prestado unTa nivel de clase). - La relación de tipos es local al método — la entrada y la salida comparten un tipo, pero la clase no.
- Quieres que diferentes llamadas del mismo método usen tipos distintos:
swapsobre unString[]yswapsobre unInteger[]deben funcionar ambas, y la clase no debería tener que comprometerse con uno.
Múltiples parámetros de tipo en un método
Se aplica la misma regla: decláralos entre los modificadores y el tipo de retorno, separados por comas:
public static <K, V> Map.Entry<K, V> entry(K key, V value) {
return new AbstractMap.SimpleImmutableEntry<>(key, value);
}
Map.Entry<String, Integer> e = entry("Ada", 100);Tanto K como V se infieren de los argumentos. Si los dos parámetros resultan compartir un tipo, el tipo inferido es aquel en el que coinciden ambos argumentos:
public static <T> T firstOf(T a, T b) { return a; }
firstOf("x", "y"); // T = String
firstOf("x", 42); // T = Object — the closest common supertypeEse último caso es a veces una trampa. El compilador no lo rechaza; simplemente ensancha T silenciosamente a Object. Si querías "dos argumentos del mismo tipo exacto," los genéricos no pueden imponerlo más allá del ensanchamiento — tendrías que convertir los argumentos en parámetros de tipo separados.
Un parámetro a nivel de método en una clase genérica
Una clase genérica puede tener métodos genéricos que introduzcan sus propios parámetros, distintos de los de la clase. Los dos parámetros coexisten:
public class Box<T> {
private T value;
public Box(T value) { this.value = value; }
public T get() { return value; }
// U is local to this method — independent of T.
public <U> Box<U> map(java.util.function.Function<T, U> fn) {
return new Box<>(fn.apply(value));
}
}
Box<String> name = new Box<>("Ada");
Box<Integer> length = name.map(String::length); // T=String, U=IntegerEl <U> de map está en ámbito solo dentro de map. Puede usar T (porque está dentro de un Box<T>) pero no puede reemplazarlo.
Inferencia de tipos en la práctica
El compilador infiere los parámetros de tipo de un método a partir de:
- Los tipos de los argumentos explícitos.
- El tipo destino — a qué estás asignando el resultado, o el tipo de parámetro de un método al que estás pasando el resultado.
La segunda fuente es la razón por la que List.of(), Collections.emptyList(), y genéricos similares de solo retorno funcionan sin un testigo de tipo explícito la mayor parte del tiempo:
List<String> empty = Collections.emptyList(); // T inferred from the left side
process(Collections.emptyList()); // T inferred from `process`'s parameterCuando ninguna fuente está disponible (sin argumentos, sin tipo destino), el compilador recurre a Object. Eso casi nunca es lo que deseas — escribe el testigo de tipo o agrega un tipo destino:
var x = Collections.emptyList(); // List<Object> — probably not what you meant
List<String> y = Collections.emptyList(); // List<String> ✓Una forma de la vida real: métodos de utilidad en colecciones
Los métodos Collections.unmodifiableList, Collections.sort, Collections.shuffle y otros de la biblioteca estándar son todos métodos genéricos en una clase de utilidad no genérica. Tomemos sort, en esencia:
public static <T extends Comparable<T>> void sort(List<T> list) {
// ... sorts using natural order
}Esa firma hace dos cosas a la vez. El <T> declara un parámetro de tipo. El extends Comparable<T> es una cota — T debe ser un tipo que sepa compararse consigo mismo. Dedicaremos un capítulo entero a los parámetros acotados; por ahora, solo observa que la cota es lo que permite al método llamar a compareTo sobre sus elementos.
Un ejemplo trabajado: swap tipado, last tipado, map tipado
Una pequeña clase de utilidad con tres métodos genéricos — uno void, uno que devuelve el mismo tipo que recibió, uno que mapea elemento a elemento a un nuevo tipo. Juntos cubren las tres formas que escribirás con más frecuencia.
Tres cosas a notar. swap funciona tanto en String[] como en Integer[] porque T se infiere por llamada. last devuelve el tipo de elemento que el llamante pasó — sin cast en el lado receptor. map introduce dos parámetros de tipo y los enlaza a través del parámetro Function<T, R> — el compilador garantiza que la función tome el tipo de elemento de la lista y retorne el tipo de elemento de la lista resultado.
Qué sigue
Has visto las dos formas de declarar un parámetro de tipo — en una clase y en un método. El siguiente paso es el tercer lugar donde puede vivir un parámetro de tipo: en una interfaz. Así es como la biblioteca estándar define List<E>, Comparator<T>, Function<T, R>, y cada otro contrato que implementas cuando escribes código polimórfico. Continúa con Interfaces Genéricas en Java.