Typy generyczne, a inaczej
generyki, już samą nazwą potrafią wzbudzać trwogę i tak naprawdę, mimo że jest to jedno z najważniejszych pojęć w programowaniu,
ciężko zabrać się do jego nauki. Okazuje się, że zupełnie niepotrzebnie, bo to naprawdę całkiem przyjemne rozwiązanie, a stosowane z głową
znacznie ułatwia proces tworzenia kodu.
Kod bez typów generycznych
Naukę generyków najlepiej zacząć od przykładu, który pokazuje, jak problematyczne jest tworzenie kodu bez ich użycia.
Appa Notka.
Drobna uwaga. Celowo wprowadziliśmy w kodzie tworzenie obiektu Long i String
za pomocą new. Chcemy przez to podkreślić typy tworzonych obiektów. W zupełności wystarczyłoby użyć tutaj literału L w przypadku longa
oraz samych cudzysłowów w przypadku stringa. Jest to nawet zalecane:
Do rzeczy. W powyższym kodzie zadaniem metody
printNumbers jest wydrukowanie dwóch liczb zapisanych wcześniej
w liście
someNumbers. Na pierwszy rzut oka wszystko wygląda elegancko. Kod nie dość, że się kompiluje, to jeszcze
rozpoczyna się jego uruchamianie.
Niestety program nie wykona się poprawnie do końca. Podczas
operacji przypisania drugiego elementu listy do zmiennej
lastNumber
otrzymamy wyjątek:
Zanim przejdziemy do wyjaśnienia konkretnej przyczyny wyjątku, omówmy sobie, co się dzieje w tej linii kodu.
Próbujemy przypisać obiekt do zmiennej typu
Long.
Ze względu na to, że nasza lista przechowuje obiekty typu
Object (domyślnie),
rzutujemy pobrany obiekt na typ Long.
Możemy to zrobić, ponieważ
Long dziedziczy z
Object.
Taka operacja wykona się poprawnie, jeśli lista przechowuje na danej pozycji faktycznie obiekt stworzony z typu
Long,
tak jak to ma miejsce w przypadku liczby
123.
Naszym problemem jest to, że liczba 21345 została podana w formie stringa -
"21345", a precyzyjniej mówiąc w postaci obiektu typu
String.
Nie jest to więc obiekt typu
Long, stąd dostajemy wyjątek opisujący błędne rzutowanie -
ClassCastException.
Appa Notka.
Rzutowanie (z ang. cast) to przekształcanie obiektu jednego typu w obiekt innego typu w ramach hierarchii dziedziczenia.
Tak więc, jeśli na przykład klasa MovieItem dziedziczy z klasy Item, wtedy po stworzeniu obiektu klasy
MovieItem możemy go zrzutować do typu Item. Podobnie obiekt klasy Long
może zostać zrzutowany do typu Number, ponieważ klasa Long dziedziczy z klasy Number:
Takie coś działa, ale jednak nie jest najlepszym zwyczajem i powinniśmy tego unikać.
Rzutowanie najczęściej świadczy o błędnie zaprojektowanym kodzie (często w innym miejscu programu). Zresztą sami przyznacie, że przy większej liczbie rzutowań można
się zgubić co i gdzie jest jakim obiektem. Lepiej zainwestować w czytelność kodu i to jest właśnie ten moment, w którym z pomocą przychodzą nam typy generyczne.
Kod z typami generycznymi
Zacznijmy od razu od konkretów. Zmienimy nasz przykład, wprowadzając do niego typy generyczne. Innymi słowy, określimy, z jakimi typami obiektów chcemy pracować w danej części kodu. Typy będziemy określać, stosując nawiasy ostre:
<, >:
Na obrazku widzimy, że obok deklaracji interfejsu
List pojawiła się informacja o typie obiektów (
Long), jaki chcemy przechowywać w tej liście.
To bardzo ważna zmiana. Dzięki niej już na etapie kompilacji jesteśmy informowani, że próbując wstawić obiekt stringowy do tej listy, popełniamy błąd
(add in list cannot be applied to String).
W ten sposób bardzo wcześnie, bo jeszcze przed uruchomieniem programu, dowiadujemy się, że mamy błąd w kodzie.
Odkąd deklarujemy "przywiązanie" naszej listy do typu
Long,
możemy pozbyć się rzutowania, o czym zresztą informuje nas IDE.
Komunikat mówi nam, że rzutowanie jest nadmiarowe
(Casting...to 'Long' is redundant).
Po zmianach nasz kod będzie wyglądał tak:
Co to jest Operator Diamond?
W podanym przykładzie może Was zastanawiać jeszcze jedna rzecz. Mianowicie, co się dzieje po prawej stronie operatora przypisania, a więc
w miejscu, w którym tworzymy instancję obiektu listy za pomocą słowa
new. Tam również wprowadziliśmy
nawiasy ostre, ale nie określiliśmy typu.
Zrobiliśmy to, ponieważ od Javy 7 istnieje możliwość, by po prawej stronie użyć
Operatora Diamond, czyli takiego właśnie oznaczenia bez ponownego określania typu.
Został on wprowadzony, aby ograniczyć potrzebę wpisywania oczywistego kodu. Wiadomo, że deklarując listę konkretnym typem (tutaj
<Long>),
faktyczna instancja obiektu będzie mogła przechowywać tylko obiekty zgodne z tym typem. Nie ma potrzeby powielania tej informacji.
Podsumowanie
W ten sposób udało nam się rozpocząć wątek typów generycznych w Javie, choć tak naprawdę omówiliśmy jedynie typy w ramach kolekcji.
Tematyka generyków jest nieco bardziej rozbudowana, dlatego będziemy ją kontynuować w kolejnym rozdziale. W przypadku kolekcji polecamy wrócić na chwilę do
rozdziałów, w których opisywaliśmy listy (
Kolekcje - Listy), sety (
Kolekcje - Sety) i mapy (
Kolekcje - Mapy). Wprowadziliśmy tam kilka generycznych deklaracji, którym z pewnością warto się przyjrzeć raz jeszcze.
Autor: Jarek Klimas
Data: 03 stycznia 2024
Labele: Backend, Podstawowy, Java
Czy informacje, które otrzymałeś, były pomocne?
Jeśli tak, zapraszam Cię do podarowania mi kawy.
Masz pytanie dotyczące prezentowanego materiału?
Coś jest dla Ciebie niejasne i Twoje wątpliwości przeszkadzają Ci w pełnym zrozumieniu treści?
Napisz do nas maila, a my chętnie znajdziemy odpowiednie rozwiązanie.
Najciekawsze pytania wraz z odpowiedziami będziemy publikować pod rozdziałem.
Nie czekaj. Naucz się programować jeszcze lepiej.