Javy nie ma końca. Kontynuujemy temat typów ogólnych. Poprzednio wspomniałem o słowie kluczowym "extends", które nie tylko jest wykorzystywane podczas dziedziczenia przez klasy, ale także przez typy generyczne. 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. Poznajcie tajemnicę drugiego znaczenia "rozszerzania", bo przy programowaniu uogólnionym ma to dwojakie zastosowanie.

TYPY GENERYCZNE. DWIE STRONY "ROZSZERZANIA"

Wiemy już, że główne zastosowanie "extends" to rozszerzanie o już istniejącą klasę tworzące w ten sposób dziedziczenie. Przypominając sobie metodę generyczną z poprzedniego przykładu:

private <T extends JComponent> void printComponents(ArrayList<T> components)

też widzimy "extends", 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 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. Uprzedzam pytanie, można wstawiać tyle ograniczeń ile się chce.

Skrzyżowane typy generyczne w Javie

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.

Zakończmy tę opowieść kolejnym przykładem szerszego kodu źródłowego. 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 ArrayList<Book> books;

	public Launcher()
	{
		createInstances();
		addBooks();
		printComponents(books);
	}

	private void createInstances()
	{
		books = new ArrayList<>();
	}

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

PODOBNE ARTYKUŁY