Programowanie Obiektowe (Java) Wyk ad pi ty ł ą 1. Polimorfizm w

Transkrypt

Programowanie Obiektowe (Java) Wyk ad pi ty ł ą 1. Polimorfizm w
Programowanie Obiektowe (Java)
Wykład piąty
1. Polimorfizm w Javie
Na poprzednim wykładzie zapoznaliśmy się z dziedziczeniem i dowiedzieliśmy się, że referencją klasy bazowej możemy
wskazywać na obiekty dowolnej klasy dziedziczącej po tej klasie podstawowej. Możemy również dla tych obiektów wywoływać
1
metody, które należą do interfejsu klasy bazowej . W tym ostatnim przypadku zachowanie tych metod niekoniecznie musi
być takie samo, jak zachowanie metod z klasy bazowej. Dzięki mechanizmowi zwanemu polimorfizmem lub późnym
2
wiązaniem lub wiązaniem dynamicznym lub wiązaniem w czasie działania możemy zapewnić, że działanie metod które
można wywołać przez interfejs klasy bazowej będzie inne dla obiektów różnych dziedziczących, wskazywanych przez
referencję klasy bazowej:
W przykładowym programie mamy zdefiniowane trzy klasy: klasę
Bazowa, klasę Pochodna, wywiedzioną z klasy Bazowa i klasę
class Bazowa {
public String toString() {
DrugaPochodna dziedziczącą po klasie Pochodna. W klasie Bazowa
3
return "Klasa Bazowa";
}
została nadpisana metoda toString() dziedziczona po klasie Object. W
klasie Pochodna nie jest zmieniana jej treść, ale za to ponownie
metoda toString() jest zmieniana w klasie DrugaPochodna. W
metodzie main() klasy publicznej tworzymy tablicę referencji klasy
}
Bazowa. W pierwszym elemencie tej tablicy (o indeksie zero)
class Pochodna extends Bazowa {}
zapisywana jest referencja do obiektu klasy Bazowa, w drugim do
obiektu klasy Pochodna i w trzecim do obiektu klasy DrugaPochodna.
W pętli for wywołujemy metodę println() dla każdego elementu tej
class DrugaPochodna extends Pochodna {
public String toString() {
return "Klasa DrugaPochodna";
tablicy. Ta metoda z kolei będzie wywoływała metodę toString() dla
każdego obiektu znajdującego się w tej tablicy. Jako pierwszy będzie tę
metodę miał wywołaną obiekt klasy Bazowa. Ponieważ ta klasa
dostarcza własnej metody toString(), to właśnie ta metoda zostanie
wywołana i na ekranie zobaczymy napis „Klasa Bazowa”. Następnie ta
metoda zostanie wywołana dla obiektu klasy Pochodna. Ponieważ ta
}
}
klasa nie dostarcza własnej definicji metody toString() to wywołana
public class Polimorfizm {
public static void main(String[] args) {
Bazowa[] tb = new Bazowa[3];
zostanie ta wersja metody toString(), która została odziedziczona
przez nią z klasy Bazowa i na ekranie zobaczymy napis „Klasa
Bazowa”. Po raz ostatni metoda toString() zostanie wywołana dla
obiektu klasy DrugaPochodna. Ta klasa dostarcza własnej definicji tej
metody, a więc na ekranie zobaczymy napis „Klasa DrugaPochodna”.
tb[0] = new Bazowa();
tb[1] = new Pochodna();
tb[2] = new DrugaPochodna();
for(int i=0; i<tb.length;i++)
System.out.println(tb[i]);
}
}
1
2
3
W jak sposób działa polimorficzne zachowanie metod? Kluczową
sprawą do poznania odpowiedzi na to zagadnienie jest poznanie
zjawiska późnego wiązania. Oznacza ono, że dopiero na etapie
wykonania programu ustalane jest połączenie między wywołaniem
metody, a jej ciałem. W językach proceduralnych takich jak C lub
Pascal zjawisko późnego wiązania nie występuje. Kompilatory takich
języków wiążą wywołanie podprogramu z miejscem jego definicji na
etapie kompilacji, gdyż posiadają całą niezbędną wiedzę aby coś
takiego uczynić. Taki mechanizm nazywa się wczesnym wiązaniem lub
wiązaniem czasu kompilacji. W przypadku języków obiektowych nie
zawsze da się go zastosować. Nie można ustalić która metoda ma
zostać wywołana dysponując tylko referencją klasy bazowej, gdyż
może ona wskazywać na obiekt dowolnej klasy pochodnej. Wiązanie tej
Innymi słowy możemy wysyłać do tych obiektów te same komunikaty co do obiektów klasy bazowej.
Ta definicja polimorfizmu jest przyjęta przez Bruce'a Eckela w książce „Thinking in Java”. Inni autorzy podają kilka innych
definicji polimorfizmu. W szczególności Henry F. Ledgard definiuje trzy rodzaje polimorfizmu: polimorfizm ad hoc polegający
na zwykłym przeciążaniu metod, polimorfizm parametryczny związany z takimi konstrukcjami językowymi jak szablony w
języku C++ lub typy generyczne, które pojawiły się w Javie 5 i w końcu polimorfizm zawierania, który jest zgodny z podaną
wyżej definicją.
Możemy również powiedzieć „przykryta”.
1
Programowanie Obiektowe (Java)
metody należy opóźnić do czasu wykonania. Wówczas odpowiedni mechanizm pozwoli zidentyfikować klasę obiektu
wskazywanego przez tę referencję i wywołać właściwą metodę. W Javie wiązania metod są domyślnie późne, chyba że dana
metoda jest zadeklarowana jako finalna lub prywatna. W Javie nie trzeba również wykonywać dodatkowych zabiegów, aby
4
skorzystać z polimorfizmu, wszystkie metody są polimorficzne , oprócz wspomnianych wcześniej metod prywatnych i
5
finalnych. Dziedziczenie określa, że pewne obiekty są typu innych obiektów . Polimorfizm pozwala na rozróżnienie tych
obiektów pozwalając na zdefiniowanie innego zachowania dla każdego z nich. Umożliwia on również programowanie
rozszerzalne. Jeśli pewne metody posługują się metodami należącymi do interfejsu klasy bazowej określonych obiektów, to
jeśli w programie pojawi się nowa klasa wywiedziona z tej klasy bazowej to nie trzeba będzie zmieniać kodu tych metod, gdyż
będą one mogły bezpośrednio współpracować z obiektami tej nowej klasy. W pewnych okolicznościach możemy więc pozwolić
sobie na pominięcie ponownej kompilacji części kodu nawet jeśli do programu została dodana nowa klasa. Przesłaniając
metody w klasach pochodnych należy pamiętać, aby miały one taką samą sygnaturę jak metody w klasie bazowej, inaczej
zamiast przesłaniania otrzymamy zwykłe przeciążanie metod.
2. Klasy abstrakcyjne
Na poprzednim wykładzie doszliśmy do wniosku, że dziedziczenie jest pewną formą stopniowego uszczegóławiania. Klasa
bazowa w hierarchii klas jest klasą najbardziej ogólną, natomiast klasy z niej wywiedzione są klasami bardziej
specjalizowanymi. Przy takim podejściu może się okazać, że jeżeli chcemy w prosty sposób skorzystać z mechanizmu
polimorfizmu, to wskazane byłoby utworzenie w klasie bazowej metod, które powinny się pojawić dopiero w klasach
6
pochodnych . Zamiast tworzyć puste metody Java pozwala nam na stworzenie metod abstrakcyjnych, według schematu:
abstract TypZwracany nazwaMetody();
np.:
abstract void metoda();
Klasa, która zawiera co najmniej jedną metodę abstrakcyjną również powinna być zadeklarowana jako abstrakcyjna, co
czynimy umieszczając przed słowem kluczowym class słowo abstract. Z powyższego opisu wynika, że użycie metody
abstrakcyjnej powoduje zadeklarowanie tej metody, ale nie zdefiniowanie. Co zatem się stanie jeśli wywołamy taką metodę?
Odpowiedź na to pytanie jest dosyć przewrotna: nic, ponieważ nie będziemy mogli stworzyć obiektu klasy, w której
zadeklarowana jest taka metoda. Klasy abstrakcyjne definiowane są w celu dostarczenia wspólnego obiektu dla innych klas,
7
które będą z nich wywiedzione . Klasy dziedziczące powinny dostarczyć definicji dla metod abstrakcyjnych lub powinny
również być klasami abstrakcyjnymi. Niemożność utworzenia obiektu klasy abstrakcyjnej gwarantuje nam kompilator, który
zgłosi błąd czasu kompilacji przy próbie wykonania takiej czynności. Klasy możemy również deklarować jako abstrakcyjne,
jeśli nie chcemy, aby tworzone były obiekty takich klas lub aby uzyskać lepszy model rozważanego problemu. Dla przykładu
rozpatrzmy następującą hierarchię klas:
Zgodnie z tym co zostało napisane wcześniej klasa Ptak jest klasą ogólną,
natomiast klasy Kura i Czapla są klasami specjalizowanymi. W tym
schemacie klasa abstrakcyjną powinna zostać klasa Ptak. Dwie pozostałe
Ptak
Kura
4
5
6
7
klasy definiują cechy pewnych obiektów, które w sposób naturalny
występują w przyrodzie, natomiast nie ma w przyrodzie „ogólnego ptaka”,
który poruszałby się w sposób ogólny, wydawał dźwięki w sposób ogólny, itd.
Tak więc w programie, w którym powinny być modelowane rzeczywiste
ptaki nie powinno być możliwe stworzenie obiektu ogólnej klasy Ptak.
Czapla
Przykładowo w języku C++, aby metoda była metodą polimorficzną należy przed nią umieścić słowo kluczowe „virtual”.
Przez typ rozumiemy tu interfejs obiektu, czyli powyższe zdanie oznacza, że pewne obiekty mają taki sam interfejs jak inne
obiekty.
Dokładniej, których zachowanie będzie można określić dopiero w klasach pochodnych.
Wkrótce poznamy inną konstrukcję językową, która pozwala na podobne działania i która jest częściej stosowana.
2
Programowanie Obiektowe (Java)
3. Inicjalizacja i finalizacja a polimorfizm
Konstruktory nie podlegają nadpisywaniu. Przeznaczenie tych metod jest specjalne, mają one na celu stworzenie i
zainicjalizowanie obiektu pewnej klasy. Z poprzedniego wykładu wiemy, że jeśli klasa dziedziczy po innej klasie, to kiedy
tworzony jest obiekt klasy pochodnej to tworzony jest również niejawnie obiekt klasy bazowej. W tym przypadku również
kompilator dba o to, aby pierwszą instrukcją realizowaną w konstruktorze było wywołanie konstruktora klasy bazowej.
Ponieważ musi być to ta konkretna metoda, posiadająca wiedzę na temat tego jak stworzyć dany obiekt i go zainicjalizować,
to w przypadku konstruktorów nie może być mowy o polimorfizmie. Nie powinno również wywoływać się innych metod
8
wewnątrz konstruktora, gdyż efekt ich działania może być nieprzewidywalny . Generalnie konstruktor powinien zrobić tylko
tyle, ile jest niezbędne do pozostawienia obiektu w stanie bezpiecznym i nic nadto, w szczególności nie powinien wywoływać
innych metod. Zaleca się również, aby wszędzie tam, gdzie to tylko możliwe inicjalizować zmienne w miejscu ich deklaracji.
Jeśli jakaś klasa przykrywa metodę finalize(), to klasa dziedzicząca po niej również powinna to czynić i w kodzie tej ponownie
przeciążonej metody powinna umieścić wywołanie metody z klasy nadrzędnej ( super.finalize() ). Wywołanie to powinno być
umieszczone jako ostatnia instrukcja w tej metodzie. Metoda finalize() powinna być deklarowana jako publiczna lub
chroniona, w przeciwnym wypadku kompilator zgłosi błąd.
4. Rozszerzanie i „czyste” dziedziczenie
W idealnym przypadku klasy dziedziczące powinny jedynie przykrywać metody interfejsu klasy bazowej. Wówczas
mielibyśmy do czynienia z czystą zastępowalnością. Opisywane klasy w takim przypadku związane są zależnością „bycia
czymś”. Niestety dosyć często zdarza się, że klasy związane są raczej zależnością „bycia podobnym do czegoś”. W takim
przypadku interfejs klasy bazowej w klasach pochodnych ulega rozszerzeniu od nowe, dodatkowe metody, co rodzi pewne
problemy. Wskazując na obiekty klas pochodnych referencją klas bazowej nadal możemy wysyłać do nich te same
komunikaty co do obiektów klasy bazowej, ale nie możemy wywoływać metod, które są właściwe tym klasom. Aby rozwiązać
ten problem, jeśli znamy klasę obiektu na który wskazuje referencja klasy bazowej, możemy zastosować bezpośrednie
rzutowanie w dół. Takie rzutowanie wykonujemy podobnie jak w przypadku zmiennych prostych typów umieszczając w
nawiasach okrągłych, przed nazwą zmiennej klasę, na którą chcemy rzutować. Niestety taki proces nie zawsze jest
bezpieczny. Aby uniknąć zagrożeń związanych z rzutowaniem w dół twórcy Javy wbudowali w ten język mechanizmy
pozwalające rozpoznać faktyczny typ obiektu wskazywanego przez referencję. Te mechanizmy zostaną omówione na
późniejszych wykładach.
8
Niektóre języki, jak C++ traktują to jako błąd i nie pozwalają na kompilację programów gdzie zastosowano opisane
rozwiązanie.
3