Métodos Genéricos em Java
Defina métodos com seus próprios parâmetros de tipo em Java, independentes da classe que os contém.
Um método genérico é um método que introduz seu próprio parâmetro de tipo na assinatura, independente de qualquer parâmetro no nível da classe. Esta é a ferramenta certa quando a relação de tipos pertence a um único método — um utilitário que troca dois elementos de um array, uma fábrica que retorna uma lista do tipo que o chamador passar, um auxiliar estático que não tem uma instância à qual associar um T. Os métodos genéricos são como quase todos os utilitários estáticos em java.util.Collections e java.util.Arrays são escritos.
Onde fica o parâmetro de tipo
O parâmetro de tipo é declarado antes do tipo de retorno, entre os modificadores e o retorno:
public static <T> T identity(T value) {
return value;
}Lendo da esquerda para a direita: "public, static, declara um parâmetro de tipo T, retorna um T, chamado identity, recebe um T." O <T> é o que torna este um método genérico, em vez de um método que por acaso usa um T no nível da classe.
Chame-o como um método normal — o compilador infere o argumento de tipo a partir dos argumentos passados:
String s = identity("hello"); // T inferred as String
Integer n = identity(42); // T inferred as IntegerSe a inferência falhar ou você quiser substituí-la, pode fornecer o argumento de tipo explicitamente com a sintaxe type witness, após o ponto:
String s = MyUtil.<String>identity("hello"); // rarely neededEm dez anos de Java, você escreverá essa forma explícita talvez uma dúzia de vezes.
Por que um parâmetro no nível do método em vez do nível da classe
Um parâmetro no nível da classe diz "esta classe inteira é sobre um tipo." Um parâmetro no nível do método diz "esta operação é polimórfica em um tipo que não precisa sobreviver à chamada." Eles não são substitutos — respondem a perguntas 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) { ... }
}Use um parâmetro no nível do método quando:
- O método é
static(não possui instância, portanto não háTno nível da classe para usar). - A relação de tipos é local ao método — entrada e saída compartilham um tipo, mas a classe não.
- Você quer que diferentes chamadas do mesmo método usem tipos diferentes:
swapem umString[]eswapem umInteger[]devem funcionar, e a classe não deve precisar se comprometer com apenas um.
Múltiplos parâmetros de tipo em um método
A mesma regra se aplica: declare-os entre os modificadores e o tipo de retorno, separados por vírgulas:
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 quanto V são inferidos a partir dos argumentos. Se os dois parâmetros compartilharem um tipo, o tipo inferido é aquele em que ambos os argumentos concordam:
public static <T> T firstOf(T a, T b) { return a; }
firstOf("x", "y"); // T = String
firstOf("x", 42); // T = Object — the closest common supertypeEsse último caso pode ser uma armadilha. O compilador não o rejeita; ele simplesmente alarga T silenciosamente para Object. Se você queria "dois argumentos do mesmo tipo exato," os generics não conseguem impor isso além do alargamento — seria necessário tornar os argumentos parâmetros de tipo separados.
Um parâmetro no nível do método em uma classe genérica
Uma classe genérica pode ter métodos genéricos que introduzem seus próprios parâmetros, distintos dos da classe. Os dois parâmetros coexistem:
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=IntegerO <U> de map está em escopo apenas dentro de map. Ele pode usar T (porque está dentro de um Box<T>), mas não pode substituí-lo.
Inferência de tipos na prática
O compilador infere os parâmetros de tipo de um método a partir de:
- Os tipos dos argumentos explícitos.
- O tipo alvo — o que você está atribuindo o resultado, ou o tipo de parâmetro de um método ao qual você está passando o resultado.
A segunda fonte é por que List.of(), Collections.emptyList() e similares com generics somente de retorno funcionam sem um type witness explícito na maioria das vezes:
List<String> empty = Collections.emptyList(); // T inferred from the left side
process(Collections.emptyList()); // T inferred from `process`'s parameterQuando nenhuma das fontes está disponível (sem argumentos, sem tipo alvo), o compilador recorre a Object. Isso quase nunca é o que você quer — escreva o type witness ou adicione um tipo alvo:
var x = Collections.emptyList(); // List<Object> — probably not what you meant
List<String> y = Collections.emptyList(); // List<String> ✓Uma forma do mundo real: métodos utilitários em coleções
Collections.unmodifiableList, Collections.sort, Collections.shuffle e similares da biblioteca padrão são todos métodos genéricos em uma classe utilitária não genérica. Veja sort, em essência:
public static <T extends Comparable<T>> void sort(List<T> list) {
// ... sorts using natural order
}Essa assinatura faz duas coisas ao mesmo tempo. O <T> declara um parâmetro de tipo. O extends Comparable<T> é um bound — T precisa ser um tipo que sabe como se comparar consigo mesmo. Dedicaremos um capítulo inteiro aos parâmetros limitados; por ora, observe apenas que o bound é o que permite ao método chamar compareTo em seus elementos.
Um exemplo prático: swap tipado, last tipado, map tipado
Uma pequena classe utilitária com três métodos genéricos — um void, um retornando o mesmo tipo recebido, um mapeando elemento a elemento para um novo tipo. Juntos, cobrem as três formas que você mais escreverá.
Três coisas a observar. swap funciona tanto em String[] quanto em Integer[] porque T é inferido por chamada. last retorna o tipo de elemento que o chamador passou — sem cast no lado receptor. map introduz dois parâmetros de tipo e os liga por meio do parâmetro Function<T, R> — o compilador garante que a função receba o tipo de elemento da lista e retorne o tipo de elemento da lista resultado.
O que vem a seguir
Você viu as duas formas de declarar um parâmetro de tipo — em uma classe e em um método. O próximo passo é o terceiro lugar onde um parâmetro de tipo pode estar: em uma interface. É assim que a biblioteca padrão define List<E>, Comparator<T>, Function<T, R> e todo outro contrato que você implementa ao escrever código polimórfico. Continue para Java Generic Interfaces.