1. Introducción
Las comparaciones en Java son bastante fáciles, hasta que no lo son.
Cuando trabajamos con tipos personalizados o intentamos comparar objetos que no son directamente comparables, debemos utilizar una estrategia de comparación. Podemos construir uno simplemente, pero haciendo uso de las interfaces Comparator o Comparable .
2. Configurar el ejemplo
Tomemos un ejemplo de un equipo de fútbol, donde queremos alinear a los jugadores según su clasificación.
Comenzaremos creando una clase de jugador simple :
public class Player { private int ranking; private String name; private int age; // constructor, getters, setters }
A continuación, creemos una clase PlayerSorter para crear nuestra colección e intentemos ordenarla usando Collections.sort :
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); }
Aquí, como se esperaba, esto da como resultado un error en tiempo de compilación:
The method sort(List) in the type Collections is not applicable for the arguments (ArrayList)
Entendamos qué hicimos mal aquí.
3. Comparable
Como sugiere el nombre, Comparable es una interfaz que define una estrategia para comparar un objeto con otros objetos del mismo tipo. A esto se le llama el "orden natural" de la clase.
En consecuencia, para poder ordenar, debemos definir nuestro objeto Player como comparable implementando la interfaz Comparable :
public class Player implements Comparable { // same as before @Override public int compareTo(Player otherPlayer) { return Integer.compare(getRanking(), otherPlayer.getRanking()); } }
El orden de clasificación se decide por el valor de retorno del método compareTo () . El Integer.compare (x, y) devuelve -1 si x es menor que y , devuelve 0 si son iguales, y los retornos 1 de otra manera.
El método devuelve un número que indica si el objeto que se compara es menor, igual o mayor que el objeto que se pasa como argumento.
Finalmente, cuando ejecutamos nuestro PlayerSorter ahora, podemos ver a nuestros jugadores ordenados por su clasificación:
Before Sorting : [John, Roger, Steven] After Sorting : [Steven, John, Roger]
Ahora que tenemos una comprensión clara del orden natural con Comparable , veamos cómo podemos usar otros tipos de ordenamiento, de una manera más flexible que implementando directamente una interfaz.
4. Comparador
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 comparadores
Para crear un Comparador, tenemos que implementar la interfaz del Comparador .
En nuestro primer ejemplo, crearemos un Comparador para usar el atributo de clasificación de Jugador para ordenar los jugadores:
public class PlayerRankingComparator implements Comparator { @Override public int compare(Player firstPlayer, Player secondPlayer) { return Integer.compare(firstPlayer.getRanking(), secondPlayer.getRanking()); } }
De manera similar, podemos crear un Comparador para usar el atributo de edad de Player para ordenar los jugadores:
public class PlayerAgeComparator implements Comparator { @Override public int compare(Player firstPlayer, Player secondPlayer) { return Integer.compare(firstPlayer.getAge(), secondPlayer.getAge()); } }
4.2. Comparadores en acción
Para demostrar el concepto, modifiquemos nuestro PlayerSorter introduciendo un segundo argumento en el método Collections.sort, que en realidad es la instancia de Comparator que queremos usar.
Con este enfoque, podemos anular el orden natural :
PlayerRankingComparator playerComparator = new PlayerRankingComparator(); Collections.sort(footballTeam, playerComparator);
Ahora, ejecutemos nuestro PlayerRankingSorter para ver el resultado:
Before Sorting : [John, Roger, Steven] After Sorting by ranking : [Steven, John, Roger]
Si queremos un orden de clasificación diferente, solo necesitamos cambiar el Comparador que estamos usando:
PlayerAgeComparator playerComparator = new PlayerAgeComparator(); Collections.sort(footballTeam, playerComparator);
Ahora, cuando ejecutamos nuestro PlayerAgeSorter , podemos ver un orden de clasificación diferente por edad:
Before Sorting : [John, Roger, Steven] After Sorting by age : [Roger, John, Steven]
4.3. Comparadores de Java 8
Java 8 proporciona nuevas formas de definir comparadores mediante el uso de expresiones lambda y el método de fábrica estática comparing () .
Veamos un ejemplo rápido de cómo usar una expresión lambda para crear un Comparador :
Comparator byRanking = (Player player1, Player player2) -> Integer.compare(player1.getRanking(), player2.getRanking());
The Comparator.comparing method takes a method calculating the property that will be used for comparing items, and returns a matching Comparator instance:
Comparator byRanking = Comparator .comparing(Player::getRanking); Comparator byAge = Comparator .comparing(Player::getAge);
You can explore the Java 8 functionality in-depth in our Java 8 Comparator.comparing guide.
5. Comparator vs Comparable
The Comparable interface is a good choice when used for defining the default ordering or, in other words, if it's the main way of comparing objects.
Then, we must ask ourselves why use a Comparator if we already have Comparable?
There are several reasons why:
- Sometimes, we can't modify the source code of the class whose objects we want to sort, thus making the use of Comparable impossible
- Using Comparators allows us to avoid adding additional code to our domain classes
- We can define multiple different comparison strategies which isn't possible when using Comparable
6. Avoiding the Subtraction Trick
Over the course of this tutorial, we used the Integer.compare() method to compare two integers. One might argue that we should use this clever one-liner instead:
Comparator comparator = (p1, p2) -> p1.getRanking() - p2.getRanking();
Although it's much more concise compared to other solutions, it can be a victim of integer overflows in 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);
Since -1 is much less than the Integer.MAX_VALUE, “Roger” should come before the “John” in the sorted collection. However, due to integer overflow, the “Integer.MAX_VALUE – (-1)” will be less than zero. So, based on the Comparator/Comparable contract, the Integer.MAX_VALUE is less than -1, which is obviously incorrect.
Hence, despite what we expected, “John” comes before the “Roger” in the sorted collection:
assertEquals("John", players.get(0).getName()); assertEquals("Roger", players.get(1).getName());
7. Conclusion
In this tutorial, we explored the Comparable and Comparator interfaces and discussed the differences between them.
To understand more advanced topics of sorting, check out our other articles such as Java 8 Comparator, Java 8 Comparison with Lambdas.
Y, como de costumbre, el código fuente se puede encontrar en GitHub.