Java Comparable e Comparator

Ordenar números e textos é fácil: o Java já sabe que 3 vem antes de 10 e "Ana" antes de "Bruno". Mas e uma lista de alunos? Por nome? Por nota? Da maior nota para a menor e, no empate, por nome? O Java não adivinha: você precisa ensinar a comparar. Há duas formas: Comparable (a ordem "natural", escrita dentro da classe) e Comparator (qualquer outra ordem, escrita fora).

Você vai aprender
  • Por que Collections.sort não compila com uma lista de objetos seus.
  • O que compareTo devolve (negativo, zero, positivo) e como escrevê-lo com Comparable.
  • O Comparator de quatro jeitos: classe, classe anônima, lambda e Comparator.comparing.
  • Combinar critérios com thenComparing e inverter com reversed.
  • Montar o ranking dos alunos da Ana: nota decrescente e, no empate, nome.

O problema: o Java não sabe comparar alunos

A Ana tem uma classe Aluno com nome e nota, e tenta ordenar a turma do jeito que aprendeu na aula de ArrayList:

Este código tem um erro de propósito.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

class Aluno {
    String nome;
    double nota;

    Aluno(String nome, double nota) {
        this.nome = nome;
        this.nota = nota;
    }

    public String toString() {
        return nome + "(" + nota + ")";
    }
}

public class Main {
    public static void main(String[] args) {
        List<Aluno> turma = new ArrayList<>();
        turma.add(new Aluno("Carla", 9.5));
        turma.add(new Aluno("Edu", 7.0));
        turma.add(new Aluno("Ana", 9.5));
        Collections.sort(turma);
        System.out.println(turma);
    }
}
$ javac Main.java
Main.java:25: error: no suitable method found for sort(List<Aluno>)
        Collections.sort(turma);
                   ^
    method Collections.<T#1>sort(List<T#1>) is not applicable
      (inference variable T#1 has incompatible bounds
        equality constraints: Aluno
        upper bounds: Comparable<? super T#1>)
    method Collections.<T#2>sort(List<T#2>,Comparator<? super T#2>) is not applicable
      (cannot infer type-variable(s) T#2
        (actual and formal argument lists differ in length))
  where T#1,T#2 are type-variables:
    T#1 extends Comparable<? super T#1> declared in method <T#1>sort(List<T#1>)
    T#2 extends Object declared in method <T#2>sort(List<T#2>,Comparator<? super T#2>)
1 error

A mensagem assusta, mas dá para ler em partes:

  • "no suitable method found for sort(List<Aluno>)": "nenhum método sort serve para uma lista de Aluno".
  • O javac então explica por que cada versão de sort não serve. A primeira, sort(List), exige elementos que sejam Comparable (a linha "upper bounds: Comparable"), e Aluno não é.
  • A segunda, sort(List, Comparator), precisa de dois argumentos ("argument lists differ in length": "as listas de argumentos têm tamanhos diferentes"), e só passamos um.

Ou seja, o próprio erro mostra as duas saídas: tornar Aluno Comparable, ou passar um Comparator. Os T#1, T#2 são variáveis de tipo de generics; não se preocupe com elas agora.


Como se compara: negativo, zero, positivo

Antes de escrever o seu, veja como o Java compara o que ele já conhece. O método compareTo compara este objeto com outro e devolve um int:

public class Main {
    public static void main(String[] args) {
        System.out.println("\"Ana\".compareTo(\"Bruno\") = " + "Ana".compareTo("Bruno"));
        System.out.println("\"Bruno\".compareTo(\"Ana\") = " + "Bruno".compareTo("Ana"));
        System.out.println("\"Ana\".compareTo(\"Ana\")   = " + "Ana".compareTo("Ana"));
        System.out.println("\"Ana\".compareTo(\"Edu\")   = " + "Ana".compareTo("Edu"));
        System.out.println("Integer.compare(3, 10) = " + Integer.compare(3, 10));
        System.out.println("Double.compare(9.5, 7.0) = " + Double.compare(9.5, 7.0));
        System.out.println("Double.compare(7.0, 7.0) = " + Double.compare(7.0, 7.0));
    }
}
$ javac Main.java
$ java Main
"Ana".compareTo("Bruno") = -1        « negativo: Ana vem ANTES de Bruno
"Bruno".compareTo("Ana") = 1         « positivo: Bruno vem DEPOIS de Ana
"Ana".compareTo("Ana")   = 0         « zero: empatados
"Ana".compareTo("Edu")   = -4        « o número exato não importa, só o sinal
Integer.compare(3, 10) = -1
Double.compare(9.5, 7.0) = 1
Double.compare(7.0, 7.0) = 0
a.compareTo(b) devolve…Quer dizer
um número negativoa vem antes de b
zeroa e b empatam (mesma posição na ordem)
um número positivoa vem depois de b

Para comparar números, use sempre Integer.compare(x, y) e Double.compare(x, y), que devolvem exatamente esse sinal. (Você vai ver em código antigo return a - b; para inteiros: funciona com números pequenos, mas dá resultado errado quando a subtração passa do limite do int; veja overflow.)


Comparable: a ordem natural da classe

Uma classe que implementa a interface Comparable<T> ("comparável") diz: "meus objetos têm uma ordem natural, e ela é esta". É o que String e Integer fazem. A interface tem um único método, compareTo. Para a Ana, a ordem natural dos alunos é a da chamada: por nome.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

class Aluno implements Comparable<Aluno> {
    String nome;
    double nota;

    Aluno(String nome, double nota) {
        this.nome = nome;
        this.nota = nota;
    }

    @Override
    public int compareTo(Aluno outro) {
        return this.nome.compareTo(outro.nome);
    }

    public String toString() {
        return nome + "(" + nota + ")";
    }
}

public class Main {
    public static void main(String[] args) {
        List<Aluno> turma = new ArrayList<>();
        turma.add(new Aluno("Carla", 9.5));
        turma.add(new Aluno("Edu", 7.0));
        turma.add(new Aluno("Ana", 9.5));
        turma.add(new Aluno("Duda", 8.0));
        turma.add(new Aluno("Bruno", 7.0));
        Collections.sort(turma);
        System.out.println(turma);
        System.out.println("Primeiro na chamada: " + Collections.min(turma));
    }
}
implements Comparable<Aluno>
"Aluno é comparável com outros Alunos." O tipo entre < > é com quem ele se compara (veja Interfaces).
@Override public int compareTo(Aluno outro)
O método exigido pela interface. Recebe o outro aluno; o "este" é o this.
return this.nome.compareTo(outro.nome);
Passa a pergunta adiante: comparar dois alunos é comparar os nomes deles, e String já sabe fazer isso.
$ javac Main.java
$ java Main
[Ana(9.5), Bruno(7.0), Carla(9.5), Duda(8.0), Edu(7.0)]
Primeiro na chamada: Ana(9.5)

Agora o Collections.sort compila, e tudo o que depende da ordem natural passa a funcionar com Aluno: Collections.min/max, lista.sort(null), e até ser elemento de um TreeSet ou chave de um TreeMap (lembra da ClassCastException da aula TreeMap e TreeSet? É isto que faltava).

E com um record?

Funciona igual: record Aluno(String nome, double nota) implements Comparable<Aluno> { public int compareTo(Aluno o) { return nome.compareTo(o.nome); } }. Nos records, o campo se lê com nome() de fora da classe (veja Records).

Missão: o Collections.sort não compila. Faça Produto implementar Comparable<Produto>, com a ordem natural pelo preço (use Double.compare). A saída deve ser [pão=0.75, leite=5.49, café=18.9].

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

class Produto {
    String nome;
    double preco;

    Produto(String nome, double preco) {
        this.nome = nome;
        this.preco = preco;
    }

    public String toString() {
        return nome + "=" + preco;
    }
}

public class Main {
    public static void main(String[] args) {
        List<Produto> cesta = new ArrayList<>();
        cesta.add(new Produto("café", 18.90));
        cesta.add(new Produto("pão", 0.75));
        cesta.add(new Produto("leite", 5.49));
        Collections.sort(cesta);
        System.out.println(cesta);
    }
}
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

class Produto implements Comparable<Produto> {
    String nome;
    double preco;

    Produto(String nome, double preco) {
        this.nome = nome;
        this.preco = preco;
    }

    @Override
    public int compareTo(Produto outro) {
        return Double.compare(this.preco, outro.preco);
    }

    public String toString() {
        return nome + "=" + preco;
    }
}

public class Main {
    public static void main(String[] args) {
        List<Produto> cesta = new ArrayList<>();
        cesta.add(new Produto("café", 18.90));
        cesta.add(new Produto("pão", 0.75));
        cesta.add(new Produto("leite", 5.49));
        Collections.sort(cesta);
        System.out.println(cesta);
    }
}

Comparator: qualquer outra ordem

Uma classe só tem uma ordem natural. Mas a Ana quer ordenar a turma por nome na chamada, por nota no boletim, por idade na festa junina… Para cada ordem extra, ela cria um comparador: um objeto separado, da interface Comparator<T>, cujo método compare(a, b) segue a mesma regra do sinal (negativo: a antes; zero: empate; positivo: a depois).

Com isso, nem é preciso mexer na classe Aluno; ela pode ser até de outra pessoa. A partir daqui usamos um record para encurtar (com um toString mais curto). Veja o mesmo comparador "por nota" escrito de quatro jeitos, do mais longo ao mais curto:

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

record Aluno(String nome, double nota) {
    public String toString() {
        return nome + "=" + nota;
    }
}

class PorNota implements Comparator<Aluno> {
    @Override
    public int compare(Aluno a, Aluno b) {
        return Double.compare(a.nota(), b.nota());
    }
}

public class Main {
    public static void main(String[] args) {
        List<Aluno> chamada = List.of(
            new Aluno("Carla", 9.5), new Aluno("Edu", 7.0), new Aluno("Ana", 9.5),
            new Aluno("Duda", 8.0), new Aluno("Bruno", 7.0));

        // 1. classe separada
        List<Aluno> t1 = new ArrayList<>(chamada);
        t1.sort(new PorNota());
        System.out.println("1. classe:    " + t1);

        // 2. classe anônima
        List<Aluno> t2 = new ArrayList<>(chamada);
        t2.sort(new Comparator<Aluno>() {
            @Override
            public int compare(Aluno a, Aluno b) {
                return Double.compare(a.nota(), b.nota());
            }
        });
        System.out.println("2. anônima:   " + t2);

        // 3. lambda
        List<Aluno> t3 = new ArrayList<>(chamada);
        t3.sort((a, b) -> Double.compare(a.nota(), b.nota()));
        System.out.println("3. lambda:    " + t3);

        // 4. Comparator.comparing
        List<Aluno> t4 = new ArrayList<>(chamada);
        t4.sort(Comparator.comparing(Aluno::nota));
        System.out.println("4. comparing: " + t4);
    }
}
$ javac Main.java
$ java Main
1. classe:    [Edu=7.0, Bruno=7.0, Duda=8.0, Carla=9.5, Ana=9.5]
2. anônima:   [Edu=7.0, Bruno=7.0, Duda=8.0, Carla=9.5, Ana=9.5]
3. lambda:    [Edu=7.0, Bruno=7.0, Duda=8.0, Carla=9.5, Ana=9.5]
4. comparing: [Edu=7.0, Bruno=7.0, Duda=8.0, Carla=9.5, Ana=9.5]
FormaComo éQuando usar
1. Classe com nomeclass PorNota implements Comparator<Aluno>Comparadores complicados, usados em vários lugares.
2. Classe anônimanew Comparator<Aluno>() { … }Código antigo (antes do Java 8). Hoje, prefira a lambda.
3. Lambda(a, b) -> Double.compare(a.nota(), b.nota())Uma regra curta, escrita na hora.
4. Comparator.comparingComparator.comparing(Aluno::nota)O padrão hoje: "compare por este campo". Lê-se quase como português.

Repare nos empates: Edu e Bruno têm 7.0, Carla e Ana têm 9.5. Nos quatro resultados eles ficaram na ordem em que já estavam na chamada (Edu antes de Bruno, Carla antes de Ana). Isso acontece porque o sort do Java é estável: elementos empatados mantêm a ordem anterior. Se você quer um desempate de verdade, precisa pedir, como na próxima seção.

A forma 4 merece explicação. Comparator.comparing recebe uma função que extrai a chave de comparação de cada aluno, e monta o comparador sozinho. Aluno::nota é uma referência de método: "a nota do aluno". O mesmo que a lambda a -> a.nota().


Combinar e inverter: thenComparing e reversed

Comparadores podem ser encadeados como peças de Lego:

        turma.sort(Comparator.comparing(Aluno::nome));
        System.out.println("nome:            " + turma);

        turma.sort(Comparator.comparing(Aluno::nome).reversed());
        System.out.println("nome desc:       " + turma);

        turma.sort(Comparator.comparing(Aluno::nota).reversed().thenComparing(Aluno::nome));
        System.out.println("nota desc, nome: " + turma);

        turma.sort(Comparator.comparing(Aluno::nota).thenComparing(Aluno::nome).reversed());
        System.out.println("tudo invertido:  " + turma);
$ java Main
nome:            [Ana=9.5, Bruno=7.0, Carla=9.5, Duda=8.0, Edu=7.0]
nome desc:       [Edu=7.0, Duda=8.0, Carla=9.5, Bruno=7.0, Ana=9.5]
nota desc, nome: [Ana=9.5, Carla=9.5, Duda=8.0, Bruno=7.0, Edu=7.0]     « o ranking que a Ana queria
tudo invertido:  [Carla=9.5, Ana=9.5, Duda=8.0, Edu=7.0, Bruno=7.0]     « inverteu o desempate também
.reversed()
"Ao contrário": inverte tudo o que vem antes dele na corrente.
.thenComparing(Aluno::nome)
"E então compare por nome": só é usado quando o critério anterior deu empate (zero).
comparing(nota).reversed().thenComparing(nome)
Nota decrescente; no empate, nome em ordem crescente. Ana e Carla (9.5) ficam em ordem alfabética.
comparing(nota).thenComparing(nome).reversed()
O reversed no fim inverte a corrente inteira: nota decrescente e nome decrescente (Carla antes de Ana). Cuidado com a posição!

Outros atalhos de Comparator

CódigoOrdem
Comparator.naturalOrder()A ordem natural (compareTo)
Comparator.reverseOrder()A ordem natural ao contrário
Comparator.comparing(Aluno::nome, String.CASE_INSENSITIVE_ORDER)Por nome, ignorando maiúsculas
Comparator.comparingInt(String::length)Por um int, sem embrulhar em Integer (também há comparingDouble, comparingLong)
Comparator.comparing(Aluno::nome).reversed()Por nome, de Z a A
Comparator.nullsFirst(…) / nullsLast(…)Põe os null no começo / no fim, em vez de dar NullPointerException

Para ordenar como num dicionário de português (com "Álvaro" junto dos "A"), existe a classe java.text.Collator, um comparador que conhece as regras do idioma do computador (aqui, pt_BR):

        List<String> l = new ArrayList<>(List.of("Carla", "ana", "Bruno", "Álvaro", "Ana"));
        l.sort(Collator.getInstance());      // import java.text.Collator;
        System.out.println(l);
$ java Main
[Álvaro, ana, Ana, Bruno, Carla]        « com Collections.sort seria [Ana, Bruno, Carla, ana, Álvaro]

Comparable ou Comparator?

Comparable<T>Comparator<T>
Onde ficaDentro da classe (implements)Fora: um objeto separado
MétodocompareTo(T outro)compare(T a, T b)
Quantas ordensUma: a ordem naturalQuantas você quiser
Usado porCollections.sort(l), l.sort(null), TreeSet sem argumentol.sort(comp), new TreeSet<>(comp), Collections.max(l, comp)
Quando usarExiste uma ordem óbvia (data, código, nome)Ordens extras, ou classes que você não pode alterar

Quando dá errado

"no suitable method found for sort(List<Aluno>)"

O que aconteceu: Collections.sort(lista) numa lista de uma classe que não implementa Comparable. Veja o começo da aula.

Como consertar: implements Comparable<Aluno> com compareTo, ou passe um comparador: turma.sort(Comparator.comparing(Aluno::nome)).

"cannot find symbol … location: variable a of type Object" com reversed()

Este código tem um erro de propósito.

        turma.sort(Comparator.comparing(a -> a.nota()).reversed());
$ javac Main.java
Main.java:10: error: cannot find symbol
        turma.sort(Comparator.comparing(a -> a.nota()).reversed());
                                              ^
  symbol:   method nota()
  location: variable a of type Object
1 error

O que quer dizer: "não achei o método nota() na variável a, do tipo Object". Sem o .reversed(), o Java deduz que a é um Aluno olhando para o sort. Com o .reversed() no meio, a dedução não chega até a lambda, e a vira um Object genérico, que não tem nota().

Como consertar: use a referência de método, que já diz o tipo: Comparator.comparing(Aluno::nota).reversed(); ou dê o tipo na lambda: Comparator.comparing((Aluno a) -> a.nota()).reversed().

A ordem saiu ao contrário do esperado

O que aconteceu: o sinal do compareTo/compare está trocado (por exemplo, Double.compare(b.nota, a.nota)), ou o reversed() está no lugar errado da corrente.

Como consertar: lembre: negativo = a vem antes. Escreva sempre na ordem crescente e use reversed() logo depois do critério que deve ser invertido.


Referência

CódigoO que faz
class X implements Comparable<X> + compareTo(X o)Define a ordem natural
Integer.compare(a, b), Double.compare(a, b)Comparar números com o sinal certo
s1.compareTo(s2)Comparar Strings (ordem dos caracteres)
(a, b) -> …Comparator como lambda
Comparator.comparing(X::campo)Comparar por um campo
.thenComparing(X::outro)Critério de desempate
.reversed()Inverte o que vem antes
Comparator.naturalOrder() / reverseOrder()Natural / natural invertida
lista.sort(comp), Collections.sort(lista, comp)Ordenar com o comparador

Exercícios

1. A interface

Complete para dar uma ordem natural à classe Livro:

class Livro implements { … }

2. Por título

Complete para ordenar a lista livros pelo título (o record tem o campo titulo):

livros.sort(Comparator.comparing());

3. Desempate

Complete para, no empate de nota, ordenar por nome:

Comparator.comparing(Aluno::nota).;

4. a.compareTo(b) devolveu -3. Na ordem natural…

5. Quantas ordens naturais (compareTo) uma classe pode ter?

6. Notas: Ana 9.5, Carla 9.5. Com comparing(Aluno::nota).thenComparing(Aluno::nome).reversed(), quem vem primeiro?

Missão final: monte o ranking da turma da Ana: nota decrescente e, no empate, nome em ordem alfabética. A saída deve começar com 1º Ana - 9.5 e 2º Carla - 9.5, e terminar com 4º Bruno - 7.0 e 5º Edu - 7.0.

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

record Aluno(String nome, double nota) { }

public class Main {
    public static void main(String[] args) {
        List<Aluno> turma = new ArrayList<>(List.of(
            new Aluno("Carla", 9.5), new Aluno("Edu", 7.0), new Aluno("Ana", 9.5),
            new Aluno("Duda", 8.0), new Aluno("Bruno", 7.0)));

        turma.sort(Comparator.comparing(Aluno::nota));
        for (int i = 0; i < turma.size(); i++) {
            Aluno a = turma.get(i);
            System.out.println((i + 1) + "º " + a.nome() + " - " + a.nota());
        }
    }
}
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

record Aluno(String nome, double nota) { }

public class Main {
    public static void main(String[] args) {
        List<Aluno> turma = new ArrayList<>(List.of(
            new Aluno("Carla", 9.5), new Aluno("Edu", 7.0), new Aluno("Ana", 9.5),
            new Aluno("Duda", 8.0), new Aluno("Bruno", 7.0)));

        turma.sort(Comparator.comparing(Aluno::nota).reversed().thenComparing(Aluno::nome));
        for (int i = 0; i < turma.size(); i++) {
            Aluno a = turma.get(i);
            System.out.println((i + 1) + "º " + a.nome() + " - " + a.nota());
        }
    }
}

Resumo

  • Para ordenar objetos seus, o Java precisa saber comparar: sem isso, Collections.sort não compila.
  • compareTo/compare devolvem negativo (antes), zero (empate) ou positivo (depois). Use Integer.compare/Double.compare.
  • Comparable: a ordem natural, dentro da classe (uma só).
  • Comparator: qualquer outra ordem, fora da classe: classe, anônima, lambda ou Comparator.comparing.
  • thenComparing desempata; reversed inverte tudo o que vem antes dele.
  • O sort é estável: empatados mantêm a ordem anterior.

Próxima aula: Generics, o que significam todos esses <T>.