Visitando uma Collection em ordem inversa
Por Paulo Silveira em 30/08/06As vezes troco algumas mensagens bem técnicas com alguns colegas pelo GTalk, e aí aparecem algumas charadas interessantes.
O Orseni Campos, um desenvolvedor que eu admiro muito, passou o seguinte problema: “Paulo, como percorrer uma coleção qualquer na ordem inversa de maneira elegante?“. Quando ele disse maneira elegante, pensei que ele queria evitar criar uma lista e acessá-la via lista.get(indice) de trás para frente. Respondi o trivial: que o Collections.reverse resolveria:
public void imprimeInvertidoClassico(Collection<? extends Object>
colecao) {
List<Object> invertida = new ArrayList<Object>(colecao);
Collections.reverse(invertida);
for (Object objeto : invertida) {
System.out.println(objeto);
}
}
Ele não gostou, queria uma maneira de percorrer a coleção invertida sem criar uma coleção temporária, nem mexer na ordem dos objetos da coleção dada.
Desisti. Ele respondeu: “basta visitar a coleção recursivamente, realizando a ação como pós processamento!“. Perfeito. Uma técnica das linguagens funcionais e adorada pelos professores de conceitos de linguagens. Aliás, universidades importantes como MIT utilizam linguagens funcionais no curso introdutório de computação, acreditam que é mais fácil do aluno assimilar os conceitos dos algoritmos (algumas dessas linguagens são dos mesmos criadores do Java).
Então traduzi a idéia para código:
public void imprimeInvertido(Collection<? extends Object> colecao) {
imprimeInvertido(colecao.iterator());
}
private void imprimeInvertido(Iterator<? extends Object> iterator) {
if (iterator.hasNext()) {
Object object = iterator.next();
imprimeInvertido(iterator);
System.out.println(object);
}
}
Aqui, o método imprimeInvertido rapidamente delega a responsabilidade para o que recebe um Iterator como argumento. Este faz uma invocação recursiva no caso de existir um próximo elemento a ser visitado. O importante aqui é que a referência trazida pelo iterator.next() só é processado depois que seus elementos vizinhos forem, realizando o processo na ordem contrária da determinada pelo Iterator. (repare que considerando que a ordem definida pelo Iterator define uma lista, e que toda lista é uma árvore, estamos percorrendo essa ávore de maneira pós-fixa).
Ok, funciona e não cria coleção temporária! Mas se você parar para analisar bem, cada invocação recursiva empilha mais um stackframe, e em cada um temos uma variável temporária (a Object object declarado dentro do if) para ser recuperada quando a invocação retornar. Pensando na última invocação (base da recursão, quando o iterator chegar ao final e não entrar no if (iterator.hasNext())) teremos uma pilha com colecao.size() stackframes, alocando uma memória proporcional ao tamanho da coleção, muito semelhante a ter alocado a coleção temporária para o Collections.reverse.
Você ainda corre o risco de um StackOverflowError por estar gastando memória da pilha em vez do heap! Outra desvantagem é a legibilidade: nós programadores java não estamos acostumados a utilizar recursão para resolver problemas que poderiam ser facilmente resolvidos de maneira iterativa.
O exercício vale para relembrar que é importante manter contato com outras linguagens, para não ficar viciado no mesmo paradigma, o que te limita na hora de encontrar soluções. Em alguns casos a solução recursiva pode ser muito mais trivial de encontrar, desde que você esteja com o paradigma afiado!
Alguma outra maneira interessante de percorrer uma coleção na ordem inversa?
Bem, já que você cria uma
Listno primeiro exemplo, outra opção seria usarListIterator, que temprevious()ehasPrevious().Comment by Michael Nascimento Santos — August 30, 2006 @ 7:46 am
Pode ser, percorro até o fim do iterator, e aí começo a andar de trás pra frente!
Comment by Paulo Silveira — August 30, 2006 @ 8:26 am
Não precisa, tem um método na interface List que te retorna um ListIterator na posição que você quiser:
ListIterator it = list.listIterator(list.size());
while(it.hasPrevious()) {
Object obj = it.previous();
…
}
Mais elegante, não?
Comment by Fabio Kung — August 30, 2006 @ 8:43 am
mais elegante, sem dúvida! mas ainda assim precisa criar a coleção temporária…
Comment by Paulo Silveira — August 30, 2006 @ 10:12 am
Não tô enxergando onde!
Comment by Fabio Kung — August 30, 2006 @ 10:28 am
Neste caso seria um método pra percorrer na ordem inversa qualquer Collection, e não somente List.
Por isto o Paulo disse que ainda seria necessário a criação de uma coleção temporária.
Excelente blog, parabéns a Caelum, nota 10…
Comment by Orseni Ferreira Campos — August 30, 2006 @ 5:24 pm
Oi Paulo.
Só para constar, numa linguagem funcional daria para transformar essa recursão para estilo CPS (continuation passing style), e o compilador se encarregaria de fazer uma tail-call optimization. Nesse caso a recursão vira um loop e não temos o custo de empilhar os stack frames.
Comment by Rafael de F. Ferreira — August 30, 2006 @ 8:18 pm
Botei em código a idéia de passar para CPS. Em java, sem closures (por pouco tempo, espero), seria assim:
interface Continuacao { public void faz(); } class ContVazia implements Continuacao { public void faz() {} } public void imprimeInvertido(Collection colecao) { imprimeInvertido(colecao.iterator(), new ContVazia()); } private void imprimeInvertido(Iterator iterator, final Continuacao cont) { if (!iterator.hasNext()) { cont.faz(); } else { final Object object = iterator.next(); imprimeInvertido(iterator, new Continuacao() {public void faz() { System.out.println(object); cont.faz(); }}); } }O javac não faz tail-call optimization, então nesse caso não faz nenhuma diferença (além do código ficar ininteligível para o pessoal que não fez MAC0316 ou similar…)
Comment by Rafael de F. Ferreira — August 30, 2006 @ 8:28 pm
muito doido! a continuacao de um chama a continuacao do outro… loucura. mas nao da pra ler mesmo.
Comment by Guilherme Silveira — September 2, 2006 @ 1:49 pm
Interessante… fiz uns testes e com collection acima de aproximandamente 10000 itens dá StackOverflowError.
Comment by Fernando Boaglio — September 6, 2006 @ 5:44 pm
tem como voce aumentar o tamanho da stack passando -Xss pela linha de comando. se bem me lembro o default era 250k. 10 mil itens de 24 bytes (que é o que um Integer ocupa +-) da o limite
Comment by Paulo Silveira — September 6, 2006 @ 6:07 pm