Comparable y Comparator

Intermedio
Java
Java
Actualizado: 18/04/2026

Dos formas de ordenar en Java

Java ofrece dos abstracciones complementarias para comparar y ordenar objetos:

  • Comparable<T>: define el orden natural de una clase. Se implementa en la propia clase.
  • Comparator<T>: define un orden ad-hoc, externo a la clase, típicamente como objeto independiente o lambda.

La diferencia esencial: Comparable va dentro del objeto ("yo sé compararme conmigo mismo"), Comparator es externo ("esta función sabe comparar dos instancias").

Comparable<T>: orden natural

Una clase que implementa Comparable<T> declara cómo se comparan dos instancias mediante el método compareTo. El contrato:

  • a.compareTo(b) < 0 si a va antes que b.
  • a.compareTo(b) == 0 si son iguales en orden.
  • a.compareTo(b) > 0 si a va después que b.
public class Version implements Comparable<Version> {
    private final int major;
    private final int minor;
    private final int patch;

    public Version(int major, int minor, int patch) {
        this.major = major;
        this.minor = minor;
        this.patch = patch;
    }

    @Override
    public int compareTo(Version o) {
        int r = Integer.compare(this.major, o.major);
        if (r != 0) return r;
        r = Integer.compare(this.minor, o.minor);
        if (r != 0) return r;
        return Integer.compare(this.patch, o.patch);
    }
}

Con Comparable, cualquier colección ordenada y Collections.sort sabrá ordenar Version:

List<Version> versiones = new ArrayList<>(List.of(
        new Version(1, 2, 0),
        new Version(2, 0, 0),
        new Version(1, 0, 5)
    ));
Collections.sort(versiones); // usa compareTo

Contrato de compareTo

Debe ser total, antisimétrico y transitivo, y debe ser consistente con equals:

  • Si a.equals(b), entonces a.compareTo(b) == 0 (fuertemente recomendado).
  • Si a.compareTo(b) == 0 pero a.equals(b) == false, algunas colecciones como TreeSet se comportan inconsistentemente con HashSet.

Las clases como String, Integer, LocalDate implementan Comparable y su orden natural es el que intuyes.

Comparator<T>: comparadores ad-hoc

Un Comparator<T> es un objeto que compara dos instancias. Se pasa explícitamente a métodos de ordenación:

Comparator<Persona> porEdad = (a, b) -> Integer.compare(a.edad(), b.edad());

List<Persona> personas = ...;
personas.sort(porEdad);

Factorías estáticas

Comparator ofrece helpers muy utiles:

// Comparar por una clave (keyExtractor)
Comparator<Persona> porNombre = Comparator.comparing(Persona::nombre);

// Especializados para primitivos (evitan autoboxing)
Comparator<Persona> porEdad = Comparator.comparingInt(Persona::edad);
Comparator<Empleado> porSalario = Comparator.comparingDouble(Empleado::salario);

// Invertir
Comparator<Persona> porEdadDesc = porEdad.reversed();

// Comparador natural (requiere que T implemente Comparable)
Comparator<String> natural = Comparator.naturalOrder();
Comparator<String> reverso = Comparator.reverseOrder();

Encadenamiento con thenComparing

Cuando la clave principal empata, se aplica la siguiente:

Comparator<Persona> orden = Comparator
.comparing(Persona::apellido)
.thenComparing(Persona::nombre)
.thenComparingInt(Persona::edad);

personas.sort(orden);

Esto ordena por apellido, luego por nombre, luego por edad. Código declarativo y muy legible.

Manejo de nulls

Si los campos pueden ser null, nullsFirst y nullsLast envuelven otro comparador:

Comparator<Persona> safe = Comparator.comparing(
    Persona::apodo,
    Comparator.nullsLast(Comparator.naturalOrder())
);

Ahora las personas con apodo == null van al final, y el resto se ordena normalmente.

Comparable vs Comparator: cuándo usar cada uno

| Situación | Usar | |-----------|------| | La clase tiene un orden "obvio" (versiones, fechas, códigos) | Comparable (orden natural) | | Necesitas ordenar por varios criterios en distintos contextos | Comparator | | No puedes modificar la clase (p.ej. clase de librería) | Comparator | | La ordenación depende de configuración externa | Comparator | | Quieres chaining complejo | Comparator |

Una buena práctica: implementar Comparable si hay un orden natural claro, y proveer varios Comparator como constantes para otros órdenes comunes:

public class Persona implements Comparable<Persona> {
    public static final Comparator<Persona> POR_EDAD = Comparator.comparingInt(Persona::edad);
    public static final Comparator<Persona> POR_APELLIDO = Comparator.comparing(Persona::apellido);

    @Override
    public int compareTo(Persona o) {
        // orden natural: apellido, luego nombre
        int r = this.apellido.compareTo(o.apellido);
        return r != 0 ? r : this.nombre.compareTo(o.nombre);
    }
}

Integración con el ecosistema

List.sort y Collections.sort

List<Persona> personas = ...;
personas.sort(Comparator.comparing(Persona::edad)); // ordena in-place
Collections.sort(personas); // usa orden natural (requiere Comparable)

Streams con sorted

List<Persona> ordenadas = personas.stream()
.sorted(Comparator.comparing(Persona::edad).reversed())
.toList();

TreeSet y TreeMap

Ambos aceptan Comparator opcional. Si no se pasa, usan Comparable:

// Orden natural: requiere Comparable
TreeSet<String> palabras = new TreeSet<>();

// Orden personalizado: con Comparator
TreeSet<Persona> porEdad = new TreeSet<>(Comparator.comparingInt(Persona::edad));

TreeMap<Persona, Integer> puntuaciones = new TreeMap<>(Comparator.comparing(Persona::nombre));

PriorityQueue

PriorityQueue<Tarea> cola = new PriorityQueue<>(
    Comparator.comparingInt(Tarea::prioridad).reversed()
);

Collections.min, max, sort

Persona mayor = Collections.max(personas, Comparator.comparingInt(Persona::edad));
Persona menor = Collections.min(personas); // usa Comparable si existe

Ejemplo completo

Ordenar una lista de libros por autor, luego título, luego año descendente:

record Libro(String titulo, String autor, int año) {}

List<Libro> biblioteca = List.of(
    new Libro("Clean Code", "Martin", 2008),
    new Libro("Effective Java", "Bloch", 2017),
    new Libro("Refactoring", "Fowler", 2018),
    new Libro("Effective Java", "Bloch", 2001)
);

Comparator<Libro> orden = Comparator
.comparing(Libro::autor)
.thenComparing(Libro::titulo)
.thenComparing(Comparator.comparingInt(Libro::año).reversed());

List<Libro> ordenada = biblioteca.stream().sorted(orden).toList();

Errores comunes

  • Devolver a - b en compareTo para enteros: puede desbordarse si los números son muy grandes. Usa Integer.compare(a, b).
  • Violar transitividad (p.ej. comparar con aproximaciones o floats sin cuidado).
  • Implementar Comparable sin redefinir equals coherentemente.
  • Mezclar streams con comparators mutables.

Comparable y Comparator son las piezas clave de la ordenación en Java. Dominarlos te permite ordenar cualquier estructura de forma legible, componible y eficiente.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Java es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Java

Explora más contenido relacionado con Java y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Implementar Comparable<T> para definir el orden natural. Usar Comparator<T> para órdenes ad-hoc sin modificar la clase. Encadenar criterios con thenComparing. Usar Comparator.comparing, comparingInt, nullsFirst, reversed. Integrar comparadores con List.sort, Collections.sort, Stream.sorted, TreeSet/TreeMap. Respetar el contrato de ordenación (transitivo, antisimétrico, total).