Sortowanie kolekcji z wykorzystaniem wyrażeń lambda w Java 8

Sortowanie kolekcji obiektów po wybranym atrybucie

Jedną z rzeczy które zawsze irytowały mnie w Javie była ilość kodu która trzeba było napisać żeby posortować kolekcję zawierającą typu zdefiniowane przez użytkownika po kluczu który implementuje już interfejs Comparable. Weźmy prosty obiekt POJO

class Person {

    public Integer   id;
    public String    firstName;
    public String    lastName;
    public LocalDate birthDate;

    public Person(Integer id, String firstName, String lastName, LocalDate birthDate) {
        this.id        = id;
        this.firstName = firstName;
        this.lastName  = lastName;
        this.birthDate = birthDate;
    }

    @Override
    public String toString() {
        return String.format(
                "Person {id=%d, firstName=%s, lastName=%s, birthDate=%s}",
                id, firstName, lastName, birthDate);
    }
}

Jak posortować listę takich obiektów po dacie urodzin? Jednym z popularnych sposobów jest zdefiniowanie komparatora jako klasy anonimowej

	Collections.sort(personList, new Comparator<Person>() {
		@Override
		public int compare(Person p1, Person p2) {
			return p1.birthDate.compareTo(p2.birthDate);
		}
	   });
   }

Kod nie wygląda zbyt elegancko a co więcej nie uwzględnia sytuacji, gdy data zatrudnienia będzie równa null
Można też oczywiście zdefiniować komparator w osobnej klasie, ma to sens gdy takie sortowanie powtarza się w wielu miejscach gdyż umożliwia to ponowne wykorzystanie kodu


class PersonByBirthDateComparator implements Comparator<Person>() {
	@Override
	public int compare(Person p1, Person p2) {
		return p1.birthDate.compareTo(p2.birthDate);
	}
   });
}
Collections.sort(personList, new PersonByBirthDateComparator());

Biblioteka na ratunek?

Skoro sortowanie kolekcji obiektów po wybranym atrybucie jest tak uciążliwe, to na pewno musi istnieć jakaś biblioteka która to usprawnia – lenistwo jest przecież jedną z cnót programistów, prawda?
I rzeczywiście, częściowo tak jest

Google Guava

Korzystając z klasy Ordering możemy problem sortowanie rozwiązać tak

Collections.sort(personList, Ordering.natural().
	onResultOf(new Function<LocalDate, Person>() {
	  public String call(Person p) {
		 return p.birthDate;
	  }))
  .nullsLast();

Znowu nie wygląda to dobrze, ale wyrażenia lambda ratują sytuację

Collections.sort(personList, Ordering.natural().
	onResultOf(p -> p.birthDate).
	nullsLast();

Apache Commons BeanUtils

Innym rozwiązaniem jest wykorzystanie klasy BeanComparator

Collections.sort(personList, new BeanComparator("birthDate"));

Kod wygląda dobrze, ale w praktyce ma dwie poważne wady. Pierwszą z nich jest duży narzuty wydajnościowy wynikający z dostępu do pól klasy przez refleksję. Drugim problem występuje przy automatycznej refaktoryzacji – jeśli ktoś ma tak jak podświadomy nawyk wciskania Alt+Shift+R gdy widzi nazwę która mu się nie podoba może narobić niezłego bałaganu

Czy może być lepiej

W dotychczasowych dywagacji nasuwają się niezbyt optymistyczne wnioski – a jak dotąd poruszyliśmy tylko prostszy wariant problemu. Co w sytuacji, gdy chcemy posortować obiekty po kluczu złożonym (tj. po kilku polach na raz)? Dlaczego sortownie po kluczu który implementuje już interfejs Comparable jest tak niewygodne?

Z przekonania, że może być lepiej narodził się projekt jarek-przygodzki/more-collections. Nie jest to jeszcze w żadnym razie dzieło skończone, co więcej kod obsługujący sortowanie po kluczach złożonych nie jest zbyt elegancki (ale tutaj chyba lepiej być nie może ze względu na ograniczenia typów uogólnionych) – ale efekty i tak są zachęcające

// Sortowanie imieniu
MoreCollections.sort(personList, p -> p.firstName);
// Sortowanie po  dacie urodzenia
MoreCollections.sort(personList, p -> p.birthDate);
// Sortowanie po kluczu złożonym [lastName, firstName], tj. najpierw po nazwisku
// a potem po imieniu
MoreCollections.sort(personList,
    p -> p.lastName,
    p -> p.firstName);

Jak to działa

Rozwiązanie opiera się o obiekty funkcyjne java.util.function.Function<T, R> które służą to wyłuskania z obiektu typu T klucza sortowania typu R gdzie typ R musi spełniać ograniczenie R extends Comparable<? super R>. Następnie na podstawie takiego operatora wyłuskania (lub kilku) budowany jest specjalny komparator i przekazywany to metody Collections.sort

Comparator<T>; comparator = CombinedComparator.build(propertyAccessor);
Collections.sort(list, comparator);

Na resztę rozwiązania składają się klasy CombinedComparator łącząca wiele komparatorów w jeden i CombinedComparator.ByPropertyComparator która sortuje obiekty typu T względem klucza zwracanego przez wskazaną funkcję. Sam kod biblioteki nie korzysta co prawda z funkcjonalności wersji 8, ale wyrażania lambda pozwalają pisać bardzo idiomatyczny kod z niej korzystający.

One thought on “Sortowanie kolekcji z wykorzystaniem wyrażeń lambda w Java 8

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Log Out / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Log Out / Zmień )

Facebook photo

Komentujesz korzystając z konta Facebook. Log Out / Zmień )

Google+ photo

Komentujesz korzystając z konta Google+. Log Out / Zmień )

Connecting to %s