Programowanie Obiektowe (Java) Wyk ad czwarty ł 1. Diagramy

Transkrypt

Programowanie Obiektowe (Java) Wyk ad czwarty ł 1. Diagramy
Programowanie Obiektowe (Java)
Wykład czwarty
1.
Diagramy klas UML
sposób symbolicznego opisu klasy
Ponieważ istnieje wiele języków programowania, które umożliwiają korzystanie z techniki obiektowej konieczne okazało się opracowanie
środków reprezentowania pojęć związanych z programowaniem obiektowym w sposób niezależny od stosowanego języka programowania.
Najpopularniejszym takim środkiem jest UML (ang. Unified Modeling Language ) ujednolicony język modelowania. Słowo „język” w
nazwie oznacza nie język programowania, lecz język opisu. UML jest bardzo złożony, my będziemy korzystać tylko z niektórych elementów
tego języka. Na wstępie zapoznamy się z diagramem klasy. Załóżmy, że chcemy zamodelować klasę, która opisuje czajnik do gotowania wody.
Ta klasa będzie nazywała się oczywiście „Czajnik” i będzie posiadała trzy metody publiczne, stanowiące jej interfejs: „napełnij”, „zagotuj”,
„opróżnij”. Oto diagram takiej klasy w UML:
Czajnik
napelnij()
Polskie litery w nazwach metod zostały celowo pominięte. UML pozwala na
umieszczać w diagramie klasy również składowe o dostępie innym niż publiczny,
jeśli jest to konieczne. My będziemy się ograniczać tylko do składowych klasy
będących częścią interfejsu klasy. Czasem będziemy nawet rezygnować z
umieszczania w diagramie klasy składowych publicznych, jeśli nie będzie to
konieczne do zrozumienia problemy. Wówczas diagram klasy będzie po prostu
prostokątem z wpisaną w niego nazwą klasy.
zagotuj()
oproznij()
2.
Powtórne wykorzystanie kodu w Javie
W językach obiektowych, do których zalicza się Java ponowne użycie kodu oparte jest na pojęciu klasy. Język Java udostępnia dwie techniki
pozwalające na wykorzystanie istniejących klas do budowania nowych bez konieczności modyfikowania ich kodu. Pierwszą z nich nazywa się
kompozycją, a druga dziedziczeniem.
3.
Kompozycja
Z kompozycją spotkaliśmy się już wcześniej. Ta technika polega na umieszczeniu referencji do obiektów1 wewnątrz nowej klasy. Oto
przykład:
Stosując kompozycję musimy pamiętać o prawidłowej
inicjalizacji składowych nowej klasy. Domyślnie wszystkie
pola mają wartość zerową, odpowiadającą ich typom. W
naszym przykładzie pola klasy „String” będą miały wartość
„null”, a pole typu „byte” będzie miało wartość „0”.
Kompilator nie sprawdza, czy pola klasy zostały
zainicjalizowane, ale maszyna wirtualna Javy wykryje każdą
próbę zapisania, czy wywołania metody obiektu za pomocą
referencji o wartości „null” i wyrzuci wyjątek. Inicjalizacji
możemy dokonać na trzy różne sposoby: W miejscu definicji
zmiennej, tak jak zostało to zrobione w przypadku pola „imie”
(wiersz 2), w konstruktorze, jak ma to miejsce w przypadku
pola „nazwisko” (wiersz 7) lub tuż przed użyciem, jak ma to
miejsce w przypadku pola „wiek” (wiersz 33). Ta ostatnia
technika nazywana jest również techniką „leniwej
inicjalizacji”.
Na
diagramach
UML
kompozycję
przedstawiamy w następujący sposób:
1 class Czlowiek {
2
private String imie = new String("Jan");
3
private String nazwisko;
4
private byte wiek;
5
6
public Czlowiek() {
7
8
nazwisko = new String("Kowalski");
}
9
10
public String getNazwisko() {
11
2
return nazwisko;
}
13
14
public String getImie() {
15
16
imie
return imie;
Czlowiek
}
17
18
public int getWiek() {
19
return wiek;
20
1
nazwisko
wiek
}
jak również zmiennych prostych typów
1
Programowanie Obiektowe (Java)
Powyższy schemat oznacza, że klasa „Czlowiek” posiada dwie
składowe: „imię”, „nazwisko” i „wiek”. Celem uproszczenia na
diagramach UML pomija się dosyć często wypełniony rąb,
którym zaczyna się połączenie między klasą i jej składową.
21
22
public void setWiek(byte a) {
23
wiek=a;
24
}
25 }
26
27 public class Kompozycja {
28
public static void main(String[] args) {
29
Czlowiek c = new Czlowiek();
30
System.out.println("Imię: "+c.getImie()+
31
" Nazwisko: "+c.getNazwisko()+
32
" Wiek: "+c.getWiek());
33
c.setWiek((byte)25);
34
System.out.println("Imię: "+c.getImie()+
35
" Nazwisko: "+c.getNazwisko()+
36
" Wiek: "+c.getWiek());
37
}
38 }
4.
Dziedziczenie
Dziedziczenie jest techniką pozwalającą na przekazanie nowej klasie interfejsu innej klasy2. Klasa, która otrzymuje go nazywana jest klasą
pochodną, lub klasą wywiedzioną, klasa od której jest pobierany interfejs jest nazywana klasą bazową lub klasą podstawową. Podobnie jak z
kompozycją, z dziedziczeniem spotkaliśmy się już wcześniej. Okazuje się, że każda klasa w Javie dziedziczy po klasie „Object” i otrzymuje od
niej takie metody jak np.: toString() i equals(). W innych przypadkach musimy jednak jawnie określić która klasa po której dziedziczy. Służy
do tego słowo kluczowe extedns. W języku angielskim oznacza ono „rozszerza” i odnosi się do faktu, że klasa pochodna może zawierać
metody, które nie wchodzą w skład interfejsu klasy bazowej, metody które zostały dodane do nowej klasy przez jej twórcę. Klasa dziedzicząca
ma dostęp do wszystkich składowych publicznych i chronionych klasy dziedziczonej. Jeśli te klasy należą do jednego pakietu, to ma również
dostęp do składowych o dostępie pakietowym. Klasa pochodna może zmienić funkcjonowanie metod klasy bazowej. Ta technika nazywa się
przykrywaniem lub nadpisywaniem metod (ang. overriding) i zostanie omówiona szerzej na następnych wykładach. Jeżeli w klasie bazowej
jakaś metoda została wielokrotnie przeciążona, to klasa pochodna ma dostęp do wszystkich wersji tej metody. W pewnych sytuacjach może to
powodować problemy, więc należy z tej własności Javy korzystać ostrożnie. Klasa bazowa jest klasą ogólną, natomiast klasy pochodne są
klasami bardziej szczegółowymi. Można więc traktować dziedziczenie jako „uszczegóławianie” istniejących już klas. Oto prosty przykład
dziedziczenia z diagramem w języku UML obok:
class Ptak {
public void lataj() {System.out.println("Leci ptak.");};
Ptak
}
class Orzel extends Ptak {
public void lataj() {System.out.println("Leci orzeł.");}
}
class Golab extends Ptak {
Orzel
public void lataj() {System.out.println("Leci gołąb.");}
}
public class Dziedziczenie {
public static void main(String[] args) {
Ptak p = new Ptak(); Golab g = new Golab(); Orzel o = new Orzel();
o.lataj(); g.lataj(); p.lataj();
}
}
2
Przekazywana jest również implementacja, ale dziedziczenie ma głównie związek z interfejsem.
2
Golab
Programowanie Obiektowe (Java)
W programie występują trzy klasy. Klasa „Ptak” jest klasą bazową, po której dziedziczą klasy „Orzeł” i „Gołąb”. W tych ostatnich klasach
zmieniane jest zachowanie metody „lataj()” z klasy bazowej. Dziedziczenie nie polega na skopiowaniu do klasy pochodnej metod i pól klasy
bazowej. Za każdym razem kiedy tworzymy obiekt klasy pochodnej tworzony jest również niejawnie obiekt klasy bazowej jako składowa
obiektu klasy pochodnej, do którego dostęp mamy poprzez referencję o nazwie „super”. Jeśli w klasie pochodnej chcemy więc odwołać się do
pola lub wywołać metodę klasy bazowej do której mamy dostęp czynimy to według schematów:
super.NazwaZmiennej;
lub
super.nazwaMetody();
Rodzi się pytanie, w jaki sposób jest inicjalizowany ten obiekt klasy bazowej? Otóż inicjalizacja tego obiektu odbywa się w konstruktorze
klasy pochodnej. Kompilator języka Java zawsze dostarcza domyślnego konstruktora klasy i umieszcza na jego początku wywołanie
konstruktora klasy bazowej. Jeśli nadpiszemy lub przeciążymy konstruktor klasy pochodnej, to i tak kompilator jako pierwszą instrukcję
umieści w nim automatycznie wywołanie konstruktora domyślnego klasy bazowej. Inaczej sytuacja wygląda jeśli klasa bazowa nie posiada
konstruktora domyślnego, tylko konstruktor z parametrami. Wówczas kompilator nie wie jak taki konstruktor wywołać i jeśli nie zrobi tego
programista zgłosi błąd kompilacji. Nie wystarczy jednakże wywołać konstruktor klasy bazowej w konstruktorze klasy pochodnej, to
wywołanie musi być pierwszą instrukcją w konstruktorze klasy pochodnej i również odbywa się przy pomocy referencji „super”:
W konstruktorze klasy „Pochodna” jest
wywoływany
konstruktor
klasy
„Bazowa”, który przyjmuje parametr
private String pole;
klasy „String”. Należy również zwrócić
uwagę, że w metodzie „main” jest
wywoływana metoda „getPole()” za
public Bazowa(String s) { pole=s; }
pomocą referencji do obiektu klasy
„Pochodna”. Metoda ta nie jest
public String getPole() {return pole;}
definiowana w klasie pochodnej, ale jest
dziedziczona z klasy bazowej3. Jeśli w
}
klasie bazowej umieszczamy metodę
sprzątającą lub nadpisujemy metodę
„finalize()”, to w klasach pochodnych
class Pochodna extends Bazowa {
nadpisując te metody w klasach
pochodnych powinniśmy pamiętać o ich
public Pochodna() {super("zainicjalizowane");}
wywołaniu ich wersji z metod bazowych
}
na końcu. Jednym z najważniejszych
pojęć
związanych
z
techniką
dziedziczenia jest rzutowanie w górę
(ang. upcasting). Ponieważ obiekt klasy
public class Konstruktory {
pochodnej jest również typu klasy
public static void main(String[] args) {
bazowej4, więc można na nie go
wskazywać referencją tej klasy i
Pochodna p = new Pochodna();
dodatkowo wywoływać metody wspólne
dla obu tych klas. Nazwa tej techniki
System.out.println(p.getPole());
pochodni od sposobu, w jaki graficznie
}
przedstawiamy dziedziczenie. Istnieje
również możliwość rzutowania w dół,
}
ale to zagadnienie ze względu na
„efekty uboczne” jest stosunkowo
skomplikowane i zostanie omówione szczegółowo na późniejszych wykładach. Oto program ilustrujący zastosowanie techniki rzutowania w
górę:
Klasy „Krzeslo” i „Szafa” dziedziczą po klasie
class Mebel { public void wypisz() {}; }
„Mebel”. W klasie publicznej „Upcasting” istnieje
metoda statyczna o nazwie „przesun”, która
class Krzeslo extends Mebel {
posiada parametr typu „Mebel” i wywołuje
public void wypisz() {System.out.println("Krzesło.");}
metodę „wypisz” należącą do obiektu tej klasy. W
metodzie „main” klasy publicznej do tej metody
public static void main(String[] args) { new Krzeslo().wypisz();}
jako parametry jej wywołania przekazywane są
adresy obiektów klasy „Szafa” i klasy „Krzeslo”.
}
Okazuje się, że na ekranie otrzymamy napisy,
które są efektem działanie metod właściwych
class Szafa extends Mebel {
danej klasie obiektu. Oprócz techniki rzutowania
public void wypisz() {System.out.println("Szafa.");}
w górę przykład ten ilustruje również pewną
technikę testowania: w klasach „Szafa” i
public static void main(String args[]) {new Szafa().wypisz();}
„Krzeslo” umieszczono metodę „main”, która
tworzy obiekt danej klasy i wywołuje jego metodę
}
„wypisz”. Okazuje się więc, że metoda „main” nie
public class Upcasting {
koniecznie musi być obecna wyłącznie w klasie
publicznej, może być również zdefiniowana w
static void przesun(Mebel m) {m.wypisz();}
dowolnej innej klasie. Aby wywołać tę metodę dla
np.: klasy krzesło wystarczy po skompilowaniu
public static void main(String[] args) {
programu napisać: java Krzeslo.
przesun(new Szafa());
class Bazowa {
3
4
Szerzej to zagadnienie będzie omówione na następnym wykładzie.
W programowaniu obiektowym słowo „typ” oznacza interfejs obiektu.
3
Programowanie Obiektowe (Java)
przesun(new Krzeslo());
}
}
5.
Kiedy używać kompozycji, a kiedy dziedziczenia?
Techniki dziedziczenie i kompozycji nie wykluczają się wzajemnie, a więc w dużych projektach wykorzystuje się je obie naraz. Statystycznie
częściej wykorzystywana jest jednak kompozycja, a dziedziczenie, jako technikę bardziej skomplikowaną stosuje się rzadziej. Kompozycję
stosujemy wtedy, kiedy dojdziemy do wniosku, że jakiś obiekt składa się z innych obiektów i że nie jest nam potrzebny ich interfejs, ale
funkcjonalność. Wówczas deklarujemy referencje do takich obiektów jako składowe prywatne nowej klasy5. Innymi słowy relacje typu:
„Książka ma strony.” modelujemy przy pomocy kompozycji. W przypadku kiedy otrzymamy relacje mówiące, że jakiś obiekt jest podobnym
do innego obiektu lub wręcz jest innym obiektem, to wówczas używamy dziedziczenia. Dziedziczenia używamy również w tych projektach,
gdzie konieczne jest rzutowanie w górę.
6.
Słowo kluczowe „final”
Słowo kluczowe „final” ma wiele zastosowań i znaczeń. Może posłużyć do tworzenia stałych czasu kompilacji i zmiennych inicjalizowanych
w trakcie działania programu, których wartości nie chcemy późnej zmieniać, np.:
public static final int PI = 3.14;
lub
final int losowa = (int)(Math.random()*20);
Pierwszy przykład dotyczy stałej czasu kompilacji. Wartość tej stałej jest znana na etapie kompilacji i nie zmieni się podczas działania
programu, druga stała jest inicjalizowana wyrażeniem, którego wartość będzie można ustalić dopiero po uruchomieniu programu. Stałe czasu
kompilacji mają nazwy pisane według konwencji dużymi literami. Zmienne „final” mogą być inicjalizowane za pomocą konstruktora lub w
wyniku „leniwej inicjalizacji”. W przypadku referencji słowo „final” oznacza, że nie zmieni się adres obiektu na który wskazuje referencja, ale
może zmienić się stan tego obiektu (jego zawartość). Słowo „final” można stosować w połączeniu z prametrami metod, wówczas takie wartości
przekazywane przez parametr oznaczony tym słowem są przekazywane przez stałą i nie mogą ulec zmianie. Słowo „final” można stosować z
metodami. Takie zastosowanie tego słowa ma dwa skutki: nie można nadpisać metody oznaczonej tym słowem w klasie pochodnej oraz ta
metoda jest traktowana tak jak metody „inline” lub makra w języku „C”. Nie ma celu stosowanie słowa „final” do metod prywatnych. W końcu
słowo „final” można umieszczać przed definicją klasy. W tym przypadku wszystkie metody i inne składowe publiczne, które należą do klasy
oznaczonej tym słowem stają się składowymi finalnymi. Innymi słowy po takiej klasie nie można dziedziczyć. Użycie słowa „final” w
połączeniu z klasami lub metodami należy bardzo starannie przemyśleć.
7.
Kolejność inicjalizowania
Po kompilacji programu napisanego w języku Java otrzymujemy pewną ilość plików wynikowych zawierających skompilowany kod
wszystkich klas zdefiniowanych w tym programie. Maszyna wirtualna Java nie ładuje od razu do pamięci całego programu, lecz umieszcza w
niej jego poszczególne części dopiero wtedy, kiedy jest to konieczne. Najpierw wyszukiwana jest klasa publiczna z metodą „main” i ładowana
do pamięci. Jeśli ta klasa dziedziczyła po innej klasie, to jej klasa bazowa jest ładowana jako następna. Proces ten jest powtarzany, aż do
zakończenia hierarchii klas. Następnie inicjalizowane są pola statyczne tych klas poczynając od klasy znajdującej się na szczycie hierarchii
dziedziczenia6. Po zakończeniu inicjalizacji składowych statycznych maszyna wirtualna zaczyna tworzyć obiekty poszczególnych klas,
najpierw przyznając dla nich pamięć, zerując jej zawartość, a następnie wywoływany jest konstruktor klasy bazowej z wnętrza konstruktora
klasy pochodnej, inicjalizowane zmienne instancyjne obiektu tej klasy i wykonywana jest reszta kodu konstruktora. Ten proces jest powtarzany
dla obiektów klas pochodnych.
5
6
Czasem, jeśli wynika to z projektu, może być uzasadnione pozostawienie tych składowych publicznymi.
Czyli klasy bazowej dla pozostałych klas.
4

Podobne dokumenty