Ordenando coleções com Comparable e Comparator

Uma tarefa comum no dia a dia dos desenvolvedores é ordenar uma lista ou array. Para não inventar a roda, a Collections API do Java (também conhecida pelo nome do seu pacote, o java.util) vem pronta para ajudar nessa tarefa. Falamos dessa API extensivamente na apostila do curso FJ-11, e vou aqui passar para o problema específico de ordenação.

Este post foi criado em 2010 e foi atualizado neste ano de 2017.

Imagine que você gostaria de ordenar uma lista de Contas bancárias. Cada conta possui um número (int) e um titular (String):


Conta conta1 = new Conta(5452, "Phillip Lahm");
Conta conta2 = new Conta(1234, "Lucas Podolski");
Conta conta3 = new Conta(3145, "Arne Friedrich");

List lista = new ArrayList();
lista.add(conta1);
lista.add(conta2);
lista.add(conta3);

O método para ordenar uma lista se encontra na classe java.util.Collections (repare o “s” no final). Ela possui métodos estáticos que ajudam a manipular coleções, entre eles o método sort. Assim podemos tentar ordenar a lista de contas:


Collections.sort(lista);

Mas infelizmente a linha acima nem compila. Antes de invocar o método sort é preciso definir o critério de ordenação: uma forma de informar, dado duas contas, qual vem “antes” e qual vem “depois”.

Considerando que queremos ordenar pelo número da conta, a classe Conta implementar a interface java.lang.Comparable que define o que será nossa “ordem natural”. A interface possui apenas um método compareTo:


public interface Comparable {
    int compareTo(T outro);
}

As contas então devem ser comparáveis. Vamos definir a ordem natural baseada no número da conta:


public Conta implements Comparable {
        
    private int numero;
    private String titular;
    // outros metodos e atributos

    public int compareTo(Conta outraConta) {
        if (this.numero < outraConta.numero) {
            return -1;
        }
        if (this.numero > outraConta.numero) {
            return 1;
        }
        return 0;
    }
}

Se o número da conta atual é menor do que da outraConta retormamos -1 (ou qualquer int negativo, indicando que this deve vir “antes” de outraConta), se for maior retornamos 1 (ou qualquer int positivo) e se for igual então devolvemos 0.

Agora podemos invocar Collections.sort(lista).

Mas se surgir a necessidade de ordenar pelo titular da conta? Não queremos alterar o método compareTo na classe Conta, já que isso mudaria a ordem natural. Queremos definir um outro critério de ordenação. Para tal, existe uma outra interface: a Comparator:


public interface Comparator {
    int compare(T o1, T o2);
}

Vamos então implementar a interface para definir a ordem pelo titular (String) da conta. Comparar duas Strings é difícil, mas, como você pode imaginar, esse problema já foi resolvido na API do Java.

A classe String já sabe compara dois strings, sabemos isso pois ela implementa a interface Comparable (por esse mesmo motivo podemos invocar Collections.sort para uma List de String). Podemos então delegar essa tarefa ao método compareTo das Strings:


public class TitularComparator implements Comparator {
    public int compare(Conta conta, Conta outraConta) {
        return conta.getTitular().
                compareTo(outraConta.getTitular());
    }
}

Como escolher para que esse critério de comparação seja utilizado em vez da ordem natural? O método sort é sobrecarregado e pode receber um objeto do tipo Comparator:


TitularComparator comparator = new TitularComparator();
Collections.sort(lista, comparator);

Falta mencionar que o método compareTo da interface Comparable deve ser consistente com o método equals. Quando uma conta é igual a outra (a classe Conta sobreescreve o método equals para definir igualdade), o método compareTo deve devolver zero também. Devemos também pensar se receberemos null como Contas para ordenar, e tomar as devidas precauções nos comparadores.

Através de Comparable e Comparator também são controladas a ordenação da coleção TreeSet e as chaves do mapa TreeMap.

Vale lembrar que muita coisa mudou desde esse post, com a entrada do Java 8, e você pode ler sobre lambdas e outras facilidades que ajudam em funções como comparação aqui no blog da Caelum.

34 Comentários

  1. Faru 24/06/2010 at 18:35 #

    Só uma pequena correção,
    Em um trecho você diz que a interface Comparable é do pacote java.util, e na verdade ela é do java.lang

    Sei que é algo simples, mas pode confundir.

  2. Paulo Silveira 24/06/2010 at 19:00 #

    Oi Faru, perfeito, ja fiz a alteracao. O Nico colocou o link pra java lang mas havia digitado o nome errado.

  3. Jeferson 25/06/2010 at 20:59 #

    otimo topico, ja me esclareceu muito, tanto quanto o de StringBuffer vs StringBuilder

  4. william 25/06/2010 at 22:49 #

    Grande artigo…

    gosto de usar em conjunto com enums para deixar um pouco mais fluente, tipo…

    public enum OrdenarClientes implements Comparator {
    
    	PorIdade() {
            public int compare(Cliente one, Cliente other) {
               return one.getIdade.compareTo(other.getIdade());
            }
         },
         
         PorNome() {
             public int compare(Cliente one, Cliente other) {
                return one.getNome().compareTo(other.getNome());
             }
          };
    
    
         public abstract int compare(Cliente one, Cliente other);
    
         public Comparator asc() {
            return this;
         }
    
         public Comparator desc() {
            return Collections.reverseOrder(this);
         }
    }
    

    dai posso fazer coisas como OrdenarClientes.PorNome().asc()

  5. Paulo Silveira 26/06/2010 at 11:18 #

    @William excelente codigo! é realmente bastante util, da para aumentar ainda mais sua ideia e criar uma fluent interface para ainda configurar casos de empate, etc…

  6. Rodrigo Facholi 28/06/2010 at 10:22 #

    Muito bom artigo, e ótima dica no comentário do william!

  7. fernando 02/07/2010 at 09:31 #

    primeiramente parabéns pela explicação, cara eu fiz exatamente como está no seu post, tenho um arrayList e quero ordená-lo sempre de acordo com um atributo dos objetos que estao no array, na classe de dominio implementei a comparable e fiz o compareTo com o atributo que será levado em conta na ordenação do mesmo jeito que vc fez no post(meu atributo também é um int) após isso chamei a collections.sort passando meu array de objetos e o array continuou com a mesma ordenação, faltou eu fazer alguma coisa? obrigado

  8. Paulo Silveira 02/07/2010 at 11:31 #

    Oi Fernando. Poste seu codigo pra gente ver, mas nao falto nada nao!

  9. Oziel Alves Cavalcante 07/07/2010 at 13:34 #

    Vendo no exemplo que TitularComparator é uma public class e lembrando da static nested class CaseInsensitiveComparator usada na classe String, me veio a dúvida se há uma solução melhor entre as duas ou se depende do caso. Dúvida também válida para o exemplo de enum do William, externo ou interno à uma classe?

    @William como disse o pimeja, http://tobega.blogspot.com/2008/05/beautiful-enums.html, o Comparator deve ser parametrizado e o método abstrato parece ser desnecessário.

    @Caelum, a sintaxe dos comments suporta html? Pelo que vi suporta formatação de código.

  10. Paulo Silveira 07/07/2010 at 13:45 #

    @ Oziel. Voce pode fazer comentarios formatados entre [ java ] e [/java].

    Sobre usar uma classe aninhada estatica, é interessante para deixar claro que aquilo só é pra ser usado com a classe String, e para nao poluir o pacote com classes pequenas… mas é bastante questionável. Uma classe publica top level teria o mesmo sentido (e é minha preferencia na maioria dos casos).

    O metodo abstrato é desnecessário mesmo, e o tipo realmente deve ser parametrizado (Acho que no post do tobe nao apareceu porque o html comeu o sinal de maior e de menor) senao o metodo compare receberia dois Objects.

  11. Larissa 26/12/2010 at 20:00 #

    Oi…estou com uma dúvida…

    eu tenho um List listaCaixas = new ArrayList ();

    como faço para ordenar essa listaCaixas pelo atributo volume da classe Caixa…e tem que ser da caixa com maior volume para a que tiver menor volume..o compareTo não está aceitando tipo double.

    obrigada

  12. thejokerbm 25/04/2011 at 14:00 #

    na verdade a sua classe caixa tem q implementar comparable ai vc comprar dentro do metodo compareTo, mas procure no GUJ la vc vai encontrar mais informação

  13. javaneiro 01/08/2011 at 16:11 #

    Ótimo artigo, entao tentei fazer uma ordenação de um HashMap medias pelo String…

    Mas não deu certo… fiz da seguinte forma usando uma coleção ordenada…

    TreeMap ordenado = new TreeMap(medias);
    Iterator i=medias.keySet().iterator();
    while (i.hasNext()){
    String chave=(String)i.next();
    int valor=(int)medias.get(chave);
    System.out.println(chave + “:” + valor);
    }
    O que posso fazer nesse caso?

  14. javaneiro 01/08/2011 at 16:15 #

    Funcionou sim… obrigado… só passei a coleção errada iterator… ficando assim…

    private void ordenandoHashMap(HashMap medias) {

    TreeMap ordenado = new TreeMap(medias);
    Iterator i = ordenado.keySet().iterator();
    while (i.hasNext()){
    String chave=(String)i.next();
    int valor=(int)medias.get(chave);
    System.out.println(chave + “:” + valor);
    }

    }

  15. Nico Steppat 01/08/2011 at 16:39 #

    Oi javaneiro,

    vc deve iterar com o mapa “ordenada”, o mapa “medias” continua sem ordem.

  16. João Henrique 14/03/2012 at 17:06 #

    Lembrando que comparator possibilita apenas um tipo de ordenação.

    Gosto de usar inner Class para fazer este serviço.

    Crio uma inner class do Tipo OrdenacaoPorNome que implementa comparable

    E ai passo essa classe comparavel para o método sort de Collections

  17. Andre Alves Pinto 15/08/2012 at 10:24 #

    Valeu, esse post me ajudou muito!

  18. Fred 20/11/2012 at 15:42 #

    Ótimo artigo. Extamente o que eu procurava.

    Caelum e Nico Steppat continuem sempre com esse trabalho!

  19. Ricardo 15/05/2013 at 10:57 #

    E quando é necessário ordernar por multiplos atributos?
    Por exemplo, em Clientes. ordenar Estado, Cidade e nome do cliente? Alguem tem alguma sugestão?

  20. Sane 29/05/2013 at 11:21 #

    O meu aparece a seguinte mensagem
    No suitable method found for sort(List,TarefaComparator)
    method Collections.sort(List,Comparator) is not applicable
    (no instance(s) of type variable(s) T#1 exist so that argument type TarefaComparator conforms to formal parameter type Comparator)
    method Collections.sort(List) is not applicable
    (cannot instantiate from arguments because actual and formal argument lists differ in length)
    where T#1,T#2 are type-variables:
    T#1 extends Object declared in method sort(List,Comparator)
    T#2 extends Comparable declared in method sort(List)

    eu estou comparando pela data, alguem pode me ajudar por favor?

  21. Geilson 31/05/2013 at 19:10 #

    Ricardo, o Guava tem umas classes ótimas pra comparar múltiplos atributos.
    Se você vai implementar o método compare ou compareTo, a ComparisonChain facilita muito:
    http://code.google.com/p/guava-libraries/wiki/CommonObjectUtilitiesExplained#compare/compareTo

    Agora, se você já tem os vários Comparators e que quiser agrupá-los e/ou reordená-los, a classe Ordering tem um método estático chamado compound que faz exatamente isso:
    http://code.google.com/p/guava-libraries/wiki/OrderingExplained

  22. Carlos Eduardo 23/12/2013 at 11:36 #

    O jeito Groovy de fazer uma ordenação, neste caso por nome:

    def string = [“Carlos”, “Henrique”, “Altair”, “Groovy”].sort( new Comparator() {
    int compare(String s1, String s2) {
    return s1 s2
    }
    });

  23. Bruno 01/12/2014 at 14:30 #

    Não entendi muito bem essa parte:

    public abstract int compare(Cliente one, Cliente other);

    public Comparator asc() {
    return this;
    }

    public Comparator desc() {
    return Collections.reverseOrder(this);
    }

    O que eu não entendi é porque tem um método abstrato em um enum, não faz com que ele também seja abstrato, sabendo que um Enum jamais poderia ser abastrato

  24. Paulo Silveira 01/12/2014 at 17:32 #

    Oi Bruno! uma enum pode sim ter metodo abstrato, bastando voce implementar aquele metodo em todas as enums da enum.

  25. Gabriel Queiroz 11/04/2015 at 01:19 #

    Cara valeu muito eu queria ordenar uma lista pela data, e nao achava nada mas você me salvou.

    Fiz assim:
    @SuppressLint(“SimpleDateFormat”)
    @Override
    public int compareTo(Invoices another) {

    SimpleDateFormat sdf = new SimpleDateFormat(“dd/MM/yyyy”);
    java.util.Date dateComparer, dateActual;

    try {
    dateActual = sdf.parse(this.data);
    dateComparer = sdf.parse(another.data);

    if (dateActual.after(dateComparer)) {
    return -1;
    }
    if (dateActual.before(dateComparer)) {
    return 1;
    }

    } catch (ParseException e) {
    e.printStackTrace();
    }

    return 0;
    }

  26. Júlio Cardoso 26/05/2015 at 18:04 #

    Muito bom o texto. Ajudou muito. A postagem do Willian também.

  27. Nathanael Lima 31/05/2015 at 16:01 #

    Excelente artigo! Utilizei em um código e funcionou perfeitamente, inclusive consegui utilizar vários critérios para gerar uma tabela de classificação de equipes. Ficou dessa forma:

    public int compareTo(Selecao outraSelecao){
    		if(this.getPontuacao() < outraSelecao.getPontuacao()){
    			return 1;
    		} else if(this.getPontuacao() > outraSelecao.getPontuacao()){
    			return -1;
    		} else{
    			return compareTo2(outraSelecao);
    		}
    	}
    	
    	public int compareTo2(Selecao outraSelecao){
    		if(this.getSaldoDeGols() < outraSelecao.getSaldoDeGols()){
    			return 1;
    		} else if(this.getSaldoDeGols() > outraSelecao.getSaldoDeGols()){
    			return -1;
    		} else {
    			return compareTo3(outraSelecao);
    		}
    	}
    	
    	public int compareTo3(Selecao outraSelecao){
    		if(this.getGolsMarcados() < outraSelecao.getGolsMarcados()){
    			return 1;
    		} else if(this.getGolsMarcados() > outraSelecao.getGolsMarcados()){
    			return -1;
    		} else {
    			return 0;
    		}
    	}
  28. Thiago Schetini 30/12/2015 at 20:38 #

    Fiz o curso presencial FJ-11 da Caelum, mas não consigo de forma alguma entender isso de Camparator vs Comparable, QUE SACO!

    Não consegui resolver os desafios abaixo, se alguém entender isso e puder explicar agradeço:

    Gere todos os números entre 1 e 1000 e ordene em ordem decrescente utilizando um TreeSet

  29. Paulo Silveira 02/01/2016 at 14:55 #

    oi Thiago

    Não se assuste. No começo realmente é muito chato entender as pequenas diferenças.

    Pense assim: Comparable é que você quer o código de ordenação dentro da propria classe. String e Integer implementam Comparable. Por isso nao precisam de Comparator para fazer o sort ou usar TreeSet.

    POREM, se quisermos ordená-los em uma outra ordem, precisaremos ter um Comparator. Tanto o Collections.sort quanto o construtor de TreeSet recebem Comparators. Assim você vai poder ordenar uma ordem diferente da “ordem natural” definida pela implementação de Comparable que eles tem.

  30. Iure 05/06/2016 at 18:46 #

    Posso perceber que o autor desse post assistiu a seleção da Alemanha jogar em 2006.

  31. Bruno 16/07/2016 at 09:35 #

    Excelente. Continue com o bom trabalho

  32. Carlos 17/02/2017 at 00:13 #

    Obrigado obrigado muito obrigado :3 tava fazendo um joguinho aqui e precisava classificar a posição Z da Sprite e isso veio a caiar

Deixe uma resposta