Generics em Java: Tipos e Métodos Genéricos
Em Java, Generics (ou obrigatoriedade de tipos genéricos) permitem criar classes, interfaces e métodos que funcionam com diferentes tipos de dados, mantendo segurança de tipos e evitando conversões desnecessárias.
O grande objetivo é não amarrar o código a um tipo específico quando não sabemos de antemão qual será usado.
O problema sem generics
Vamos supor que temos um Account
que guarda o ID da conta e o saldo:
class Account {
private int id;
private int sum;
Account(int id, int sum) {
this.id = id;
this.sum = sum;
}
public int getId() { return id; }
public int getSum() { return sum; }
public void setSum(int sum) { this.sum = sum; }
}
Aqui, o id
foi definido como int
. Mas em alguns sistemas, IDs são strings (ex.: "A9837"
) ou até objetos mais complexos.
Se quisermos aceitar qualquer tipo, poderíamos mudar id
para Object
:
class Account {
private Object id;
private int sum;
Account(Object id, int sum) {
this.id = id;
this.sum = sum;
}
public Object getId() { return id; }
public int getSum() { return sum; }
public void setSum(int sum) { this.sum = sum; }
}
Isso “funciona”, mas perde a segurança de tipos. Veja:
Account acc1 = new Account("2345", 5000);
int id = (int) acc1.getId(); // Erro em tempo de execução (ClassCastException)
O compilador não nos avisa que getId()
não retorna um int
.
Esse é o tipo de problema que Generics resolvem.
Classe genérica
Com Generics, podemos dizer que Account
aceitará um tipo genérico T
para o ID:
class Account<T> {
private T id;
private int sum;
Account(T id, int sum) {
this.id = id;
this.sum = sum;
}
public T getId() { return id; }
public int getSum() { return sum; }
public void setSum(int sum) { this.sum = sum; }
}
Uso:
public class Program {
public static void main(String[] args) {
Account<String> acc1 = new Account<>("2345", 5000);
String id1 = acc1.getId(); // não precisa cast
System.out.println(id1);
Account<Integer> acc2 = new Account<>(2345, 5000);
Integer id2 = acc2.getId();
System.out.println(id2);
}
}
Vantagens:
- O compilador sabe qual tipo está sendo usado.
- Erros como
ClassCastException
são detectados em tempo de compilação. - O código fica mais flexível e reutilizável.
⚠️ Generics não funcionam com tipos primitivos (int
, double
etc.).
Se precisar, use classes wrapper (Integer
, Double
, etc.).
Generics em interfaces
Interfaces também podem ser genéricas:
interface Accountable<T> {
T getId();
int getSum();
void setSum(int sum);
}
Implementação com tipo fixo
Podemos fixar o tipo ao implementar:
class Account implements Accountable<String> {
private String id;
private int sum;
Account(String id, int sum) {
this.id = id;
this.sum = sum;
}
public String getId() { return id; }
public int getSum() { return sum; }
public void setSum(int sum) { this.sum = sum; }
}
Uso:
Accountable<String> acc = new Account("123", 5000);
System.out.println(acc.getId());
Implementação mantendo o tipo genérico
Outra abordagem é deixar a classe também genérica:
class Account<T> implements Accountable<T> {
private T id;
private int sum;
Account(T id, int sum) {
this.id = id;
this.sum = sum;
}
public T getId() { return id; }
public int getSum() { return sum; }
public void setSum(int sum) { this.sum = sum; }
}
Uso:
Account<String> acc1 = new Account<>("123", 5000);
Account<Integer> acc2 = new Account<>(456, 3000);
Métodos genéricos
Além de classes e interfaces, métodos também podem ser genéricos:
class Printer {
public <T> void print(T[] items) {
for (T item : items) {
System.out.println(item);
}
}
}
Uso:
Printer p = new Printer();
p.<String>print(new String[] {"Tom", "Alice"});
p.<Integer>print(new Integer[] {1, 2, 3});
O <T>
antes do tipo de retorno indica que o método é genérico.
Múltiplos parâmetros genéricos
Podemos usar mais de um tipo genérico:
class Account<T, S> {
private T id;
private S sum;
Account(T id, S sum) {
this.id = id;
this.sum = sum;
}
public T getId() { return id; }
public S getSum() { return sum; }
public void setSum(S sum) { this.sum = sum; }
}
Uso:
Account<String, Double> acc = new Account<>("123", 5000.75);
Construtores genéricos
Até construtores podem ter parâmetros genéricos próprios:
class Account {
private String id;
private int sum;
<T> Account(T id, int sum) {
this.id = id.toString();
this.sum = sum;
}
public String getId() { return id; }
}
Aqui, o construtor aceita qualquer tipo para id
e converte para String
.
Resumo
- Permitem criar classes, interfaces e métodos que funcionam com diferentes tipos, mantendo segurança de tipos.
- Evitam o uso de casting manual e tornam o código mais legível.
- São definidos por parâmetros de tipo como
<T>
, que podem ser usados em variáveis, parâmetros e retornos. - Mantêm a verificação de tipos em tempo de compilação, prevenindo erros de execução.
- Podem ser combinados com herança e interfaces para criar estruturas mais flexíveis.