Wednesday, September 20, 2006

Arrays, genéricos e o maldito compilador (e uma errata)

Há um tempinho eu postei aqui uma dica que deveria facilitar a construção de listas e maps em Java. A idéia básica era escrever uma classe utilitária com métodos estáticos usando varargs e genéricos que depois seriam importados (via static imports) nas classes clientes. Algo assim:

public class CollectionUtils {
public static List listWith(T... elements) {
return Arrays.asList(elements);
}
...
}

Isso até que funciona direto, mas não posso dizer o mesmo sobre a implementação do mapWith. Este método retorna um java.util.Map criado a partir de uma série de pares chave-valor. Cada par é representado por um objeto da classe Map.Entry<K,V> criado usando outro método estático. Assim um uso do mapWih poderia ser:

Map<NumeroNatural, String> numeros = mapWith(pair(ZERO, "0"), pair(ONE, "1"));

Para programar o mapWith eu usei o mesmo truque do listWith, receber os argumentos (os pares) via varargs:

public static <K, V> Map<K, V> mapWith(Map.Entry<K, V>... entries) {
HashMap<K, V> map = new HashMap<K, V>();
for (Map.Entry<K, V> eachEntry : entries)
map.put(eachEntry.getKey(), eachEntry.getValue());
return map;
}

Essa definição de método compila normalmente, mas todas as chamadas à ele receberão um warning do compilador avisando que um array genérico está sendo criado. "Huh? Mas eu não estou criando porcaria de array nenhum!", foi o que eu respondi para o compilador. Depois de me assegurar que ninguém me viu conversando com o javac.exe, que aliás é um interlocutor bem antipático, pesquisei um pouco e descobri que varargs no Java 5 usa arrays por baixo do pano. "Ok, mas qual é o problema de criar um array de tipo genérico?". Bom, vamos por partes, como diria o Leibniz. Primeiro precisamos lembrar que arrays são covariantes no tipo do seu elemento; veja:

String[] sa = new String[10];
Object[] oa = sa;
oa[0] = new Integer(5);

Esse trecho de código compila, mas a última linha atira uma exceção (ArrayStoreException) em tempo de execução. Ou seja, a VM precisa testar se o tipo do array é adequado ao tipo de um objeto sendo inserido nele. Agora, pense nesse trecho:

Class<String>[] ca = new Class<String>[10];
Object[] oa = ca;
ca[0] = new Integer(8);

Aqui, o compilador reclama já na primeira linha que não consegue criar um array de Class<String>. E ele não consegue por um detalhe da implementação de generics no Java: a técnica escolhida para especificar tipos genéricos, chamada "Erasure", remove as informações sobre os parâmetros de tipo (as coisas dentro de <>) ao produzir o bytecode. Como a linguagem não teria informação suficiente para fazer as verificações necessárias quando da inserção de elementos no array, a solução adotada é simplesmente proibir arrays de tipos genéricos.

Agora vou fazer como todo bom mau programador e botar a culpa na linguagem por um bug que eu escrevi. Começando pela escolha de implementar generics via erasure, que significou sacrificar naturalidade em nome da tal "migration compatibility". Volta e meia acontece de eu escrever algum statement que parece perfeitamente inocente e o compilador me chama de burro (ou quase isso). Outra decisão que não me agradou foi a de usar arrays como base dos varargs, em vez de algo menos capenga como Iterable ou Collection. E minha última reclamação é sobre a covariância dos arrays, que enfraquece a tipagem estática a troco de nada (alguém sabe de um bom caso de uso para arrays covariantes?).