1. Introducción
Las comparaciones en Java son bastante sencillas, hasta que dejan de serlo. Cuando trabajamos con tipos personalizados o tratamos de comparar objetos que no son directamente comparables, necesitamos hacer uso de una estrategia de comparación. Esta estrategia puede construirse fácilmente utilizando las interfaces Comparator
o Comparable
.
2. Configurando el Ejemplo
Vamos a utilizar un ejemplo de un equipo de fútbol, donde queremos organizar a los jugadores según sus rankings. Comenzaremos creando una clase simple Player
:
public class Player {
private int ranking;
private String name;
private int age;
// Constructor, getters, setters
public Player(int ranking, String name, int age) {
this.ranking = ranking;
this.name = name;
this.age = age;
}
public int getRanking() {
return ranking;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return name;
}
}
A continuación, crearemos una clase PlayerSorter
para crear nuestra colección e intentar ordenarla utilizando Collections.sort
:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class PlayerSorter {
public static void main(String[] args) {
List footballTeam = new ArrayList<>();
Player player1 = new Player(59, "John", 20);
Player player2 = new Player(67, "Roger", 22);
Player player3 = new Player(45, "Steven", 24);
footballTeam.add(player1);
footballTeam.add(player2);
footballTeam.add(player3);
System.out.println("Before Sorting : " + footballTeam);
Collections.sort(footballTeam);
System.out.println("After Sorting : " + footballTeam);
}
}
Como era de esperar, esto resulta en un error en tiempo de compilación:
The method sort(List) in the type Collections is not applicable for the arguments (ArrayList)
Vamos a entender qué hicimos mal aquí.
3. Comparable
Como su nombre indica, Comparable
es una interfaz que define una estrategia para comparar un objeto con otros objetos del mismo tipo. Esto se llama el “ordenamiento natural” de la clase. Para poder ordenar, debemos definir nuestro objeto Player
como comparable implementando la interfaz Comparable
:
public class Player implements Comparable {
// mismo código de antes...
@Override
public int compareTo(Player otherPlayer) {
return Integer.compare(getRanking(), otherPlayer.getRanking());
}
}
El orden de ordenamiento se decide por el valor de retorno del método compareTo()
. El método Integer.compare(x, y)
devuelve -1 si x
es menor que y
, 0 si son iguales, y 1 de lo contrario.
Ejecutando nuestra PlayerSorter
, podemos ver nuestros Players
ordenados por su ranking:
Before Sorting : [John, Roger, Steven]
After Sorting : [Steven, John, Roger]
Ahora que tenemos una comprensión clara del ordenamiento natural con Comparable
, veamos cómo podemos utilizar otros tipos de ordenamiento de una manera más flexible que implementar directamente una interfaz.
4. Comparator
La interfaz Comparator
define un método compare(arg1, arg2)
con dos argumentos que representan objetos comparados, y funciona de manera similar al método Comparable.compareTo()
.
4.1. Creando Comparators
Para crear un Comparator
, debemos implementar la interfaz Comparator
:
import java.util.Comparator;
public class PlayerRankingComparator implements Comparator {
@Override
public int compare(Player firstPlayer, Player secondPlayer) {
return Integer.compare(firstPlayer.getRanking(), secondPlayer.getRanking());
}
}
De igual forma, podemos crear un Comparator
para utilizar el atributo age
de Player
para ordenar a los jugadores:
public class PlayerAgeComparator implements Comparator {
@Override
public int compare(Player firstPlayer, Player secondPlayer) {
return Integer.compare(firstPlayer.getAge(), secondPlayer.getAge());
}
}
4.2. Comparators en Acción
Para demostrar el concepto, vamos a modificar nuestra PlayerSorter
introduciendo un segundo argumento en el método Collections.sort
, que en realidad es la instancia de Comparator
que queremos usar.
Utilizando este enfoque, podemos anular el ordenamiento natural:
PlayerRankingComparator playerComparator = new PlayerRankingComparator();
Collections.sort(footballTeam, playerComparator);
Ahora ejecutamos nuestra PlayerRankingSorter
para ver el resultado:
Before Sorting : [John, Roger, Steven]
After Sorting by ranking : [Steven, John, Roger]
Si queremos un orden de ordenamiento diferente, solo necesitamos cambiar el Comparator
que estamos utilizando:
PlayerAgeComparator playerComparator = new PlayerAgeComparator();
Collections.sort(footballTeam, playerComparator);
Ahora cuando ejecutamos nuestra PlayerAgeSorter
, vemos un orden diferente por age
:
Before Sorting : [John, Roger, Steven]
After Sorting by age : [Roger, John, Steven]
4.3. Comparators de Java 8
Java 8 proporciona nuevas maneras de definir Comparators
mediante expresiones lambda, y el método de fábrica estático comparing()
.
Veamos un ejemplo rápido de cómo utilizar una expresión lambda para crear un Comparator
:
Comparator byRanking = (Player player1, Player player2) -> Integer.compare(player1.getRanking(), player2.getRanking());
El método Comparator.comparing
toma un método que calcula la propiedad que se utilizará para comparar elementos, y devuelve una instancia de Comparator
correspondiente:
Comparator byRanking = Comparator.comparing(Player::getRanking);
Comparator byAge = Comparator.comparing(Player::getAge);
Para explorar la funcionalidad de Java 8 en profundidad, consulta nuestra guía sobre Java 8 Comparator.comparing.
5. Comparator vs Comparable
La interfaz Comparable
es una buena opción para definir el orden predeterminado, o en otras palabras, si es la manera principal de comparar objetos.
Entonces, ¿por qué utilizar un Comparator
si ya tenemos Comparable
? Hay varias razones para esto:
- A veces no podemos modificar el código fuente de la clase cuyos objetos queremos ordenar, lo que hace imposible el uso de
Comparable
. - Utilizar
Comparators
nos permite evitar agregar código adicional a nuestras clases de dominio. - Podemos definir múltiples estrategias diferentes de comparación, lo que no es posible al usar
Comparable
.
6. Evitando el Truco de la Sustracción
A lo largo de este tutorial, hemos utilizado el método Integer.compare()
para comparar dos enteros. Sin embargo, algunos podrían argumentar que deberíamos usar esta línea ingeniosa en su lugar:
Comparator comparator = (p1, p2) -> p1.getRanking() - p2.getRanking();
Aunque es mucho más concisa que otras soluciones, puede ser víctima desbordamientos de enteros en Java:
Player player1 = new Player(59, "John", Integer.MAX_VALUE);
Player player2 = new Player(67, "Roger", -1);
List players = Arrays.asList(player1, player2);
players.sort(comparator);
Dado que -1 es mucho menor que el Integer.MAX_VALUE
, “Roger” debería estar antes que “John” en la colección ordenada. Sin embargo, debido al desbordamiento de enteros, el resultado de “Integer.MAX_VALUE – (-1)” será menos que cero, lo que lleva a un resultado inesperado.
Inesperadamente, “John” aparece antes que “Roger” en la colección ordenada:
assertEquals("John", players.get(0).getName());
assertEquals("Roger", players.get(1).getName());
7. Conclusión
En este artículo, exploramos las interfaces Comparable
y Comparator
, y discutimos las diferencias entre ellas. Comprender cómo y cuándo usar estas interfaces es vital para mejorar la calidad y eficacia del código en Java.
Para entender temas más avanzados sobre ordenamiento, consulta nuestros otros artículos, como Java 8 Comparator y Java 8 Comparison with Lambdas.