Generics em Java
Type Safety, Reutilização de Código e Eliminação de Casting
1. O que são Generics?
Generics permitem criar classes, interfaces e métodos que funcionam com diferentes tipos enquanto mantêm type safety (segurança de tipos).
Ao invés de trabalhar com Object, você especifica o tipo quando usa a classe.
Generics eliminam a necessidade de casting e permitem erros de tipo serem detectados em tempo de compilação.
2. O Problema sem Generics
Sem Generics, era necessário fazer casting manual e havia risco de erros em runtime.
❌ Sem Generics (Java antigo):
// Sem generics - trabalha com Object
ArrayList lista = new ArrayList();
lista.add("Java");
lista.add(123); // Diferente tipo!
// Precisa fazer casting
String s = (String) lista.get(0); // OK
String numero = (String) lista.get(1); // ClassCastException!
// Problema: erro só aparece em runtime
✅ Com Generics (Moderno):
// Com generics - type safety
ArrayList lista = new ArrayList<>();
lista.add("Java");
lista.add(123); // ERRO em COMPILE TIME!
// Sem casting
String s = lista.get(0); // Automático
// Problema prevenido em tempo de compilação
3. Sintaxe Básica de Generics
Generics usam type parameters entre <>.
// Genérico com um tipo
ArrayList nomes = new ArrayList<>();
ArrayList numeros = new ArrayList<>();
ArrayList pessoas = new ArrayList<>();
// Genérico com múltiplos tipos
Map notas = new HashMap<>();
// Iteração é type-safe
for (String nome : nomes) {
System.out.println(nome); // Já sabe que é String
}
// Sem casting necessário
Integer primeiro = numeros.get(0); // Automático
4. Criando Classes Genéricas
// Classe genérica simples
public class Caixa {
private T conteudo;
public void colocar(T item) {
this.conteudo = item;
}
public T remover() {
return conteudo;
}
public T obter() {
return conteudo;
}
}
// USO
Caixa caixaTexto = new Caixa<>();
caixaTexto.colocar("Java");
System.out.println(caixaTexto.obter()); // Java
Caixa caixaNumero = new Caixa<>();
caixaNumero.colocar(42);
System.out.println(caixaNumero.obter()); // 42
// Múltiplos type parameters
public class Par {
private K chave;
private V valor;
public Par(K chave, V valor) {
this.chave = chave;
this.valor = valor;
}
public K getChave() { return chave; }
public V getValor() { return valor; }
}
// USO
Par par = new Par<>("idade", 25);
System.out.println(par.getChave() + ": " + par.getValor());
5. Métodos Genéricos
// Método genérico
public class Utilitarios {
// Tipo T é local ao método
public static void imprimir(T elemento) {
System.out.println(elemento);
}
// Múltiplos tipos
public static void exibir(K chave, V valor) {
System.out.println(chave + " = " + valor);
}
// Com tipo de retorno
public static T primeiro(T[] array) {
return array.length > 0 ? array[0] : null;
}
// Encontrar máximo
public static > T maximo(T[] array) {
if (array.length == 0) return null;
T max = array[0];
for (T elemento : array) {
if (elemento.compareTo(max) > 0) {
max = elemento;
}
}
return max;
}
}
// USO
Utilitarios.imprimir("Java");
Utilitarios.exibir(1, "um");
Integer[] numeros = {1, 5, 3, 9, 2};
System.out.println(Utilitarios.primeiro(numeros)); // 1
System.out.println(Utilitarios.maximo(numeros)); // 9
6. Bounded Type Parameters
Você pode limitar qual tipo pode ser usado com um generic.
// Extends - limite superior (upper bounded)
public class NumericoGenerico {
private T valor;
public NumericoGenerico(T valor) {
this.valor = valor;
}
public double obterDouble() {
return valor.doubleValue(); // Number tem este método
}
}
// USO - funciona com tipos que estendem Number
new NumericoGenerico<>(42); // Integer extends Number - OK
new NumericoGenerico<>(3.14); // Double extends Number - OK
new NumericoGenerico<>("texto"); // ERRO - String não estende Number
// Interface como bound
public interface Comparavel {
int comparar(Object outro);
}
public class Ordenador {
public void ordenar(T[] array) {
// T tem método comparar()
}
}
// Múltiplos bounds
public > T processar(T valor) {
return valor;
}
7. Wildcards e ? extends
Wildcards (?) permitem trabalhar com tipos desconhecidos.
// Wildcard - qualquer tipo
public static void imprimirLista(List> lista) {
for (Object elemento : lista) {
System.out.println(elemento);
}
}
// ? extends - tipo ou subtipo (covariance)
public static double somarNumeros(List extends Number> lista) {
double soma = 0;
for (Number num : lista) {
soma += num.doubleValue();
}
return soma;
}
// USO
List inteiros = Arrays.asList(1, 2, 3);
List decimais = Arrays.asList(1.5, 2.5);
System.out.println(somarNumeros(inteiros)); // 6.0
System.out.println(somarNumeros(decimais)); // 4.0
// ? super - tipo ou supertipo (contravariance)
public static void preencherComNumeros(List super Number> lista) {
lista.add(42);
lista.add(3.14);
lista.add(new BigDecimal("99.99"));
}
List
8. Type Erasure
Type Erasure: Em runtime, informações de generics são apagadas. Generics existem apenas em compile-time.
// Compile time - tem tipo
ArrayList strings = new ArrayList<>();
ArrayList numeros = new ArrayList<>();
// Runtime - tipo é apagado
// Ambos são simplesmente ArrayList em runtime!
// Por isso ISSO não funciona:
// public void processar(ArrayList lista) { }
// public void processar(ArrayList lista) { }
// ERRO - ambos viram ArrayList em runtime
// Nem isso:
// ArrayList lista = new ArrayList(); // ERRO compile
// Mas em runtime seria apenas ArrayList
// Implicações
public boolean ehInstancia(Object obj, Class tipo) {
return tipo.isInstance(obj); // Precisa de Class para runtime
}
// USO
ehInstancia("texto", String.class); // true
ehInstancia(42, Integer.class); // true
9. Exemplo Prático: Repositório Genérico
// Classe base
public abstract class Entidade {
protected int id;
public int getId() { return id; }
}
// Repositório genérico
public class Repositorio {
private List dados = new ArrayList<>();
private int proximoId = 1;
public void salvar(T entidade) {
entidade.id = proximoId++;
dados.add(entidade);
System.out.println("Salvo: " + entidade);
}
public T buscarPorId(int id) {
return dados.stream()
.filter(e -> e.getId() == id)
.findFirst()
.orElse(null);
}
public List buscarTodos() {
return new ArrayList<>(dados);
}
public void deletar(int id) {
dados.removeIf(e -> e.getId() == id);
}
}
// Entidades
class Usuario extends Entidade {
private String nome;
public Usuario(String nome) { this.nome = nome; }
public String toString() { return "Usuario(" + nome + ")"; }
}
class Produto extends Entidade {
private String descricao;
public Produto(String descricao) { this.descricao = descricao; }
public String toString() { return "Produto(" + descricao + ")"; }
}
// USO
Repositorio repUsuario = new Repositorio<>();
repUsuario.salvar(new Usuario("João"));
repUsuario.salvar(new Usuario("Maria"));
Repositorio repProduto = new Repositorio<>();
repProduto.salvar(new Produto("Notebook"));
repProduto.salvar(new Produto("Mouse"));
System.out.println(repUsuario.buscarPorId(1)); // Usuario(João)
System.out.println(repProduto.buscarTodos()); // [Produto(Notebook), Produto(Mouse)]
10. Boas Práticas com Generics
✓ FAÇA:
- Use generics para type safety
- Use bounded types quando necessário
- Prefira List> a List
- Use wildcards apropriadamente
- Entenda type erasure
✗ EVITE:
- Não misture tipos raw e generics
- Não use Object desnecessariamente
- Não assuma que generics existem em runtime
- Não ignore warnings do compilador
- Não use generics para primitivos (use Integer, não int)
11. Exercícios Práticos
Exercício 1: Classe Stack Genérica
Implemente uma classe Pilha<T> genérica com push(), pop(), peek().
Exercício 2: Método de Busca
Crie um método genérico que busque um elemento em um array.
Exercício 3: Bounded Types
Crie classe que funcione apenas com tipos Comparable.
Exercício 4: Repositório
Implemente repositório genérico com salvar, buscar, deletar.
Exercício 5: Cache Genérico
Crie sistema de cache que funcione com qualquer tipo.
Conclusão
Generics são essenciais para código Java seguro, reutilizável e profissional.
Use type parameters, bounded types e wildcards para criar APIs flexíveis e type-safe.
Domine generics e seu código será muito mais robusto! 🔒✨