Skip to content

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

  1. El problema del Null: Si tienes una lista con elementos null y haces Collections.sort(), ¿qué pasa? ¿Cómo lo arreglarías usando Comparator.nullsFirst?
  2. Consistencia: ¿Qué pasaría en un TreeSet si tu compareTo dice que dos objetos son iguales (return 0) pero tu equals dice que son diferentes (return false)? (Pista: El TreeSet se fía del compareTo e ignorará el segundo objeto pensando que es duplicado).
  3. Rendimiento: ¿Es más rápido ordenar un ArrayList o insertar datos en un TreeSet si al final quieres lo mismo? (Pista: ArrayList.sort es O(n log n) una vez. TreeSet hace log n en cada inserción).

Aplicación en el Mundo Real

  1. 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 Comparator de Precio a la lista de resultados antes de enviártela. Si pinchas "Mejor valorados", aplica un Comparator de Estrellas.
  2. Leaderboards de Juegos: Los rankings globales se gestionan a menudo con estructuras tipo TreeSet o 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