Temat stary jak świat, jednak często powraca, gdyż nie każdy wie lub pamięta, w jaki sposób skutecznie posortować obiekty w Javie.
W tym krótkim wpisie przypomnę i pokażę jak to zrobić, zwłaszcza w przypadku streamów, które czasem mogą lekko zaskoczyć.
Wymagania
- Java 8
- 15 minut wolnego czasu
Interfejs Comparable
Aby prawidłowo posortować dowolny obiekt musimy zaimplementować na nim interfejs Comparable.
Interfejs ten ma jedną metodę: compareTo(..), która zwraca int
- ujemnego jeżeli obiekt porównywany jest mniejszy
- 0 – jeżeli oba obiekty są takie same
- dodatniego jeżeli obiekt porównywany jest większy
Napiszmy więc prostą klasę i zaimplementujmy w niej interfejs Comparable
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public class Person implements Comparable<Person>{ private String name; private String lastName; public Person(String name, String lastName) { this.name = name; this.lastName = lastName; } public String getName() { return name; } public String getLastName() { return lastName; } @Override public int compareTo(Person o) { return this.name.compareTo(o.name); } } |
Chcemy, aby obiekt typu Person był sortowany według imienia, a więc napisaliśmy metodę compareTo, która porówna same imiona. String ma swoją wbudowaną metodę compareTo, przez co nie musimy się już martwić jej implementacją.
Sortowanie
Napiszmy prostą listę z kilkoma obiektami typu Person:
1 2 3 4 |
List<Person> persons = new ArrayList<>(); persons.add(new Person("Janek","Kowalski")); persons.add(new Person("Piotr","Nowak")); persons.add(new Person("Anna","Piotrowska")); |
Teraz ją posortujmy:
1 |
Collections.sort(persons); |
Wynik -> posortowana lista osób:
[Person{name=’Anna’, lastName=’Piotrowska’}, Person{name=’Janek’, lastName=’Kowalski’}, Person{name=’Piotr’, lastName=’Nowak’}]
Jednak ważną uwagą jest to, że zmodyfikowaliśmy kolejność inicjalnej listy, co w dobie programowania funkcyjnego, niemutowalności, nie zawsze jest dobrą lub polecaną praktyką.
Możemy więc użyć streamów:
1 |
persons.parallelStream().sorted().map(Person::getName).forEach(System.out::println); |
Czego się spodziewamy? Zapewne wyświetlenia posortowanych imion. Niestety… tak się nie musi stać. Jeśli popatrzymy na dokumentację metody forEach, znajdziemy w niej zapis:
For parallel stream pipelines, this operation does not guarantee to respect the encounter order of the stream, as doing so would sacrifice the benefit of parallelism.
Czyli, jeżeli użyjemy wielowątkowości, to sort order nie musi być zachowany.
Na szczęście mamy metodę forEachOrdered 🙂
1 |
persons.parallelStream().sorted().map(Person::getName).forEachOrdered(System.out::println); |
Tutaj już mamy posortowane imiona.
Natomiast, jeżeli chcemy zwrócić listę posortowaną, to już nie mamy z tym problemu:
1 |
List<String> sorted = persons.parallelStream().sorted().map(Person::getName).collect(Collectors.toList()); |
Tym samym stworzyliśmy nową, posortowaną listę nie modyfikując poprzedniej. Jest to rozwiązanie funkcyjne.
Własny Comparator
Czasami pomimo posiadania interfejsu Comparable, dla jakiejś funkcjonalności potrzebujemy go zmienić. To wcale nie oznacza koszmarnego kodu i siwiejącego programisty złorzeczącego na dostawcę tak porąbanego wymagania 🙂
Przydaje się to też w momencie, kiedy nie mamy dostępu do klasy i nie mamy jak w niej zaimplementować interfejsu Comparable.
1 |
persons.sort(Comparator.comparing(Person::getLastName)); |
Zrobiliśmy własny Comparator, za pomocą którego posortowaliśmy listę, tym razem według nazwisk. Możemy ją też odwrócić jeśli trzeba:
1 |
persons.sort(Comparator.comparing(Person::getLastName).reversed()); |
Za pomocą streamów jest podobnie:
1 |
persons.stream().sorted(Comparator.comparing(Person::getLastName).reversed()).map(Person::getLastName).forEachOrdered(System.out::println); |
Sortowanie jest dość proste, o ile wiemy po czym mamy sortować. Reszta jest tylko odpowiednim użyciem kilku nieskomplikowanych metod 🙂
Złota myśl mojej Żony: Jak pada deszcz to internet wolniej chodzi, bo musi omijać kropelki.