Javy nie ma końca. Kontynuujemy temat typów ogólnych. Poprzednio wspomniałem o słowie kluczowym "extends" w języku Java, które nie tylko jest wykorzystywane podczas dziedziczenia przez klasy, ale także przez typy generyczne. Poznajcie tajemnicę drugiego znaczenia "rozszerzania", bo przy programowaniu w Javie ma to dwojakie zastosowanie.
Tweet |
"EXTENDS" W JĘZYKU JAVA ZNACZY COŚ WIĘCEJ NIŻ "ROZSZERZANIE"
Wiemy już, że najczęstsze zastosowanie "extends" to rozszerzanie o już istniejącą klasę oznaczające dziedziczenie. Ale to NIE JEST jedyne zastosowanie, bo znajdziecie je także w definicjach typów generycznych! Zajmijmy się najpierw tą bardziej pospolitą częścią.
DZIEDZICZENIE PRZEZ KLASĘ POTOMNĄ
Nie da się ukryć, że zdecydowanie większy odsetek początkujących programistów ujrzało po raz pierwszy słowo "extends" w akcji właśnie podczas definiowania dziedziczenia dla jakiejś klasy. Znajome?
public class B extends A {
}
Oczywiście! Reszta szczegółów została uwieczniona w stosownym artykule. Teraz żeby nie przedłużać, druga rzadziej spotykana sytuacja.
OGRANICZANIE ZAKRESU TYPÓW GENERYCZNYCH
Jeśli nie przeczytaliście fragmentu o metodach uogólnionych, to przerwijcie czytanie w tym miejscu i wróćcie do tamtego artykułu, bo to będzie kontynuacja i na nic się zda tłumaczenie do czego to jest bez znajomości typu ogólnego. Przypominając sobie metodę generyczną z poprzedniego przykładu:
private <T extends JComponent> void printComponents(ArrayList<T> components)
też widzimy "extends" w języku Java, które tym razem "ogranicza" nam zbiór dopuszczalnych typów obiektów, które są przechowywane w kolekcji. Czyli inaczej rzecz ujmując, możemy podstawić kolekcję dowolnego typu będącego podaną klasą macierzystą lub jej jedną z klas potomnych. Jednak zdaje się, że brakuje jeszcze jednej "cegły" w tym wszystkim. Tu jest mowa o klasach, ale co z interfejsami?
Interfejsami TEŻ można wyznaczać granice i też za pomocą tego samego słowa kluczowego i to jest ten wyjątek jaki charakteryzuje typy generyczne w Javie. Tu nie ma "implements", tak jak ma to miejsce przy implementowaniu interfejsu do klasy. Nie ma też ŻADNEGO INNEGO słowa kluczowego. Zanim przejdę do przykładu, warto sobie wyjaśnić czemu postanowiono trochę namieszać w głowie przez taki manewr.
NOWE SŁOWO KLUCZOWE RODZI NOWY PROBLEM
Kiedy twórcy wypuścili Javę w wersji 5.0, nikt nie przypuszczał wtedy, że dodadzą nową funkcję jaką były właśnie typy generyczne. Oznaczało to nowe możliwości, nowe przyzwyczajenia oraz nowe wyzwanie stojące przed ludźmi zarządzającymi językiem. Co pierwsze wtedy przychodzi do głowy? Nowe słowo kluczowe! Tylko to mogłoby wyrządzić dużo szkód dla już występujących projektów, które zostały napisane przy użyciu starszej wersji sprzed 5.0. W jaki sposób?
Wystarczy, że dowolna zmienna, metoda, klasa albo cokolwiek innego będzie nazwane DOKŁADNIE tak samo jak słowo, które od nowej wersji będzie już zarezerwowanym słowem kluczowym. I co wtedy? Trzeba zmienić! A to oznacza wejście do projektu, modyfikację, zapis zmian i rekompilację. Jeśli to jest jakiś "domowy" projekt to pół biedy, ale jeśli to jest jakaś poważna aplikacja biznesowa, która posiada ponad 15,000 klas? Niezbyt kolorowa historia.
Jednak dalej trzeba stawiać pytanie: czemu w takim razie typy generyczne postanowiono odciąć od "implements"? Moim zdaniem, powodowałoby to konieczność napisania dwóch podobnych do siebie ciągów ograniczeń między nawiasami kątowymi, co już brzmi jak czysta abstrakcja, ponieważ nie można utworzyć dwóch osobnych warunków dla jednego T. Tworząc parametryzację typów na podstawie nakładania ograniczeń, które już opisałem, nie można zrobić dwóch takich "bloczków" stojących obok siebie. Można osadzić jeden w drugi, ale nie dwa osobno. Ten akapit potraktujcie jako tylko moje podejrzenie dlaczego "scalono" jedno słowo kluczowe dla wymuszania zarówno "pochodzenia" od klasy, jak i implementowania interfejsu. Tak jest i tak trzeba się stosować.
TYPY GENERYCZNE RAZEM Z WYMUSZENIEM INTERFEJSU
Wyjaśniliśmy sobie ten dylemat więc podsumujmy fakty. Jeżeli kiedykolwiek będziecie chcieli wymusić restrykcję nie tylko od klasy, ale również od interfejsu, to piszecie tak:
<T extends [klasa] & [interfejs]>
Powyższy zapis nazywany jest po angielsku "intersection types", czyli w wolnym tłumaczeniu "skrzyżowane typy" czy też "część wspólna typów". "Nosicielem" tego terminu jest znak "ampersandu" (&) rozdzielającego nazwy klas lub interfejsów. Przykładowo, gdybyśmy mieli klasę i interfejs, i chcielibyśmy podstawić za T tylko konkretne klasy implementujące konkretny interfejs, to musielibyśmy to zapisać tak:
<T extends Document & Printable>
Uprzedzam pytanie, można wstawiać tyle ograniczeń ile się chce. Po pierwszym interfejsie, podajecie kolejny zaraz po tym samym znaczku. Na przykład:
<T extends Document & Printable & Updatable>
Skrzyżowane typy zwane jako "intersection types" umożliwiają stosowanie pewnej koniunkcji warunków dla typów kolekcji, które MUSZĄ być spełnione wszystkie bez wyjątku, aby doszło do zaakceptowania właściwych typów obiektów. Definiuje się je za pomocą tego samego słowa "extends" w języku Java!
PRZYKŁAD KODU ŹRÓDŁOWEGO
Zakończmy tę opowieść kolejnym przykładowym kodem źródłowym. Powrócimy znowu do klasy książki:
KLASA "MAIN"
public class Main {
public static void main(String[] args) {
new Launcher();
}
}
INTERFEJS "PRINTABLE"
public interface Printable {
void printMessage();
}
KLASA "BOOK"
public class Book implements Printable {
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 void printMessage() {
System.out.println("WYPISUJĘ KSIĄŻKĘ: " + author + ". " + title + ". " + publisher + ", " + year);
}
}
KLASA "LAUNCHER"
import java. util.ArrayList;
public class Launcher {
private final ArrayList<Book> books = new ArrayList<>();
public Launcher() {
addBooks();
printComponents(books);
}
private void addBooks() {
books.add(new Book("Kathy Sierra, Bert Bates", "Java. Rusz głową!", "O'Reilly", 2011));
books.add(new Book("Cay S. Horstmann", "Java. Podstawy", "Helion", 2020));
books.add(new Book("Brian W. Kernighan, Dennis M. Ritchie", "Język ANSI C. Programowanie", "Helion", 2010));
}
private <T extends Book & Printable> void printComponents(ArrayList<T> components) {
System.out.println("WYPISYWANIE ELEMENTÓW LISTY GENERYCZNEJ");
for (T t : components) {
t.printMessage();
}
}
}
Mamy ponownie "ArrayList". Wykorzystując typy generyczne w miejsce nagłówka metody, możemy doskonale nakładać zabezpieczenia dla akceptowanych kolekcji i tu też widzimy "extends" w języku Java. W tym przypadku metoda będzie akceptować kolekcję "ArrayList" tylko o takich typach, które pochodzą od "Book" lub jej dowolnej klasy potomnej ORAZ implementują interfejs "Printable". Gdyby klasa "Book" sama w sobie nie miała interfejsu "Printable", to wystąpiłby błąd kompilacji ze względów zapewnienia bezpieczeństwa typów, gdyż wykorzystywana jest metoda interfejsu "printMessage".
W takiej sytuacji moglibyście zrobić klasę potomną "Book" i w niej implementować interfejs "Printable", a później w drodze tworzenia instancji obiektów w oparciu o tę klasę potomną (pochodzącą od klasy "Book" ORAZ implementującą interfejs "Printable", czyli OBA warunki spełnione), móc bez problemu osadzić taką kolekcję do parametru formalnego metody uogólnionej. Jest to naprawdę super potężna broń i cały czas będę to powtarzał.
Jeżeli będziecie rozumieć każdy zapis typów generycznych, to na rynku pracy będziecie "kozakami"! I tym zdaniem skończę niniejszy artykuł.