Programowanie Obiektowe (Java) Wyk ad drugi ł 1. Programowanie
Transkrypt
Programowanie Obiektowe (Java) Wyk ad drugi ł 1. Programowanie
Programowanie Obiektowe (Java) Wykład drugi 1. Programowanie obiektowe, a programowanie strukturalne Załóżmy, że w projekcie programu obsługującego bibliotekę wyodrębniliśmy dwie części, jedną odpowiedzialną za obsługę czytelników, drugą za obsługę książek. Stosując podejście strukturalne moglibyśmy zdefiniować odpowiednie struktury danych, napisać podprogramy, które będą wykonywać na nich operacje i całość zamknąć w dwóch osobnych modułach1. Tak napisany kod ma kilka wad. Zmienne i struktury danych istnieją w oderwaniu od procedur je obsługujących. Nie można „na pierwszy rzut oka” wskazać procedur, które są pomocnicze od procedur z których powinien korzystać programista używający wspomnianych modułów. Nie mamy również gwarancji, że ów programista nie zdefiniuje własnych procedur, które będą wykonywały operacje na zmiennych i strukturach, które z jakiegoś powodu nie powinny być na nich wykonywane, innymi słowy nie mamy kontroli nad dostępem do poszczególnych elementów zawartych w module. Dodatkowo ponowne użycie tak napisanego kodu w innych projektach jest trudne. W podejściu obiektowym tworzymy typ danych nazywający się np.: „Książka” lub „Czytelnik” i definiujemy powiązane z nim operacje2. Dodatkowo możemy ukryć dane oraz procedury, którymi nie powinien posługiwać się programista korzystający z naszego kodu, a udostępnić to co jest niezbędne do realizacji zamierzonego celu3. Ponowne wykorzystanie kodu jest bardzo ułatwione dzięki dziedziczeniu i kompozycji.4 2. Założenia programowania obiektowego 1. Wszystko jest obiektem. Obiekt jest to swego rodzaju zmienna przechowująca dane, oraz mająca możliwość wykonywania operacji na tych danych. Obiekt modeluje nam jakiś fragment rozwiązywanego problemu. 2. Program jest zbiorem obiektów, które przesyłają między sobą komunikaty, powodując tym samym wykonanie określonych czynności. W praktyce wysłania komunikatu jest odpowiednikiem wywołania funkcji w języku C (w językach obiektowych podprogramy nazywane są metodami). 3. Każdy obiekt posiada swoją własną pamięć, na którą mogą składać się inne obiekty. Każdy obiekt ma swój wewnętrzny stan, na który składają się informacje przechowywane w zmiennych będących składowymi tego obiektu. W szczególności te zmienne mogą być również obiektami. 4. Każdy obiekt posiada swój typ. Typ obiektu w języku Java określa jego klasa5. Jak się przekonamy programowania obiektowe składa się głównie z trzech czynności: definiowania klas tworzenia ich obiektów i przesyłania między nimi komunikatów. Typ obiektu określa jakie komunikaty mogą zostać mu wysłane. Mówimy również, że obiekty są instancjami swoich klas. 5. Wszystkie obiekty danego typu mogą otrzymywać te same komunikaty. Ta cecha obiektów jest szczególnie ważna, kiedy stosujemy techniki dziedziczenia i kompozycji, pozwalające na wielokrotne wykorzystanie kodu. Techniki te będą omawiane na przyszłych wykładach. 3. Podstawowe pojęcia 1 Klasa – jest nowym zdefiniowanym przez nas typem danych, który opisuje stan oraz możliwe operacje jakie możemy przeprowadzić na jakimś elemencie rozwiązywanego problemu. Obiekt – jest zmienną, której typ jest opisany przez klasę, innymi słowy klasa stanowi typ tej danej. Pole – jest to zmienna, która stanowi część obiektu, może być zmienną prostego typu lub obiektem. Metoda – podprogram związany z obsługą danych przechowywanych w polach obiektu. Wywołanie metody nazywa się w terminologii obiektowej wysłaniem komunikatu do obiektu. Poniewa przykład dotyczy biblioteki, czyli miejsca gdzie wypoycza si ksiki celowo nie zostało uyte słowo biblioteka w sensie zbiór procedur 2 To zjawisko nazywa si enkapsulacj 3 4 Ta własno nazywa si ukrywaniem implementacji lub hermetyzacj i bdzie omówiona na nastpnych wykładach Znaczenie tych poj bdzie wyjanione na nastpnych wykładach 5 To stwierdzenie nie do koca jest cisłe, o czym bdzie mowa na nastpnych wykładach 1 Programowanie Obiektowe (Java) Przykład: Klasa: Człowiek – kady człowiek posiada imi, nazwisko, wiek. Obiekt: konkretna instancja klasy, np.: Jan Kowalski, lat 37 Zanim przejdziemy do przedstawienia przykładowego kodu źródłowego, zawierającego klasy musimy zapoznać się bliżej ze zmiennymi w Javie. Ze względu na to co one przechowują możemy podzielić je na dwie kategorie: zmienne typów prostych i referencje. Zmienne prostych typów przechowują wprost dane i są odpowiednikami zmiennych takich typów w języku C, jak double, int itd. Referencje są odpowiednikami wskaźników w języku C, czyli są zmiennymi zawierającymi adres, pod którym w pamięci operacyjnej znajdują się inna zmienna, zawierająca dane (w przypadku języka Java jest to zawsze obiekt). W porównaniu ze wskaźnikami referencje są prostsze w użyciu (nie trzeba wykonywać dereferencji – mówiąc prosto nie trzeba stosować gwiazdek, ani ampersandów) oraz bezpieczniejsze (nie pozwalają na stosowanie arytmetyki wskaźników, a pamięć na którą wskazują jest automatycznie zwalniana, co będzie opisane w dalszej części wykładu). Należy jeszcze wyjaśnić, gdzie magą być tworzone zmienne. Jeśli są to zmienne typów prostych lub referencje, to mogą one być tworzone w obszarze pamięci przeznaczonym na zmienne statyczne, na stosie (jeśli są zmiennymi lokalnymi metod) oraz na stercie (jeśli są składowymi obiektów). Zmienne będące obiektami mogą być tworzone wyłącznie na stercie. Oto program, w którym zdefiniujemy klasę, stworzymy obiekt tej klasy i wywołamy odpowiednie metody tego obiektu: class Czlowiek { return nazwisko; String imie, nazwisko; } int wiek; void setNazwisko(String n) { int getWiek() { nazwisko=n; return wiek; } } } void setWiek(int w) { public class PrzykladObiektu { public static void main(String[] args) { if(w>0) wiek=w; Czlowiek o = new Czlowiek(); } o.setNazwisko("Kowalski"); o.setImie("Jan"); String getImie() { o.setWiek(37); return imie; System.out.println("Imi: "+ o.getImie()); } System.out.println("Nazwisko: "+ o.getNazwisko()); System.out.println("Wiek: "+o.getWiek()); void setImie(String i) { } imie=i; } } String getNazwisko() { W programie tym zdefiniowaliśmy klasę o nazwie „Człowiek”. Posiada ona dwa pola, które są obiektami pozwalającymi przechowywać ciągi znaków („imię” i „nazwisko”) oraz pole, które przechowuje liczby całkowite („wiek”). Dodatkowo 2 Programowanie Obiektowe (Java) zawiera ona metody pozwalające modyfikować zawartości tych pól. Takimi metodami są np.: „getWiek()” i „setWiek()”6. Jak widać metody są bardzo podobne do funkcji w języku C, z tym, że nie istnieją one samodzielnie, ale są częścią klasy. Metody mogą zwracać wartości typów prostych lub referencje do obiektów. Mogą również nic nie zwracać, jeśli jako typ wartości zwracanej zadeklarujemy „void”. Słowo kluczowe „return” powoduje zwrócenie przez metodę wartości i zakończenie jej działania. Można go również użyć w metodzie nie zwracającej żadnej wartości, wówczas bezpośrednio po nim będzie występował średnik, a jedynym skutkiem jego działania będzie zakończenie wykonywania metody. W metodzie „main” tworzymy obiekt tej klasy. Wszystkie obiekty w Javie są tworzone w sposób dynamiczny przy pomocy operatora „new” i wywołania metody zwanej konstruktorem7. Zmienna, która występuje po lewej stronie instrukcji przypisania nie jest obiektem, lecz referencją do obiektu, czyli zmienną, która przechowuje adres obiektu, który umieszczony jest na stercie. Zwolnieniem pamięci przeznaczonej na obiekt zajmuje się mechanizm języka zwany odśmiecaczem (ang. garbage collector). Po utworzeniu obiektu wywołujemy jego metody (wysyłamy do niego komunikaty) nadające jego polom odpowiednie wartości. Wywołanie metody ma postać: nazwaReferencjiDoObiektu.nazwaMetody(). Następnie wywołujemy metody, które zwracają wartości pól. Te wartości są następnie konwertowane na wartości klasy „String” i łączone przy pomocy operatora „+” z odpowiednimi napisami, po czym całość jest wypisywana na ekran. Programiści piszący w Javie przyjęli pewną konwencję nadawania nazw klasom i ich składowym. Nazwa klasy może składać się z jednego lub kilku połączonych wyrazów. Każdy z tych wyrazów powinien rozpoczynać się dużą literą. Pola, zmienne automatyczne (lokalne) oraz metody również mogą mieć nazwy składające się z kilku wyrazów, ale pierwszy wyraz w tej nazwie powinien być pisany małą literą, pozostałe, tak jak ma to miejsce w nazwach klas, są pisane dużymi literami. 3. Inicjalizacja zmiennych Domyślnie pola obiektu są inicjalizowane wartościami zerowymi, natomiast zmiennym automatycznym (lokalnym) nie są nadawane żadne wartości początkowe, ale próba ich użycia przed przypisaniem im określonej wartości zostanie wychwycona przez kompilator i potraktowana jako błąd. Ponieważ brak inicjalizacji zmiennych jest częstym i poważnym błędem popełnianym przez wielu programistów, Java podobnie jak inne języki obiektowe dostarcza środków, które pozwalają sobie z tym problemem poradzić. Każda klasa w Javie jest wyposażona w specjalną metodę, która jest odpowiedzialna przynajmniej za przydzielenie i wyzerowanie pamięci na tworzony obiekt. Taka metoda nazywa się tak samo jak klasa, nie zwraca żadnej wartości (nawet typu „void”) i określana jest mianem konstruktora. Jeśli nie zdefiniuje jej programista, to do klasy dołączany jest automatycznie przez kompilator konstruktor domyślny. Nie posiada on żadnych parametrów i realizuje tylko te czynności, które zostały opisane wyżej. Autor klasy może określić własny konstruktor, który podobnie jak konstruktor domyślny nie będzie miał parametrów, ale oprócz swojego podstawowego działania będzie wykonywał jeszcze dodatkowe czynności, lub konstruktor z parametrami. W takim wypadku konstruktor domyślny przestaje być dostępny, gdyż kompilator uznaje, że jest zbyteczny. Można w klasie zdefiniować kilka konstruktorów, każdy z inną listą argumentów i o innym sposobie działania. Takie zjawisko nazywa się przeciążaniem konstruktorów: class Liczba { Liczba(int i, float y) { int a; a=i; float f; f=y; } Liczba() { } a=1; public class Konstruktory { } public static void main(String[] args) { Liczba l = new Liczba(); Liczba(int i) { a=i; Liczba l1 = new Liczba(3); f=0.0f; Liczba l2 = new Liczba(3,3.0f); } } } 6 Uycie w nazwach metod słów angielskich „set” i „get” podyktowane jest tym, e w Javie istniej mechanizmy, które wymagaj ich uycia. W tym przykładzie nie s one konieczne, ale dobrze jest od pocztku przyzwyczaja si od pewnych konwencji. Słowo „set” oznacza „ustaw”, słowo „get” oznacza „pobierz”. 7 Pewien wyjtek od tej reguły stanowi zmienne klasy „String”. 3 Programowanie Obiektowe (Java) Wszystkim zmiennym można nadać wartość początkową bezpośrednio w miejscu ich deklaracji, np.: int a=5; Dodatkowo w klasach można użyć tzw. inicjalizacji instancyjnej: class Liczba { int a; float f; { a=2; f=3.0f; } } Jeśli pola są referencjami do obiektów, to domyślnie nadawana jest im, tak jak i pozostałym polom, wartość zerowa, czyli w ich wypadku „null”. Te zmienne można inicjalizować przypisując im adresy obiektów tworzonych za pomocą operatora „new”. W przypadku obiektów klasy String można to uczynić prostszym sposobem np.: String str = „Napis”; Jeśli pole w klasie klasie poprzedzimy słowem kluczowym „static”, to będzie ono istniało w obrębie klasy, a nie obiektu i możemy odwoływać się do niego według schematu: NazwaKlasy.nazwaPola. Aby wykonać to odwołanie nie musimy tworzyć obiektu tej klasy. Należy pamiętać, że to pole jest tworzone tylko raz i jest wspólne dla wszystkich obiektów tej klasy. Zmienne statyczne klasy możemy inicjalizować w miejscu ich deklaracji lub za pomocą konstrukcji podobnej do inicjalizacji instancyjnej, różniącej się od niej tylko tym, że jest poprzedzona słowem kluczowym „static”. 4. Kolejność inicjalizacji Jako pierwsze są inicjalizowane pola statyczne klas w momencie utworzenia obiektu klasy je zawierającej lub w momencie odwołania się do tych pól. Wtedy maszyna wirtualna Javy wczytuje plik z rozszerzeniem .class (takie pliki są tworzone na etapie kompilacji dla każdej z klas, które występują w kodzie źródłowym) i dokonuje inicjalizacji statycznych składowych (pól) klasy. Te pola są inicjalizowane tylko raz. Jeśli tworzymy obiekt tej klasy, to najpierw jest alokowana odpowiednia ilość pamięci na stercie dla niego, następnie ta pamięć jest zerowana i inicjalizowane są wszystkie pola niestatyczne, którym nadawana jest wartość w miejscu ich deklaracji. Na koniec wywoływane są konstruktory i wykonywana zawarta w ich ciele inicjalizacja. 5. Inicjalizacja tablic Tablice w Javie są również obiektami. Możemy je tworzyć na dwa sposoby, np.: int[] a = new int[20]; lub int a[] = new int [20]; Te instrukcje tworzą tablice, które mogą pomieścić 20 elementów typu int. Tablice mogą również przechowywać obiekty. Sam zapis int[] a oznacza stworzenie referencji do tablicy, ale nie obiektu tablicy. Z kolei dwie poprzednie instrukcje tworzą obiekty tablic, ale wartości ich elementów są domyślnie zerowe. Aby nadać im inne wartości możemy użyć instrukcji iteracyjnych, np.: pętli for. Możemy również użyć wyrażenia inicjalizującego, które zarówno tworzy obiekt tablicy, jak i wypełnia go wartościami, np. Int[] a = {1, 3, 5, 6}; W Javie, podobnie jak w C i C++ możliwe jest tworzenie tablic wielowymiarowych. Mając obiekt tablicy, którego adres przechowuje referencja „a” i mając referencję „c” do tablicy tej samej klasy, możemy przypisać jej adres tablicy z referencji „a”: c=a; 6. Przeciążania metod Podobnie jak konstruktory można przeciążać również zwykłe metody. Nazwa metody wraz z listą parametrów tworzy sygnaturę metody. Zachowując nazwę metody, ale zmieniając listę parametrów, np.: dodając lub usuwając jakiś parametr możemy uzyskać dwie metody wewnątrz klasy o takiej samej nazwie, ale innym zachowaniu i przyjmujące inne argumenty wywołania. W Javie można nawet przeciążać metody zmieniając kolejność parametrów na liście, ale nie jest to zalecany sposób. Również w przypadku przeciążania metod, których parametry są zmiennymi prostych typów należy zachować ostrożność, ze względu na zjawisko automatycznego promowania typów. Powstaje pytanie w jaki sposób kompilator rozpoznaje, która wersja metody ma zostać wywołana. Otóż bierze on pod uwagę nie tylko 4 Programowanie Obiektowe (Java) nazwę metody, ale również argumenty jej wywołania i na tej podstawie ustala właściwą metodę. Innymi słowy, kompilator rozpoznaje metody po ich sygnaturach. Metod nie można przeciążać za pomocą zmiany typu wartości zwracanej. 7. Referencja „this” Powstaje pytanie, jak metoda może rozpoznać na rzecz jakiego obiektu została wywołana? Okazuje się, że do każdej metody kompilator niejawnie dopisuje parametr, przez który przekazywana jest referencja do tego obiektu. Ta referencja nazywa się „this” i można z niej skorzystać wewnątrz metody. Zapis this.nazwaPola pozwala sięgnąć do pola obiektu o podanej nazwie, zapis this.nazwaMetody() pozwala wywołać odpowiednią metodę. Dodatkowo z referencji this można korzystać w konstruktorach, co pozwala wywołać konstruktor wewnątrz innego konstruktora, np.: this („Napis”); spowoduje wywołanie konstruktora przyjmującego jako parametr obiekt klasy „String”. Ten zapis można wykorzystać tylko i wyłącznie w konstruktorach, nie wolno go używać w „zwykłych” metodach. 8. Metody statyczne Metody statyczne (taką metodą jest metoda „main”) tworzymy pisząc słowo kluczowe „static” przed definicją tej metody. Wywołujemy je według wzorca: NazwaKlasy.nazwaMetody(); Metody statyczne są związane z klasą, a nie z obiektem, w związku z tym nie otrzymują referencji „this” i nie mogą odwoływać się do pól, ani metod niestatycznych swojej klasy, ani innych klas, chyba że przekażemy im przez parametr referencję do obiektu tej klasy, lub stworzymy w ich wnętrzu obiekt tej klasy i jego adres zapiszemy w referencji, która jest zmienną automatyczną. Przykład: public class Statyczne { void pierwszaMetoda() { System.out.println("Została wywołana pierwsza metoda."); } static void drugaMetoda() { System.out.println("Została wywołana druga metoda."); } public static void main(String[] args) { drugaMetoda(); Statyczne s = new Statyczne(); s.pierwszaMetoda(); } } W przykładzie, wywołując metodę statyczną klasy pominięto nazwę tej klasy. Jest to dopuszczalne, jeśli metodą wywołującą jest metoda należąca do tej samej klasy. 9. Porównywanie obiektów Pola obiektów, które są prostych typów można porównywać bezpośrednio, za pomocą operatora (==) ten sam operator zastosowany do referencji również działa, ale porównuje tylko referencje (adresy obiektów), a nie ich zawartość. Zawartość obiektów klasy „String” możemy porównywać przy pomocy metody „equals”, np.: a.equals(b); gdzie „a” i „b” są referencjami do obiektów klasy „String”. Aby móc porównywać zawartość stworzonych przez nas obiektów musimy zdefiniować w ich klasach własne metody „equals”. Problem ten będzie dokładniej omówiony przy omawianiu zjawiska polimorfizmu. 10. Sprzątanie Jak już wcześniej zostało napisane, nie trzeba zwalniać pamięci po obiektach, zajmuje się tym odpowiedni mechanizm języka. Niemniej jednak czasem zachodzi potrzeba, aby obiekt wykonał jakąś czynność zanim zostanie usunięty, np.: żeby wymazał z ekranu figurę, którą narysował. Można w tym celu wykorzystać metodę „finalize()”, co zostanie pokazane na przykładzie: 5 Programowanie Obiektowe (Java) class Odpadek { public void finalize() { System.out.println("Usunito"); } } public class Finalizacja { public static void main(String[] args) { for(int i=1; i<20; i++) new Odpadek(); System.runFinalization(); System.gc(); } } W przykładzie tworzone są w pętli nowe obiekty, ale adresy do nich nie są nigdzie zapisywane, stanowią więc one idealnych kandydatów do odśmiecenia. Przed wykonaniem tej czynności jest wywoływana metoda „finalize()” obiektu, więc wydaje się ona idealnym rozwiązaniem dla problemów, które zostały opisane wyżej. Niestety działanie odśmiecacza nie jest deterministyczne, tzn. nie można przewidzieć kiedy zadziała i czy usunie wszystkie nieużywane obiekty. Nawet wywołanie metod „runFinalization()” i „gc()” klasy „System”, których nazwy sugerują, że wymuszają finalizację i uruchomienie odśmiecacza nie koniecznie musi dać takie efekty. Według dokumentacji te metody jedynie „sugerują” maszynie wirtualnej Javy, że należy posprzątać po nieużywanych obiektach i zwolnić po nich pamięć. 6