Java Generics
Em todas as aulas de coleções você escreveu coisas como List<String> e Map<String, Integer>. O que vai entre < > é um tipo passado como parâmetro, e o recurso que permite isso se chama generics (tipos genéricos). Nesta aula você vai entender por que ele existe, como criar as suas próprias classes e métodos genéricos e como ler as mensagens de erro que envolvem <T>.
- O problema que os generics resolvem (o
ClassCastExceptiondas listas "sem tipo"). - Criar uma classe genérica:
Caixa<T>, e com dois tipos:Par<A, B>. - Criar métodos genéricos:
static <T> T primeiro(List<T> lista). - Limitar o tipo:
<T extends Comparable<T>>. - O básico dos coringas:
? extendse? super. - Os erros
incompatible typeseunexpected type(por que não existeList<int>).
A ideia: uma caixa com etiqueta
Imagine caixas de mudança. Uma caixa sem etiqueta aceita qualquer coisa, mas quando você a abre, precisa adivinhar o que tem dentro. Uma caixa com a etiqueta "LIVROS" só recebe livros, e quem abre já sabe o que vai encontrar.
Generics são essas etiquetas. List<String> é "uma lista com etiqueta String": o compilador recusa qualquer coisa que não seja texto e garante que o que sai é texto. O erro, que antes aparecia no meio da execução, passa a aparecer na hora de compilar, antes de o programa rodar.
Por que generics: a vida sem eles
Os generics chegaram no Java 5 (2004). Antes disso, as listas guardavam Object (qualquer objeto). Esse código antigo ainda compila hoje, e mostra o problema. A Ana guarda nomes numa lista "sem etiqueta" e, por engano, coloca um número:
Este código tem um erro de propósito.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List nomes = new ArrayList();
nomes.add("Ana");
nomes.add("Bruno");
nomes.add(42);
for (int i = 0; i < nomes.size(); i++) {
String nome = (String) nomes.get(i);
System.out.println(nome.toUpperCase());
}
}
}
$ javac Main.java Note: Main.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details. $ java Main ANA BRUNO Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap') at Main.main(Main.java:12)
Repare na sequência:
- O
javaccompilou, só com uma nota (não é erro): "Main.java usa operações não verificadas ou inseguras". - O
add(42)foi aceito sem reclamação: a lista aceita qualquer coisa. - Para usar cada elemento como texto, foi preciso um cast:
(String) nomes.get(i), "confie em mim, isto é uma String". - No terceiro elemento, a "confiança" falhou:
ClassCastException, "a classe Integer não pode ser convertida para String". O programa já tinha rodado pela metade.
Agora a mesma ideia com a etiqueta <Integer> numa lista de notas:
Este código tem um erro de propósito.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> notas = new ArrayList<>();
notas.add(10);
notas.add("dez");
}
}
$ javac Main.java
Main.java:8: error: incompatible types: String cannot be converted to Integer
notas.add("dez");
^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error
O erro agora é de compilação: "tipos incompatíveis: String não pode ser convertida para Integer". O programa nem chega a rodar, e o ^ aponta exatamente o culpado. A última nota só avisa que o javac resumiu a mensagem; ela não muda nada.
O mesmo vale na saída: o que sai de uma List<String> é String, sem cast, e o compilador não deixa guardar em outro tipo:
List<String> nomes = new ArrayList<>();
nomes.add("Ana");
Integer x = nomes.get(0);
String s = nomes.get(0);
A linha do Integer x está errada (o que sai é String); a do String s está certa, e sem cast:
$ javac Main.java
Main.java:8: error: incompatible types: String cannot be converted to Integer
Integer x = nomes.get(0);
^
1 error
Sem generics (List) | Com generics (List<String>) | |
|---|---|---|
| O que entra | Qualquer coisa | Só String |
| O que sai | Object: precisa de cast | String, direto |
| Erro de tipo aparece | Ao rodar (ClassCastException) | Ao compilar (incompatible types) |
List sem < > se chama raw type ("tipo cru"). Ele só existe para o código anterior a 2004 continuar compilando. Se você vir a nota "uses unchecked or unsafe operations", procure um < > faltando.
Sua primeira classe genérica: Caixa<T>
A ArrayList não foi escrita uma vez para String, outra para Integer… Ela foi escrita uma vez só, com um "tipo a definir". Você pode fazer o mesmo. Esta é uma caixa que guarda uma coisa de um tipo qualquer, decidido por quem usa:
class Caixa<T> {
private T conteudo;
void guardar(T coisa) {
this.conteudo = coisa;
}
T abrir() {
return conteudo;
}
boolean vazia() {
return conteudo == null;
}
}
public class Main {
public static void main(String[] args) {
Caixa<String> bilhete = new Caixa<>();
System.out.println("vazia? " + bilhete.vazia());
bilhete.guardar("Parabéns, Ana!");
String texto = bilhete.abrir();
System.out.println(texto + " (" + texto.length() + " letras)");
Caixa<Integer> cofre = new Caixa<>();
cofre.guardar(150);
int moedas = cofre.abrir();
System.out.println("moedas + 50 = " + (moedas + 50));
}
}
- class Caixa<T>
Té uma variável de tipo: um nome provisório para "o tipo que alguém vai escolher". Ele vale dentro de toda a classe.- private T conteudo;
- Um atributo do tipo
T. NumaCaixa<String>, é como se estivesse escritoprivate String conteudo;. - void guardar(T coisa)
- Só aceita coisas do tipo
T. - T abrir()
- Devolve um
T: quem abre umaCaixa<String>recebeString, sem cast. - Caixa<String> bilhete = new Caixa<>();
- Aqui o
Té escolhido:String. O<>vazio donew(o "diamante", Java 7+) repete o tipo da esquerda.
$ javac Main.java $ java Main vazia? true Parabéns, Ana! (14 letras) « abrir() devolveu String: deu para usar length() moedas + 50 = 200 « abrir() devolveu Integer: virou int sozinho
A mesma classe serviu para textos e números, e em cada uso o compilador conferiu os tipos. Tentar cofre.guardar("cem") daria incompatible types: String cannot be converted to Integer.
Nomes das variáveis de tipo
Por convenção, variáveis de tipo são uma letra maiúscula, para não serem confundidas com nomes de classes:
| Letra | Costuma significar | Exemplo da biblioteca |
|---|---|---|
T | Type, um tipo qualquer | Optional<T> |
E | Element, elemento de coleção | List<E>, Set<E> |
K, V | Key e Value, chave e valor | Map<K, V> |
R | Result, resultado | Function<T, R> |
A, B, U… | Segundo, terceiro tipo | BiFunction<T, U, R> |
Mais de um tipo: Par<A, B>
Uma classe pode ter várias variáveis de tipo, separadas por vírgula, como Map<K, V>:
class Par<A, B> {
private final A primeiro;
private final B segundo;
Par(A primeiro, B segundo) {
this.primeiro = primeiro;
this.segundo = segundo;
}
A primeiro() { return primeiro; }
B segundo() { return segundo; }
public String toString() {
return "(" + primeiro + ", " + segundo + ")";
}
}
public class Main {
public static void main(String[] args) {
Par<String, Double> nota = new Par<>("Ana", 9.5);
Par<String, Integer> idade = new Par<>("Bruno", 17);
System.out.println(nota + " " + idade);
double n = nota.segundo();
System.out.println(nota.primeiro() + " precisa de " + (10 - n) + " para o 10");
}
}
$ javac Main.java $ java Main (Ana, 9.5) (Bruno, 17) Ana precisa de 0.5 para o 10
Records também podem ser genéricos: record Par<A, B>(A primeiro, B segundo) { } faz o mesmo em uma linha (veja Records).
Missão: rode e veja a saída estranha 9.50.5: a caixa guarda um texto, e + com texto concatena. Mude a caixa para Caixa<Double> e guarde o número 9.5 (sem aspas), para a saída ser 10.0.
class Caixa<T> {
private T conteudo;
void guardar(T coisa) {
this.conteudo = coisa;
}
T abrir() {
return conteudo;
}
}
public class Main {
public static void main(String[] args) {
Caixa<String> nota = new Caixa<>();
nota.guardar("9.5");
System.out.println(nota.abrir() + 0.5);
}
}
class Caixa<T> {
private T conteudo;
void guardar(T coisa) {
this.conteudo = coisa;
}
T abrir() {
return conteudo;
}
}
public class Main {
public static void main(String[] args) {
Caixa<Double> nota = new Caixa<>();
nota.guardar(9.5);
System.out.println(nota.abrir() + 0.5);
}
}
Métodos genéricos
Às vezes só um método precisa ser genérico, não a classe inteira. Aí a variável de tipo é declarada antes do tipo de retorno:
import java.util.List;
public class Main {
static <T> T primeiro(List<T> lista) {
return lista.get(0);
}
static <T> void mostrarTodos(T[] itens) {
for (T item : itens) {
System.out.print("[" + item + "] ");
}
System.out.println();
}
public static void main(String[] args) {
String nome = primeiro(List.of("Ana", "Bruno"));
Integer nota = primeiro(List.of(10, 7));
System.out.println(nome + " " + nota);
mostrarTodos(new String[] {"uva", "kiwi"});
mostrarTodos(new Integer[] {1, 2, 3});
System.out.println(Main.<Double>primeiro(List.of(9.5, 7.0)));
}
}
- static <T> T primeiro(List<T> lista)
- Lê-se: "para qualquer tipo
T, este método recebe uma lista deTe devolve umT". O primeiro<T>declara a variável; oTseguinte é o tipo de retorno. - primeiro(List.of("Ana", "Bruno"))
- Você não diz qual é o
T: o compilador deduz (inferência de tipo) pelo argumento. Aqui,T = String. - Main.<Double>primeiro(…)
- A forma explícita, dizendo o tipo. Quase nunca é necessária.
$ javac Main.java $ java Main Ana 10 [uva] [kiwi] [1] [2] [3] 9.5
É assim que muitos métodos da biblioteca são declarados. Collections.max, List.of e Collections.emptyList são métodos genéricos: por isso List.of("a") devolve uma List<String> e List.of(1), uma List<Integer>.
Limitar o tipo: <T extends Comparable<T>>
Com um T "qualquer", você só pode usar o que todo objeto tem (toString, equals…). Para achar o maior de uma lista, é preciso comparar, e nem todo tipo sabe se comparar. A solução é exigir: "T tem que ser um tipo Comparable":
import java.util.List;
public class Main {
static <T extends Comparable<T>> T maior(List<T> lista) {
T m = lista.get(0);
for (T item : lista) {
if (item.compareTo(m) > 0) {
m = item;
}
}
return m;
}
public static void main(String[] args) {
System.out.println(maior(List.of(7, 10, 3)));
System.out.println(maior(List.of("Bruno", "Ana", "Carla")));
System.out.println(maior(List.of(9.5, 8.25)));
}
}
- <T extends Comparable<T>>
- "Qualquer tipo
T, desde que seja comparável com outroT." Isso se chama limite (bound). Aqui,extendsvale tanto para classes quanto para interfaces. - item.compareTo(m)
- Só compila por causa do limite: o compilador sabe que todo
TtemcompareTo(veja Comparable e Comparator).
$ javac Main.java $ java Main 10 Carla 9.5
Funciona com Integer, String e Double, que são Comparable. E com uma classe que não é?
Este código tem um erro de propósito.
class Aluno {
String nome;
Aluno(String nome) { this.nome = nome; }
}
…
Aluno a = maior(List.of(new Aluno("Ana"), new Aluno("Bruno")));
$ javac Main.java
Main.java:20: error: method maior in class Main cannot be applied to given types;
Aluno a = maior(List.of(new Aluno("Ana"), new Aluno("Bruno")));
^
required: List<T>
found: List<Aluno>
reason: inference variable E has incompatible bounds
upper bounds: Comparable<T>,Object
lower bounds: Aluno
where T,E are type-variables:
T extends Comparable<T> declared in method <T>maior(List<T>)
E extends Object declared in method <E>of(E,E)
1 error
"O método maior não pode ser aplicado a estes tipos". A linha que explica é upper bounds: Comparable<T>: o tipo precisava ser Comparable, e Aluno não é. O erro apareceu na compilação, e não como ClassCastException ao rodar (compare com o TreeSet da aula TreeMap e TreeSet).
Coringas: ? extends e ? super (o básico)
Uma surpresa dos generics: Integer é um Number, mas List<Integer> não é uma List<Number>. Veja:
Este código tem um erro de propósito.
import java.util.List;
public class Main {
static double somar(List<Number> numeros) {
double soma = 0;
for (Number n : numeros) {
soma += n.doubleValue();
}
return soma;
}
public static void main(String[] args) {
List<Integer> faltas = List.of(2, 0, 5);
System.out.println(somar(faltas));
}
}
$ javac Main.java
Main.java:14: error: incompatible types: List<Integer> cannot be converted to List<Number>
System.out.println(somar(faltas));
^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error
Por que o Java proíbe? Porque numa List<Number> é permitido fazer add(9.5) (um Double também é Number). Se o método pudesse receber a lista de Integer, ele poderia enfiar um Double numa lista que só devia ter inteiros. Pense numa caixa "FRUTAS" e numa caixa "MAÇÃS": quem recebe uma caixa de frutas pode colocar uma banana nela, então não dá para entregar a caixa de maçãs no lugar.
Os coringas (wildcards, o ?) resolvem isso dizendo o que o método pretende fazer com a lista:
import java.util.ArrayList;
import java.util.List;
public class Main {
static double somar(List<? extends Number> numeros) {
double soma = 0;
for (Number n : numeros) {
soma += n.doubleValue();
}
return soma;
}
static void encher(List<? super Integer> destino) {
for (int i = 1; i <= 3; i++) {
destino.add(i);
}
}
public static void main(String[] args) {
List<Integer> faltas = List.of(2, 0, 5);
List<Double> notas = List.of(9.5, 7.0);
System.out.println(somar(faltas));
System.out.println(somar(notas));
List<Number> numeros = new ArrayList<>();
List<Object> coisas = new ArrayList<>();
coisas.add("oi");
encher(numeros);
encher(coisas);
System.out.println(numeros + " " + coisas);
}
}
$ javac Main.java $ java Main 7.0 16.5 [1, 2, 3] [oi, 1, 2, 3]
| Escrita | Lê-se | Aceita | O método pode… |
|---|---|---|---|
List<Number> | "lista de Number" | só List<Number> | ler e escrever |
List<? extends Number> | "lista de algum tipo que é Number" | List<Integer>, List<Double>, List<Number>… | ler (cada elemento é um Number), mas não acrescentar |
List<? super Integer> | "lista de algum tipo acima de Integer" | List<Integer>, List<Number>, List<Object> | acrescentar Integers, mas o que sai é só Object |
List<?> | "lista de qualquer coisa" | qualquer lista | ler como Object, size, isEmpty |
Uma regra para lembrar: quem só lê, extends; quem só escreve, super (em inglês, PECS: Producer Extends, Consumer Super). Você vai encontrar isso na documentação da biblioteca, como em Collections.max(Collection<? extends T>) e Comparator<? super T>. Para escrever o seu código no dia a dia, o básico desta seção é suficiente.
Generics não aceitam tipos primitivos
A variável de tipo só pode ser uma classe (um tipo de referência). int, double, char, boolean… não são classes:
Este código tem um erro de propósito.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<int> notas = new ArrayList<>();
}
}
$ javac Main.java
Main.java:6: error: unexpected type
List<int> notas = new ArrayList<>();
^
required: reference
found: int
1 error
"Tipo inesperado: era necessário um tipo de referência (uma classe), foi encontrado int". Use a classe "embrulho" (wrapper) correspondente: List<Integer>, List<Double>, List<Character>, List<Boolean>. O Java converte entre int e Integer sozinho (autoboxing), por isso notas.add(10) e int n = notas.get(0) funcionam.
Os generics são uma conferência feita pelo compilador. Depois de conferir, o javac apaga os < > do código gerado (isso se chama type erasure, "apagamento de tipo"): para a JVM, uma ArrayList<String> e uma ArrayList<Integer> são a mesma classe. Por isso não dá para fazer new T() nem perguntar lista instanceof List<String>.
Quando dá errado
O que aconteceu: você tentou colocar um texto numa coleção (ou variável) de Integer, ou guardar o que sai de uma List<String> num Integer. O ^ aponta o valor errado.
Como consertar: confira a etiqueta. Se o texto é um número digitado ("10"), converta: Integer.parseInt("10").
O que aconteceu: um tipo primitivo entre < >.
Como consertar: int → Integer, double → Double, char → Character, boolean → Boolean, long → Long.
O que aconteceu: não é erro: o programa compilou. Mas em algum lugar há um tipo cru (List, new ArrayList() sem <>). Para ver onde: javac -Xlint:unchecked Main.java.
Como consertar: acrescente o tipo: List<String> e new ArrayList<>().
O que aconteceu: um método pede List<Number> e recebeu List<Integer>.
Como consertar: se o método só lê a lista, declare o parâmetro como List<? extends Number>.
Referência
| Escrita | Significado |
|---|---|
class Caixa<T> { … } | Classe genérica com a variável de tipo T |
class Par<A, B> | Duas variáveis de tipo |
Caixa<String> c = new Caixa<>(); | Usar, escolhendo T = String (diamante no new) |
static <T> T metodo(List<T> l) | Método genérico |
<T extends Comparable<T>> | T limitado a tipos comparáveis |
<T extends Number> | T limitado a Number e subclasses |
List<? extends Number> | Lista de algum subtipo de Number (para ler) |
List<? super Integer> | Lista de algum supertipo de Integer (para escrever) |
List<?> | Lista de tipo desconhecido |
List (sem <>) | Tipo cru: não use |
Exercícios
1. A etiqueta certa
Complete para declarar uma lista de números decimais:
2. Declarar uma classe genérica
Complete a declaração de uma classe Pacote com uma variável de tipo T:
3. Só leitura de números
Complete o parâmetro para aceitar List<Integer> e List<Double>:
4. Num programa sem generics (List cru), quando aparece o erro de colocar um número numa lista de nomes?
5. Por que List<double> não compila?
6. Em static <T> T primeiro(List<T> l), o que é o primeiro <T>?
Missão final: escreva o método genérico static <T> T ultimo(List<T> lista), que devolve o último elemento de uma lista de qualquer tipo. O programa deve mostrar Carla e 3.
import java.util.List;
public class Main {
// escreva o método ultimo aqui
public static void main(String[] args) {
String nome = ultimo(List.of("Ana", "Bruno", "Carla"));
Integer nota = ultimo(List.of(7, 10, 3));
System.out.println(nome);
System.out.println(nota);
}
}
import java.util.List;
public class Main {
static <T> T ultimo(List<T> lista) {
return lista.get(lista.size() - 1);
}
public static void main(String[] args) {
String nome = ultimo(List.of("Ana", "Bruno", "Carla"));
Integer nota = ultimo(List.of(7, 10, 3));
System.out.println(nome);
System.out.println(nota);
}
}
Resumo
- Generics são "etiquetas" de tipo:
List<String>só aceita e só devolveString. - Sem eles (tipo cru), erros de tipo viram
ClassCastExceptionao rodar; com eles, viramincompatible typesao compilar. - Classe genérica:
class Caixa<T>; oTé escolhido por quem usa:new Caixa<String>(). - Método genérico:
static <T> T primeiro(List<T> l); o tipo é deduzido pelos argumentos. <T extends Comparable<T>>limitaTa tipos que sabem se comparar.List<Integer>não éList<Number>; para ler, use? extends; para escrever,? super.- Primitivos não entram:
unexpected type; useInteger,Double…
Fim do grupo Coleções. Próxima aula: Lambdas, o começo da programação funcional em Java.