Komparator i porównywalny w Javie

1. Wstęp

Porównania w Javie są dość łatwe - dopóki nie są.

Podczas pracy z typami niestandardowymi lub próbując porównać obiekty, które nie są bezpośrednio porównywalne, musimy skorzystać ze strategii porównania. Możemy go zbudować w prosty sposób, ale korzystając z interfejsów Komparatora lub Porównywalnych .

2. Konfigurowanie przykładu

Weźmy przykład drużyny piłkarskiej - gdzie chcemy wyrównać graczy według ich rankingów.

Zaczniemy od stworzenia prostej klasy Player :

public class Player { private int ranking; private String name; private int age; // constructor, getters, setters }

Następnie utwórzmy klasę PlayerSorter, aby utworzyć naszą kolekcję i podejmijmy próbę sortowania jej za pomocą 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); } 

Tutaj, zgodnie z oczekiwaniami, powoduje to błąd w czasie kompilacji:

The method sort(List) in the type Collections is not applicable for the arguments (ArrayList)

Zrozummy, co zrobiliśmy źle.

3. Porównywalne

Jak sama nazwa wskazuje, Comparable to interfejs definiujący strategię porównywania obiektu z innymi obiektami tego samego typu. Nazywa się to „naturalnym porządkiem” klasy.

W związku z tym, aby móc sortować - musimy zdefiniować nasz obiekt Player jako porównywalny, implementując interfejs Comparable :

public class Player implements Comparable { // same as before @Override public int compareTo(Player otherPlayer) { return Integer.compare(getRanking(), otherPlayer.getRanking()); } } 

O kolejności sortowania decyduje wartość zwracana przez metodę compareTo () . Integer.compare (x, y), powraca -1 jeśli x jest mniejsze niż Y , zwraca 0, jeżeli są one identyczne, i zwraca 1 w inny sposób.

Metoda zwraca liczbę wskazującą, czy porównywany obiekt jest mniejszy, równy lub większy niż obiekt przekazywany jako argument.

Wreszcie, kiedy uruchomimy teraz nasz PlayerSorter , możemy zobaczyć naszych graczy posortowanych według ich rankingu:

Before Sorting : [John, Roger, Steven] After Sorting : [Steven, John, Roger]

Teraz, gdy mamy już jasne zrozumienie naturalnego uporządkowania z porównywalnym , zobaczmy, jak możemy używać innych typów uporządkowania, w sposób bardziej elastyczny niż bezpośrednie wdrażanie interfejsu.

4. Komparator

Komparator interfejs definiuje Porównaj (arg1, arg2) metodę z dwoma argumentami, które reprezentują porównywanych obiektów i działa podobnie do Comparable.compareTo () metody.

4.1. Tworzenie komparatorów

Aby utworzyć komparator, musimy zaimplementować interfejs komparatora .

W naszym pierwszym przykładzie utworzymy komparator, aby użyć atrybutu rankingu gracza do sortowania graczy:

public class PlayerRankingComparator implements Comparator { @Override public int compare(Player firstPlayer, Player secondPlayer) { return Integer.compare(firstPlayer.getRanking(), secondPlayer.getRanking()); } }

Podobnie możemy utworzyć komparator, aby użyć atrybutu wieku gracza do sortowania graczy:

public class PlayerAgeComparator implements Comparator { @Override public int compare(Player firstPlayer, Player secondPlayer) { return Integer.compare(firstPlayer.getAge(), secondPlayer.getAge()); } }

4.2. Komparatory w akcji

Aby zademonstrować tę koncepcję, zmodyfikujmy nasz PlayerSorter , wprowadzając drugi argument do metody Collections.sort, która jest w rzeczywistości instancją komparatora, której chcemy użyć.

Korzystając z tego podejścia, możemy przesłonić naturalny porządek :

PlayerRankingComparator playerComparator = new PlayerRankingComparator(); Collections.sort(footballTeam, playerComparator); 

Teraz uruchommy nasz PlayerRankingSorter, aby zobaczyć wynik:

Before Sorting : [John, Roger, Steven] After Sorting by ranking : [Steven, John, Roger]

Jeśli chcemy mieć inną kolejność sortowania, musimy tylko zmienić używany komparator :

PlayerAgeComparator playerComparator = new PlayerAgeComparator(); Collections.sort(footballTeam, playerComparator);

Teraz, kiedy uruchamiamy nasz PlayerAgeSorter , możemy zobaczyć inną kolejność sortowania według wieku:

Before Sorting : [John, Roger, Steven] After Sorting by age : [Roger, John, Steven]

4.3. Komparatory Java 8

Java 8 zapewnia nowe sposoby definiowania komparatorów przy użyciu wyrażeń lambda i statycznej metody fabryki comparing () .

Zobaczmy szybki przykład użycia wyrażenia lambda do utworzenia komparatora :

Comparator byRanking = (Player player1, Player player2) -> Integer.compare(player1.getRanking(), player2.getRanking());

Metoda Comparator.comparing przyjmuje metodę obliczającą właściwość, która będzie używana do porównywania elementów, i zwraca pasującą instancję Comparator :

Comparator byRanking = Comparator .comparing(Player::getRanking); Comparator byAge = Comparator .comparing(Player::getAge);

Możesz szczegółowo zapoznać się z funkcjonalnością Java 8 w naszym przewodniku porównywania Java 8 Comparator.com.

5. Komparator a porównywalny

Porównywalne interfejs jest dobry wybór, gdy wykorzystywane do definiowania domyślnego zamawiania lub, innymi słowy, jeśli jest głównym sposobem porównywania obiektów.

Następnie musimy zadać sobie pytanie, po co używać Komparatora, skoro już mamy Porównywalny ?

Istnieje kilka powodów, dla których:

  • 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.

I jak zwykle kod źródłowy można znaleźć w serwisie GitHub.