Las interfaces Comparable y Comparator definen cómo establecer un orden entre objetos, ya sea natural o específico para una operación.
Si tienes una List<String>, ordenarla es fácil. Llamas a Collections.sort(lista) y Java la pone en orden lexicográfico. Si es List<Integer>, la pone en orden numérico.
Pero, ¿qué pasa si tienes una List<Usuario>?
Java no tiene ni idea de cómo ordenar usuarios.
- ¿Por ID?
- ¿Por edad?
- ¿Por apellido?
Si intentas hacer Collections.sort(usuarios), el compilador te dará un error. Te dirá: “No sé comparar estos objetos”.
Para enseñar a Java a ordenar, tenemos dos interfaces: Comparable (el orden por defecto) y Comparator (los órdenes alternativos).
Comparable: el “orden natural”
La interfaz Comparable<T> se implementa dentro de la propia clase que quieres ordenar.
Define el orden “por defecto” o “natural” del objeto. Por ejemplo, el orden natural de un número es de menor a mayor, y el de una palabra es alfabético.
Para nuestros usuarios, decidimos que su orden natural será por ID.
Implementación
La clase debe implementar Comparable<Usuario> y sobrescribir el método compareTo.
public class Usuario implements Comparable<Usuario> {
private int id;
private String nombre;
private int edad;
// Constructor, getters...
@Override
public int compareTo(Usuario otroUsuario) {
// Lógica de comparación:
// Devuelve negativo si 'this' va ANTES que 'otroUsuario'
// Devuelve 0 si son IGUALES
// Devuelve positivo si 'this' va DESPUÉS que 'otroUsuario'
return this.id - otroUsuario.id; // Truco matemático para enteros
}
}
Ahora sí:
Collections.sort(usuarios); // ¡Funciona! Ordena por ID.
this.id - otro.id funciona genial para enteros simples. Pero cuidado con números muy grandes (Overflow) o con flotantes. Para esos casos, usa Integer.compare(a, b) o Double.compare(a, b).
Comparator: el “orden flexible”
Vale, ya tenemos los usuarios ordenados por ID. Pero, ¿y si ahora quiero sacar un listado ordenado por Edad? ¿Y luego otro por Nombre?
No podemos modificar la clase Usuario cada vez. La clase solo puede tener un compareTo.
Para criterios externos usamos Comparator, una interfaz que compara dos objetos sin modificar su clase.
Estilo clásico (pre-Java 8)
Creamos una clase que implemente Comparator.
public class OrdenarPorEdad implements Comparator<Usuario> {
@Override
public int compare(Usuario u1, Usuario u2) {
return u1.getEdad() - u2.getEdad();
}
}
// Uso:
Collections.sort(usuarios, new OrdenarPorEdad());
Estilo moderno (lambdas - Java 8+)
Crear una clase entera para una resta es ridículo. Hoy en día usamos expresiones Lambda.
// Ordenar por edad
usuarios.sort((u1, u2) -> u1.getEdad() - u2.getEdad());
// O mejor aún, usando referencias a métodos (La forma más limpia):
usuarios.sort(Comparator.comparingInt(Usuario::getEdad));
Comparadores encadenados
Imagina que quieres ordenar por Edad, pero si dos tienen la misma edad, quieres desempatar por Nombre. Con la API moderna de Comparator esto es una delicia:
usuarios.sort(
Comparator.comparingInt(Usuario::getEdad)
.thenComparing(Usuario::getNombre)
);