W3docs

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 through

O 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.functionFunction, 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.

java— editable, runs on the server

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.

Prática

Prática
Você tem `interface Repository<E> { E find(int id); }` e `class UserRepo implements Repository<User>`. Um chamador escreve `Repository r = new UserRepo();` (sem argumento de tipo) e depois `User u = r.find(1);`. Qual é o problema?
Você tem `interface Repository<E> { E find(int id); }` e `class UserRepo implements Repository<User>`. Um chamador escreve `Repository r = new UserRepo();` (sem argumento de tipo) e depois `User u = r.find(1);`. Qual é o problema?
Was this page helpful?