Comparable y Comparator

Intermedio
Java
Java
Actualizado: 27/04/2026

Comparable vs Comparator

flowchart TB
    Order[Ordenación Java] --> Cmpb[Comparable T]
    Order --> Cmpr[Comparator T]
    Cmpb --> Met1[compareTo this contra otro]
    Cmpb --> Nat[Orden natural intrínseco]
    Cmpr --> Met2[compare a y b]
    Cmpr --> Mult[Múltiples órdenes externos]
    Cmpr --> Then[thenComparing chain]
    Cmpr --> Reverse[reversed]
    Cmpr --> Lambda[Comparator.comparing keyExtractor]

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 útiles:

// 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 anno) {}

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::anno).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.

Caso B2B: ordenación de carteras y rankings

En entidades bancarias que gestionan carteras de clientes con segmentación por riesgo, el orden compuesto (segmento → saldo → antigüedad) se modela con Comparator.comparing(...).thenComparing(...).thenComparing(...). Vuestro equipo gana porque el orden queda declarativo y se documenta en una sola expresión, fácil de auditar por riesgos. En reportes regulatorios (BCE, FROB), la trazabilidad del criterio de ordenación es exigible: declararlo como public static final Comparator<Cliente> ORDEN_RIESGO = ... cumple esa exigencia sin ambigüedad.

En retail, los rankings de productos por relevancia combinan varios criterios (stock disponible → margen → ventas semanales). Sin Comparator componible, esos rankings se implementan con bucles imperativos o con ORDER BY masivos en la base de datos, lo cual encarece la licencia. Mover la ordenación al servicio Java permite cachear la lista ya ordenada en Redis y servir respuestas en milisegundos.

En administraciones públicas, las listas de espera (sanidad, educación, vivienda) requieren orden estable y reproducible. Comparator.thenComparing con un campo final (id, fecha de registro) garantiza que dos invocaciones consecutivas devuelven el mismo orden, requisito legal para procedimientos auditables.

Versiones y disponibilidad

Comparable<T> está en java.lang desde Java 1.2 (1998), y Comparator<T> desde la misma versión. Las factorías estáticas modernas (Comparator.comparing, thenComparing, nullsFirst, nullsLast, reversed) se añadieron en Java 8 (2014) junto con las lambdas. Comparator.comparingInt/Long/Double evita autoboxing y rinde mejor en bucles calientes. Java 21 LTS y Java 25 LTS los mantienen sin cambios.

Anti-patrones y pitfalls

a - b con enteros grandes. Provoca desbordamiento silencioso cuando a = Integer.MIN_VALUE. Usad siempre Integer.compare(a, b) o las factorías especializadas.

Comparators inestables con equals inconsistentes. Si compareTo devuelve 0 pero equals devuelve false, TreeSet y TreeMap se comportan distinto que HashSet y HashMap. En estructuras compartidas entre capas, este bug aparece solo bajo cargas reales.

Comparar objetos mutables y meterlos en TreeSet. Si el campo de ordenación cambia mientras el objeto está en la estructura, el árbol queda corrupto: contains, remove y next dejan de funcionar. Para conjuntos ordenados, usad campos finales o copias defensivas.

thenComparing sin desambiguación final. Dos elementos con todos los campos iguales mantienen un orden arbitrario que puede variar entre JDKs o entre llamadas. Añadid siempre un criterio último estable (id, hash, posición original).

Comparators encadenados costosos. Cada thenComparing recalcula la clave si no se cachea. Para listas grandes con cálculos costosos (reflexión, parsing), construid el comparador una vez y reutilizadlo.

Comparativa con alternativas

Frente a Apache Commons (ComparableUtils, ComparatorUtils.chainedComparator), las factorías estándar cubren todos los casos comunes sin dependencia externa.

Frente a Guava (Ordering, Ordering.compound), Comparator con thenComparing es funcionalmente equivalente y menos verboso. Guava sigue útil si vuestro código ya depende de él para otras utilidades.

Frente a Kotlin (compareBy, compareByDescending), Java 8+ ofrece sintaxis equivalente, aunque ligeramente más verbosa. La interoperabilidad es total.

Frente a ORDER BY en SQL, ordenar en Java desplaza coste desde la base de datos a la JVM. En sistemas con licencias por core (Oracle, SQL Server), esto reduce factura. En sistemas con base ligera (PostgreSQL 17), suele ser indiferente; medid antes de mover lógica.

Documentación oficial

La especificación está en la Javadoc de java.lang.Comparable y java.util.Comparator para Java 21+. La JLS no impone el contrato; lo definen las javadocs. El libro "Effective Java" 3.ª edición, ítem 14, detalla el contrato y las trampas de transitividad.

Comparable y Comparator son las piezas clave de la ordenación en Java. Dominarlos permite a vuestro equipo ordenar cualquier estructura de forma legible, componible y eficiente, con criterios trazables para auditoría B2B.

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).