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étodoint compareTo(T other)é a ordenação natural do tipo.Comparator<T>— um objeto externo que ordena instâncias. Seu métodoint 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:
- negativo —
avem antes deb - zero — iguais para fins de ordenação
- positivo —
avem depois deb
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:
- Anti-simétrico —
a.compareTo(b)eb.compareTo(a)têm sinais opostos. - Transitivo — se
a < beb < c, entãoa < c. - Consistente com
equals(fortemente recomendado) —a.compareTo(b) == 0ssea.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 valoresnullsejam tratados como menores que qualquer não-nulo. Útil ao ordenar coleções que podem conternull.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ção | Sobrecarga de ordem natural | Sobrecarga com Comparator |
|---|---|---|
| Ordenar uma lista | Collections.sort(list) | Collections.sort(list, cmp) |
| Ordenar uma lista (moderno) | list.sort(null) | list.sort(cmp) |
| Ordenar um stream | stream.sorted() | stream.sorted(cmp) |
| Conjunto baseado em árvore | new TreeSet<>() | new TreeSet<>(cmp) |
| Mapa baseado em árvore | new TreeMap<>() | new TreeMap<>(cmp) |
| Min/max | Collections.min(list) | Collections.min(list, cmp) |
| Busca binária | Collections.binarySearch(list, key) | Collections.binarySearch(list, key, cmp) |
PriorityQueue | ordenação natural do tipo de elemento | construtor 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.
O que extrair da execução:
- A implementação de
Comparableordenou por nome e desempatou nomes por idade. Nenhum comparador explícito foi necessário — a ordenação natural é o padrão paraCollections.sorte 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. nullsFirstpermitiu que o comparador lidasse com uma lista que continha entradasnullsem umNullPointerException. Sem esse wrapper, a primeira comparação envolvendo umnullteria 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ãobadrelata queMAX_VALUEé menor que-1.Integer.compareproduz 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.