Como definir a ordem dos seus objetos, ter vários critérios de ordenação para a mesma classe, ordenar texto como uma pessoa espera e aproveitar as coleções que se mantêm ordenadas.
"Listas, Conjuntos e Mapas" terminou perguntando como um TreeSet sabe em que ordem colocar os elementos. Com números e textos, a resposta é a ordem natural deles. Mas uma turma de alunos pode ser ordenada por nome, por nota ou por idade, conforme a necessidade, e a ordem natural dos textos põe "Banana" antes de "abacaxi" e "árvore" depois de "uva", o que nenhum leitor espera. Esta aula mostra as duas ferramentas do Java para definir ordens, Comparable e Comparator. No caminho, apresenta as classes aninhadas e anônimas, e depois explora as coleções ordenadas e a fila de prioridade.
Comparable: a ordem natural de um tipo
Uma classe declara a sua ordem natural implementando Comparable<T>, que você já usou como limite em "Escreva uma Vez para Todo Tipo". O único método, compareTo, devolve um número negativo se este objeto vem antes do outro, zero se são equivalentes na ordem, e positivo se vem depois:
public record Aluno(String nome, int idade, double nota) implements Comparable<Aluno> {
@Override
public int compareTo(Aluno outro) {
return nome.compareTo(outro.nome);
}
}
A ordem natural de um Aluno é por nome, e a implementação simplesmente delega ao compareTo de String. Com isso, uma lista de alunos pode ser ordenada sem mais nada:
List<Aluno> turma = new ArrayList<>(List.of(
new Aluno("Davi", 19, 8.5),
new Aluno("ana", 22, 9.0),
new Aluno("Bia", 20, 7.0),
new Aluno("Caio", 19, 9.0)
));
Collections.sort(turma);
Ordem natural: Bia(7.0,20) Caio(9.0,19) Davi(8.5,19) ana(9.0,22)
A "ana" com minúscula ficou por último. O compareTo de String compara os códigos Unicode dos caracteres, e todas as maiúsculas vêm antes das minúsculas. Isso será resolvido mais adiante.
Muitas classes da biblioteca são Comparable: String, os números embrulhados, os enums (na ordem de declaração) e as datas do pacote java.time. Por isso TreeSet<String> e Arrays.sort com números funcionavam sem nenhum código extra.
Compare sem subtrair
Uma forma tentadora de comparar inteiros é subtrair um do outro, porque o resultado tem o sinal certo:
public int compare(Integer a, Integer b) {
return a - b; // ERRADO
}
Ordenando [-2000000000, 2000000000, 0] com isso, o resultado é [0, 2000000000, -2000000000], fora de ordem. A subtração 2000000000 - (-2000000000) estoura o int e dá a volta para um número negativo, o overflow de "Cada Valor em sua Caixa" de novo. Use sempre Integer.compare(a, b), Long.compare ou Double.compare, que comparam sem fazer conta.
Comparator: outros critérios
Uma classe tem uma só ordem natural, mas os programas precisam de várias ordens. Um Comparator<T> é um objeto que sabe comparar dois T, de fora da classe:
public interface Comparator<T> {
int compare(T a, T b);
// ... e vários métodos default
}
Para ordenar alunos por nota, do maior para o menor, criamos uma classe que implementa Comparator<Aluno>. Como ela só interessa ao programa que ordena, pode ser declarada dentro dele.
Classes aninhadas estáticas
public class Ordenar {
static class PorNotaDecrescente implements Comparator<Aluno> {
@Override
public int compare(Aluno a, Aluno b) {
return Double.compare(b.nota(), a.nota());
}
}
public static void main(String[] args) {
// ... turma criada como antes
turma.sort(new PorNotaDecrescente());
}
}
Por nota: Caio(9.0,19) ana(9.0,22) Davi(8.5,19) Bia(7.0,20)
Uma classe aninhada estática é uma classe comum declarada dentro de outra, só para organização: ela fica agrupada com quem a usa, e pode ser private para ficar invisível fora dali. De fora, seu nome completo é Ordenar.PorNotaDecrescente. O compilador gera um arquivo Ordenar$PorNotaDecrescente.class, o mesmo $ que apareceu em "Moldes e Instâncias". As classes Veiculo, Carro e Taxi de "Do Geral ao Específico" eram desse tipo. Para inverter a ordem, a comparação foi feita de b para a.
Classes internas
Sem o static, a classe aninhada vira uma classe interna: cada instância dela fica ligada a uma instância da classe de fora e pode acessar os campos dela, inclusive os privados:
public class Loja {
private final String prefixo;
Loja(String prefixo) { this.prefixo = prefixo; }
class Etiqueta {
private final String texto;
Etiqueta(String texto) { this.texto = texto; }
String rotulo() { return prefixo + "-" + texto; } // usa o campo da Loja
}
}
Criar uma Etiqueta exige uma Loja: loja.new Etiqueta("001"), com uma sintaxe estranha que já denuncia o vínculo. rotulo() devolve "LJ-001". Classes internas são úteis quando o objeto de dentro realmente pertence ao de fora, como os iteradores das coleções, que precisam acessar os dados internos da lista que percorrem. Mas a ligação escondida tem custo: cada objeto interno guarda uma referência ao externo, e isso mantém o externo vivo na memória. Prefira aninhadas estáticas, a menos que precise do acesso à instância de fora.
Classes anônimas
Quando o comparador é usado num único lugar, dar nome a ele é cerimônia. Uma classe anônima declara e instancia a classe na mesma expressão:
Comparator<Aluno> porIdade = new Comparator<Aluno>() {
@Override
public int compare(Aluno a, Aluno b) {
return Integer.compare(a.idade(), b.idade());
}
};
turma.sort(porIdade);
Por idade: Caio(9.0,19) Davi(8.5,19) Bia(7.0,20) ana(9.0,22)
Lê-se: "crie um objeto de uma classe sem nome que implementa Comparator<Aluno>, com este corpo". O compilador gera um arquivo Ordenar$1.class, com um número no lugar do nome. As constantes da Operacao de "Dados sem Cerimônia", com corpos próprios, eram classes anônimas.
Classes anônimas e internas podem usar variáveis locais do método em que foram declaradas, desde que essas variáveis sejam final ou efetivamente finais, isto é, nunca reatribuídas depois da inicialização. Reatribuir dá erro:
Efetivo.java:5: error: local variables referenced from an inner class must be final or effectively final
O motivo é que a classe anônima guarda uma cópia do valor. Se a variável pudesse mudar depois, a cópia e a original divergiriam em silêncio. A regra impede essa confusão, e vai reaparecer com as lambdas.
Combinando comparadores
Os comparadores podem ser combinados por métodos default da própria interface, sem nenhuma classe nova:
turma.sort(new PorNotaDecrescente().thenComparing(porIdade));
IO.println("Nota, idade: " + nomes(turma));
turma.sort(porIdade.reversed());
IO.println("Idade desc: " + nomes(turma));
turma.sort(Comparator.naturalOrder());
turma.sort(Comparator.reverseOrder());
Nota, idade: Caio(9.0,19) ana(9.0,22) Davi(8.5,19) Bia(7.0,20)
Idade desc: ana(9.0,22) Bia(7.0,20) Caio(9.0,19) Davi(8.5,19)
thenComparing(outro)desempata: se o primeiro critério considera dois alunos equivalentes, como Caio e ana, com 9,0, o segundo decide.reversed()inverte qualquer comparador.Comparator.naturalOrder()ereverseOrder()usam ocompareToda classe, na ordem normal ou invertida.
A ordenação do Java é estável: elementos equivalentes pelo critério mantêm a ordem relativa que tinham antes. Isso permite ordenar em etapas, mas thenComparing deixa a intenção muito mais clara.
As classes anônimas funcionam, mas são verbosas: seis linhas para dizer "compare pela idade". A partir de "Comportamento como Valor", a mesma ordenação da tabela acima vai caber numa linha legível:
turma.sort(Comparator.comparingDouble(Aluno::nota).reversed().thenComparingInt(Aluno::idade));
Por enquanto, guarde a forma. Os :: e o que eles significam são o assunto daquela aula.
Ordenando texto para pessoas
A ordem natural de String é a ordem dos códigos Unicode, adequada para computadores e inadequada para pessoas:
List<String> palavras = new ArrayList<>(List.of(
"ônibus", "abacaxi", "Banana", "árvore", "uva", "Égua", "caju"));
Collections.sort(palavras);
// [Banana, abacaxi, caju, uva, Égua, árvore, ônibus]
palavras.sort(String.CASE_INSENSITIVE_ORDER);
// [abacaxi, Banana, caju, uva, árvore, Égua, ônibus]
palavras.sort(java.text.Collator.getInstance(Locale.of("pt", "BR")));
// [abacaxi, árvore, Banana, caju, Égua, ônibus, uva]
O CASE_INSENSITIVE_ORDER resolve as maiúsculas, mas as letras acentuadas continuam no fim, porque seus códigos são maiores que os de todas as letras sem acento. Um Collator é um comparador que conhece as regras de ordenação de um idioma: trata "á" como uma variação de "a" e ignora a diferença entre maiúsculas e minúsculas no primeiro nível de comparação. É a resposta à pendência de "Muitos Valores sob um Nome". Para qualquer lista exibida a um usuário, como nomes de clientes ou cidades, use um Collator com o idioma certo.
Coleções que se mantêm ordenadas
O TreeSet e o TreeMap mantêm os elementos sempre ordenados, pela ordem natural ou por um Comparator passado no construtor. Por dentro, são árvores rubro-negras, árvores de busca binária que se reequilibram a cada inserção. Por isso, buscar, inserir e remover custam O(log n), como a tabela da aula anterior mostrou.
A vantagem que só elas têm é a navegação por ordem: encontrar o elemento imediatamente abaixo ou acima de um valor, ou uma faixa inteira. Um exemplo, com uma tabela de frete por peso (valores fictícios):
TreeMap<Integer, Double> fretePorPeso = new TreeMap<>();
fretePorPeso.put(0, 12.90); // de 0 kg em diante
fretePorPeso.put(1, 18.50); // de 1 kg em diante
fretePorPeso.put(5, 29.90);
fretePorPeso.put(10, 44.00);
fretePorPeso.put(30, 89.00);
for (int gramas : new int[] {300, 1000, 7500, 45000}) {
int kg = gramas / 1000;
IO.println(gramas + " g -> R$ " + fretePorPeso.floorEntry(kg).getValue());
}
300 g -> R$ 12.9
1000 g -> R$ 18.5
7500 g -> R$ 29.9
45000 g -> R$ 89.0
floorEntry(k) devolve a entrada com a maior chave menor ou igual a k, exatamente a faixa em que o peso se encaixa. Sem ela, seria uma cadeia de if que precisa mudar a cada alteração da tabela. A família completa de métodos de navegação:
| Método | Devolve |
|---|---|
floorKey(k) |
a maior chave ≤ k |
ceilingKey(k) |
a menor chave ≥ k |
lowerKey(k) / higherKey(k) |
a maior chave < k / a menor chave > k |
firstKey() / lastKey() |
a menor / a maior chave |
headMap(k) / tailMap(k) |
as entradas com chave < k / ≥ k |
Os métodos com Entry no nome, como floorEntry, devolvem o par completo. O TreeSet tem os equivalentes para elementos: floor, ceiling, first, last, headSet, tailSet e descendingSet().
A armadilha da consistência com equals
As coleções ordenadas usam o compareTo ou o comparador no lugar do equals para decidir se dois elementos são o mesmo. Se o critério de ordem não bate com a igualdade, o resultado surpreende:
record Aluno(String nome, double nota) { }
Set<Aluno> ranking = new TreeSet<>(porNota); // porNota compara só a nota
ranking.add(new Aluno("Ana", 9.0));
ranking.add(new Aluno("Caio", 9.0));
ranking.add(new Aluno("Bia", 7.0));
IO.println(ranking.size() + " " + ranking);
2 [Aluno[nome=Bia, nota=7.0], Aluno[nome=Ana, nota=9.0]]
O Caio sumiu. Para o comparador, um aluno com nota 9,0 é equivalente à Ana, e um conjunto não aceita equivalentes. Pior: ranking.contains(new Aluno("Zeca", 9.0)) devolve true, para um aluno que nunca foi inserido. A documentação chama isso de comparador inconsistente com equals. A solução é desempatar até que só elementos realmente iguais sejam equivalentes, com porNota.thenComparing(...) pelo nome. Guarde essa armadilha: ela aparece de novo com o BigDecimal em "Tempo, Dinheiro e Padrões", em que 2.0 e 2.00 são iguais pelo compareTo e diferentes pelo equals.
PriorityQueue: o mais importante primeiro
Uma fila de prioridade entrega sempre o elemento de maior prioridade, segundo um comparador, independentemente da ordem de chegada. Uma triagem de pronto-socorro:
record Paciente(String nome, int gravidade) { }
Queue<Paciente> triagem = new PriorityQueue<>(new Comparator<Paciente>() {
@Override
public int compare(Paciente a, Paciente b) {
return Integer.compare(b.gravidade(), a.gravidade()); // mais grave primeiro
}
});
triagem.offer(new Paciente("Ana", 2));
triagem.offer(new Paciente("Bruno", 5));
triagem.offer(new Paciente("Carla", 3));
triagem.offer(new Paciente("Davi", 5));
while (!triagem.isEmpty()) {
IO.print(triagem.poll().nome() + " ");
}
// Bruno Davi Carla Ana
Bruno saiu antes de Davi por acaso: entre elementos de mesma prioridade, a fila não garante ordem nenhuma, e quem precisar da ordem de chegada deve desempatar no comparador. Por dentro, a PriorityQueue é um heap binário, uma estrutura que mantém só o suficiente de ordem para achar o mais prioritário rapidamente: inserir e retirar custam O(log n). O mais prioritário está sempre no topo, mas o resto não está ordenado, e imprimir a fila mostra isso: new PriorityQueue<>(List.of(5, 1, 4, 2, 3)) imprime [1, 2, 4, 5, 3]. Só as retiradas sucessivas com poll saem em ordem. Filas de prioridade aparecem em agendadores de tarefas, simulações e algoritmos de caminho mínimo, como o de rotas em mapas.
Todos os exemplos até aqui supuseram que as coisas dão certo. O Comparator recebe dois alunos válidos, o mapa tem a chave procurada, o texto digitado é um número. Mas em várias aulas você viu programas pararem com mensagens como NumberFormatException, IllegalArgumentException e ConcurrentModificationException, e em "O que Fica Dentro, o que Fica Fora" você mesmo lançou uma. Até agora, toda exceção encerrava o programa.
Fontes e leituras recomendadas
- Documentação de
Comparator(Java SE 25): https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Comparator.html. Todos os métodos de combinação, incluindo os que usam lambdas, que aparecem em "Comportamento como Valor". - Documentação de
Comparable: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Comparable.html. O contrato docompareToe a explicação da consistência comequals. - Oracle, The Java Tutorials, "Nested Classes": https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html. As classes aninhadas estáticas, internas, locais e anônimas, com exemplos de cada uma.
- Joshua Bloch, Effective Java, 3ª edição, itens 14 e 24: "Consider implementing Comparable" e "Favor static member classes over nonstatic". O item 14 traz o erro da comparação por subtração.
- Documentação de
java.text.Collator: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/text/Collator.html. A ordenação sensível ao idioma e os níveis de comparação (primário, secundário, terciário).
Exercícios
Exercício 1
Crie um record Versao(int maior, int menor, int correcao) que implemente Comparable<Versao>, de modo que 1.10.0 venha depois de 1.9.5, e 2.0.0 depois de ambas. Teste ordenando uma lista.
Ver resposta
✓ Resposta:
record Versao(int maior, int menor, int correcao) implements Comparable<Versao> {
@Override
public int compareTo(Versao outra) {
int c = Integer.compare(maior, outra.maior);
if (c != 0) {
return c;
}
c = Integer.compare(menor, outra.menor);
if (c != 0) {
return c;
}
return Integer.compare(correcao, outra.correcao);
}
}
A comparação é lexicográfica: o primeiro campo decide, e os seguintes só desempatam. Ordenando [Versao(2,0,0), Versao(1,10,0), Versao(1,9,5)], a ordem é 1.9.5, 1.10.0, 2.0.0. É exatamente o erro que a comparação de textos cometeria, já que "1.10.0" vem antes de "1.9.5" alfabeticamente, porque o caractere "1" vem antes de "9". Usar Integer.compare, e não subtração, evita o overflow. Como o record gera um equals baseado nos mesmos três campos, a ordem natural é consistente com equals.
Exercício 2
Um record Produto(String nome, double preco) precisa ser exibido em ordem crescente de preço e, em caso de empate, por nome em ordem alfabética. Escreva os comparadores como classes anônimas e combine-os.
Ver resposta
✓ Resposta:
Comparator<Produto> porPreco = new Comparator<>() {
@Override
public int compare(Produto a, Produto b) {
return Double.compare(a.preco(), b.preco());
}
};
Comparator<Produto> porNome = new Comparator<>() {
@Override
public int compare(Produto a, Produto b) {
return a.nome().compareTo(b.nome());
}
};
produtos.sort(porPreco.thenComparing(porNome));
O diamante new Comparator<>() funciona em classes anônimas desde o Java 9. thenComparing aplica o segundo critério só quando o primeiro devolve zero. Com produtos de mesmo preço, o nome decide. Se os nomes fossem exibidos a usuários, porNome deveria usar um Collator em vez de compareTo, para lidar com maiúsculas e acentos, e o Collator é ele próprio um comparador de textos que pode ser usado dentro do compare.
Exercício 3
Explique por que o TreeSet do exemplo do ranking perdeu o Caio, e corrija o comparador para que alunos diferentes com a mesma nota convivam no conjunto, ordenados por nota e depois por nome.
Ver resposta
✓ Resposta: O TreeSet não usa equals para detectar duplicatas: usa o comparador. Para porNota, dois alunos com nota 9,0 são equivalentes (o compare devolve zero), e o conjunto rejeita o segundo, como faria com uma duplicata de verdade. O comparador é inconsistente com o equals do record, que considera Ana e Caio diferentes. A correção é desempatar até a equivalência coincidir com a igualdade:
Comparator<Aluno> porNome = new Comparator<>() {
@Override
public int compare(Aluno a, Aluno b) {
return a.nome().compareTo(b.nome());
}
};
Set<Aluno> ranking = new TreeSet<>(porNota.thenComparing(porNome));
Agora Ana e Caio convivem, na ordem Bia, Ana, Caio. Só dois alunos com mesmo nome e mesma nota seriam equivalentes, e esses são iguais pelo equals do record. A regra: um comparador usado em coleções ordenadas deve comparar todos os campos que o equals compara.
Exercício 4
Um colega escreveu um comparador de idades como return a.idade() - b.idade(); e diz que "sempre funcionou". Em que condição ele falha, e por que é melhor corrigir mesmo que idades nunca cheguem a esse ponto?
Ver resposta
✓ Resposta: Ele falha quando a diferença entre os dois valores não cabe num int, como com valores perto de ±2 bilhões: a subtração estoura e troca de sinal, e o comparador passa a dizer que o maior é menor. Com idades reais, isso nunca acontece. Mesmo assim, vale corrigir para Integer.compare(a.idade(), b.idade()), por três motivos. O código é copiado: o mesmo padrão reaparece depois para comparar saldos em centavos, códigos ou identificadores, e aí o defeito aparece. Integer.compare expressa a intenção de forma mais clara do que um truque aritmético. E um comparador errado não dá erro: produz ordenações incorretas em silêncio, ou até faz o algoritmo de ordenação lançar "Comparison method violates its general contract", uma das mensagens mais confusas da biblioteca. O custo da correção é zero.
Exercício 5
IO.println(new PriorityQueue<>(List.of(5, 1, 4, 2, 3))) mostra [1, 2, 4, 5, 3], que não está ordenado. Explique por quê, e mostre como obter os elementos em ordem a partir da fila.
Ver resposta
✓ Resposta: A PriorityQueue é um heap binário. A única garantia dele é que cada elemento é menor ou igual aos seus "filhos" na árvore, o que põe o menor no topo, mas não ordena o resto. O toString percorre o array interno na ordem em que os elementos estão guardados, que é a ordem do heap, e não a ordem de valor. Manter tudo ordenado a cada inserção custaria mais, e a fila de prioridade só promete entregar o menor elemento. Para obter todos em ordem, retira-se um por um:
PriorityQueue<Integer> pq = new PriorityQueue<>(List.of(5, 1, 4, 2, 3));
while (!pq.isEmpty()) {
IO.print(pq.poll() + " "); // 1 2 3 4 5
}
Cada poll remove o topo e reorganiza o heap em O(log n), e o conjunto das retiradas sai ordenado. Esse é, aliás, um algoritmo de ordenação completo, o heapsort. Se a intenção for só ter uma lista ordenada, é mais simples copiar para um ArrayList e usar sort.