Interfaces Genéricas em Java
Aprenda a criar interfaces genéricas em Java que parametrizam suas assinaturas de métodos sobre um tipo.
Uma interface genérica é uma interface cuja declaração recebe um ou mais parâmetros de tipo, assim como uma classe genérica. É o terceiro lugar onde parâmetros de tipo podem existir em Java, ao lado de classes genéricas e métodos genéricos, e é o mais importante — porque quase todo contrato reutilizável na biblioteca padrão é uma interface genérica. List<E>, Map<K, V>, Comparator<T>, Function<T, R>, Supplier<T>, Iterable<T>, Iterator<T> — são a espinha dorsal do Java moderno.
A sintaxe
A lista de parâmetros de tipo fica entre o nome da interface e o corpo:
public interface Container<T> {
void add(T item);
T get(int index);
int size();
}Leia como "um Container parametrizado sobre algum tipo de elemento T." Dentro da interface, T pode aparecer em parâmetros de método, tipos de retorno e em qualquer outra posição onde um tipo possa ser usado. Métodos padrão (Java 8+) e métodos privados de interface (Java 9+) também podem usar T.
Quando você implementa a interface, precisa fazer uma escolha — e essa escolha é toda a decisão arquitetural:
// 1. Pick a concrete type — the implementation is specialised.
public class StringContainer implements Container<String> {
private final List<String> items = new ArrayList<>();
public void add(String s) { items.add(s); }
public String get(int i) { return items.get(i); }
public int size() { return items.size(); }
}
// 2. Stay generic — pass the parameter through to the class.
public class ListContainer<E> implements Container<E> {
private final List<E> items = new ArrayList<>();
public void add(E e) { items.add(e); }
public E get(int i) { return items.get(i); }
public int size() { return items.size(); }
}Ambas são válidas. A primeira é "um Container que armazena Strings, especificamente." A segunda é "um Container parametrizado sobre o mesmo E que o chamador escolhe." A maioria dos contêineres reutilizáveis usa a segunda forma; os especializados (um JsonObject é um "contêiner de JsonValues, nada mais") usam a primeira.
Múltiplos parâmetros de tipo
A forma se generaliza diretamente para dois ou mais parâmetros. Veja java.util.Map:
public interface Map<K, V> {
V put(K key, V value);
V get(Object key); // Object on purpose — see below
Set<K> keySet();
Collection<V> values();
...
}A declaração Map<K, V> diz "dois parâmetros: K para chaves, V para valores." As implementações os fixam ou os repassam:
public class StringIntMap implements Map<String, Integer> { ... } // pinned
public class HashMap<K, V> implements Map<K, V> { ... } // passed throughO get(Object key) na assinatura de Map é uma decisão deliberada de design de API — ele aceita qualquer objeto como chave de busca por razões históricas. Voltaremos a isso na parte de Collections; não é uma regra de generics, apenas um compromisso específico do Map.
Interfaces funcionais são interfaces genéricas
As interfaces em java.util.function — Function, Predicate, Consumer, Supplier, BiFunction, e assim por diante — são todas interfaces genéricas com um único método abstrato, o que as torna alvos para lambdas:
public interface Function<T, R> {
R apply(T t);
}
public interface Predicate<T> {
boolean test(T t);
}
public interface Comparator<T> {
int compare(T a, T b);
}Quando você escreve s -> s.length(), o compilador infere um Function<String, Integer> a partir do contexto. Os dois parâmetros de tipo de Function<T, R> são preenchidos pelo código ao redor — geralmente uma operação de stream ou um parâmetro de método:
List<String> names = List.of("Ada", "Grace", "Linus");
List<Integer> lengths = names.stream()
.map(s -> s.length()) // Function<String, Integer> — both inferred
.toList();Isso é uma interface genérica e um método genérico (Stream.map) cooperando. A assinatura do método é aproximadamente <R> Stream<R> map(Function<? super T, ? extends R> mapper) — wildcards que veremos em Wildcards, e um parâmetro de tipo que escolhe R com base na função passada.
Interfaces auto-referenciais — Comparable<T>
Um dos padrões mais úteis na biblioteca padrão é a interface genérica auto-referencial, onde o argumento de tipo é a própria classe que a implementa:
public interface Comparable<T> {
int compareTo(T other);
}
public class Money implements Comparable<Money> {
private final long cents;
// ...
@Override public int compareTo(Money other) {
return Long.compare(this.cents, other.cents);
}
}Leia class Money implements Comparable<Money> como "Money sabe como se comparar com outras instâncias de Money." É isso que faz Collections.sort(List<Money> list) funcionar sem um Comparator — cada elemento já carrega um compareTo(Money) herdado do contrato da interface, e o sistema de tipos garante que o argumento tenha o mesmo tipo que o receptor.
Comparable<T> é o exemplo canônico desse padrão — todo tipo de valor no JDK que possui uma ordem natural o implementa: Integer implements Comparable<Integer>, String implements Comparable<String>, LocalDate implements Comparable<LocalDate>, e assim por diante.
Herança de uma interface genérica
As mesmas três escolhas aparecem para herança de interface para interface — extends em vez de implements, mas as regras são as mesmas:
// Pin the parameter.
public interface StringList extends List<String> { ... }
// Pass it through.
public interface MyList<E> extends List<E> { ... }
// Add new ones.
public interface IndexedList<E, I> extends List<E> { I indexOf(E e); }Mesma ideia que para classes — o parâmetro do pai deve ser fornecido (com um tipo real ou um repassado), e o filho pode adicionar parâmetros próprios.
Métodos padrão podem usar o parâmetro de tipo
O Java 8 adicionou métodos padrão às interfaces. Eles podem usar o parâmetro de tipo da interface exatamente como qualquer método abstrato:
public interface Container<T> {
void add(T item);
T get(int index);
int size();
default boolean isEmpty() { return size() == 0; }
default void addAll(Iterable<T> items) {
for (T item : items) add(item);
}
}O método padrão addAll funciona para todo implementador, independentemente do T escolhido. É assim que Collection<E> fornece forEach, removeIf, stream e outros — um único corpo padrão, e cada implementação o recebe automaticamente.
Um exemplo prático: uma interface genérica Repository
Uma pequena abstração de repositório — interface + duas implementações. A primeira implementação fixa o tipo de entidade (UserRepo armazena apenas usuários); a segunda permanece genérica (InMemoryRepo<E> armazena o que o chamador pedir). Ambas satisfazem o mesmo contrato do ponto de vista do chamador.
InMemoryRepo<E> é a forma reutilizável — o parâmetro de tipo é repassado da interface para a classe, então o mesmo corpo funciona para User, String ou qualquer outra coisa. UserRepo é a forma especializada — ela fixa E em User e então adiciona métodos que só fazem sentido para usuários. Ambas respeitam o mesmo contrato Repository<E>, e ambas herdam isEmpty() gratuitamente do método padrão.
O que vem a seguir
Até agora, todo parâmetro de tipo foi completamente irrestrito — T poderia ser qualquer coisa. Na prática, você frequentemente quer dizer "T deve ser um Number" ou "T deve implementar Comparable", para que você possa chamar métodos nele dentro do corpo. É para isso que servem os parâmetros de tipo limitados, e eles são o próximo capítulo. Continue para Java Bounded Type Parameters.