W3docs

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étodo int compareTo(T other) es el orden natural del tipo.
  • Comparator<T> — un objeto externo que ordena instancias. Su método int 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:

  • negativoa va antes que b
  • cero — iguales a efectos de ordenación
  • positivoa va después de b

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:

  1. Antisimétricoa.compareTo(b) y b.compareTo(a) tienen signos opuestos.
  2. Transitivo — si a < b y b < c, entonces a < c.
  3. Consistente con equals (muy recomendado)a.compareTo(b) == 0 si y solo si a.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 valores null se traten como menores que cualquier no-nulo. Útil al ordenar colecciones que pueden contener null.
  • 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ónSobrecarga de orden naturalSobrecarga con Comparator
Ordenar una listaCollections.sort(list)Collections.sort(list, cmp)
Ordenar una lista (moderno)list.sort(null)list.sort(cmp)
Ordenar un streamstream.sorted()stream.sorted(cmp)
Conjunto basado en árbolnew TreeSet<>()new TreeSet<>(cmp)
Mapa basado en árbolnew TreeMap<>()new TreeMap<>(cmp)
Mínimo/máximoCollections.min(list)Collections.min(list, cmp)
Búsqueda binariaCollections.binarySearch(list, key)Collections.binarySearch(list, key, cmp)
PriorityQueueorden natural del tipo de elementoel 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.

java— editable, runs on the server

Lo que se puede extraer de la ejecución:

  • La implementación de Comparable ordenó por nombre y desempató los nombres iguales por edad. No fue necesario ningún comparador explícito — el orden natural es el predeterminado para Collections.sort y 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.
  • nullsFirst permitió que el comparador manejara una lista que contenía entradas null sin un NullPointerException. Sin ese envoltorio, la primera comparación que involucrase un null habrí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 que bad reporta que MAX_VALUE es menor que -1. Integer.compare produce 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.

Práctica

Práctica
Escribes `list.sort((a, b) -> a.scoreDifference(b))` donde `scoreDifference` devuelve `a.score - b.score` como un `int`. La lista contiene puntuaciones que incluyen `Integer.MAX_VALUE` e `Integer.MIN_VALUE`, y el resultado es claramente incorrecto. ¿Cuál es la solución?
Escribes `list.sort((a, b) -> a.scoreDifference(b))` donde `scoreDifference` devuelve `a.score - b.score` como un `int`. La lista contiene puntuaciones que incluyen `Integer.MAX_VALUE` e `Integer.MIN_VALUE`, y el resultado es claramente incorrecto. ¿Cuál es la solución?
Was this page helpful?