Klasy i metody abstrakcyjne, Interfejsy
Klasy abstrakcyjne
Przyglądnijmy się przykładowi programu ilustrującego użycie klas abstrakcyjnych.
P19Abstrakcyjna.java
Klasy i metody abstrakcyjne
RodzinaKotowatych jest klasą abstrakcyjną. Definicja klasy abstrakcyjnej jest poprzedzona słowem kluczowym abstract.
Klasa abstrakcyjna zawiera dwie publiczne metody abstrakcyjne; są to metody upoluj(), zjedz(). Metoda abstrakcyjna nie ma ciała.
Klasa abstrakcyjna RodzinaKotowatych zawiera 2 pola. Zauważmy, że pole i jest polem statycznym, a to oznacza, że dla wszystkich budowanych przedstawicieli rodziny kotowatych będzie zajmowało to samo miejsce w pamięci; będzie więc możliwość zliczania tworzonych obiektów.
Klasa abstrakcyjna RodzinaKotowatych zawiera 2 konstruktory.
Nie można utworzyć obiektu klasy abstrakcyjnej za pomocą konstruktora i operatora new, można zbudować obiekt klasy abstrakcyjnej za pomocą rzutowania - o tym później. Algorytm zapisany w konstruktorach może być wykorzystany podczas budowania obiektów klas pochodnych. Można dziedziczyć po klasie abstrakcyjnej.
Klasy dziedziczące po klasie RodzinaKotowatych muszą zdefiniować rzeczywiste metody upoluj(), zjedz(). Będą to metody określające cel polowania i sposób odżywiania. Jeszcze raz warto zauważyć, że metody abstrakcyjne nie mają ciała i to metody w klasach dziedziczących muszą zadbać o to by określić sposób zachowania obiektu.
Klasa KotyDomowe (jest rozszerzeniem klasy abstrakcyjnej) ma dwa konstruktory.
W pierwszym konstruktorze (o dwóch argumentach) zostanie domyślnie wywołany konstruktor bezargumentowy klasy bazowej. Wszystkie koty tworzone za pomocą tego konstruktora będą kotami Ali i będą jadały z miski.
Drugi konstruktor (o trzech argumentach) wywołuje w sposób jawny konstruktor klasy bazowej - super("Uli"). Będą to koty Uli. Te koty będą mogły polować na coś innego niż miskę.
Przypatrzymy się teraz metodom zbudowanym w klasie KotyDomowe.
Słowo kluczowe this użyte w klasie KotyDomowe zastępuje obiekt - referencję do obiektu, na rzecz którego metoda została wywołana. Słowo kluczowe this może być użyte tylko wewnątrz metody. Metoda toString() w funkcji zjedz() na rzecz tego samego obiektu co metoda upoluj(). W metodzie zjedz() metoda upoluj() zostanie wywołana na rzecz tego samego obiektu co metota zjedz(). Referencja do obiektu, na rzecz którego wywołana została metoda, jest takim niejawnym dodatkowym argumentem.
a.metoda(x) <=> a.metoda(a,x) b.metoda(x) <=> b.metoda(b,x)
To tak, jakby przekazywać dodatkowy argument, za pomocą słowa this mamy dostęp do tego argumentu.
Należy pamiętać, że wywołując metodę klasy z wnętrza innej metody tej klasy, nie ma potrzeby używania słowa kluczowego this i ten przypadek zachodzi w metodzie zjedz(). W odwołaniu do funkcji System.out.println() tej samej funkcji, użycie słowa kluczowego this jest niezbędne, w funkcji upoluj jest to również niezbędne. W obydwu przypadkach należy przekazać referencję do metody spoza tej klasy. Tu już nie można się obyć bez słowa this.
Klasa KotyDuze zawiera dwa pola, dwa konstruktory dwie niezbędne metody: upoluj() i zjedz(), ponadto zawiera metodę wybierzOfiare(), za pomocą której można określić dowolny rodzaj ofiary. W klasie tej jest również metoda toString(), której umieszczanie w budowanych klasach w zasadzie powinno stać się nawykiem.
Każdy duży kot zdefiniowany za pomocą pierwszego konstruktora jest kotem Eli - wywoływany jest odpowiedni konstruktor klasy bazowej (super(Eli)) i może być to kot dowolnego gatunku.
W drugim z konstruktorów jest użyte słowo kluczowe this z argumentem w nawiasach okrągłych. Słowo kluczowe this z listą argumentów umieszczone w konstruktorze ma inne znaczenie niż to poprzednio objaśniane - powoduje wywołanie konstruktora tej samej klasy z odpowiednią listą argumentów. Można więc odwołać się do konstruktora tej samej klasy. Pierwsza instrukcja konstruktora z dwoma argumentami jest odwołaniem do konstruktora z tej samej klasy. Odwołanie to potwierdza, że wszystkie duże koty są kotami Eli.
Prześledźmy wykonanie programu
Obiekt o nazwie fruzia jest kotem domowm w kolorze czarnym i ma imię Fruzia, jego ofiarą jest wyłącznie miska (pierwszy linia wydruku).
Wykonanie metody zjedz() na obiekcie fruzia powoduje wykonanie metody upoluj() na obiekcie fruzia (druga spośród linia wydruku). Następną informacją o pierwszym kocie (o imieniu Fruzia) jest potwierdzenie skonsumowania upolowanej miski - kot ma apetyt, a to oznaka zdrowia.
Budujemy obiekt tygrys, jest to duży kot Eli. Na wydruku jest to kot o numerze 2.
Następnym poleceniem jest polecenie zjedz() dla tygrysa. Powoduje to wydanie polecenia upoluj() dla tygrysa i to upoluj() antylopę. Dla drugiego kota dostajemy odpowiednie wydruki.
Nadszedł czas na trzeciego kota (obiekt kufa). Jest to pręgowany kot domowy o imieniu Kufa polujący na myszy. Jest to kot Uli; niestety Ula nie karmi swoich kotów - one muszą polować. Patrz wydruki dotyczące kota numer 3.
Polecenie nakazujące konsumpcję dla obiektu kufa powoduje polecenie upolowania myszy a następnie jej skonsumowania.
Następny biekt to lew, jest to duży kot, którego zadaniem jest polowanie na zebry. Duży kot należący do gatunku lwów, polujący na zebry jest kotem Eli (wydruki dotyczące kota numer 4).
Polecenie zjedz dla lwa powoduje upolowanie zebry a następnie jej skonsumowanie.
Przejdźmy do piątego kota. Tworzymy geparda. Gepard poluje na antylopy. Polecenie zjedzenia powoduje uplowanie i zjedzenie antylopy. Gepardowi można nakazać upolowanie gazeli. Nakaz zjedzenia pozwala gepardowi na skonsumowanie gazeli. Jak zwykle duże koty są kotami Eli.
Ostani, szósty kot jest kotem domowym o imieniu Tusia. Jak wszystkie koty domowe jest kotem Ali. Jedynym sposobem polowania jego polowania jest polowanie na miskę. I jak wszystkie dziś utworzone koty ma dobry apetyt.
Na zakończenie, ponownie wyprowadzona jest informacja o kocie noszącym imię Fruzia - pierwszy zbudowany kot, zmienna statyczna i ma wartość równą 6. Zmienna ta przechowuje liczbę utworzonych obiektów.
Ilustracja procesu sprzątania pamięci
W Javie nie ma operatorów zwalniających pamięć przeznaczoną na obiekty, do których została utracona referencja. Jeżeli referencja obiektu przestaje być dostępna, to i tak zajmuje pamięć. Maszyna wirtualna Javy zaczyna sprzątanie (garbage collection - zbieranie nieużytków) w takim momencie, gdy jest to konieczne. Proces ten jest zilustrowany w poniższym programie.
P20Sprzatanie.java
Opis programu
Budujemy klasę KotySrednie, zawiera ona pola, które służyć będą do ilustracji przebiegu sprzątania wykonywanego przez maszynę wirtualną Javy
Pole zbytduzo - zostanie zmienione z wyjściowej wartości false na true w metodzie finalize() - podczas pierwszego odwołania się do niej. Wtedy też zostanie wydrukowany komunikat o niepokojącym zużyciu pamięci. Pole to jest statyczne, czyli wspólne dla wszystkich obiektów.
Pole koniec steruje pętlą budującą obiekty. Pole to ma początkową wartość równą false - daje to nieskończoną pętlę. Przestawienie tej wartości na true następuje w funkcji finalize(). Przestawienie to następuje w chwili, gdy jak się okaże unicestwiany jest kot o numerze o wybranym numerze (1000 w przykładzie). Dzięki temu istnieje szansa na zakończenie pętli. Obiekty przestają być tworzone, jeśli zachodzi konieczność unicestwienia obiektu o numerze 10000. Pole to jest polem statycznym.
Pole urodzonych zlicza ile obiektów powstało, służy do śledzenia, po którym urodzonym kocie zaczyna być za ciasno. Jest to pole statyczne.
Pole zaginionych jest zmieniane w metodzie finalize(). Zlicza liczbę unicestwionych obiektów. Jest to pole statyczne.
Pole numer nie jest polem statycznym. Każdy obiekt ma swój własny numer zgodny z kolejnością pojawiania się. Obiekt o numerze 1000 ma wyjątkowe znaczenie. Jego unicestwienie powoduje koniec narodzin. Pole koniec dostaje wartość true. Teraz pozostaje czekać czy populacja kotów przetrwa do zakończenia pracy programu.
Pozostaje znaleźć miejsce wywołania metody finalize(). W tym programie nie ma jawnego wywołania tej metody. Kiedy zatem jest ona wywoływana.
Metoda finalize() jest wywoływana przed każdym uruchomieniem odśmiecacza. Jeżeli zostanie utworzonych tyle obiektów, że zaczyna niebezpiecznie brakować pamięci i istnieją obiekty zbędne to włączy się odśmiecacz. Metoda finalize() w tym przykładzie służy do śledzenia procesu odśmiecania. Metoda finalize() może służyć do finalizacji obiektów nie związanej z pamięcią.
Metoda finalize() nie należy wywoływać bezpośrednio.
Metoda System.gc> wymusza odśmiecanie pamięci. Powoduje wywołanie metody finalize() i razem mogą służyć podczas testowania programu. Wywołanie tej metody może służyć do śledzenia poprawności finalizacji.
Problemy związane z metodą finalize(), to temat na przyszłość.
Polimorfizm, rzutowanie w górę
Następny przykład ilustruje dwa istotne pojęcia programowania zorientowanego obiektowo - rzutowanie w górę i polimorfizm.
P21Polimorfizm.java
Opis programu
Program zawiera klasę abstrakcyjną RodzinaKoty (podobną do klasy RodzinaKotowatych z poprzednich przykładów). W klasie tej są dwie metody zwykłe i dwie metody abstrakcyjne.
Metoda zwykła bawSie() przyjmuje argument klasy RodzinaKoty.
Metoda zwykła mojeImie() zwraca łancuch tekstowy, metoda ta będzie nadpisana w klasie Koteczek.
Dalej są dwie klasy Kotek iKoteczek i obydwie są klasami wywiedzionymi z klasy RodzinaKoty. W bydwu tych klasach są zbudowane, wymagane przez klasę bazową, metody, tzn. zjedz() i upoluj().
Każda z klas zawiera metodę spij().
W odwołaniach do metody bawSie() występuje rzutowanie w górę. W odwołaniu do tej metody argument klasy RodzinaKoty jest zastępowany argumentem klasy Kotek w pierwszym odwołaniu, a następnie klasy Koteczek. Jak widać ten sposób odwołania jest poprawny i daje zadawalające rezultaty - wykonywane są metody z odpowiednich klas.
Obiekty a5 i a6 są klasy RodzinaKoty, są klasami abstrakcyjnymi, ale zbudowane są za pomocą konstruktorów odpowiednio z klasy Kotek i Koteczek. Są zrzutowane na interfejs. Zauważmy, wykonanie metody zjedz() na obiektach klasy RodzinaKoty wykonuje metodę zjedz() zgodnie z rzeczywistym typem obiektu. Obiekt, po zrzutowaniu na klasę abstrakcyjną zachowa się tak, jak obiekt zbudowany wg użytego konstruktora, ale nie będą dostępne metody, które nie występują w klasie bazowej.
Przyjrzyjmy się metodzie spij(). Metoda ta występuje w obydwu klasach wywiedzionych, nie występuje ani jako metoda zwykła ani jako metoda abstrakcyjna w klasie bazowej. W tym przypadku konieczne jest rzutowanie w dół, musi być wykonane jawnie. Zwróć uwagę na na linie komentarzy - powodują błąd kompilacji, gdyby w komentarzach nie były.
Metoda mojeImie() jest nadpisana w klasie Koteczek, wykonanie tej metody na rzecz obiektu klasy RodzinaKoty, który powstaje przez zrzutownie w górę obiektu klasy Koteczek jest wywołaniem metody z klasy .. Rzutowanie nie zmieniło zachowania obiektu klasy Koteczek
Interfejsy
Przykład programu, w którym zastosowane są interfejsy jest zaprezentowany poniżej.
P22Interfejsy.java
Postać interfejsu
Interfejs to esencja klas abstrakcyjnych, zawiera wyłącznie metody bez ciała i pola finalne, pola finalne muszą być ustawione w interfejsie, nie mogą być zmienione. Definiuje więc cechy jakie powinna zawierać każda klasa implememtująca dany interfejs.
Interfejs jest klasą w pełni abstrakcyjną
Interfejs ma postać
interface NazwaInterfesu {
ciało interfejsu
}
Interfejs można implementować w klasach, jeśli klasa ma implementować interfejs to ma postać:
class NazwaKlasy implements Nazwanterfesu_1, Nazwanterfesu_2, ...{
ciało klasy
}
Interfejs może być rozszerzany przez dziedziczenie i wygląda wtedy
interface NazwaInterfesuRozszerzonego extends NazwaInterfejsu{
ciało interfejsu
}
Jak widać można implementować wiele interfejsów, ale co ważne można dziedziczyć tylko po jednej klasie i to bez względu na to czy jest to klasa zwykła czy klasa abstrakcyjna. Nie ma wielokrotnego dziedziczenia. Są pewne mechanizmy zastępujące wielokrotne dziedziczenie, wymagają one głębszego rozpatrzenia, ale to już przy innej okazji.
Ciało interfejsu może zawierać wyłącznie metody abstrakcyjne - nie trzeba pisać słowa kluczowego abstract. Wszystkie metody interfejsu muszą być publiczne - nie trzeba pisać słowa kluczowego public. Wszystkie klasy implementujące dany interfejs muszą przyjąć metody zapisane w interfejsie jako metody publiczne.
Ciało interfejsu może zawierać pola, muszą być to pola statyczne i, tu nowe pojęcie, muszą być to pola finalne - ich wartość nie może być zmieniana.
Pola finalne są poprzedzane słowem kluczowym final, a pola statyczne są oznaczone słowem kluczowym static, w przypadku interfejsów słowa kluczowe mogą być pominięte, są przyjmowane domyślnie. Każde Pole interfejsu jest statyczne i finalne z natury.
Pola interfejsu często są inicjowane stałymi. Nie jest to konieczne. Inicjowanie pola w interfejsie może mieć postać, np: int losowa = (int)Math.random();. Pola statyczne są inicjowane przy pierwszym do nich odwołaniu - podczas ładowania klasy.
Pola zdefiniowane jako finalne nie mogą być zmieniane - koty NaszeDomowe muszą się zadowolić miską. Metodę polowanie musimy umieścić w klasie NaszeDomowe ponieważ implementujemy interfejs WszystkieKoty. Niemniej żadne polecenie polowania na cokolwiek nie zostanie zrealizowane przez naszych milusińskich. Zawsze będzie wymagana micha i zjedzona micha.
Zmienne ostateczne - finalne, tzn. poprzedzone słowem final, są używane z dwóch powodów:
stałe czasu kompilacji, które nigdy nie są zmieniane, kompilator może te wartości wprowadzić do kodu podczas kompilacji, żeby takie postępownie było możliwe muszą być to zmienne typów podstawowych
wartości inicjowne podczas obliczeń, których w dalszym ciągu nie chcemy zmieniać,
Należy pamiętać, że jeśli obiekt zrobimy finalnym, to referencja staje się stała, tak więc po zainicjowaniu jej, nie będzie można tej referencji zmienić, sam obiekt będzie mógł być modyfikowany.
Obiekty f, g są tej samej klasy, tzn. klasy interfejsu, metoda jedzenie() pochodzi z klasy odpowiednio Nasze domowe i PozostaleKoty. Można rzutować na interfejs, nadal obowiązuje polimorficzne wykonanie metod.
Podsumowanie
Wykorzystanie pól interfejsów
Prześledźmy poniższy przykład.
Interfejs 1
Interfejs 2
Program je implementujący
P23Interfejs.java
Uwaga! Jeśli interfejs znajduje się w tym samym katalogu co klasa go implementująca, to nie ma potrzeby informowania kompilatora o implementacji interfejsów. W powyższym przykładzie wystarczy napisać:
public class P23Implement{
.....
}
Uwagi dotyczące poprzedniego przykładu
Interfejsy to czyste klasy abstrakcyjne. Pozwalają na utworzenie szkieletu aplikacji. Określają nazwy metod, listę ich argumentów, typy zwracanych wartości. Interfejs dostarcza formę bez implementacji.
Należy pamiętać, że interfejs może zawierać pola, które są wyłącznie statyczne i finalne.
Interfejsy są (dzięki własnościom z poprzedniego punktu) wygodnym narzędziem do grupowania stałych.
Plik MiesiacDzien.java zawiera stałe (tablicę - pole statyczne i finalne) przyporządkowujące każdemu miesiącowi i każdemu dniu w miesiącu wartość odpowiadającą znakowi zodiaku zapisanemu w interfejsie TablicaZodiak.
Plik TablicaZodiak.java zawiera interfejs zawierający tablicę tekstów odpowiadających znakom zodiaku, liczby zapisane w poprzedniej tablicy (plik MiesiacDzien.java) pozwalają na odczytanie tych tekstów.
Kompilacja pliku P23Implement.java za pomocą polecenia javac P23Implements.java pozwala na wykorzystanie interfejsów z plików TablicaZodiak.java i MiesiacDzien.java, o ile obydwa te pliki znajdują się w tym samym katalogu, na ich bezpośrednie wykorzystanie.