VapVup👤
← Voltar
🔷

Generics em Java

Generics foram introduzidos no Java 5 para trazer type safety em tempo de compilação. Por trás da elegância do <T>, existe um mecanismo complexo de Type Erasure que todo desenvolvedor sênior precisa entender — especialmente nas limitações que ele impõe.

📦Analogia

Generics: O Molde Industrial

Imagine um molde industrial que produz caixas. O mesmo molde pode criar uma caixa para parafusos, uma para pregos, ou uma para porcas — mas uma vez escolhido o conteúdo, a caixa só aceita aquele tipo. Você não mistura parafusos com pregos. Generics funcionam assim: Box<T> é o molde, e quando você cria Box<String>, o compilador garante que só Strings entram. Antes do Java 5, era tudo 'caixa genérica' (Object) e você descobria que colocou um prego na caixa de parafusos só quando tentava usar.

💡Conceito

Generic Classes e Type Parameters

Uma classe genérica declara type parameters entre < >: class Box<T> { T value; }. Convenções: T = Type, E = Element, K = Key, V = Value, R = Return, N = Number. Múltiplos parameters são separados por vírgula: class Pair<K, V>. O type parameter pode ser usado como tipo de campos, parâmetros, retornos e variáveis locais dentro da classe.

public class Box<T> {
    private T content;

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

// Uso: o compilador garante type safety
Box<String> box = new Box<>();
box.set("Hello");    // OK
// box.set(42);       // ERRO de compilação!
String s = box.get(); // Sem cast!
💡Conceito

Generic Methods e Bounded Type Parameters

Métodos genéricos declaram seus próprios type parameters ANTES do retorno: <T> T método(). São independentes dos type parameters da classe — um método estático pode ser genérico mesmo que a classe não seja. Bounded type parameters restringem o tipo: <T extends Comparable<T>> garante que T tenha compareTo(). Para múltiplos bounds, use '&': <T extends Serializable & Comparable<T>>. Atenção: sempre use 'extends', nunca 'implements', mesmo para interfaces.

// Generic method — <T> declarado antes do retorno
public static <T> T getFirst(List<T> list) {
    return list.isEmpty() ? null : list.get(0);
}

// Bounded — T deve implementar Comparable
public static <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}

// Múltiplos bounds — classe primeiro, depois interfaces
public static <T extends Number & Comparable<T>> T findMin(List<T> list) {
    return list.stream().min(Comparator.naturalOrder()).orElse(null);
}
🅿️Analogia

Invariância: A Vaga de Estacionamento

Um estacionamento com vagas para 'Veículos' não aceita automaticamente um caminhão de 'Carros'. Mesmo que Carro extends Veículo, uma List<Carro> NÃO é subtipo de List<Veículo>. Por quê? Se fosse, você poderia pegar a referência List<Veículo>, adicionar uma Moto, e corromper a lista que deveria ter só Carros. Generics são INVARIANTES por segurança — diferente de arrays, que são covariant (Carro[] é subtipo de Veículo[]) e pagam o preço com ArrayStoreException em runtime.

💡Conceito

Wildcards: ?, extends e super

Wildcards resolvem a inflexibilidade da invariância. Existem três tipos: (1) Unbounded: List<?> — aceita qualquer List, mas só permite leitura como Object. (2) Upper bounded: List<? extends Number> — aceita List<Integer>, List<Double>, etc. Permite leitura como Number, mas não escrita (exceto null). (3) Lower bounded: List<? super Integer> — aceita List<Integer>, List<Number>, List<Object>. Permite escrita de Integer, mas leitura só como Object.

// Unbounded — quando o tipo não importa
public static int size(List<?> list) {
    return list.size(); // Não precisa saber o tipo
}

// Upper bounded — lê como Number (Producer)
public static double sum(List<? extends Number> list) {
    double total = 0;
    for (Number n : list) total += n.doubleValue();
    // list.add(1);  // ERRO! Não pode adicionar
    return total;
}

// Lower bounded — escreve Integer (Consumer)
public static void addIntegers(List<? super Integer> list) {
    list.add(1);     // OK! Integer é aceito
    list.add(2);
    // Integer i = list.get(0); // ERRO! Só pode ler como Object
}
🍽️Analogia

PECS: O Buffet e a Lixeira

Pense em PECS com duas metáforas: o BUFFET (Producer Extends) e a LIXEIRA (Consumer Super). No buffet, você PEGA comida — não importa se é comida italiana ou japonesa, desde que seja comida (extends Comida). Você lê/produz valores. Na lixeira, você COLOCA lixo — não importa o tipo exato do container, desde que aceite seu lixo (super MeuLixo). Você escreve/consome valores. Se você só pega → extends. Se você só coloca → super. Se precisa dos dois → sem wildcard.

🔧Exemplo

PECS na Prática: Collections.copy()

O exemplo canônico de PECS é Collections.copy(). O destino é Consumer (recebe/consome elementos) → super. A origem é Producer (fornece/produz elementos) → extends. Essa assinatura permite copiar de List<Integer> para List<Number>, algo impossível sem wildcards.

// Assinatura real na JDK:
public static <T> void copy(
    List<? super T> dest,     // Consumer → super
    List<? extends T> src     // Producer → extends
) { ... }

// Uso:
List<Integer> integers = List.of(1, 2, 3);
List<Number> numbers = new ArrayList<>(Arrays.asList(0, 0, 0));
Collections.copy(numbers, integers);
// numbers agora contém [1, 2, 3]

// Sem PECS, isso NÃO funcionaria!
// copy(List<Number>, List<Integer>) ← Integer ≠ Number
🏗️Analogia

Type Erasure: A Planta Baixa Descartada

Type Erasure é como construir um prédio com uma planta baixa detalhada e depois queimar a planta. Durante a construção (compilação), o engenheiro (compilador) segue a planta rigorosamente — verifica se cada parede está no lugar certo. Mas depois de construído (bytecode), a planta não existe mais. Se alguém perguntar 'esse prédio foi projetado para escritórios ou apartamentos?', ninguém sabe — a planta foi queimada. É por isso que em runtime, List<String> e List<Integer> são o mesmo prédio: List.

💡Conceito

Type Erasure: Como Funciona

O compilador Java remove TODAS as informações genéricas após validar type safety. As regras são: (1) Type parameters unbounded (T) viram Object. (2) Type parameters bounded (T extends Number) viram o primeiro bound (Number). (3) Casts são inseridos automaticamente onde necessário. (4) Bridge methods são gerados para manter o polimorfismo. Resultado: no bytecode, Box<String> e Box<Integer> são a mesma classe Box com campos do tipo Object.

// O que você escreve:
public class Box<T> {
    private T value;
    public T get() { return value; }
    public void set(T value) { this.value = value; }
}

// O que o compilador gera (após erasure):
public class Box {
    private Object value;
    public Object get() { return value; }
    public void set(Object value) { this.value = value; }
}

// No ponto de uso:
Box<String> box = new Box<>();
box.set("Hello");
String s = box.get();  // Compilador insere: (String) box.get()

// Bridge method gerado em subclasses:
// class StringBox extends Box<String> { void set(String v) {...} }
// Compilador gera: void set(Object v) { set((String) v); } ← bridge
💡Conceito

Limitações, Raw Types e Heap Pollution

Type Erasure causa limitações importantes: (1) new T() — impossível, JVM não sabe o tipo (workaround: passar Class<T>). (2) instanceof T — impossível, tipo não existe em runtime (use instanceof com raw type). (3) new T[] — impossível, arrays são reified (workaround: Array.newInstance()). (4) Campos estáticos com T — type parameter é por instância, não por classe. Raw types (List em vez de List<String>) existem para compatibilidade com Java < 5, mas bypassam type safety e podem causar Heap Pollution: quando uma variável genérica aponta para um objeto de tipo incompatível. Tipos reifiable (int, String, List<?>) mantêm tipo em runtime. Tipos non-reifiable (List<String>, T) perdem tipo após erasure.

// ❌ Impossível — Type Erasure impede
public <T> T create() {
    return new T();           // ERRO: cannot instantiate T
}

// ✅ Workaround com Class<T> (type token)
public <T> T create(Class<T> clazz) throws Exception {
    return clazz.getDeclaredConstructor().newInstance();
}

// ❌ Heap Pollution com raw types
List<String> strings = new ArrayList<>();
List rawList = strings;       // Warning: raw type
rawList.add(42);              // Sem erro! Type safety bypassada
String s = strings.get(0);    // ClassCastException! 💥

// ❌ Arrays de tipos parametrizados
List<String>[] array = new List<String>[10]; // ERRO!
// Se fosse permitido:
// Object[] objArr = array;
// objArr[0] = List.of(42);  // ArrayStoreException não detectada!