Blog

Generics em Java: type erasure, limites e wildcards

Generics deixam o compilador verificar o que entra numa lista, numa caixa ou num método, e depois apagam essa informação antes de o programa rodar. Aprenda limites, wildcards, PECS e como ler uma assinatura do JDK rodando programas pequenos.

Generics deixam você escrever List<String> em vez de uma lista de qualquer coisa. Assim o compilador verifica o que entra, e você nunca faz cast no que sai. Eles também explicam por que um código perfeitamente razoável não compila: você não pode escrever new T(), e uma List<Integer> não é uma List<Number>. As duas regras fazem sentido quando você vê o que o compilador faz com o tipo depois de verificá-lo.

Este post cobre o bug que os generics corrigiram, classes, records e métodos genéricos, tipos com limite, type erasure, wildcards e a regra PECS, interfaces genéricas, tipos primitivos e como ler uma assinatura como a de Collections.max. Todo programa abaixo rodou no Java 25, e a saída foi colada da execução. Para rodar um você mesmo, salve como Main.java e rode java Main.java.

Por que generics: uma lista de Object falha tarde

Antes do Java 5, uma List guardava Object. Você podia colocar qualquer coisa dentro e precisava fazer cast em tudo que saía. Aqui está esse estilo, que ainda é válido hoje:

@SuppressWarnings({"rawtypes", "unchecked"})
void main() {
    List names = new ArrayList();
    names.add("Ada");
    names.add(42);
    for (Object o : names) {
        String name = (String) o;
        IO.println(name.toUpperCase());
    }
}

Ele imprime e para:

ADA
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')

O bug é names.add(42), mas o programa quebra duas linhas depois, no cast. Num código real, o add e o cast podem estar em arquivos diferentes, escritos por pessoas diferentes, com meses de distância. A exceção diz onde o valor errado foi encontrado, não onde ele foi colocado.

Uma List sem argumento de tipo se chama raw type. Todo programa desta série é compilado com javac -Xlint:all -Werror, e é por isso que a linha @SuppressWarnings está ali. Tire essa linha e o build para:

void main() {
    List names = new ArrayList();
    names.add("Ada");
    names.add(42);
    IO.println(names.size());
}

O build falha com:

Main.java:2: warning: [rawtypes] found raw type: List
Main.java:2: warning: [rawtypes] found raw type: ArrayList
Main.java:3: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
Main.java:4: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
error: warnings found and -Werror specified

Sem -Werror, isso são só warnings, e java Main.java roda o código mesmo assim. Agora dê um argumento de tipo à lista:

void main() {
    List<String> names = new ArrayList<>();
    names.add("Ada");
    names.add(42);
    for (String name : names) {
        IO.println(name.toUpperCase());
    }
}

O build falha com:

Main.java:4: error: incompatible types: int cannot be converted to String
    names.add(42);
              ^

Agora o erro aponta para a linha que está de fato errada, e nada chega a rodar. O loop não precisa de cast, porque o compilador já sabe que todo elemento é uma String. O <> vazio em new ArrayList<>() é o diamond: o compilador copia o argumento de tipo do lado esquerdo.

Classes e records genéricos

Uma classe genérica declara um parâmetro de tipo entre sinais de menor e maior, e cada uso da classe o preenche. T é um marcador para um tipo, do mesmo jeito que um parâmetro de método é um marcador para um valor:

class Box<T> {
    private T value;

    Box(T value) {
        this.value = value;
    }

    T get() {
        return value;
    }

    void set(T value) {
        this.value = value;
    }
}

record Pair<A, B>(A first, B second) {}

void main() {
    Box<String> label = new Box<>("fragile");
    label.set("this side up");
    IO.println(label.get().length());

    var entry = new Pair<>("Ada", 36);
    String name = entry.first();
    int age = entry.second();
    IO.println(name + " is " + age);
    IO.println(entry);
}

Ele imprime:

12
Ada is 36
Pair[first=Ada, second=36]

label.get() devolve uma String, então .length() funciona sem cast. Pair<A, B> tem dois parâmetros, e o record preenche os dois a partir dos argumentos do construtor: Pair<String, Integer>. Por convenção, parâmetros de tipo são uma única letra maiúscula: T para tipo, E para elemento, K e V para chave e valor.

Métodos genéricos e inferência de tipo

Um método pode ter seus próprios parâmetros de tipo, declarados logo antes do tipo de retorno. O compilador descobre quais são a partir dos argumentos de cada chamada. Isso se chama inferência de tipo:

static <T> T firstOrDefault(List<T> items, T fallback) {
    return items.isEmpty() ? fallback : items.getFirst();
}

void main() {
    String city = firstOrDefault(List.of("Lisbon", "Pune"), "nowhere");
    Integer score = firstOrDefault(List.of(), 0);
    IO.println(city);
    IO.println(score);

    var mixed = List.<Number>of(1, 2.5);
    mixed.forEach(n -> IO.println(n.doubleValue()));
}

Ele imprime:

Lisbon
0
1.0
2.5

Na primeira chamada, T é String. Na segunda, List.of() está vazia, então o compilador tira T do valor padrão 0 e faz dele Integer.

List.<Number>of(1, 2.5) é um type witness explícito: você mesmo diz quem é T, em vez de deixar o compilador inferir. Raramente você precisa de um. Tentamos a mesma coisa no nosso próprio método, Main.<String>firstOrDefault(...), e o javac respondeu cannot find symbol para Main. A classe de um arquivo-fonte compacto não tem um nome que você possa escrever no código, então não dá para colocar um witness nos métodos estáticos dela.

Tipos com limite: pedindo mais de T

Um T simples pode ser qualquer tipo, então os únicos métodos que você pode chamar nele são os de Object. Para chamar compareTo, você precisa dizer ao compilador que T é algo comparável. Veja o que acontece se você não disser:

static <T> T max(List<T> items) {
    T best = items.getFirst();
    for (T item : items) {
        if (item.compareTo(best) > 0) {
            best = item;
        }
    }
    return best;
}

void main() {
    IO.println(max(List.of(3, 9, 4)));
}

O build falha com:

Main.java:4: error: cannot find symbol
        if (item.compareTo(best) > 0) {
                ^

Um limite (bound) resolve. <T extends Comparable<T>> significa “qualquer T, desde que um T possa ser comparado com outro T“. Aqui extends também vale para interfaces:

static <T extends Comparable<T>> T max(List<T> items) {
    T best = items.getFirst();
    for (T item : items) {
        if (item.compareTo(best) > 0) {
            best = item;
        }
    }
    return best;
}

void main() {
    IO.println(max(List.of(3, 9, 4)));
    IO.println(max(List.of("pear", "apple", "plum")));
}

Ele imprime:

9
plum

Um parâmetro de tipo pode ter vários limites, unidos com &. A classe, se houver uma, vem primeiro. <T extends Number & Comparable<T>> deixa um mesmo método usar doubleValue() de Number e compareTo de Comparable:

static <T extends Number & Comparable<T>> String summary(List<T> items) {
    T best = items.getFirst();
    double total = 0;
    for (T item : items) {
        total += item.doubleValue();
        if (item.compareTo(best) > 0) {
            best = item;
        }
    }
    return "max " + best + ", total " + total;
}

void main() {
    IO.println(summary(List.of(3, 9, 4)));
    IO.println(summary(List.of(1.5, 0.25)));
}

Ele imprime:

max 9, total 16.0
max 1.5, total 1.75

Guarde este na cabeça. Nosso max tem uma falha que vamos encontrar no fim do post, e Collections.max tem uma assinatura mais estranha justamente porque não tem essa falha.

Type erasure: o tipo é verificado e depois removido

O Java verifica os tipos genéricos na compilação e depois os remove. Em tempo de execução, uma List<String> e uma List<Integer> são as duas apenas ArrayList:

record Box<T>(T value) {}

record Ranked<T extends Comparable<T>>(T value) {}

void main() {
    List<String> words = new ArrayList<>();
    List<Integer> numbers = new ArrayList<>();
    IO.println(words.getClass() == numbers.getClass());
    IO.println(words.getClass().getName());

    IO.println(Box.class.getRecordComponents()[0].getType());
    IO.println(Ranked.class.getRecordComponents()[0].getType());
}

Ele imprime:

true
java.util.ArrayList
class java.lang.Object
interface java.lang.Comparable

Existe um único objeto de classe para todas as listas. Nada em tempo de execução registra se uma lista era para strings ou para números. As duas últimas linhas mostram o que sobra de T no record compilado: um T simples vira Object, e um T com limite vira o seu primeiro limite, Comparable. Essa substituição se chama type erasure.

Explicado como se você tivesse dez anos

Uma caixa genérica é uma lancheira com uma etiqueta na tampa: “só sanduíches”. Quando você arruma a lancheira, um adulto confere a etiqueta. Tente colocar uma meia e ele te para na hora.

Depois que a lancheira está pronta, a etiqueta é arrancada antes de ela ir para a mochila. Na hora do lanche, ninguém confere a etiqueta, porque ela não existe mais. E ninguém precisa conferir. A verificação já aconteceu, então o que está dentro é sanduíche.

É por isso que todas as lancheiras na mochila parecem iguais. Você não pode perguntar a uma lancheira na mochila “você era uma caixa de sanduíche?”, porque a resposta foi para o lixo junto com a etiqueta.

A versão precisa

O javac verifica cada uso de um tipo genérico contra os seus argumentos de tipo. Depois compila o código com cada parâmetro de tipo trocado pela sua erasure, que é o seu primeiro limite, ou Object se ele não tiver nenhum. Onde o seu código lê um T e o usa como String, o javac insere um cast escondido. O bytecode de ArrayList é uma classe só, compartilhada por toda ArrayList<Whatever>.

O Java escolheu a erasure no Java 5 para que código genérico e código antigo, não genérico, pudessem rodar juntos usando os mesmos arquivos de classe. O custo é que o argumento de tipo não existe em tempo de execução, então nenhuma operação que precise dele em tempo de execução pode ser escrita.

Onde a analogia falha: a etiqueta nem sempre some. Os tipos declarados de campos, parâmetros de método e superclasses mantêm as suas assinaturas genéricas no arquivo de classe, e a reflection consegue lê-las. O que se perde é o argumento de tipo de cada objeto: uma ArrayList específica não sabe que foi criada como ArrayList<String>. E dá para enganar o adulto: um raw type ou um cast unchecked pula a verificação, e aí uma meia pode mesmo acabar na lancheira.

O que a erasure proíbe

Você não pode criar um T, porque em tempo de execução não existe um T cujo construtor chamar:

class Factory<T> {
    T make() {
        return new T();
    }
}

void main() {
    IO.println(new Factory<String>());
}

O build falha com:

Main.java:3: error: unexpected type
        return new T();
                   ^

Também não pode criar um T[]. Um array guarda o tipo dos seus elementos em tempo de execução, e a erasure faz com que não haja nenhum tipo para guardar:

<T> T[] makeArray(int size) {
    return new T[size];
}

void main() {
    IO.println(makeArray(3).length);
}

O build falha com:

Main.java:2: error: generic array creation
    return new T[size];
           ^

O jeito comum de contornar os dois casos é passar algo que saiba fazer a criação, como um Class<T> ou um Supplier<T>.

Você também não pode perguntar a um objeto se ele é uma List<String>, porque o objeto não sabe:

void main() {
    Object thing = new ArrayList<String>();
    IO.println(thing instanceof List<String>);
}

O build falha com:

Main.java:3: error: Object cannot be safely cast to List<String>
    IO.println(thing instanceof List<String>);
               ^

Este nos surpreendeu. O erro não diz que generics são proibidos em instanceof. Ele fala de segurança. Se a variável for uma Collection<String>, então thing instanceof List<String> compila e roda, porque tudo que passa na verificação é mesmo uma lista de strings. Isso entrou no Java 16: javac --release 15 rejeita. A partir de Object, pergunte instanceof List<?>.

Por fim, duas sobrecargas que só diferem nos argumentos de tipo não podem ficar na mesma classe:

void print(List<String> items) {
    IO.println("strings");
}

void print(List<Integer> items) {
    IO.println("integers");
}

void main() {
    print(List.of("a"));
}

O build falha com:

Main.java:5: error: name clash: print(List<Integer>) and print(List<String>) have the same erasure
void print(List<Integer> items) {
     ^

Depois da erasure, os dois métodos são print(List), e um arquivo de classe não pode ter dois métodos com o mesmo nome e os mesmos parâmetros. Dê nomes diferentes a eles.

Quando um raw type ou um cast unchecked deixa uma variável List<String> apontar para uma lista que guarda um Integer, isso se chama heap pollution (poluição do heap). O ClassCastException aparece depois, longe da linha que causou o problema, como no primeiro programa.

Wildcards: List<Integer> não é uma List<Number>

Um Integer é um Number, então parece que uma List<Integer> deveria ser uma List<Number>. O compilador diz que não:

void main() {
    List<Integer> ints = new ArrayList<>(List.of(1, 2));
    List<Number> nums = ints;
    nums.add(2.5);
    IO.println(ints);
}

O build falha com:

Main.java:3: error: incompatible types: List<Integer> cannot be converted to List<Number>
    List<Number> nums = ints;
                        ^

A linha 4 é o motivo. Se a linha 3 fosse permitida, nums.add(2.5) colocaria um Double numa lista que ints ainda promete ter só inteiros. Tipos genéricos são invariantes: List<A> e List<B> não têm relação nenhuma, a menos que A e B sejam o mesmo tipo.

Os arrays fizeram a escolha oposta, e pagam por ela em tempo de execução:

void main() {
    Integer[] ints = {1, 2};
    Number[] nums = ints;
    IO.println(nums[0]);
    nums[1] = 2.5;
}

Ele imprime e para:

1
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Double

Arrays são covariantes: um Integer[] é um Number[], então a atribuição compila. A mesma escrita errada falha quando o programa roda. Isso só funciona porque um array lembra o tipo dos seus elementos, que é exatamente o que generics apagados não conseguem fazer. Por isso os generics pegam o erro na compilação.

? extends T: uma lista da qual você lê

A invariância tornaria inútil um método simples. Um sum(List<Number>) não poderia receber uma List<Integer>. Um wildcard resolve isso. List<? extends Number> significa “uma lista de algum tipo que é Number ou um subtipo dele”:

double sum(List<? extends Number> items) {
    double total = 0;
    for (Number n : items) {
        total += n.doubleValue();
    }
    return total;
}

void main() {
    IO.println(sum(List.of(1, 2, 3)));
    IO.println(sum(List.of(1.5, 2.5)));
}

Ele imprime:

6.0
4.0

Ler é seguro. Seja o que for que a lista guarde de verdade, todo elemento é algum tipo de Number. Escrever não é:

void main() {
    List<Integer> ints = new ArrayList<>();
    List<? extends Number> nums = ints;
    nums.add(2.5);
}

O build falha com:

Main.java:4: error: incompatible types: double cannot be converted to CAP#1
    nums.add(2.5);
             ^

CAP#1 é o nome que o javac dá para “o tipo desconhecido por trás deste ?“. Pode ser Integer, pode ser Double, e o compilador não consegue provar que 2.5 cabe, então recusa. Você não pode adicionar nada além de null a uma List<? extends Number>.

? super T: uma lista na qual você escreve

List<? super Integer> significa “uma lista de Integer ou de um dos seus supertipos”. Pode ser uma List<Integer>, uma List<Number> ou uma List<Object>. Todas elas conseguem guardar um Integer, então adicionar um é seguro. Ler só te dá Object, porque você não sabe qual dessas listas tem na mão.

Os dois wildcards se encontram num método de cópia, que lê de uma lista e escreve em outra:

static <T> void copy(List<? super T> dst, List<? extends T> src) {
    for (T item : src) {
        dst.add(item);
    }
}

void main() {
    List<Integer> ints = List.of(1, 2);
    List<Double> doubles = List.of(2.5);
    List<Number> numbers = new ArrayList<>();
    List<Object> anything = new ArrayList<>(List.of("start"));

    copy(numbers, ints);
    copy(numbers, doubles);
    copy(anything, ints);
    IO.println(numbers);
    IO.println(anything);
}

Ele imprime:

[1, 2, 2.5]
[start, 1, 2]

Um único método copia inteiros para uma List<Number> e para uma List<Object>, e doubles para a mesma List<Number>. Com copy(List<T> dst, List<T> src), só a primeira chamada compilaria, e só se T fosse Integer para as duas listas, o que numbers não é. O próprio Collections.copy do JDK tem essa assinatura.

A regra prática é PECS: producer extends, consumer super. Se um parâmetro produz valores para o seu método ler, use extends. Se ele consome valores que o seu método escreve, use super. Se faz as duas coisas, use um T simples.

producer extends: valor sai, você lê src: List<? extends T> for (T item : src) copy(dst, src) consumer super: valor entra, você grava copy(dst, src) dst.add(item) dst: List<? super T> adicionar a src: não compila. ler de dst: só vem Object.

A lista de origem produz valores T, então o seu parâmetro usa extends e o método só lê dela. A lista de destino consome valores T, então o seu parâmetro usa super e o método só escreve nela.

? sozinho: qualquer lista

Um wildcard sem limite, List<?>, significa “uma lista de algum tipo que não me interessa”. Use quando o método só precisa de coisas que funcionam para qualquer lista, como size() ou imprimir:

String describe(List<?> items) {
    return items.size() + " items, first is " + items.getFirst();
}

void main() {
    IO.println(describe(List.of("a", "b")));
    IO.println(describe(List.of(3.5, 7.0, 1.0)));
}

Ele imprime:

2 items, first is a
3 items, first is 3.5

List<?> não é a mesma coisa que o raw List. O raw type desliga a verificação, então você pode adicionar qualquer coisa. List<?> mantém a verificação ligada, então você não pode adicionar nada além de null, e o javac não dá warning.

Interfaces genéricas: Comparable<T> e um repositório

Interfaces também recebem parâmetros de tipo, e a mais usada do JDK é Comparable<T>. Implementar Comparable<Version> quer dizer que um Version sabe se comparar com outro Version, e aí o nosso max com limite o aceita:

record Version(int major, int minor) implements Comparable<Version> {
    @Override
    public int compareTo(Version other) {
        if (major != other.major) {
            return Integer.compare(major, other.major);
        }
        return Integer.compare(minor, other.minor);
    }
}

static <T extends Comparable<T>> T max(List<T> items) {
    T best = items.getFirst();
    for (T item : items) {
        if (item.compareTo(best) > 0) {
            best = item;
        }
    }
    return best;
}

void main() {
    var versions = List.of(new Version(21, 0), new Version(25, 1), new Version(25, 0));
    IO.println(max(versions));
}

Ele imprime:

Version[major=25, minor=1]

compareTo recebe um Version, não um Object, então não há cast nem instanceof dentro dele.

As suas próprias interfaces podem ser genéricas do mesmo jeito. Uma comum em código de negócio é o repositório, que guarda e encontra entidades por ID. Repository<T, ID> funciona para qualquer tipo de entidade e qualquer tipo de ID:

interface Repository<T, ID> {
    void save(ID id, T item);

    Optional<T> findById(ID id);

    List<T> findAll();
}

record User(String name) {}

class InMemoryRepository<T, ID> implements Repository<T, ID> {
    private final Map<ID, T> items = new LinkedHashMap<>();

    @Override
    public void save(ID id, T item) {
        items.put(id, item);
    }

    @Override
    public Optional<T> findById(ID id) {
        return Optional.ofNullable(items.get(id));
    }

    @Override
    public List<T> findAll() {
        return List.copyOf(items.values());
    }
}

void main() {
    Repository<User, Integer> users = new InMemoryRepository<>();
    users.save(1, new User("Ada"));
    users.save(2, new User("Linus"));
    IO.println(users.findById(2));
    IO.println(users.findById(7));
    IO.println(users.findAll());
}

Ele imprime:

Optional[User[name=Linus]]
Optional.empty
[User[name=Ada], User[name=Linus]]

users.save("one", ...) não compilaria, porque ID é Integer neste repositório. Frameworks como o Spring Data constroem os seus repositórios sobre a mesma ideia. A parte sobre Optional explica o que findById devolve.

Tipos primitivos e generics: nada de List<int>

Um argumento de tipo precisa ser um tipo de referência, então tipos primitivos como int não são permitidos:

void main() {
    List<int> scores = new ArrayList<>();
    IO.println(scores);
}

O build falha com:

Main.java:2: error: unexpected type
    List<int> scores = new ArrayList<>();
         ^

O motivo é a erasure. Um parâmetro de tipo vira Object ou um limite, e um int não é um objeto. Você escreve List<Integer>, e o Java faz boxing de cada int para Integer na entrada e unboxing na saída. É prático, e tem uma armadilha que pega todo mundo uma vez:

void main() {
    List<Integer> scores = new ArrayList<>(List.of(1, 2, 3));
    scores.remove(1);
    IO.println(scores);
    scores.remove(Integer.valueOf(1));
    IO.println(scores);
}

Ele imprime:

[1, 3]
[3]

List tem dois métodos remove, remove(int index) e remove(Object o). scores.remove(1) escolhe o de int, porque ele não precisa de boxing, então remove o elemento no índice 1, que é 2. Se você queria o valor 1, passe um Integer, como faz a segunda chamada.

O boxing também custa memória: uma List<Integer> guarda uma referência para um objeto separado para cada número. O Project Valhalla está trabalhando em value classes que diminuiriam essa diferença, mas elas não fazem parte do Java 25.

Lendo uma assinatura genérica do JDK

As assinaturas genéricas do JDK parecem densas, mas cada pedaço responde a uma pergunta, e você já viu todos os pedaços. Aqui está a declaração de Collections.max:

public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)

Leia um pedaço de cada vez:

  1. <T ...>: max é um método genérico com um parâmetro de tipo, T.
  2. T extends ... Comparable<? super T>: um T precisa ser comparável com T ou com algum supertipo de T. É PECS de novo. A comparação consome um T, então é super.
  3. Collection<? extends T> coll: o argumento produz valores T, então é extends. Qualquer coleção cujos elementos sejam T ou um subtipo funciona, não só uma List.
  4. T antes de max: o método devolve um T.
  5. Object &: o primeiro limite decide a erasure. Com ele, o método apagado devolve Object, exatamente como max fazia antes do Java 5, então código antigo já compilado que o chamava continua linkando.

O passo 2 é a falha do nosso próprio max. LocalDate não implementa Comparable<LocalDate>. Ele implementa Comparable<ChronoLocalDate>, uma interface acima dele. O nosso <T extends Comparable<T>> não consegue aceitar isso:

static <T extends Comparable<T>> T max(List<T> items) {
    T best = items.getFirst();
    for (T item : items) {
        if (item.compareTo(best) > 0) {
            best = item;
        }
    }
    return best;
}

void main() {
    var dates = List.of(LocalDate.of(2026, 3, 1), LocalDate.of(2026, 9, 14));
    IO.println(max(dates));
}

O build falha com:

Main.java:13: error: method max in class Main cannot be applied to given types;
  reason: inference variable T has incompatible equality constraints ChronoLocalDate,LocalDate

Collections.max aceita a mesma lista, e a reflection confirma o passo 5:

void main() throws NoSuchMethodException {
    var dates = List.of(LocalDate.of(2026, 3, 1), LocalDate.of(2026, 9, 14));
    IO.println(Collections.max(dates));

    var erased = Collections.class.getMethod("max", Collection.class);
    IO.println(erased.getReturnType());
}

Ele imprime:

2026-09-14
class java.lang.Object

Comparable<? super T> deixa T ser LocalDate enquanto a comparação é definida em ChronoLocalDate. Se você escrever um método de biblioteca que compara coisas, copie esse formato. getMethod("max", Collection.class) encontra o método pelo tipo de parâmetro apagado, porque esse é o único tipo que sobra em tempo de execução.

O que lembrar

  • Generics levam os erros de tipo do tempo de execução para o tempo de compilação. Uma List raw traz de volta o ClassCastException tardio, e -Xlint:all avisa sobre isso.
  • Parâmetros de tipo vão depois do nome de uma classe (Box<T>) ou antes do tipo de retorno de um método (<T> T first(...)). Normalmente o compilador os infere.
  • Um limite, <T extends Comparable<T>>, deixa você chamar os métodos do limite num T. Vários limites se unem com &.
  • Argumentos de tipo são apagados depois da verificação. É por isso que new T(), new T[], instanceof List<String> a partir de Object e sobrecargas que só diferem nos argumentos de tipo não compilam.
  • List<Integer> não é uma List<Number>. Arrays são covariantes e, em vez disso, falham em tempo de execução com ArrayStoreException.
  • PECS: use ? extends T para um parâmetro do qual você lê, ? super T para um no qual você escreve, e ? quando o tipo não importa.
  • Argumentos de tipo precisam ser tipos de referência, então int passa por boxing para Integer. Cuidado com remove(int) versus remove(Object).

O compilador verifica o argumento de tipo e depois o joga fora, então toda regra de generics é sobre o que ele consegue provar antes disso.

Quanto este post te ajudou?

Clique em um coração para avaliar!

Média das avaliações 0 / 5. Total de votos: 0

Nenhum voto até agora. Seja o primeiro a avaliar este post.