1. El problema de comparar objetos
Recordatorio: comparar referencias y comparar objetos
Ya sabes que en Java el operador == al trabajar con objetos compara sus referencias — es decir, si están en la misma dirección de memoria. Dos objetos con los mismos campos, pero creados con new, serán distintos para ==.
Person p1 = new Person("Alice", 20);
Person p2 = new Person("Alice", 20);
System.out.println(p1 == p2); // false — ¡son objetos distintos en memoria!
Y si queremos saber si son iguales por contenido, usamos equals(), hashCode()... ya lo sabes. ¿Y si necesitamos entender quién es «mayor», «menor» o quién va «antes en el alfabeto»? Por ejemplo, para ordenar una lista de usuarios por edad o por nombre.
Necesidad de ordenar y buscar objetos
Supongamos que tenemos una lista de usuarios y queremos ordenarlos por edad:
List<Person> people = new ArrayList<>();
people.add(new Person("Bob", 25));
people.add(new Person("Charlie", 20));
people.add(new Person("David", 30));
// ¿Cómo ordenar?
Collections.sort(people); // ¡Ups! Java no sabe cómo comparar Person.
El compilador se quejará enseguida: la clase Person no implementa la interfaz Comparable. Java no lee la mente y no sabe qué significa para nosotros «mayor» o «menor» para Person. Para enseñárselo, debemos definir explícitamente las reglas de comparación.
2. Interfaz Comparable
Declaración de la interfaz
La interfaz Comparable es la manera estándar de decirle a Java: «Mi clase se puede comparar y así es como se hace».
public interface Comparable<T> {
int compareTo(T o);
}
El conocido a.compareTo(b) devuelve:
- un número negativo — significa que a es «menor» que b.
- Si es 0 — los objetos se consideran iguales.
- un número positivo — a es «mayor» que b.
Ejemplo: implementación de compareTo para la clase Person
Vamos a crear la clase Person, que se puede comparar por edad:
public class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Getters (para los ejemplos de más adelante)
public String getName() { return name; }
public int getAge() { return age; }
// Implementación del método compareTo
@Override
public int compareTo(Person other) {
// Ordenación por edad (ascendente)
return Integer.compare(this.age, other.age);
// Alternativa: return this.age - other.age;
}
}
Punto importante: si quieres ordenar en orden descendente, basta con intercambiar los argumentos: Integer.compare(other.age, this.age).
Analogía. compareTo es como un juez en una competición: debe decidir con claridad quién va delante, quién detrás y quién está al mismo nivel. Si cada juez (los métodos compareTo) juzga de forma distinta — ¡habrá caos!
3. Uso de Comparable
Ordenar colecciones con Comparable
Ahora que nuestra clase implementa Comparable, la ordenación funciona «de serie»:
List<Person> people = new ArrayList<>();
people.add(new Person("Bob", 25));
people.add(new Person("Charlie", 20));
people.add(new Person("David", 30));
Collections.sort(people); // ¡Usa compareTo!
for (Person p : people) {
System.out.println(p.getName() + " (" + p.getAge() + ")");
}
// Charlie (20)
// Bob (25)
// David (30)
De forma análoga, el método sort de la lista:
people.sort(null); // Si se pasa null, se usa compareTo
Ordenación por nombre
Si queremos ordenar por nombre — cambiamos la implementación:
@Override
public int compareTo(Person other) {
return this.name.compareTo(other.name);
}
Ordenación por varios campos
A veces hay que comparar primero por un campo y, en caso de igualdad, por otro:
@Override
public int compareTo(Person other) {
int cmp = Integer.compare(this.age, other.age);
if (cmp != 0) return cmp;
return this.name.compareTo(other.name);
}
4. Buenas prácticas al implementar Comparable
Cumpla el contrato de Comparable
- Si a.compareTo(b) == 0, entonces b.compareTo(a) debe ser necesariamente 0.
- Si a.compareTo(b) < 0, entonces b.compareTo(a) debe ser > 0 (y viceversa).
- Si a.compareTo(b) == 0, es deseable que a.equals(b) sea true (aunque no es estrictamente obligatorio).
¿Por qué es importante?
Las colecciones (por ejemplo, TreeSet, TreeMap) y los métodos de ordenación pueden comportarse de forma impredecible si se viola el contrato. Por ejemplo, pueden aparecer «duplicados» en una colección donde no debería haberlos.
No olvide equals y hashCode
Si implementa compareTo, plantéese si equals y hashCode están bien implementados. Especialmente si su clase se va a usar en colecciones como HashSet o Map.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person other = (Person) o;
return age == other.age && Objects.equals(name, other.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
¡No use en compareTo campos que puedan ser null sin comprobarlos!
Si un campo puede ser null, utilice comparaciones seguras:
@Override
public int compareTo(Person other) {
return Objects.compare(this.name, other.name, Comparator.nullsFirst(String::compareTo));
}
No modifique los campos que participan en compareTo si el objeto ya está en una colección ordenada
Esto puede provocar que el objeto se «pierda» dentro de la colección — por ejemplo, en TreeSet o TreeMap.
5. Desarrollamos la aplicación de aprendizaje: ordenación de usuarios
Paso 1: Definir la clase
public class Person implements Comparable<Person> {
private String name;
private int age;
// ... constructor, getters, compareTo, equals, hashCode ...
}
Paso 2: Añadimos usuarios
List<Person> people = new ArrayList<>();
people.add(new Person("Eve", 23));
people.add(new Person("Frank", 19));
people.add(new Person("Grace", 25));
Paso 3: Ordenar e imprimir
Collections.sort(people);
for (Person p : people) {
System.out.println(p.getName() + " (" + p.getAge() + ")");
}
Resultado:
Frank (19)
Eve (23)
Grace (25)
6. Esquema de funcionamiento de Comparable
┌────────────────────────────┐
│ Tu clase (Person) │
├────────────────────────────┤
│ implements Comparable │
│ ↓ │
│ public int compareTo(T o) │
│ ↓ │
│ (this < o) → -1 │
│ (this == o) → 0 │
│ (this > o) → 1 │
└────────────────────────────┘
│
▼
Collections.sort(list)
│
▼
¡La ordenación funciona!
7. Detalles útiles
Cómo funciona Collections.sort
- Si la lista contiene objetos que implementan Comparable, la ordenación usará su método compareTo.
- Si no lo implementa — habrá un error de compilación.
- Para los tipos estándar (Integer, String, etc.) Comparable ya está implementado.
¿Se pueden tener varios modos de comparación?
- En una clase — solo un «orden natural» a través de Comparable.
- Para órdenes alternativos, use Comparator (siguiente lección).
Ejemplo: compareTo para cadenas
String a = "apple";
String b = "banana";
System.out.println(a.compareTo(b)); // número negativo, porque "apple" < "banana"
Tabla: qué devuelve compareTo
| Comparación | Valor devuelto |
|---|---|
|
|
|
|
|
|
8. Errores típicos al implementar Comparable
Error n.º 1: Incumplir el contrato de compareTo.
Si a.compareTo(b) devuelve 0 y b.compareTo(a) no devuelve 0, las colecciones se comportarán de manera extraña. Por ejemplo, TreeSet puede considerar los objetos distintos y añadir ambos.
Error n.º 2: Usar campos no inicializados (null).
Si el campo por el que comparas puede ser null y no lo compruebas — obtendrás un NullPointerException.
Error n.º 3: Incoherencia entre compareTo y equals.
Si compareTo dice que los objetos son iguales (0) pero equals dice que son distintos (false), esto provocará errores al trabajar con colecciones.
Error n.º 4: Modificar los campos que participan en compareTo después de añadir a una colección ordenada.
Es como cambiarse el apellido en el pasaporte cuando ya estás en una cola ordenada alfabéticamente. La colección puede «perder» tu objeto.
Error n.º 5: Devolver solo -1, 0 o 1.
El método compareTo puede devolver cualquier número negativo o positivo, no necesariamente estrictamente -1 o 1. Aunque por simplicidad a menudo se usan -1/0/1.
GO TO FULL VERSION