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