Rodrigo Kumpera Weblog

Meus achados sobre tecnologia

A parte bizarra de Generics

August 8th, 2006 · 6 Comments

Eu já assisti uma dúzia de palestras e apresentações sobre como generics é implementado em Java, mas até hoje ainda estou para ver quem falasse como o mecanismo de wildcards funciona. Cansei de esperar e resolvi investigar esse assunto e relato meus achados a seguir.

Para entender como wildcards funcionam vamos implementar um container que funciona feito um array. Bom, um array suporta basicamente duas operações, ler e atribuir, então a interface pode ser escrita assim:

public interface IArray < T > {

void set(int i, T val);
T get(int i);
int length();

}

Legal, até agora tudo bem, mas eu também quero ter operações de cópia entre instancias de IArray, uma versão mais OO de System.arrayCopy. Antes de definirmos estas operações, vamos primeiro entender melhor como devem se comportar, quero dizer, qual a exata semântica de ler e copiar valores entre arrays.

Quando lemos valores de um array, digamos java.lang.Number[], o tipo do resultado pode ser o mesmo, ou de uma classe pai, java.lang.Object ou java.lang.Number. Então o contrato de leitura de um array é que somente tipos >= podem ser usados (por >= entenda a mesma, ou uma classe pai, pode ser usada). Esse contrato é conhecido por upper-bound type.

Para escrita, o comportamento é o contrário, quando atribuimos um valor para uma posição de um array ele tem que ser do mesmo tipo ou de uma classe filha. Para um array de java.lang.Number, podemos atribuir java.lang.Integer, por exemplo. O contrato de escrita é que somente tipos <= podem ser usados (por <= entenda a mesma, ou uma classe filha, pode ser usada). Esse contrato é conhecido por lower-bound type.

Agora como isso se relaciona aos wildcards de generics? Basta pensar como implementar um método copyFrom() para IArray. Esse método copia os valores do array informado. Podemos definir esse método da seguinte forma: “void copyFrom(IArray< T > from)“, mas se testarmos o seguinte trecho de código, vamos descobrir que não funciona:

IArray< Number > arrayA = new ..;
IArray< Integer > arrayB = new …;
arrayA.copyFrom(arrayB);

Deveria funcionar, não? Afinal, Integer é uma classe filha de Number. Apesar de ser, o contrato de copyFrom aceita apenas instancias de IArray com o mesmo tipo. O mecanismo de generics não tem como saber qual contrato deve usar entre os seus tipos de parâmetro (T em IArray é um parâmetro do tipo). Uma implementação de copyFrom vai atribuir os valores de entrada ao array que mantém, isso quer dizer que estes valores precisam obedecer o contrato de escrita, ou seja tem que ser um lower-bound type. Em generics dizemos isso da seguinte forma: “void copyFrom(IArray< ? extends T > from)“.

Agora que vimos isso, o método copyTo fica simples, sua assinatura é “void copyTo(IArray < ? super T > from)“, agora usamos o wildcard de upper-bound, pois temos que atribuir valores a arrays de forma a respeitar o contrato de leitura. Nossa interface para um array fica da forma mostrada abaixo, implementar ela é trivial.

public interface IArray < T > {

void set(int i, T val);
T get(int i);
int length();
void copyFrom(IArray< ? extends T > from);
void copyTo(IArray< ? super T > from);

}

O mecanismo de wildcards que generics possui nos permite definir qual o contrato que existe entre instâncias diferentes de um tipo genérico. A maioria das explicações que eu vi até hoje não eram claras o suficiente para me permitir entender o conceito, e espero que esse artigo tenha ajudado a tornar o assunto menos obscuro. Para concluir, se compararmos IArray com os arrays do Java, vamos notar que a hierarquia de classes não é correta, pois quebra o contrato de escrita, que se manifesta via ArrayStoreException.

Tags: Programming · java

6 responses so far ↓

  • 1 Michael Nascimento Santos // Aug 8, 2006 at 10:49 am

    Louds, vc precisa fazer escaping dos caracteres < e >. Simplesmente nao faz sentido o post pra quem estah lendo :-)

  • 2 kumpera // Aug 8, 2006 at 12:10 pm

    Valeu Michael, corrigido :)

  • 3 Bruno // Aug 24, 2006 at 9:10 pm

    Muito bom, parabéns pela explicação.

    Mas ainda não entendo porque o compilador não aceita:
    List l1 = new ArrayList();
    List l2 = new ArrayList();
    l1 = l2;

    O compilador não deveria saber que na atribuição o ‘contrato’ é de lower-bound type? E que é possível converter um List em um List?

  • 4 Bruno // Aug 24, 2006 at 9:10 pm

    Muito bom, parabéns pela explicação.

    Mas ainda não entendo porque o compilador não aceita:
    List l1 = new ArrayList();
    List l2 = new ArrayList();
    l1 = l2;

    O compilador não deveria saber que na atribuição o ‘contrato’ é de lower-bound type? E que é possível converter um List em um List?

  • 5 Bruno // Aug 24, 2006 at 9:12 pm

    Droga, não sei como escapar o html… mas deve dar pra imaginar que l1 é uma lista de Object e l2 é uma lista de String.

  • 6 kumpera // Aug 25, 2006 at 10:42 am

    Tudo bem Bruno.

    Os parâmetros de um tipo genérico não implicam em uma hierarquia, ou seja, List<String> não extende List<Object>. Logo não dá para fazer esse cast de forma válida.

    Fora isso, não dá para o compilador adivinhar se em um dado trecho de código, o correto são tipos lower-bound ou upper-bound. Tanto que precisamos informar isso explicitamente.

    O compilador não tem como adivinhar, pois o contrato correto depende de como usarmos esses objetos. No teu exemplo, é feito um cast de List<Object> para List<String>, isso não dá para o compilador adivinhar sozinho, já que são tipos distintos, um não herda do outro.

    No final das contas, wildcards são usadas principalmente para definir os parâmetros ou retorno de um método, e um método de uma classe genérica.

    Existem algorítmos que permitiriam ao compilador adivinhar qual o wildcard correto, a linguagem ML faz isso ao máximo, só que isso acaba por confundir demais os desenvolvedores, que em alguns momentos ficam simplesmente perdidos tentando entender que maluquice o compilador assumiu.

Leave a Comment