Comparator y Comparable: Poniendo Orden en el Caos
Introducción: El Árbitro y el ADN
¡Buenas, equipo de desarrollo! 👔
Imaginad una clase de primaria cuando suena el timbre del recreo. Todos los niños salen corriendo en estampida. Caos total. Ahora, imaginad esa misma clase en la fila para entrar al comedor. Están ordenados. ¿Pero cómo? ¿Por altura? ¿Por orden alfabético? ¿Por notas?
En Java, cuando tenemos una colección de objetos (una lista de Productos, de Usuarios, de Zombies...), estos no saben ordenarse solos. Si le dices a Java "ordena esta lista de Zombies", Java te preguntará: "¿Cómo? ¿El más feo primero? ¿El que tiene menos brazos?".
Para responder a eso tenemos dos herramientas: 1. Comparable (El ADN): El objeto sabe ordenarse a sí mismo (ej: un número "sabe" que 5 es mayor que 3). Es su "orden natural". 2. Comparator (El Árbitro): Un juez externo que decide quién va antes y quién después según reglas personalizadas (ej: ordenar por precio, luego por nombre, luego por stock...).
¡Vamos a aprender a poner firmes a nuestros objetos! 📏📐
Conceptos Fundamentales
Definición: Comparable (java.lang.Comparable)
Es una interfaz que se implementa DENTRO de la clase que queremos ordenar. Define el "Orden Natural" del objeto.
* Método: public int compareTo(T o)
* Lógica: "Yo (this) me comparo contigo (o)".
Definición: Comparator (java.util.Comparator)
Es una interfaz funcional que se implementa FUERA de la clase (o como lambda). Permite crear múltiples estrategias de ordenación sin tocar el código original del objeto.
* Método: public int compare(T o1, T o2)
* Lógica: "Yo (Juez) comparo al objeto 1 y al objeto 2".
La Regla de Oro del Retorno (int)
Ambos métodos devuelven un número entero (int). Debéis memorizar esto:
* Negativo (< 0): this (o o1) va ANTES. (Es menor).
* Cero (0): Son IGUALES (en cuanto a orden).
* Positivo (> 0): this (o o1) va DESPUÉS. (Es mayor).
Mapa Mental: ¿Cuál elijo?
Desarrollo y Ejemplos Prácticos
Para todos los ejemplos, usaremos esta clase Jugador. Fijaos que implementa Comparable para tener un orden por defecto (por ID).
La Clase Base (Comparable)
import java.util.Objects;
// Implementamos Comparable para definir el orden "Natural" (por defecto)
public class Jugador implements Comparable<Jugador> {
private int id;
private String nombre;
private int puntuacion;
public Jugador(int id, String nombre, int puntuacion) {
this.id = id;
this.nombre = nombre;
this.puntuacion = puntuacion;
}
public int getId() { return id; }
public String getNombre() { return nombre; }
public int getPuntuacion() { return puntuacion; }
// --- EL MÉTODO DEL ADN (Comparable) ---
// Queremos que, por defecto, se ordenen por ID de menor a mayor.
@Override
public int compareTo(Jugador otro) {
// Truco: Para enteros podemos restar (si no hay overflow)
// O usar Integer.compare(this.id, otro.id) <-- MEJOR
return Integer.compare(this.id, otro.id);
}
@Override
public String toString() {
return "J{" + "id=" + id + ", nom='" + nombre + '\'' + ", pts=" + puntuacion + '}';
}
// hashCode y equals omitidos por brevedad, pero necesarios en Sets
}
1. ArrayList (Ordenación Estándar)
El ArrayList es el cliente número 1 de la ordenación.
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class EjemploArrayList {
public static void main(String[] args) {
List<Jugador> equipo = new ArrayList<>();
equipo.add(new Jugador(3, "Mario", 5000));
equipo.add(new Jugador(1, "Luigi", 8000));
equipo.add(new Jugador(2, "Peach", 2000));
System.out.println("Original: " + equipo);
// 1. Usando COMPARABLE (Orden natural por ID)
Collections.sort(equipo); // O equipo.sort(null) en Java 8+
System.out.println("Por ID (Comparable): " + equipo);
// 2. Usando COMPARATOR (Orden personalizado por Puntuación)
// Forma Clásica (Clase Anónima)
equipo.sort(new Comparator<Jugador>() {
@Override
public int compare(Jugador j1, Jugador j2) {
// De mayor a menor puntuación (invertimos orden)
return Integer.compare(j2.getPuntuacion(), j1.getPuntuacion());
}
});
// 3. Forma Moderna (Java 8 Lambda + Métodos estáticos)
// "Ordena comparando el nombre"
equipo.sort(Comparator.comparing(Jugador::getNombre));
System.out.println("Por Nombre: " + equipo);
}
}
2. LinkedList (Igual que ArrayList)
Aunque la estructura interna es diferente, al implementar List, se ordena igual.
import java.util.LinkedList;
import java.util.Comparator;
public class EjemploLinkedList {
public static void main(String[] args) {
LinkedList<Jugador> lista = new LinkedList<>();
lista.add(new Jugador(10, "Zelda", 100));
lista.add(new Jugador(5, "Link", 300));
// Queremos ordenar por Puntuación, y si empatan, por Nombre
Comparator<Jugador> logicaCompleja = Comparator
.comparingInt(Jugador::getPuntuacion) // Primero puntos
.thenComparing(Jugador::getNombre) // Luego nombre
.reversed(); // ¡Todo al revés! (Mayor a menor)
lista.sort(logicaCompleja);
System.out.println("Ranking LinkedList: " + lista);
}
}
3. Queue (PriorityQueue - La Cola VIP)
Aquí NO usamos .sort(). Una cola normal (LinkedList) es FIFO. Pero una PriorityQueue ordena los elementos automáticamente según entran. El que tiene más prioridad (según el Comparator/Comparable) se pone en la cabeza (peek/poll) para salir antes.
import java.util.PriorityQueue;
import java.util.Queue;
public class EjemploQueue {
public static void main(String[] args) {
// PriorityQueue ordena AUTOMÁTICAMENTE.
// Si no pasamos nada, usa el compareTo (por ID).
// Pero aquí pasamos un Comparator para que la prioridad sea PUNTUACIÓN ALTA.
// (Nota: PriorityQueue saca el "menor" primero, así que invertimos lógica para que "menor" sea el de más puntos)
Queue<Jugador> colaPrioritaria = new PriorityQueue<>(
(j1, j2) -> Integer.compare(j2.getPuntuacion(), j1.getPuntuacion())
);
colaPrioritaria.offer(new Jugador(1, "Noob", 100));
colaPrioritaria.offer(new Jugador(2, "Pro", 9999));
colaPrioritaria.offer(new Jugador(3, "Avg", 500));
System.out.println("--- Atendiendo por Puntuación ---");
while(!colaPrioritaria.isEmpty()) {
// Saldrá primero el de 9999 puntos, luego 500, luego 100
System.out.println("Atendiendo a: " + colaPrioritaria.poll());
}
}
}
4. Deque (ArrayDeque - Ordenación Manual)
ArrayDeque no tiene método sort y no reordena sola como PriorityQueue. Para ordenarla, lo habitual es convertirla a Stream, ordenar y volver a recolectar (o volcar a lista).
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Comparator;
import java.util.stream.Collectors;
public class EjemploDeque {
public static void main(String[] args) {
Deque<Jugador> pila = new ArrayDeque<>();
pila.push(new Jugador(1, "A", 10));
pila.push(new Jugador(2, "B", 20));
// No podemos hacer pila.sort()...
// Estrategia: Stream -> Sorted -> Coleccionar a nueva List o Deque
System.out.println("Pila ordenada por nombre (Visualización):");
pila.stream()
.sorted(Comparator.comparing(Jugador::getNombre))
.forEach(System.out::println);
// La estructura original NO ha cambiado (Deque no está pensada para reordenarse in-situ)
}
}
5. HashSet (El Caos Inordenable)
El HashSet no tiene orden. No puedes ordenarlo. Lo que sí puedes hacer es sacarlo a una lista y ordenar la lista para mostrarla.
import java.util.HashSet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class EjemploHashSet {
public static void main(String[] args) {
HashSet<Jugador> conjunto = new HashSet<>();
conjunto.add(new Jugador(1, "Uno", 10));
conjunto.add(new Jugador(2, "Dos", 20));
// conjunto.sort(...) // ¡ERROR DE COMPILACIÓN! No existe.
// Solución: Volcar a lista
List<Jugador> listaTemporal = new ArrayList<>(conjunto);
Collections.sort(listaTemporal); // Usa Comparable (por ID)
System.out.println("HashSet mostrado ordenado: " + listaTemporal);
}
}
6. TreeSet (Ordenación Automática)
El TreeSet SIEMPRE mantiene los datos ordenados. Usa el Comparable por defecto o un Comparator en el constructor.
import java.util.TreeSet;
import java.util.Comparator;
public class EjemploTreeSet {
public static void main(String[] args) {
// Caso A: Sin argumentos -> Usa Comparable (Orden por ID)
TreeSet<Jugador> ordenId = new TreeSet<>();
ordenId.add(new Jugador(2, "B", 50));
ordenId.add(new Jugador(1, "A", 10));
System.out.println("TreeSet por ID: " + ordenId);
// Caso B: Con Comparator -> Ignora Comparable y usa nuestra regla
// Ordenamos por Nombre (Alfabético inverso)
TreeSet<Jugador> ordenNombre = new TreeSet<>(
Comparator.comparing(Jugador::getNombre).reversed()
);
ordenNombre.add(new Jugador(2, "Ana", 50));
ordenNombre.add(new Jugador(1, "Bea", 10));
// Saldrá Bea primero (porque es reversed, B > A en orden descendente)
System.out.println("TreeSet por Nombre (Desc): " + ordenNombre);
}
}
7. TreeMap (Ordenación por Clave)
Igual que el TreeSet, pero ordena basándose en la Key (Clave). El Valor (Value) es irrelevante para el orden.
import java.util.TreeMap;
import java.util.Comparator;
public class EjemploTreeMap {
public static void main(String[] args) {
// Clave: Jugador, Valor: String (Estado)
// Pasamos un Comparator al constructor para que ordene las CLAVES (Jugadores) por Puntuación.
TreeMap<Jugador, String> ranking = new TreeMap<>(
Comparator.comparingInt(Jugador::getPuntuacion).reversed()
);
ranking.put(new Jugador(1, "Low", 100), "Bronce");
ranking.put(new Jugador(2, "High", 9000), "Oro");
ranking.put(new Jugador(3, "Mid", 5000), "Plata");
// Al imprimir, veremos primero al de 9000 (Key), luego 5000, luego 100
ranking.forEach((k, v) -> {
System.out.println(v + ": " + k.getNombre() + " (" + k.getPuntuacion() + " pts)");
});
}
}
Diagrama Mermaid: Flujo de Decisión
¿Cómo decide Java quién gana?
graph TD
Start["Inicio Comparación"] --> Check["¿Hay Comparator?"]
Check -- Sí (Externo) --> UseComparator["Ejecutar compare(o1, o2)"]
Check -- No (Defecto) --> Cast["Casting a Comparable"]
Cast --> UseComparable["Ejecutar o1.compareTo(o2)"]
UseComparator --> Result{Resultado}
UseComparable --> Result
Result -- "< 0" --> Menor["o1 va ANTES"]
Result -- "== 0" --> Igual["Orden Indiferente"]
Result -- "> 0" --> Mayor["o1 va DESPUÉS"]
Tabla Resumen: Métodos Modernos de Comparator (Java 8+)
Estos métodos estáticos hacen el código mucho más legible. ¡Úsalos!
| Método | Descripción | Ejemplo |
|---|---|---|
comparing(Function) |
Crea comparador basado en un campo. | Comparator.comparing(Jugador::getNombre) |
comparingInt(...) |
Optimizado para enteros (evita boxing). | Comparator.comparingInt(Jugador::getPuntuacion) |
reversed() |
Invierte el orden actual. | miComparador.reversed() |
thenComparing(...) |
Si empatan en el primero, usa este segundo criterio. | comp1.thenComparing(comp2) |
nullsFirst() / nullsLast() |
Gestiona nulos para que no explote. | Comparator.nullsFirst(...) |
Ejercicios Reflexivos
- El problema del Null: Si tienes una lista con elementos
nully hacesCollections.sort(), ¿qué pasa? ¿Cómo lo arreglarías usandoComparator.nullsFirst? - Consistencia: ¿Qué pasaría en un
TreeSetsi tucompareTodice que dos objetos son iguales (return 0) pero tuequalsdice que son diferentes (return false)? (Pista: ElTreeSetse fía delcompareToe ignorará el segundo objeto pensando que es duplicado). - Rendimiento: ¿Es más rápido ordenar un
ArrayListo insertar datos en unTreeSetsi al final quieres lo mismo? (Pista:ArrayList.sortes O(n log n) una vez.TreeSethace log n en cada inserción).
Aplicación en el Mundo Real
-
E-Commerce (Amazon/PCComponentes): Cuando buscas "Teclado Mecánico", por defecto sale por "Relevancia" (algoritmo complejo). Pero tú pinchas en "Ordenar por: Precio más bajo".
- El backend no cambia los objetos. Simplemente aplica un
Comparatorde Precio a la lista de resultados antes de enviártela. Si pinchas "Mejor valorados", aplica unComparatorde Estrellas.
- El backend no cambia los objetos. Simplemente aplica un
-
Leaderboards de Juegos: Los rankings globales se gestionan a menudo con estructuras tipo
TreeSeto bases de datos ordenadas (Redis Sorted Sets). Cada vez que tu puntuación cambia, el sistema te reubica automáticamente en el árbol en tiempo logarítmico.
Para Saber Más
-
Documentación Oficial Comparator: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Comparator.html
-
Baeldung - Comparator vs Comparable: La guía definitiva técnica. https://www.baeldung.com/java-comparator-comparable
-
Refactoring Guru: Patrones de diseño relacionados con estrategias de ordenación. https://refactoring.guru/design-patterns/strategy