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