CodeGym /Cursos /JAVA 25 SELF /Interfaz Comparator: creación y uso

Interfaz Comparator: creación y uso

JAVA 25 SELF
Nivel 29 , Lección 3
Disponible

1. Introducción

En la vida rara vez basta con una única forma de comparar objetos. Imagina que tienes una lista de usuarios: a veces quieres ordenarlos por nombre, a veces — por edad, y otras — por la longitud del apellido. O tienes una clase de la que no eres el autor, y no puedes añadirle compareTo. Precisamente para estos casos en Java existe la interfaz Comparator.

Cuándo Comparable no basta

  • No se puede modificar la clase (por ejemplo, es de una biblioteca de terceros).
  • Se necesitan múltiples formas de ordenación (por distintos campos).
  • Quieres separar la lógica de comparación de la propia clase (por ejemplo, ordenar de forma distinta en distintas partes del programa).

Analogía
Si Comparable es el «orden natural» incorporado del objeto, entonces Comparator es un juez externo que puede evaluar tus objetos con cualquier criterio: hoy por nombre, mañana — por edad, pasado mañana — por la longitud del nombre.

2. Interfaz Comparator: sintaxis y contrato

Declaración de la interfaz

public interface Comparator<T> {
    int compare(T o1, T o2);
}

El método compare debe devolver:

  • Un número negativo si el primer objeto es «menor» que el segundo.
  • 0, si son iguales.
  • Un número positivo si el primero es «mayor» que el segundo.

El contrato es el mismo que el de Comparable, solo que ahora se comparan dos objetos, y no el «actual» y el «otro» mediante compareTo.

Ejemplo: comparador para ordenar por apellido

Supongamos que tenemos la clase Person:

public class Person {
    private String firstName;
    private String lastName;
    private int age;

    // Constructor y 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; }
}

Creemos un comparador que ordene por apellido:

import java.util.Comparator;

public class LastNameComparator implements Comparator<Person> {
    @Override
    public int compare(Person a, Person b) {
        return a.getLastName().compareTo(b.getLastName());
    }
}

Nota: el método compareTo de las cadenas (String) las compara en orden alfabético.

3. Uso de Comparator: ordenación de colecciones

Ordenación con un 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));

        // Ordenación por apellido
        Collections.sort(people, new LastNameComparator());

        for (Person p : people) {
            System.out.println(p.getLastName() + " " + p.getFirstName());
        }
    }
}

Resultado:

Novak Boris
Kostetskaya Anna
Bell Viktoriya

Ordenación por edad con un comparador

Aunque la clase ya implemente Comparable por nombre, se puede crear un comparador aparte — por edad:

public class AgeComparator implements Comparator<Person> {
    @Override
    public int compare(Person a, Person b) {
        return Integer.compare(a.getAge(), b.getAge());
    }
}

Y usarlo de forma análoga:

Collections.sort(people, new AgeComparator());

Resultado:

Boris Novak (20)
Viktoriya Bell (22)
Anna Kostetskaya (25)

Ejemplo: elegir un comparador «sobre la marcha»

Collections.sort(people, new LastNameComparator()); // Por apellido
Collections.sort(people, new AgeComparator());      // Por edad

4. Clases anónimas y expresiones lambda

Los comparadores pueden crearse «sobre la marcha», sin declarar clases separadas.

Clase anónima

Collections.sort(people, new Comparator<Person>() {
    @Override
    public int compare(Person a, Person b) {
        return a.getFirstName().compareTo(b.getFirstName());
    }
});

Expresión lambda

Collections.sort(people, (a, b) -> a.getFirstName().compareTo(b.getFirstName()));

O aún más corto con el método de la lista List.sort:

people.sort((a, b) -> a.getFirstName().compareTo(b.getFirstName()));
  • Las clases anónimas son el modo antiguo y verboso.
  • Las lambda son modernas y compactas.

5. Ejemplos: ordenación por distintos criterios

Ordenación por la longitud del apellido

Comparator<Person> byLastNameLength = (a, b) ->
        Integer.compare(a.getLastName().length(), b.getLastName().length());
people.sort(byLastNameLength);

Ordenación por edad y luego por nombre (multinivel)

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 un comparador para búsqueda (ejemplo)

El comparador es aplicable no solo a la ordenación, sino también a la búsqueda en colecciones ordenadas:

// ¡people debe estar ordenada por edad!
Person key = new Person("?", "?", 22);
int idx = Collections.binarySearch(people, key, new AgeComparator());
if (idx >= 0) {
    System.out.println("Se encontró una persona con 22 años: " + people.get(idx));
}

6. Buenas prácticas y particularidades al trabajar con Comparator

No incumplas el contrato

  • Si compare(a, b) devuelve 0, entonces compare(b, a) también debe devolver 0.
  • Si compare(a, b) > 0, entonces compare(b, a) < 0.
  • Ten en cuenta los posibles valores null (ver abajo).

No olvides equals y hashCode

Aunque los comparadores comparan los objetos «a su manera», para estructuras como TreeSet o al buscar claves en TreeMap es importante que la lógica de comparación del comparador esté alineada con equals. De lo contrario, puedes obtener resultados inesperados: dos objetos distintos se consideran iguales según el comparador, pero no son iguales según equals.

Ordenación teniendo en cuenta null

Si los campos pueden ser null, usa «helpers» ya preparados:

Comparator<Person> byLastNameNullSafe = Comparator.comparing(
    Person::getLastName,
    Comparator.nullsLast(String::compareTo)
);
people.sort(byLastNameNullSafe);

7. Matices útiles

Tabla: comparación entre Comparable y Comparator

Comparable Comparator
¿Dónde se implementa? En la propia clase En una clase/lambda aparte
Método
int compareTo(T o)
int compare(T o1, T o2)
¿Cuántas variantes? Solo uno «natural» Las que quieras, para cualquier necesidad
Aplicación
Collections.sort(list)
Collections.sort(list, comp)
¿Se puede para clases de terceros? No

Ejemplo: ordenación descendente

Se puede invertir el orden manualmente:

Comparator<Person> byAgeDesc = (a, b) -> Integer.compare(b.getAge(), a.getAge());
people.sort(byAgeDesc);

O mediante reversed():

Comparator<Person> byAge = Comparator.comparingInt(Person::getAge);
people.sort(byAge.reversed());

8. Errores típicos al trabajar con Comparator

Error n.º 1: incumplir el contrato de comparación. Si olvidas que compare(a, b) y compare(b, a) deben ser opuestos en signo, o devuelves valores arbitrarios (por ejemplo, simplemente la resta — a.getAge() - b.getAge(), que puede desbordarse), el resultado será impredecible. Usa Integer.compare en lugar de la resta: es más seguro.

Error n.º 2: ignorar los valores null. Si los campos por los que comparas pueden ser null, maneja este caso (por ejemplo, con Comparator.nullsFirst/Comparator.nullsLast), de lo contrario es fácil obtener un NullPointerException en el momento más inesperado.

Error n.º 3: criterios de ordenación inestables. Si el comparador devuelve valores distintos para los mismos objetos (por ejemplo, usa un número aleatorio o un campo muy mutable), la ordenación puede comportarse de forma caótica.

Error n.º 4: inconsistencia con equals. Si compare(a, b) == 0, pero a.equals(b) es false, colecciones como TreeSet y TreeMap pueden no funcionar como esperas. Es deseable que la igualdad según el comparador y según equals coincidan.

Error n.º 5: ordenar sin comparador clases de terceros. Si intentas ordenar objetos de una clase «ajena» sin Comparable y sin pasar un Comparator, obtendrás un error de compilación. Pasa un comparador explícito.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION