1. Invariância de generics: List<Number> ≠ List<Integer>
Em Java, os genéricos são rígidos: eles são invariantes. Isso significa que, mesmo que um tipo seja subtipo de outro (por exemplo, Integer é subtipo de Number), as coleções com esses tipos não estão relacionadas.
Imagine: você tem uma caixa de maçãs (List<Integer>), e alguém diz que isso é uma “caixa de frutas” (List<Number>). Parece lógico, afinal maçãs são frutas. Mas então poderíamos colocar uma banana (Double) nessa caixa, e tudo quebraria — a caixa na verdade é “de maçãs”.
Exemplo de código:
List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // Erro de compilação!
O compilador é propositalmente rígido aqui: ele não permite transformar uma lista de “maçãs” em uma lista de “frutas”. Caso contrário, teríamos a possibilidade de adicionar qualquer coisa, e o programa quebraria em tempo de execução.
Portanto, List<Number> e List<Integer> são dois tipos completamente diferentes, apesar de Integer por si só ser um subtipo de Number.
Em contraste com arrays:
Integer[] intArr = new Integer[10];
Number[] numArr = intArr; // Permitido (arrays são covariantes)
numArr[0] = 3.14; // Erro em tempo de execução (ArrayStoreException)
Conclusão: os genéricos são invariantes para garantir a segurança de tipos em tempo de compilação.
2. Limites de parâmetros de tipo
Às vezes é preciso restringir com quais tipos uma classe ou método genérico pode trabalhar. Para isso, usamos limites (bounds).
Limite superior (extends)
class Stats<T extends Number> {
private T[] nums;
// ...
}
Agora, Stats pode trabalhar apenas com tipos que são subclasses de Number (Integer, Double, Float etc.). Tentar criar Stats<String> causará erro de compilação.
Restrição com interface
class Sorter<T extends Comparable<T>> {
void sort(List<T> list) { /* ... */ }
}
Agora, Sorter pode trabalhar apenas com tipos que implementam a interface Comparable.
Múltiplos limites
É possível declarar vários limites usando &:
class MyClass<T extends Number & Comparable<T>> { /* ... */ }
T deve ser uma subclasse de Number e implementar Comparable<T>.
A ordem importa: primeiro a classe, depois as interfaces.
3. Introdução ao curinga: ?
Um wildcard é um tipo “curinga” que diz: “Pode haver algum tipo aqui, mas não sei qual exatamente”.
Exemplos:
- List<?> — lista de qualquer coisa.
- List<? extends Number> — lista de qualquer tipo que seja subclasse de Number (por exemplo, Integer, Double).
- List<? super Integer> — lista de qualquer tipo que seja supertipo de Integer (por exemplo, Integer, Number, Object).
Para que servem os curingas?
Eles permitem escrever métodos que funcionam com coleções de tipos diferentes, porém relacionados.
Exemplo: somente leitura (producer)
void printNumbers(List<? extends Number> list) {
for (Number n : list) {
System.out.println(n);
}
// list.add(123); // Erro! Não é permitido adicionar elementos
}
- É possível ler os elementos como Number.
- Não é permitido adicionar elementos (exceto null).
Exemplo: somente escrita (consumer)
void addIntegers(List<? super Integer> list) {
list.add(42); // OK
// Integer x = list.get(0); // Erro! Não sabemos qual tipo será retornado
}
- É possível adicionar elementos do tipo Integer (ou seus subtipos).
- Não é seguro ler elementos como Integer (apenas como Object).
Regra PECS
PECS — “Produtor Extends, Consumidor Super”:
- Produtor — Extends: se a coleção apenas fornece dados (producer), use ? extends T.
- Consumidor — Super: se a coleção apenas recebe dados (consumer), use ? super T.
Fácil de lembrar: extends — para leitura (producer), super — para escrita (consumer).
Comparação: arrays são covariantes, generics são invariantes
- Arrays são covariantes: Integer[] pode ser atribuído a uma variável do tipo Number[].
- Generics são invariantes: List<Integer> não pode ser atribuído a uma variável do tipo List<Number>.
Curingas permitem “amenizar” parcialmente a invariância dos generics.
5. Métodos genéricos e inferência de tipos; limitações (type erasure)
Métodos genéricos
É possível declarar métodos genéricos que funcionam com quaisquer tipos:
public static <T> void printList(List<T> list) {
for (T elem : list) {
System.out.println(elem);
}
}
Inferência de tipo (type inference)
O Java pode “deduzir” o tipo do parâmetro:
List<String> strings = List.of("a", "b");
printList(strings); // T = String
Limitações dos generics: apagamento de tipos
- Em Java, os genéricos são implementados via apagamento de tipos: as informações dos parâmetros de tipo são removidas após a compilação.
- No bytecode, não há diferença entre List<String> e List<Integer>.
- Não é possível criar arrays de tipos genéricos: new List<String>[10] — erro.
- Não é possível usar instanceof com parâmetros de tipo: obj instanceof List<String> — erro.
6. Prática com coleções e a Stream API
Copiando elementos entre listas
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T item : src) {
dest.add(item);
}
}
- src — produtor (? extends T)
- dest — consumidor (? super T)
Uso:
List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new ArrayList<>();
copy(nums, ints); // OK: Number é supertipo de Integer
Stream API e curingas
List<Integer> ints = List.of(1, 2, 3);
List<? extends Number> numbers = ints;
numbers.stream()
.map(Number::doubleValue)
.forEach(System.out::println);
Filtragem com curinga
public static void printAll(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
7. Erros comuns
Erro nº 1: raw types (tipos brutos).
O uso de “tipos brutos” desativa a verificação de tipos e leva a erros em tempo de execução.
List list = new ArrayList(); // raw type — ruim!
list.add("texto");
list.add(123); // Dá para adicionar qualquer coisa
String s = (String) list.get(1); // ClassCastException!
Nunca use raw types. Sempre indique os parâmetros de tipo: List<String>, List<Integer>.
Erro nº 2: Conversões inseguras com extends.
Tentar adicionar elementos em uma coleção com ? extends ... resultará em erro de compilação.
List<? extends Number> nums = new ArrayList<Integer>();
nums.add(3.14); // Erro de compilação!
Erro nº 3: “apagamento” de tipo em sobrecarga.
Não é possível sobrecarregar métodos apenas pelos parâmetros genéricos — após o apagamento de tipos, as assinaturas coincidem.
public void process(List<String> list) { /* ... */ }
public void process(List<Integer> list) { /* ... */ } // Erro de compilação!
Erro nº 4: arrays de tipos genéricos.
Não é possível criar arrays de tipos parametrizados por causa do apagamento de tipos.
List<String>[] arr = new List<String>[10]; // Erro de compilação!
GO TO FULL VERSION