Java Comparable y Comparator
Define el orden natural con Comparable y el orden externo con Comparator en Java, y aprende a componer comparadores.
Dos interfaces, un objetivo: indicarle a Java cuándo un objeto es "menor que" otro. Se ven casi idénticas en el punto de uso, y sus métodos incluso devuelven el mismo tipo de valor — un int negativo, cero o un int positivo. La diferencia está en dónde vive el orden:
Comparable<T>— el propio tipo sabe cómo ordenar sus instancias. Su métodoint compareTo(T other)es el orden natural del tipo.Comparator<T>— un objeto externo que ordena instancias. Su métodoint compare(T a, T b)describe uno de los muchos órdenes posibles.
Implementas Comparable cuando hay un "menor que" obvio para un tipo — Integer, String, LocalDate. Escribes un Comparator para cualquier otro orden — por longitud, por nombre sin distinción de mayúsculas, por precio descendente, por lo que puedas expresar en código. La mayoría de los tipos tienen un Comparable (o ninguno) y docenas de Comparators útiles.
El contrato: −/0/+
Ambos métodos devuelven un int cuyo signo es la respuesta:
- negativo —
ava antes queb - cero — iguales a efectos de ordenación
- positivo —
ava después deb
La magnitud exacta no importa. -1 y -1_000_000 significan lo mismo. Nunca hagas return a.size - b.size cuando sea posible el desbordamiento: restar Integer.MIN_VALUE de un número positivo produce un wraparound. Usa Integer.compare(a.size(), b.size()) en su lugar — es seguro ante desbordamientos y tiene la misma cantidad de caracteres.
Comparable<T> — orden natural
Un tipo implementa Comparable<Self> y proporciona compareTo:
public record Version(int major, int minor, int patch) implements Comparable<Version> {
@Override public int compareTo(Version other) {
int m = Integer.compare(this.major, other.major);
if (m != 0) return m;
int n = Integer.compare(this.minor, other.minor);
if (n != 0) return n;
return Integer.compare(this.patch, other.patch);
}
}Ahora Collections.sort(versions), versions.stream().sorted(), new TreeSet<Version>() y new TreeMap<Version, X>() funcionan sin necesidad de pasar ningún argumento adicional.
El contrato tiene tres reglas que todo compareTo debe cumplir:
- Antisimétrico —
a.compareTo(b)yb.compareTo(a)tienen signos opuestos. - Transitivo — si
a < byb < c, entoncesa < c. - Consistente con
equals(muy recomendado) —a.compareTo(b) == 0si y solo sia.equals(b).
La tercera regla es la que la gente rompe por accidente. BigDecimal es el ejemplo famoso: new BigDecimal("1.0").compareTo(new BigDecimal("1.00")) es 0, pero .equals devuelve false. Como resultado, un TreeSet<BigDecimal> y un HashSet<BigDecimal> no estarán de acuerdo sobre si "1.0" y "1.00" son duplicados. Si puedes, mantenlos consistentes.
Comparator<T> — orden externo
Un Comparator es un objeto separado. Puede comparar cualquier par de Ts, incluyendo tipos que no escribiste tú:
Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());
list.sort(byLength);Dado que Comparator<T> es una interfaz funcional (un único método abstracto, compare), todo Comparator es simplemente una lambda o una referencia a método. Esa es la forma moderna del código con comparadores — casi nunca se escribe una clase anónima completa.
Los constructores en Comparator
La clase tiene métodos de fábrica estáticos que hacen que construir comparadores sea breve y legible:
Comparator<Person> byAge = Comparator.comparingInt(Person::age);
Comparator<Person> byName = Comparator.comparing(Person::name);
Comparator<Person> byNameCi = Comparator.comparing(Person::name, String.CASE_INSENSITIVE_ORDER);
Comparator<Person> oldestFirst = byAge.reversed();
Comparator<String> nullsFirst = Comparator.nullsFirst(Comparator.naturalOrder());Usa los constructores especializados para primitivos — comparingInt, comparingLong, comparingDouble — cuando la clave es un primitivo. Evitan el boxing en cada comparación, lo cual se acumula en una ordenación larga.
Comparadores encadenados con thenComparing
La otra razón para preferir los constructores: puedes encadenar múltiples claves.
Comparator<Person> ordering =
Comparator.comparing(Person::lastName)
.thenComparing(Person::firstName)
.thenComparingInt(Person::age);Esto se lee de arriba a abajo como "clave primaria apellido; desempate por nombre; luego por edad." thenComparing se invoca sobre el comparador anterior y devuelve uno nuevo que solo consulta la segunda clave cuando la primera reportó un empate. No hay límite en la cadena.
reversed(), nullsFirst, nullsLast
Tres modificadores aparecen constantemente:
reversed()invierte el orden de cualquier comparador.byAge.reversed()es "el mayor primero."nullsFirst(cmp)envuelve un comparador para que los valoresnullse traten como menores que cualquier no-nulo. Útil al ordenar colecciones que pueden contenernull.nullsLast(cmp)es el complemento simétrico.
No uses reversed() en un comparador encadenado esperando que solo se invierta la última clave — reversed() invierte el orden completo, todas las claves de la cadena.
Comparable vs Comparator en las APIs del JDK
Muchos métodos vienen en dos variantes — una que usa el orden natural y otra que acepta un Comparator:
| Operación | Sobrecarga de orden natural | Sobrecarga con Comparator |
|---|---|---|
| Ordenar una lista | Collections.sort(list) | Collections.sort(list, cmp) |
| Ordenar una lista (moderno) | list.sort(null) | list.sort(cmp) |
| Ordenar un stream | stream.sorted() | stream.sorted(cmp) |
| Conjunto basado en árbol | new TreeSet<>() | new TreeSet<>(cmp) |
| Mapa basado en árbol | new TreeMap<>() | new TreeMap<>(cmp) |
| Mínimo/máximo | Collections.min(list) | Collections.min(list, cmp) |
| Búsqueda binaria | Collections.binarySearch(list, key) | Collections.binarySearch(list, key, cmp) |
PriorityQueue | orden natural del tipo de elemento | el constructor acepta un Comparator |
Las formas de orden natural requieren que el tipo de elemento implemente Comparable. Si el tuyo no lo hace y las llamas igualmente, verás un ClassCastException en tiempo de ejecución — no un error de compilación — porque el cast ocurre dentro de la implementación de ordenación.
Un ejemplo completo: orden natural, comparadores personalizados, claves encadenadas y nulos
El programa a continuación define un record con un orden natural (Comparable) más tres ordenaciones externas: por una sola clave, por claves encadenadas con una secundaria invertida, y una que tolera entradas null.
Lo que se puede extraer de la ejecución:
- La implementación de
Comparableordenó por nombre y desempató los nombres iguales por edad. No fue necesario ningún comparador explícito — el orden natural es el predeterminado paraCollections.sorty sus equivalentes. Comparator.comparingDouble(Person::salary)es más corto y rápido que escribir(a, b) -> Double.compare(a.salary(), b.salary())porque evita el boxing.- El comparador encadenado ordenó principalmente por edad y usó
reversed()solo en la parte de salario — ese es el patrón correcto cuando quieres direcciones distintas en claves diferentes. Compáralo con llamar.reversed()en toda la cadena, lo que invertiría ambas claves. nullsFirstpermitió que el comparador manejara una lista que contenía entradasnullsin unNullPointerException. Sin ese envoltorio, la primera comparación que involucrase unnullhabría fallado.- El "truco de la resta" produjo la respuesta incorrecta para
Integer.MAX_VALUE - (-1): ese cálculo desborda hasta un número negativo, por lo quebadreporta queMAX_VALUEes menor que-1.Integer.compareproduce el signo correcto en todo momento. Siempre es preferible usarlo.
¿Qué viene después?
Ya tienes cubiertos la iteración (Iterator / ListIterator) y la ordenación (Comparable / Comparator). El siguiente capítulo los une en la clase utilitaria java.util.Collections — la caja de herramientas estática de sort, search, reverse, shuffle, min, max y métodos para "envolver esta colección como inmutable" que operan sobre cualquier List, Set o Map. Después, dos capítulos cortos profundizan específicamente en la ordenación y la búsqueda.