A Collection genérica: métodos que recebem Object

Os desenvolvedores costumam levar um susto com as assinaturas dos métodos das interfaces do framework das coleções pós-java5: muito mais difícil do que o monte de Es, supers, extends, ?s e &s são os métodos que ainda recebem Object como argumento.

Um exemplo é o método contains. Apesar dele estar na interface parametrizada Collection<E> ele recebe Object em vez de E! Parece um paradoxo: a vantagem do generics não seria exatamente ter uma tipagem mais forte nesses casos, em especial em classes containers? Algumas pessoas acham que é por compatibilidade binária, mas não é, porque a erasure de um método que recebe T é Object, e isso manteria a compatibilidade.

Para entender melhor a decisão da Sun em cada uma dessas assinaturas de métodos você sempre precisa levar em conta dois pontos: erasure e o uso do coringa. Antes vamos relembrar esses conceitos. Considere o código que imprime uma lista de objetos Comparable:

void imprime(List<Comparable> lista) {
    for (Comparable c : lista)
      System.out.println(c);
  }

Podemos passar para esse método uma List<Comparable>. E uma List<String>? Não podemos, porque uma List<String> não é uma List<Comparable>. Se isso fosse possível, poderíamos adicionar objetos Comparable que não apenas Strings dentro de uma List<String>, quebrando o contrato! Para resolver esse problema, mudamos a assinatura do nosso método para:

 void imprime(List<? extends Comparable> lista) {
    for (Comparable c : lista)
      System.out.println(c);
  }

Agora podemos passar uma List<String>, pois isso é uma List<? extends Comparable>, isto é, uma lista de algum tipo que é compatível com Comparable. Porque então não usamos sempre esse idiomismo? Repare então no seguinte método:

 public void adicionaString(List<Comparable> lista) {
    lista.add("caelum");    
  }

Aqui temos o mesmo problema. Não podemos receber como referência uma List<String>! Vamos tentar aplicar o mesmo procedimento:

 public void adicionaString(List<? extends Comparable> lista) {
    lista.add("caelum"); // não podemos invocar métodos que recebem o tipo
      // parametrizado (a menos que seja passado null)
  }

O código acima não compila! Isso ocorre porque List<? extends Comparable> pode receber uma lista de qualquer tipo que seja Comparable. Se isso fosse possível, alguém poderia passar como argumento uma List<Integer>, e no fim do método esta lista de inteiro estaria contendo uma String, quebrando o contrato novamente!

Como resolver esse problema? Não tem como! Se você recebe um objeto parametrizado pelo coringa ?, você nunca poderá invocar um método desse objeto, porque você não sabe qual o tipo que ele realmente aceita! A linguagem não permite isso porque não sabe exatamente o que você vai fazer com esse objeto. Em alguns casos, semanticamente, apesar de um método receber um tipo parametrizado, não tem problema ele receber um objeto que não esteja de acordo com seu tipo parametrizado. Esse é exatamente o caso do método contains: não haveria problema de passar um Integer para o contains de uma List<String>! Mas se contains recebesse E, o seguinte código não compilaria:

 public boolean contemCaelum(List<? extends Comparable> lista) {
    return lista.contains("caelum"); // ok, recebe Object!
  }

Essa situação é a mesma para outros métodos, como o Collection.remove(Object), e o Map.get(Object). Ambos os casos seriam catastróficos se recebessem o tipo parametrizado como argumento.

A API de coleções traz assinaturas muito mais estranhas. Eu demorei bastante até entender por completo a assinatura de alguns métodos da Collections, como por exemplo o Collections.min:

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

em vez de simplesmente uma ingênua assinatura como:

public static <T extends Comparable<T>> T min(Collection<T> coll);

Fica aí o desafio para você também queimar neurônios…

1 Comentário

  1. Eduardo Simioni 07/05/2007 at 12:16 #

    Paulo,

    Seu blog está excelente, leio sempre!

    Apenas uma pequena correção para que os programadores mais novos não se confundam.

    Collections.emptyList();

    retorna uma Lista vazia, porém é uma lista única, imutável. É mais útil quando se deseja que um método retorne uma lista vazia.
    O singletonList() realmente cria uma lista com um único elemento, porém a lista também é imutável!

    Infelizmente o Java, por algumas razões óbvias como a tipagem forte, e outras nem tanto, está bem longe do Smalltalk tem termos de praticidade ao trabalhar com coleções.

    Sinto falta de um Collections.onlyOne() List.first() List.last(), construtores com 1, 2 e 3 elmenentos. Seria pedir demais?!

Deixe uma resposta