W3docs

Java Comparable e Comparator

Defina ordenação natural com Comparable e ordenação externa com Comparator em Java, e componha comparadores.

Duas interfaces, uma tarefa: dizer ao Java quando um objeto é "menor que" outro. Elas parecem quase idênticas no ponto de chamada, e seus métodos até retornam o mesmo formato de valor — um int negativo, zero ou um int positivo. A diferença está em onde a ordem reside:

  • Comparable<T> — o próprio tipo sabe como ordenar suas instâncias. Seu método int compareTo(T other) é a ordenação natural do tipo.
  • Comparator<T> — um objeto externo que ordena instâncias. Seu método int compare(T a, T b) descreve uma das muitas ordenações possíveis.

Você implementa Comparable quando há um "menor que" óbvio para um tipo — Integer, String, LocalDate. Você escreve um Comparator para qualquer outra ordenação — por comprimento, por nome sem distinção de maiúsculas, por preço decrescente, por qualquer coisa que você possa expressar em código. A maioria dos tipos tem um Comparable (ou nenhum) e dezenas de Comparators úteis.

O contrato: −/0/+

Ambos os métodos retornam um int cujo sinal é a resposta:

  • negativoa vem antes de b
  • zero — iguais para fins de ordenação
  • positivoa vem depois de b

A magnitude exata não importa. -1 e -1_000_000 significam a mesma coisa. Nunca use return a.size - b.size quando há possibilidade de overflow: subtrair Integer.MIN_VALUE de um número positivo causa estouro. Use Integer.compare(a.size(), b.size()) em vez disso — é seguro contra overflow e tem o mesmo número de caracteres para digitar.

Comparable<T> — ordenação natural

Um tipo implementa Comparable<Self> e fornece compareTo:

public record Version(int major, int minor, int patch) implements Comparable<Version> {
  @Override public int compareTo(Version other) {
    int m = Integer.compare(this.major, other.major);
    if (m != 0) return m;
    int n = Integer.compare(this.minor, other.minor);
    if (n != 0) return n;
    return Integer.compare(this.patch, other.patch);
  }
}

Agora Collections.sort(versions), versions.stream().sorted(), new TreeSet<Version>() e new TreeMap<Version, X>() simplesmente funcionam sem você precisar passar nenhum argumento extra.

O contrato tem três regras que todo compareTo deve respeitar:

  1. Anti-simétricoa.compareTo(b) e b.compareTo(a) têm sinais opostos.
  2. Transitivo — se a < b e b < c, então a < c.
  3. Consistente com equals (fortemente recomendado)a.compareTo(b) == 0 sse a.equals(b).

A terceira regra é a que as pessoas quebram por acidente. BigDecimal é o exemplo famoso: new BigDecimal("1.0").compareTo(new BigDecimal("1.00")) é 0, mas .equals retorna false. Como resultado, um TreeSet<BigDecimal> e um HashSet<BigDecimal> vão discordar sobre se "1.0" e "1.00" são duplicatas. Se puder, mantenha-os consistentes.

Comparator<T> — ordenação externa

Um Comparator é um objeto separado. Ele pode comparar quaisquer dois Ts, incluindo tipos que você não escreveu:

Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());
list.sort(byLength);

Como Comparator<T> é uma interface funcional (um único método abstrato, compare), todo Comparator é apenas um lambda ou uma referência a método. Essa é a forma moderna do código de comparador — você quase nunca escreve uma classe anônima completa mais.

Os construtores em Comparator

A classe tem métodos fábrica estáticos que tornam a construção de comparadores curta e legível:

Comparator<Person> byAge       = Comparator.comparingInt(Person::age);
Comparator<Person> byName      = Comparator.comparing(Person::name);
Comparator<Person> byNameCi    = Comparator.comparing(Person::name, String.CASE_INSENSITIVE_ORDER);
Comparator<Person> oldestFirst = byAge.reversed();
Comparator<String> nullsFirst  = Comparator.nullsFirst(Comparator.naturalOrder());

Use os construtores especializados para primitivos — comparingInt, comparingLong, comparingDouble — quando a chave for um primitivo. Eles evitam boxing em cada comparação, o que faz diferença em uma ordenação longa.

Comparadores encadeados com thenComparing

O outro motivo para preferir os construtores: você pode encadear múltiplas chaves.

Comparator<Person> ordering =
    Comparator.comparing(Person::lastName)
              .thenComparing(Person::firstName)
              .thenComparingInt(Person::age);

Isso se lê de cima para baixo como "chave primária sobrenome; desempate por primeiro nome; depois por idade." thenComparing é invocado no comparador anterior e retorna um novo que só consulta a segunda chave quando a primeira reportou empate. Não há limite para o encadeamento.

reversed(), nullsFirst, nullsLast

Três modificadores aparecem constantemente:

  • reversed() inverte a ordem de qualquer comparador. byAge.reversed() é "mais velho primeiro."
  • nullsFirst(cmp) envolve um comparador de forma que valores null sejam tratados como menores que qualquer não-nulo. Útil ao ordenar coleções que podem conter null.
  • nullsLast(cmp) é o companheiro simétrico.

Não use reversed() em um comparador encadeado esperando que apenas a última chave seja invertida — reversed() inverte a ordenação inteira, todas as chaves da cadeia.

Comparable vs Comparator nas APIs do JDK

Muitos métodos vêm em dois sabores — um que usa ordenação natural, um que recebe um Comparator:

OperaçãoSobrecarga de ordem naturalSobrecarga com Comparator
Ordenar uma listaCollections.sort(list)Collections.sort(list, cmp)
Ordenar uma lista (moderno)list.sort(null)list.sort(cmp)
Ordenar um streamstream.sorted()stream.sorted(cmp)
Conjunto baseado em árvorenew TreeSet<>()new TreeSet<>(cmp)
Mapa baseado em árvorenew TreeMap<>()new TreeMap<>(cmp)
Min/maxCollections.min(list)Collections.min(list, cmp)
Busca bináriaCollections.binarySearch(list, key)Collections.binarySearch(list, key, cmp)
PriorityQueueordenação natural do tipo de elementoconstrutor recebe um Comparator

As formas de ordem natural exigem que o tipo de elemento implemente Comparable. Se o seu não implementar e você chamá-las assim mesmo, você verá um ClassCastException em tempo de execução — não um erro de compilação — porque o cast acontece dentro da implementação de ordenação.

Um exemplo prático: ordem natural, comparadores personalizados, chaves encadeadas, nulos

O programa abaixo define um record com uma ordem natural (Comparable) mais três ordenações externas: por uma única chave, por chaves encadeadas com uma secundária invertida, e uma que tolera entradas null.

java— editable, runs on the server

O que extrair da execução:

  • A implementação de Comparable ordenou por nome e desempatou nomes por idade. Nenhum comparador explícito foi necessário — a ordenação natural é o padrão para Collections.sort e similares.
  • Comparator.comparingDouble(Person::salary) é mais curto e mais rápido do que escrever (a, b) -> Double.compare(a.salary(), b.salary()) porque evita boxing.
  • O comparador encadeado ordenou primariamente por idade e usou reversed() somente na parte de salário — esse é o padrão correto quando você quer direções diferentes em chaves diferentes. Compare com chamar .reversed() em toda a cadeia, o que inverteria ambas as chaves.
  • nullsFirst permitiu que o comparador lidasse com uma lista que continha entradas null sem um NullPointerException. Sem esse wrapper, a primeira comparação envolvendo um null teria falhado.
  • O "truque da subtração" produziu a resposta errada para Integer.MAX_VALUE - (-1): esse cálculo sofre overflow e resulta em um número negativo, então bad relata que MAX_VALUE é menor que -1. Integer.compare produz o sinal correto sempre. Sempre prefira-o.

O que vem a seguir

Agora você tem iteração (Iterator / ListIterator) e ordenação (Comparable / Comparator) cobertos. O próximo capítulo une ambos na classe utilitária java.util.Collections — a caixa de ferramentas estática de sort, search, reverse, shuffle, min, max e métodos "envolva esta coleção como imutável" que operam em qualquer List, Set ou Map. Depois disso, dois capítulos curtos aprofundam especificamente a ordenação e a busca.

Prática

Prática
Você escreve `list.sort((a, b) -> a.scoreDifference(b))` onde `scoreDifference` retorna `a.score - b.score` como um `int`. A lista contém pontuações incluindo `Integer.MAX_VALUE` e `Integer.MIN_VALUE`, e o resultado está claramente errado. Qual é a correção?
Você escreve `list.sort((a, b) -> a.scoreDifference(b))` onde `scoreDifference` retorna `a.score - b.score` como um `int`. A lista contém pontuações incluindo `Integer.MAX_VALUE` e `Integer.MIN_VALUE`, e o resultado está claramente errado. Qual é a correção?
Was this page helpful?