1. Introdução
Na vida, raramente basta uma única maneira de comparar objetos. Imagine que você tem uma lista de usuários: às vezes você quer ordená-los por nome, às vezes – por idade, e às vezes – pelo comprimento do sobrenome. Ou você tem uma classe da qual você não é o autor, e não dá para adicionar compareTo a ela. É justamente para esses casos que existe, em Java, a interface Comparator.
Quando Comparable não é suficiente
- A classe não pode ser modificada (por exemplo, ela vem de uma biblioteca de terceiros).
- São necessários vários critérios de ordenação (por campos diferentes).
- Você quer separar a lógica de comparação da própria classe (por exemplo, ordenar de formas diferentes em partes distintas do programa).
Analogia
Se Comparable é a “ordem natural” embutida do objeto, então Comparator é um árbitro externo que pode avaliar seus objetos por qualquer critério: hoje por nome, amanhã – por idade, depois de amanhã – pelo comprimento do nome.
2. Interface Comparator: sintaxe e contrato
Declaração da interface
public interface Comparator<T> {
int compare(T o1, T o2);
}
O método compare deve retornar:
- Número negativo, se o primeiro objeto for “menor” que o segundo.
- 0, se forem iguais.
- Número positivo, se o primeiro for “maior” que o segundo.
O contrato é o mesmo que o de Comparable; a diferença é que comparam-se dois objetos, e não “o atual” e “o outro” via compareTo.
Exemplo: comparador para ordenar por sobrenome
Suponha que temos a classe Person:
public class Person {
private String firstName;
private String lastName;
private int age;
// Construtor e getters
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
}
Vamos criar um comparador que ordena por sobrenome:
import java.util.Comparator;
public class LastNameComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return a.getLastName().compareTo(b.getLastName());
}
}
Observação: o método compareTo em strings (String) as compara em ordem alfabética.
3. Uso de Comparator: ordenando coleções
Ordenação com comparador
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Anna", "Kostetskaya", 25));
people.add(new Person("Boris", "Novak", 20));
people.add(new Person("Viktoriya", "Bell", 22));
// Ordenação por sobrenome
Collections.sort(people, new LastNameComparator());
for (Person p : people) {
System.out.println(p.getLastName() + " " + p.getFirstName());
}
}
}
Resultado:
Novak Boris
Kostetskaya Anna
Bell Viktoriya
Ordenação por idade com um comparador
Mesmo se a classe já implementa Comparable por nome, é possível criar um comparador separado – por idade:
public class AgeComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return Integer.compare(a.getAge(), b.getAge());
}
}
E usar de forma análoga:
Collections.sort(people, new AgeComparator());
Resultado:
Boris Novak (20)
Viktoriya Bell (22)
Anna Kostetskaya (25)
Exemplo: escolher o comparador “na hora”
Collections.sort(people, new LastNameComparator()); // Por sobrenome
Collections.sort(people, new AgeComparator()); // Por idade
4. Classes anônimas e expressões lambda
É possível criar comparadores “na hora”, sem declarar classes separadas.
Classe anônima
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person a, Person b) {
return a.getFirstName().compareTo(b.getFirstName());
}
});
Expressão lambda
Collections.sort(people, (a, b) -> a.getFirstName().compareTo(b.getFirstName()));
Ou ainda mais curto com o método da lista List.sort:
people.sort((a, b) -> a.getFirstName().compareTo(b.getFirstName()));
- Classes anônimas – abordagem antiga, verbosa.
- Lambda – moderno e compacto.
5. Exemplos: ordenação por diferentes critérios
Ordenação pelo comprimento do sobrenome
Comparator<Person> byLastNameLength = (a, b) ->
Integer.compare(a.getLastName().length(), b.getLastName().length());
people.sort(byLastNameLength);
Ordenação por idade, depois por nome (múltiplos níveis)
Comparator<Person> byAgeThenName = (a, b) -> {
int cmp = Integer.compare(a.getAge(), b.getAge());
if (cmp != 0) return cmp;
return a.getFirstName().compareTo(b.getFirstName());
};
people.sort(byAgeThenName);
Uso de um comparador para busca (exemplo)
O comparador se aplica não só à ordenação, mas também à busca em coleções já ordenadas:
// a lista people deve estar ordenada por idade!
Person key = new Person("?", "?", 22);
int idx = Collections.binarySearch(people, key, new AgeComparator());
if (idx >= 0) {
System.out.println("Pessoa com 22 anos encontrada: " + people.get(idx));
}
6. Boas práticas e particularidades ao usar Comparator
Não viole o contrato
- Se compare(a, b) retornar 0, então compare(b, a) também deve retornar 0.
- Se compare(a, b) > 0, então compare(b, a) < 0.
- Leve em conta possíveis valores null (veja abaixo).
Não se esqueça de equals e hashCode
Embora os comparadores comparem objetos “do seu jeito”, para estruturas como TreeSet ou ao buscar chaves em TreeMap é importante que a lógica de comparação do comparador esteja alinhada com equals. Caso contrário, podem surgir resultados inesperados: dois objetos diferentes são considerados iguais pelo comparador, mas não são iguais por equals.
Ordenação considerando null
Se os campos puderem ser null, use “helpers” prontos:
Comparator<Person> byLastNameNullSafe = Comparator.comparing(
Person::getLastName,
Comparator.nullsLast(String::compareTo)
);
people.sort(byLastNameNullSafe);
7. Nuances úteis
Tabela: Comparação de Comparable e Comparator
| Comparable | Comparator | |
|---|---|---|
| Onde é implementado? | Na própria classe | Em classe separada/lambda |
| Método | |
|
| Quantas variações? | Apenas uma “natural” | Quantas forem necessárias, para quaisquer necessidades |
| Aplicação | |
|
| Funciona para classes de terceiros? | Não | Sim |
Exemplo: ordenação em ordem decrescente
É possível inverter a ordem manualmente:
Comparator<Person> byAgeDesc = (a, b) -> Integer.compare(b.getAge(), a.getAge());
people.sort(byAgeDesc);
Ou com reversed():
Comparator<Person> byAge = Comparator.comparingInt(Person::getAge);
people.sort(byAge.reversed());
8. Erros comuns ao trabalhar com Comparator
Erro nº 1: violação do contrato de comparação. Se você esquecer que compare(a, b) e compare(b, a) devem ter sinais opostos, ou se retornar valores arbitrários (por exemplo, apenas a diferença — a.getAge() - b.getAge(), o que pode causar overflow), o resultado será imprevisível. Use Integer.compare, e não subtração – é mais seguro.
Erro nº 2: ignorar valores null. Se os campos pelos quais você compara puderem ser null, trate esse caso (por exemplo, com Comparator.nullsFirst/Comparator.nullsLast), caso contrário é fácil obter NullPointerException no momento mais inesperado.
Erro nº 3: critérios de ordenação instáveis. Se o comparador retorna valores diferentes para os mesmos objetos (por exemplo, usa um número aleatório ou um campo altamente mutável), a ordenação pode se comportar de forma caótica.
Erro nº 4: inconsistência com equals. Se compare(a, b) == 0, mas a.equals(b) é false, coleções como TreeSet e TreeMap podem não funcionar como você espera. O ideal é que a igualdade pelo comparador e por equals coincidam.
Erro nº 5: ordenar classes de terceiros sem um comparador. Se você tentar ordenar objetos de uma classe “alheia” sem Comparable e sem fornecer um Comparator, obterá um erro de compilação. Forneça um comparador explícito.
GO TO FULL VERSION