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) < 0siava antes queb.a.compareTo(b) == 0si son iguales en orden.a.compareTo(b) > 0siava después queb.
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), entoncesa.compareTo(b) == 0(fuertemente recomendado). - Si
a.compareTo(b) == 0peroa.equals(b) == false, algunas colecciones comoTreeSetse comportan inconsistentemente conHashSet.
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 - bencompareTopara enteros: puede desbordarse si los números son muy grandes. UsaInteger.compare(a, b). - Violar transitividad (p.ej. comparar con aproximaciones o floats sin cuidado).
- Implementar
Comparablesin redefinirequalscoherentemente. - 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
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).