Jason. Cała informatyka w jednym miejscu!

Czy już wiecie jak możemy posortować dowolną kolekcję w języku Java, która przyjmuje obiekty klasy niestandardowej? Mamy dwie drogi: interfejs "Comparator" lub interfejs "Comparable". Dzięki nim, jesteśmy w stanie ustalić kryterium w jaki sposób mają być sortowane obiekty, o czym pisałem w pierwszej części. Przyjrzymy się najpierw interfejsowi "Comparator" w języku Java.

INTERFEJS "COMPARATOR" W JĘZYKU JAVA NA DWA SPOSOBY

"Comparator" to interfejs funkcyjny definiujący kryterium według czego (dane składowe) i w jaki sposób (wyrażenie arytmetyczne) obiekty niestandardowe mają podlegać sortowaniu. Interfejs funkcyjny oznacza prostymi słowy tyle, że zawiera on tylko jedną metodę do przesłonięcia. Tytułowy "Comparator" posiada w sobie tę jedyną metodę i nosi nazwę "compare". Zwracając liczbę całkowitą, ustala kolejność według której obiekty mają znaleźć swoje miejsce. O tym, jak liczba całkowita decyduje o uszeregowaniu, w dalszej części materiału. Macie dwa sposoby określania takiego interfejsu:

  1. poprzez implementację metody "compare" w klasie implementującej interfejs "Comparator" w języku Java
  2. poprzez bezpośrednie osadzenie definicji metody "compare" w wywołaniu metody sortującej przyjmującej za parametr formalny interfejs "Comparator"

Różnica między nimi polega na miejscu osadzenia naszego kryterium. Pierwszy sposób nadaje się do przydzielenia konkretnego filtru wewnątrz klasy, a w drugim możemy je ustalić w formie wyrażenia lambda lub klasy wewnętrznej na zewnątrz klasy, której instancje będziemy chcieli sortować na swój własny sposób w pewnym sensie "jednorazowo". Kolejną różnicą jest później korzystanie z odpowiedniego nagłówka metody sortującej, ale do tego przejdziemy w odpowiednim czasie.

Muszę uświadomić Wam jednak, że ten temat prezentuje pewien "cosik", który spokojnie potraktowałbym jako temat na poziomie mocno zaawansowanym. Klasy kolekcji korzystają z typów generycznych ("generic types" albo "generics"), nazywanych również "typami ogólnymi". Aby rozpoznać je w kodzie, należy dostrzec parę nawiasów kątowych (<>). W środku nich umieszcza się dużą literę według powszechnie przyjętej konwencji nazewniczej (najczęściej to T lub E). Typy ogólne wymagają całej serii odrębnych artykułów, aby dostatecznie zobrazować Wam powagę sytuacji, a zarazem możliwości jakie one dają piszącemu. Na tę chwilę wystarczy Wam wiedzieć, że jak widzicie literę T w nawiasach kątowych, to jest to typ generyczny.

DEFINICJA INTERFEJSU "COMPARATOR"

Teraz zrobimy sobie przykład kolekcji, która pobiera instancje typu jakiejś klasy. Załóżmy, że to będzie książka ("Book"). Książka będzie klasą posiadająca kilka składowych, która w ten sposób pokaże sytuację, w której nie ma jak "domyślnie" posortować obiektów. "Comparator" w języku Java możemy zapisać na kilka sposobów. Albo tak:

Comparator<Book> comparator = new Comparator<Book>() {
	@Override
	public int compare(Book ba, Book bb) {
		return [stała / wyrażenie arytmetyczne];
	}
};

albo tak (wyrażenie lambda):

Comparator<Book> comparator = (ba, bb) -> {
	return [stała / wyrażenie arytmetyczne];
};

a jeszcze krócej można zapisać lambdę w taki sposób (tylko, gdy treść składa się z pojedynczej instrukcji):

Comparator<Book> comparator = (ba, bb) -> [stała / wyrażenie arytmetyczne];

To są przykłady definicji kryterium na zewnątrz klasy. Jeśli chcemy ustalić filtr sortowania bezpośrednio w klasie, postępujemy identycznie jak przy implementacji każdego innego interfejsu w klasie. Dodajemy do nazwy klasy końcówkę (zwrócić uwagę na identyczność typu klasy):

public class Book implements Comparator<Book>

i przesłaniamy wymaganą metodę:

@Override
public int compare(Book ba, Book bb) {
	return [stała / wyrażenie arytmetyczne];
}

Trzeba zwrócić uwagę, że "Comparator" zdradza w tym momencie parę istotnych szczegółów:

  • metoda "compare" zawiera dwa parametry formalne typu, według którego chcemy sortować obiekty (co ciekawe, to wcale nie musi być ten sam typ, ale najczęściej podaje się ten sam)
  • metoda "compare" zwraca liczbę całkowitą ("int")

To już mniej więcej sugeruje w jaki sposób będzie wyglądać sortowanie, aczkolwiek mogą być potrzebne wyjaśnienia. Sortowanie polega na zbadaniu wartości zwracanej przez metodę "compare". Te dwa parametry są po to, aby porównywać do siebie sąsiadujące ze sobą argumenty kolekcji celem podjęcia decyzji który z nich jest "lepszy" (tylko w sensie ustalenia pierwszeństwa). Robi się to przeważnie przy pomocy getterów pobierając konkretne dane składowe jakie mają wpływać na wynik i to staje się odpowiedzią w jaki sposób można utworzyć klasyfikowanie. Są trzy możliwości otrzymania rezultatu:

  1. jeśli wartość jest mniejsza bądź równa -1, to obiekt jest "niższego rzędu"
  2. jeśli wartość jest równa 0, to obiekty są sobie równe ("ex aequo")
  3. jeśli wartość jest większa bądź równa 1, to obiekt jest "wyższego rzędu"

Wynika z tego, że "Comparator" sortuje na podstawie porównania określanego przedziałem liczbowym.

Schemat sortowania przez interfejs "Comparator" w języku Java

Interfejs "Comparator" w języku Java sortuje według podanego kryterium pobieranego z metody "compare". Wynik całkowitoliczbowy decyduje o kolejności obiektów.

PRZYKŁAD KODU ŹRÓDŁOWEGO

Na sam koniec skwitujemy to sobie prostym przykładowym kodem. Skompilujcie to sobie zwracając uwagę na zapis porównania w kodzie:

KLASA "MAIN"

public class Main {
	public static void main(String[] args) {
		new Launcher();
	}
}

KLASA "BOOK"

public class Book {
	private final String title, author, publisher;
	private final int year;
	
	public Book(String author, String title, String publisher, int year) {
		this.author = author;
		this.title = title;
		this.publisher = publisher;
		this.year = year;
	}
	
	@Override
	public String toString() {
		return author + ". " + title + ". " + publisher + ", " + year;
	}
	
	public int getYear() {
		return year;
	}
}

KLASA "LAUNCHER"

import java. util.ArrayList;
import java. util.Comparator;
import java. util.Collections;

public class Launcher {
	private final ArrayList<Book> books = new ArrayList<>();

	public Launcher() {
		addBooks();
		printBooks();
		sortBooks();
		printBooks();
	}

	private void addBooks() {
		books.add(new Book("Adam Mickiewicz", "Pan Tadeusz", "Aleksander Jełowicki", 1834));
		books.add(new Book("Gustaw Herling-Grudziński", "Inny świat", "Roy", 1951));
		books.add(new Book("Aleksander Głowacki", "Lalka", "Gebethner i Wolff", 1890));
	}

	private void printBooks() {
		System.out.println("WYPISYWANIE LISTY:");
		books.forEach(System.out::println);
	}

	private void sortBooks() {
		Comparator<Book> comparator = (b1, b2) -> b1.getYear() - b2.getYear();

		Collections.sort(books, comparator);
	}
}

Koniecznie przyjrzyjcie się metodzie "sortBooks". Mamy w nim nasz "Comparator" w języku Java i zaraz po operatorze wyrażenia lambda, ustalamy kryterium. Za przykład zaprogramowałem sortowanie na podstawie roku wydania książki. W ten sposób, książki zostaną posortowane według roku od najstarszego do najmłodszego. Gdy będziemy chcieli sortować w drugą stronę, zmieniamy miejscami nazwy odwołań tak, żeby "b2" było odjemną, a "b1" odjemnikiem:

Comparator<Book> comparator = (b1, b2) -> b2.getYear() - b1.getYear();

albo potem odwracamy całą kolekcję za pomocą "reverseOrder":

Comparator<Book> comparator = (b1, b2) -> b1.getYear() - b2.getYear();

Collections.sort(books, Comparator.reverseOrder());

Możecie również zastąpić lambdę poniższą metodą:

Comparator.comparingInt(Book::getYear);

To będzie równoważne z zapisem w klasie "Launcher". Tak mi zasugerował "IntelliJ". Gdybyście chcieli ustalić inne kryterium np. przez sumę danych składowych, trzeba już pisać odręcznie.


Trudny temat? Być może, ale na pewno przy którymś projekcie się przyda.

PODOBNE ARTYKUŁY