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>.

Você vai aprender
  • O problema que os generics resolvem (o ClassCastException das 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: ? extends e ? super.
  • Os erros incompatible types e unexpected type (por que não existe List<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:

  1. O javac compilou, só com uma nota (não é erro): "Main.java usa operações não verificadas ou inseguras".
  2. O add(42) foi aceito sem reclamação: a lista aceita qualquer coisa.
  3. Para usar cada elemento como texto, foi preciso um cast: (String) nomes.get(i), "confie em mim, isto é uma String".
  4. 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 entraQualquer coisaSó String
O que saiObject: precisa de castString, direto
Erro de tipo apareceAo rodar (ClassCastException)Ao compilar (incompatible types)
Nunca use o "tipo cru"

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. Numa Caixa<String>, é como se estivesse escrito private String conteudo;.
void guardar(T coisa)
Só aceita coisas do tipo T.
T abrir()
Devolve um T: quem abre uma Caixa<String> recebe String, sem cast.
Caixa<String> bilhete = new Caixa<>();
Aqui o T é escolhido: String. O <> vazio do new (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:

LetraCostuma significarExemplo da biblioteca
TType, um tipo qualquerOptional<T>
EElement, elemento de coleçãoList<E>, Set<E>
K, VKey e Value, chave e valorMap<K, V>
RResult, resultadoFunction<T, R>
A, B, U…Segundo, terceiro tipoBiFunction<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 de T e devolve um T". O primeiro <T> declara a variável; o T seguinte é 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 outro T." Isso se chama limite (bound). Aqui, extends vale tanto para classes quanto para interfaces.
item.compareTo(m)
Só compila por causa do limite: o compilador sabe que todo T tem compareTo (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]
EscritaLê-seAceitaO 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 listaler 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.

Curiosidade: os tipos somem depois de compilar

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

"incompatible types: String cannot be converted to Integer"

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").

"unexpected type … required: reference … found: int"

O que aconteceu: um tipo primitivo entre < >.

Como consertar: int → Integer, double → Double, char → Character, boolean → Boolean, long → Long.

"Note: Main.java uses unchecked or unsafe operations."

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<>().

"incompatible types: List<Integer> cannot be converted to List<Number>"

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

EscritaSignificado
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:

List<> precos = new ArrayList<>();

2. Declarar uma classe genérica

Complete a declaração de uma classe Pacote com uma variável de tipo T:

class { private T item; }

3. Só leitura de números

Complete o parâmetro para aceitar List<Integer> e List<Double>:

static double media(List<> nums)

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ó devolve String.
  • Sem eles (tipo cru), erros de tipo viram ClassCastException ao rodar; com eles, viram incompatible types ao compilar.
  • Classe genérica: class Caixa<T>; o T é 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>> limita T a tipos que sabem se comparar.
  • List<Integer> não é List<Number>; para ler, use ? extends; para escrever, ? super.
  • Primitivos não entram: unexpected type; use Integer, Double…

Fim do grupo Coleções. Próxima aula: Lambdas, o começo da programação funcional em Java.