W3docs

Classes Genéricas em Java

Aprenda a criar classes genéricas em Java com parâmetros de tipo, múltiplos parâmetros, o operador diamante e um exemplo prático de Stack tipada.

Uma classe genérica é uma classe cuja declaração contém um ou mais parâmetros de tipo — marcadores que o chamador preenche ao criar uma instância. O mesmo corpo da classe descreve então uma família inteira de tipos: Box<String>, Box<Integer>, Box<User> são tipos distintos em tempo de compilação que compartilham uma única fonte. Esta é a forma mais comum que os genéricos assumem, e é assim que todas as coleções em java.util, todos os Optional, todos os Future e todos os CompletableFuture são escritos.

A sintaxe

A lista de parâmetros de tipo fica entre o nome da classe e o corpo, entre colchetes angulares:

public class Box<T> {
  private T value;

  public Box(T value) { this.value = value; }

  public T get()              { return value; }
  public void set(T value)    { this.value = value; }
}

Leia a declaração como "um Box parametrizado sobre algum tipo T." Dentro da classe, T se comporta como qualquer outro tipo — você pode declarar campos do tipo T, métodos que retornam T, parâmetros do tipo T. O compilador o trata como um tipo real e desconhecido até o chamador escolher um.

No ponto de chamada, você fornece o tipo real:

Box<String>  greeting = new Box<>("hello");
Box<Integer> answer   = new Box<>(42);

String s = greeting.get();   // already a String — no cast
int i    = answer.get();     // auto-unboxed from Integer

O <> à direita é o operador diamante — o compilador infere o argumento de tipo a partir da declaração à esquerda. Você pode escrever new Box<String>("hello") explicitamente, mas quase nunca precisa.

Múltiplos parâmetros de tipo

Uma classe pode declarar mais de um parâmetro de tipo. O exemplo clássico é um par chave/valor:

public class Entry<K, V> {
  private final K key;
  private final V value;

  public Entry(K key, V value) {
    this.key   = key;
    this.value = value;
  }

  public K key()   { return key; }
  public V value() { return value; }
}

Entry<String, Integer> score = new Entry<>("Ada", 100);
String name = score.key();
int    n    = score.value();

A convenção é usar nomes de uma única letra — K para chave, V para valor, E para elemento, R para retorno, T para "tipo genérico." Quando for necessária mais clareza (raramente), nomes mais longos são permitidos: Map<KeyType, ValueType> é válido, apenas pouco usado.

Limitando o parâmetro de tipo

Por padrão, um parâmetro de tipo representa "qualquer tipo possível," portanto, dentro da classe, você só pode chamar métodos que todo objeto possui (equals, toString, hashCode). Se a sua classe precisa fazer algo com os valores — compará-los, somá-los, ler uma propriedade — você restringe T com um limite superior usando extends:

// T can be any type that is (or extends) Number, so .doubleValue() is callable.
public class NumberBox<T extends Number> {
  private final T value;

  public NumberBox(T value) { this.value = value; }

  public double asDouble() { return value.doubleValue(); }
}

NumberBox<Integer> n = new NumberBox<>(42);   // fine — Integer is a Number
// NumberBox<String> bad = ...;               // ❌ String is not a Number

extends aqui significa "é um subtipo de," e funciona tanto para classes quanto para interfaces. Você pode até exigir vários limites ao mesmo tempo — <T extends Number & Comparable<T>> — com o limite de classe (se houver) listado primeiro. O limite também é o que torna o tipo utilizável: sem extends Number, value.doubleValue() não compilaria.

Construtores genéricos

O parâmetro de tipo é fixado pela instância, portanto, todos os construtores de uma classe genérica já têm acesso a T:

public class Pair<T> {
  private final T first;
  private final T second;

  public Pair(T first, T second) { this.first = first; this.second = second; }
  public Pair(T both)            { this(both, both); }
}

Os próprios construtores também podem ser genéricos sobre parâmetros de tipo adicionais que não estão na classe — mas isso é incomum o suficiente para pertencer ao próximo capítulo sobre métodos genéricos.

Classes genéricas podem herdar umas das outras

Uma subclasse pode herdar de uma classe genérica de três maneiras. Cada uma significa algo diferente:

// 1. Lock the parent's type parameter — concrete subclass for one element type.
public class StringList extends ArrayList<String> { ... }

// 2. Pass the type parameter through — the subclass is still generic.
public class MyList<E> extends ArrayList<E> { ... }

// 3. Add new type parameters of your own.
public class TaggedList<E, Tag> extends ArrayList<E> { ... }

A forma intermediária é a mais comum — você propaga o parâmetro do pai para os seus próprios chamadores. A primeira forma é o que você faz quando a subclasse é especializada: uma árvore de nós de strings apenas.

Campos e o parâmetro de tipo

Cada instância de Box<...> carrega seu próprio T. O bytecode não — em tempo de execução, a JVM vê apenas Box (isso é type erasure, abordado mais adiante nesta parte). A consequência é que o parâmetro de tipo pertence à instância, não ao objeto da classe:

Box<String>  a = new Box<>("hi");
Box<Integer> b = new Box<>(5);

a.getClass() == b.getClass();   // true — both are class Box

Esse é um fato útil para se ter em mente: Box<String> e Box<Integer> são tipos diferentes para o compilador, mas a mesma classe em tempo de execução. Voltaremos a isso em Java Type Erasure.

Membros estáticos não podem ver o parâmetro de tipo

Campos estáticos e métodos estáticos pertencem à classe, não a nenhuma instância específica — portanto, não podem ver o T da instância. Isso é inválido:

public class Box<T> {
  private static T defaultValue;        // ❌ won't compile — no T at the static level
  public  static T empty() { ... }      // ❌ same problem
}

Um método estático que precisa de um parâmetro de tipo deve declarar o seu próprio, independente do parâmetro da classe. Esse é o tema do próximo capítulo.

Projetando o seu próprio: uma pequena pilha tipada

Uma classe funcional completa para reunir tudo — uma Stack genérica com push, pop, peek e size. Ela é parametrizada sobre E (elemento), sustentada internamente por um Object[] (por causa das restrições de arrays genéricos), e o cast não verificado em pop é o tipo de solução bem contida que você verá em código real.

java— editable, runs on the server

As anotações @SuppressWarnings("unchecked") estão nas duas leituras que precisam fazer cast de Object de volta para E. Esses casts são seguros — push só armazena valores do tipo E — mas o compilador não consegue ver isso, porque a erasure removeu E do bytecode. Suprimir o aviso localmente, no menor escopo possível, é a decisão correta.

O que vem a seguir

Você viu o parâmetro no nível da classe. Às vezes, você precisa que um único método seja genérico, com seu próprio parâmetro de tipo independente da classe — útil para métodos utilitários, auxiliares estáticos e qualquer operação cuja relação de tipo vive apenas dentro daquele único método. Continue em Java Generic Methods.

Prática

Prática
Você escreve `public class Box<T> { private static T value; }`. O compilador rejeita. Por quê?
Você escreve `public class Box<T> { private static T value; }`. O compilador rejeita. Por quê?
Was this page helpful?