Ordenando Objetos em Java: Interfaces Comparable e Comparator
Coleções ordenadas em Java, como a TreeSet
, conseguem organizar elementos de tipos padrão (como String
e Integer
) automaticamente. Mas como uma coleção pode ordenar objetos de uma classe que você mesmo criou, como uma classe Person
? Para que o Java entenda como comparar e classificar objetos personalizados, a classe precisa seguir um contrato de comparação. Esse contrato é definido através de duas interfaces fundamentais: Comparable
e Comparator
.
class Person {
private String name;
Person(String name) {
this.name = name;
}
String getName() {
return name;
}
}
Não seria possível tipar um objeto TreeSet
com essa classe, pois, ao adicionar objetos, o TreeSet
não saberia como compará-los. O código a seguir não funcionaria:
TreeSet<Person> people = new TreeSet<>();
people.add(new Person("Tom"));
Ao executar esse código, ocorreria um erro indicando que o objeto Person
não pode ser convertido para o tipo java.lang.Comparable
.
Para que objetos Person
possam ser comparados e ordenados, a classe deve implementar a interface Comparable<E>
. Ao implementar, a interface é tipada com a classe atual. A implementação na classe Person
fica assim:
class Person implements Comparable<Person> {
private String name;
Person(String name) {
this.name = name;
}
String getName() {
return name;
}
public int compareTo(Person p) {
return name.compareTo(p.getName());
}
}
A interface Comparable
contém apenas o método int compareTo(E item)
, que compara o objeto atual com o objeto passado como parâmetro. Se o método retornar um número negativo, o objeto atual fica antes do passado por parâmetro. Se retornar um número positivo, o objeto atual fica depois. Se retornar zero, os objetos são iguais.
Nesse caso, a comparação se baseia no mecanismo interno da classe String
. Também é possível definir uma lógica própria, como comparar pelo comprimento do nome:
public int compareTo(Person p) {
return name.length() - p.getName().length();
}
Agora, o TreeSet
pode ser tipado com Person
e objetos correspondentes podem ser adicionados:
TreeSet<Person> people = new TreeSet<>();
people.add(new Person("Tom"));
Interface Comparator
Pode surgir um problema se o desenvolvedor não implementou a interface Comparable
na classe desejada, ou se a implementação existente não atende às necessidades e precisa ser alterada. Para isso, existe uma abordagem mais flexível com a interface Comparator<E>
.
A interface Comparator
contém vários métodos, mas o principal é o método compare()
:
public interface Comparator<E> {
int compare(E a, E b);
// outros métodos
}
O método compare
retorna um valor numérico: negativo se o objeto a
precede b
, positivo se a
segue b
, e zero se são iguais. Para aplicar a interface, primeiro cria-se uma classe comparadora que a implementa:
class PersonComparator implements Comparator<Person> {
public int compare(Person a, Person b) {
return a.getName().compareTo(b.getName());
}
}
Aqui, a comparação ocorre por strings novamente. Agora, a classe comparadora é usada para criar o objeto TreeSet
:
PersonComparator pcomp = new PersonComparator();
TreeSet<Person> people = new TreeSet<>(pcomp);
people.add(new Person("Tom"));
people.add(new Person("Nick"));
people.add(new Person("Alice"));
people.add(new Person("Bill"));
for (Person p : people) {
System.out.println(p.getName());
}
Para criar o TreeSet
, usa-se uma versão do construtor que recebe o comparador como parâmetro. Independentemente de a classe Person
implementar Comparable
, a lógica de comparação e ordenação segue a definida na classe comparadora.
Ordenação por vários critérios
No Java, vários comparadores podem ser aplicados em sequência, com base em prioridade. Por exemplo, a classe Person
é alterada para incluir idade:
class Person {
private String name;
private int age;
public Person(String n, int a) {
name = n;
age = a;
}
String getName() {
return name;
}
int getAge() {
return age;
}
}
Aqui, um campo armazena a idade do usuário. Suponha que os usuários precisam ser ordenados por nome e por idade. Para isso, definem-se dois comparadores:
class PersonNameComparator implements Comparator<Person> {
public int compare(Person a, Person b) {
return a.getName().compareTo(b.getName());
}
}
class PersonAgeComparator implements Comparator<Person> {
public int compare(Person a, Person b) {
if (a.getAge() > b.getAge()) {
return 1;
} else if (a.getAge() < b.getAge()) {
return -1;
} else {
return 0;
}
}
}
A interface comparadora define o método padrão thenComparing
, que permite encadear comparadores para ordenar o conjunto:
Comparator<Person> pcomp = new PersonNameComparator().thenComparing(new PersonAgeComparator());
TreeSet<Person> people = new TreeSet<>(pcomp);
people.add(new Person("Tom", 23));
people.add(new Person("Nick", 34));
people.add(new Person("Tom", 10));
people.add(new Person("Bill", 14));
for (Person p : people) {
System.out.println(p.getName() + " " + p.getAge());
}
A saída no console mostra:
Bill 14 Nick 34 Tom 10 Tom 23
Nesse caso, a ordenação ocorre primeiro por nome e depois por idade.
Resumo
Comparable
: Interface implementada na classe para permitir comparação e ordenação natural, com o métodocompareTo
.Comparator
: Interface flexível para definir comparações personalizadas, útil quandoComparable
não está disponível ou precisa de ajuste.- Ordenação por vários critérios: O método
thenComparing
permite encadear comparadores, aplicando prioridades na ordenação.
📝 Exercícios
Tarefa 1: Ordenação Natural de Produtos (Usando Comparable
)
Descrição: Você tem uma classe Produto
. Faça com que ela implemente a interface Comparable
para que os produtos sejam ordenados naturalmente pelo preço, do menor para o maior.
Código inicial:
import java.util.TreeSet;
class Produto implements Comparable<Produto> {
private String nome;
private double preco;
public Produto(String nome, double preco) {
this.nome = nome;
this.preco = preco;
}
public double getPreco() { return this.preco; }
public String getNome() { return this.nome; }
@Override
public String toString() {
return "Produto: " + nome + ", Preço: R$" + preco;
}
// Implemente o método compareTo aqui
}
public class Main {
public static void main(String[] args) {
TreeSet<Produto> produtos = new TreeSet<>();
produtos.add(new Produto("Caneta", 1.50));
produtos.add(new Produto("Caderno", 15.00));
produtos.add(new Produto("Borracha", 0.75));
// A saída deve ser ordenada pelo preço
for (Produto p : produtos) {
System.out.println(p);
}
}
}
Resposta
// Código completo para a classe Produto e Main
import java.util.TreeSet;
class Produto implements Comparable<Produto> {
private String nome;
private double preco;
public Produto(String nome, double preco) {
this.nome = nome;
this.preco = preco;
}
public double getPreco() {
return this.preco;
}
public String getNome() {
return this.nome;
}
@Override
public String toString() {
return "Produto: " + nome + ", Preço: R$" + preco;
}
@Override
public int compareTo(Produto outro) {
// Double.compare é a forma segura de comparar doubles
return Double.compare(this.preco, outro.getPreco());
}
}
public class Main {
public static void main(String[] args) {
TreeSet<Produto> produtos = new TreeSet<>();
produtos.add(new Produto("Caneta", 1.50));
produtos.add(new Produto("Caderno", 15.00));
produtos.add(new Produto("Borracha", 0.75));
for (Produto p : produtos) {
System.out.println(p);
}
}
}
Comparable<Produto>
, somos obrigados a fornecer o método compareTo()
. A forma mais segura e recomendada para comparar tipos primitivos numéricos (como double
ou int
) é usar o método estático compare
de suas classes wrapper (Double.compare()
ou Integer.compare()
). Ele retorna -1, 0 ou 1, exatamente como o contrato de compareTo()
exige, evitando problemas com subtração de ponto flutuante. Com isso, o TreeSet
sabe como ordenar os produtos pelo preço.
Tarefa 2: Ordenação Alternativa de Alunos (Usando Comparator
)
Descrição: A classe Aluno
abaixo não pode ser modificada. Crie uma classe Comparator
externa chamada ComparadorPorMedia
que ordene os objetos Aluno
pela média, da maior para a menor (ordem decrescente).
Código inicial:
import java.util.TreeSet;
import java.util.Comparator;
// VOCÊ NÃO PODE MODIFICAR ESTA CLASSE
class Aluno {
private String nome;
private double media;
public Aluno(String nome, double media) {
this.nome = nome;
this.media = media;
}
public double getMedia() { return this.media; }
public String getNome() { return this.nome; }
@Override
public String toString() {
return "Aluno: " + nome + ", Média: " + media;
}
}
// Crie a classe ComparadorPorMedia aqui
public class Main {
public static void main(String[] args) {
// Crie uma instância do seu comparador
// Passe o comparador para o construtor do TreeSet
TreeSet<Aluno> turma = new TreeSet<>(); // Mude esta linha
turma.add(new Aluno("Carlos", 7.5));
turma.add(new Aluno("Ana", 9.5));
turma.add(new Aluno("Beatriz", 8.0));
// A saída deve ser ordenada pela média, da maior para a menor
for(Aluno a : turma) {
System.out.println(a);
}
}
}
Resposta
// Código completo para as classes Aluno, ComparadorPorMedia e Main
import java.util.TreeSet;
import java.util.Comparator;
class Aluno {
private String nome;
private double media;
public Aluno(String nome, double media) {
this.nome = nome;
this.media = media;
}
public double getMedia() { return this.media; }
public String getNome() { return this.nome; }
@Override
public String toString() {
return "Aluno: " + nome + ", Média: " + media;
}
}
// Comparator para ordenar Alunos pela média em ordem decrescente
class ComparadorPorMedia implements Comparator<Aluno> {
@Override
public int compare(Aluno a1, Aluno a2) {
// Para ordem decrescente, invertemos a comparação
return Double.compare(a2.getMedia(), a1.getMedia());
}
}
public class Main {
public static void main(String[] args) {
ComparadorPorMedia comparador = new ComparadorPorMedia();
TreeSet<Aluno> turma = new TreeSet<>(comparador);
turma.add(new Aluno("Carlos", 7.5));
turma.add(new Aluno("Ana", 9.5));
turma.add(new Aluno("Beatriz", 8.0));
for(Aluno a : turma) {
System.out.println(a);
}
}
}
Aluno
, a solução foi criar uma classe externa, ComparadorPorMedia
, que implementa Comparator<Aluno>
. Dentro do método compare
, usamos Double.compare(a2.getMedia(), a1.getMedia())
. Note a inversão de a1
e a2
: comparar a2
com a1
em vez de a1
com a2
é um truque simples para obter a ordem decrescente. Finalmente, uma instância do nosso comparador é passada para o construtor do TreeSet
, que passa a usar essa lógica para ordenar os alunos.
Tarefa 3: Ordenação por Múltiplos Critérios
Descrição: Você precisa ordenar uma lista de funcionários. O critério principal é o departamento (em ordem alfabética). Se os funcionários forem do mesmo departamento, o critério de desempate é o salário (do maior para o menor). Use Comparator.thenComparing()
para combinar as lógicas.
Código inicial:
import java.util.TreeSet;
import java.util.Comparator;
class Funcionario {
private String nome;
private String departamento;
private double salario;
public Funcionario(String n, String d, double s) {
this.nome = n;
this.departamento = d;
this.salario = s;
}
public String getNome() { return nome; }
public String getDepartamento() { return departamento; }
public double getSalario() { return salario; }
@Override
public String toString() {
return "Dep: " + departamento + ", Salário: " + salario + ", Nome: " + nome;
}
}
public class Main {
public static void main(String[] args) {
Comparator<Funcionario> comparadorFinal = null;
// Crie o comparador combinado aqui
TreeSet<Funcionario> funcionarios = new TreeSet<>(comparadorFinal);
funcionarios.add(new Funcionario("Ana", "TI", 5000));
funcionarios.add(new Funcionario("Carlos", "RH", 4500));
funcionarios.add(new Funcionario("Beatriz", "TI", 6000));
for(Funcionario f : funcionarios) {
System.out.println(f);
}
}
}
Resposta
// Código completo para as classes Funcionario e Main
import java.util.TreeSet;
import java.util.Comparator;
class Funcionario {
private String nome;
private String departamento;
private double salario;
public Funcionario(String n, String d, double s) {
this.nome = n;
this.departamento = d;
this.salario = s;
}
public String getNome() { return nome; }
public String getDepartamento() { return departamento; }
public double getSalario() { return salario; }
@Override
public String toString() {
return "Dep: " + departamento + ", Salário: " + salario + ", Nome: " + nome;
}
}
public class Main {
public static void main(String[] args) {
// Critério 1: Departamento (ordem natural da String)
Comparator<Funcionario> porDepartamento =
Comparator.comparing(Funcionario::getDepartamento);
// Critério 2: Salário (ordem decrescente)
Comparator<Funcionario> porSalarioDesc =
Comparator.comparingDouble(Funcionario::getSalario).reversed();
// Combinando os dois comparadores
Comparator<Funcionario> comparadorFinal =
porDepartamento.thenComparing(porSalarioDesc);
TreeSet<Funcionario> funcionarios = new TreeSet<>(comparadorFinal);
funcionarios.add(new Funcionario("Ana", "TI", 5000));
funcionarios.add(new Funcionario("Carlos", "RH", 4500));
funcionarios.add(new Funcionario("Beatriz", "TI", 6000));
for(Funcionario f : funcionarios) {
System.out.println(f);
}
}
}
Comparator
, que são muito mais concisos.
Comparator.comparing(Funcionario::getDepartamento)
: Cria um Comparator
que ordena os funcionários com base no valor retornado por getDepartamento()
, usando a ordem natural (alfabética).Comparator.comparingDouble(Funcionario::getSalario).reversed()
: Cria um Comparator
para double
e, em seguida, usa o método .reversed()
para inverter a ordem natural (crescente -> decrescente)..thenComparing(porSalarioDesc)
: Encadeia os dois. O TreeSet
usará porDepartamento
como critério principal. Somente se dois funcionários tiverem o mesmo departamento (como Ana e Beatriz), ele usará porSalarioDesc
para decidir a ordem entre eles.