Pamiętacie nasz rozdział
Klasy - Dziedziczenie? Pisaliśmy tam o między innymi o tym,
że obiekt klasy rozszerzającej superklasę jest tego samego typu co superklasa. Pora teraz wrócić do tego zagadanienia. Faktem jest, że
interfejsy w Javie są również rozumiane jako typ, a więc
jeśli tworzymy obiekt implementujący dany interfejs to obiekt ten staje się obiektem jego
typu.
Typ obiektu implementującego interfejs
Wróćmy do naszej klasy
DocumentItem. Miała ona już do tej pory okazję dziedziczyć z regularnej klasy
Item, z abstrakcyjnej klasy
Item, a nawet implementowała interfejs
Item. W ten sposób pokazaliśmy jak różnie można przedstawić taką relację, w której
jeden byt wywodzi się z innego, dziedzicząc jego cechy i dodając swoje. Poniżej wracamy do przykładu, w którym nasza klasa implementowała dwa interfejsy. Przypominamy przy okazji, że w Javie możliwe jest
rozszerzanie tylko
jednej klasy, ale
implementować można wiele interfejsów.
Co możemy powiedzieć o naszej klasie, a właściwie co możemy powiedzieć o obiekcie stworzonym na bazie takiej klasy? Czy możemy powiedzieć, że obiekt tej klasy będzie obiektem typu
Item, czy też raczej obiektem typu
Image?
Jak się zapewne domyślacie obiekt ten będzie obiektem typu
Item oraz
Image.
W takiej sytuacji mówimy, że obiekt jest wielotypowy. Zobaczmy teraz co nam to daje.
Wielotypowy obiekt - przykład
Spróbujmy teraz stworzyć obiekt wielotypowy w klasie
Start i przeanalizujmy co się tam po kolei dzieje:
Najpierw tworzymy obiekt klasy
DocumentItem i przypisujemy go do typu
DocumentItem. W kilku kolejnych
linijkach (aż do przerwy) korzystamy z pełnego zestawu metod klasy. Pamiętamy, że część z nich została zaimplementowa, ponieważ wymusił to na nas interfejs
Item,
a część - a dokładnie jedna metoda
getImageName - została wymuszona przez interfejs
Image.
Zaraz po przerwie tworzymy kolejny obiekt klasy
DocumentItem, ale tym razem przypisujemy go do typu
Item.
Tutaj możemy korzystać już jedynie z metod, które są dostępne w ramach interfejsu
Item. Z tego powodu nie możemy wydrukować nazwy obrazka.
Nazwa ta jest dostępna tylko z interfejsu
Image, bądź też z klasy implementującej ten interfejs.
Na końcu, po kolejnej przerwie tworzymy trzeci obiekt klasy
DocumentItem, ale w tym przypadku przypisujemy go do typu
Image.
Spośród wszystkich zdefiniowanych przez nas metod mamy tutaj możliwość odwołania się tylko do metody
getImageName, gdyż tylko taka metoda jest zdefiniowana w interfejsie
Image.
Podsumowując, uzyskaliśmy dowód na to, iż obiekt klasy
DocumentItem jest wielotypowy. Najszerszym z typów jest
DocumentItem sam w sobie,
który implementuje wszystkie interfejsy, a więc zawiera też wszystkie metody tych interfejsów. Później mamy kolejne dwa typy
Item i
Image,
które pozwalają na uzyskanie dostępu do metod związanych tylko bezpośrednio z nimi.
Co nam daje wielotypowość
Po przeczytaniu poprzedniego paragrafu można się zastanawiać, po co właściwie rozbijać metody na różne interfejsy i tworzyć tyle typów. Musimy pamiętać, że
interfejsy w Javie
są stworzone po to, by definiować co powinno być wykonane w ramach danego interfejsu, a nie jak ma to być wykonane. Dlatego też należy tworzyć interfejsy, które
będą specjalizowały się w jednej konkretnej dziedzinie (definiującej jakie operacje mają być wykonane) i nie będą przeładowane metodami. Lepiej jest posiadać więcej mniejszych interfejsów niż mniej dużych.
Jeśli jest odwrotnie, to zwykle oznacza to, że źle przemyśleliśmy temat i nieodpowiednio podzieliliśmy interfejsy.
Mając interfejsy z odpowiednio wydzielonymi odpowiedzialnościami, możemy bardzo łatwo tworzyć w Javie program w oparciu o to co ma robić, a implementacja jest tak naprawdę tylko uzupełnieniem.
Jeśli mamy wiele interfejsów, możemy dowolnie sterować aplikacją. Na przykład możemy utworzyć metodę, która będzie oczekiwała obiektu danego interfejsu i będzie zajmowała się
zadaniami związanymi tylko z tym interfejsem.
W takiej sytuacji jeśli mamy klasę, której obiekty powinny być przetwarzane w tej metodzie, a wcześniej nie implementowała ona takiego interfejsu,
może w bardzo łatwy sposób zostać do tego przygotowana. Wystarczy, że zaimplementuje dany interfejs.
Przykładowo możemy mieć w aplikacji klasę odpowiedzialną za przetwarzanie obrazków:
Jeśli nasz
DocumentItem nie implementowałby interfejsu
Image, wówczas nie moglibyśmy przekazać obiektu typu
DocumentItem
do powyższej metody. Natomiast jako że implementujemy ten interfejs, możemy bez problemu przesłać obiekt do tej metody:
Inaczej mówiąc mamy klasę specjalizującą się w obróbce obrazków i pracuje ona na obiektach typu
Image, więc jeśli w naszym kodzie mamy obiekt innego typu,
a checemy by był on przetwarzany przez tą klasę, wówczas musimy go dopasować do jej wymagań.
Oczywiście w razie potrzeby możemy dorzucać kolejne interfejsy do danej klasy, tak aby jej obiekty stawały się obiektami typów oczekiwanych przez kolejne metody specjalizujące się
w jakiejś dziedzinie (tak jak
processImage specjalizuje się w obróbce obrazków).
Interfejsy bez metod
Na koniec jeszcze jedna ważna sprawa. Interfejsy często mają mało metod, a zdarza się nawet, że nie posiadają metod wcale. Wtedy interfejsy takie nazywamy
interfejsami znacznikowymi. Można się zapytać, po co nam interfejs, który
nie daje nam żadnej dodatkowej metody? I tutaj właśnie znowu musimy pomyśleć o interfejsach jako o mechanizmie kategoryzowania działań na obiektach.
W przypadku naszej klasy
ImageProcessor nie wspomnieliśmy nawet o tym, że będziemy wykorzystywać metodę interfejsu
Image.
Wystarczyło nam jedynie to, że obiekt przekazywany do tej metody ma być typu
Image. I to jest dosyć często spotykana sytuacja.
Przykłady interfejsów Javy
Przyjrzyjmy się teraz kilku interfejsom, które pochodzą bezpośrednio z samego języka. To jest zawsze dobry sposób na ugruntowanie zdobytej wiedzy.
- Serializable - interfejs z pakietu java.io. Oznacza on, że obiekt implementujący ten interfejs jest gotowy do zserializowania, czyli gotowy do przekształcenia do postaci binarnej.
To oznacza, że można go będzie wysłać przez sieć lub na przykład zapisać na dysk. Ten interfejs nie posiada żadnej metody - jest interfejsem znacznikowym.
- Comparable - interfejs z pakietu java.lang. Zaimplementowanie go w klasie powoduje, że obiekty tej klasy mogą być porównywane ze sobą.
To w jaki sposób będziemy porównywać obiekty, na przykład jakich pól będziemy używać i jak będzie wyglądał algorytm porównujący, pozostaje już w naszej gestii.
Musimy dostarczyć implementację metody compareTo tegoż interfejsu. Litera T oznacza tutaj typ obiektu jaki będziemy porównywać.
Co dokładnie oznacza użycie takiej litery wytłumaczymy, gdy zaczniemy mówić o typach generycznych. Na razie nie zaprzątajcie sobie tym głowy.
- Collection - interfejs z pakietu java.util. Definiuje on nam zbiór metod możliwych do wykonania na kolekcjach w Javie (zbiory danych trochę podobne do tablic - już niedługo poświęcimy temu zagadnieniu co najmniej jeden rozdział kursu).
Interfejs zawiera sporo metod, a niektóre z nich są bardziej złożone, dlatego na razie przedstawiamy tylko fragment. Możliwości kolekcji są naprawdę imponujące:
Przykład w programie
Na koniec zobaczmy jak kod naszego programu z wielotypowym obiektem wygląda w IDE:
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.