1. Klasy W programowaniu obiektowym posługujemy się obiektami

Transkrypt

1. Klasy W programowaniu obiektowym posługujemy się obiektami
1. Klasy
W programowaniu obiektowym posługujemy się obiektami.
Jak wiemy (por, jeszcze raz pkt. 4.2), obiekty charakteryzują się :
•
•
cechami (inaczej - atrybutami lub stanami)
operacjami, które na nich moŜna wykonywać (inaczej - usługami, które są obowiązane
świadczyć; inaczej - poleceniami czy komunikatami, które moŜna im wydawać czy do
nich posylac)
Obiekty w programie odzwierciedlają rzeczywiste obiekty, które mogą być konkretne
(fizyczne) lub abstrakcyjne.
Na przykład, gdyby nasz program symulował ruch uliczny to musielibyśmy zapewne
odzwierciedlić w nim takie konkretne obiekty jak samochody.
KaŜdy z obiektów- samochodów ma jakieś cechy (atrybuty, stany) np.
•
•
•
cięŜar,
wysokość,
aktualną prędkość jazdy
oraz udostępnia jakieś usługi, wykonanie których moŜemy mu zlecić za pomocą
odpowiednich poleceń np.
•
•
•
•
włącz się do ruchu,
zatrzymaj się,
zwiększ prędkość
skręć w lewo itp.
Gdybyśmy zatem mieli w programie dwa obiekty-samochody, to kaŜdy z nich moŜna by było
opisać przez wartości jego atrybutów:
Samochód A (oznaczony w programie samA) Samochód B (oznaczony w programie samB)
cięŜar = 1000
wysokość = 1.5
aktualna prędkość = 0
cięŜar = 1000
wysokość = 1.5
aktualna prędkość = 60
Przypomnienie: polecenia do obiektów posyłamy za pomocą kropki
Do kaŜdego z nich moglibyśmy teŜ posłać komunikat (polecenie) np.
samA.włącz_się do_ruchu();
samB.zatrzymaj_się();
Skąd wiemy jakie atrybuty mają obiekty-samochody? Skąd wiemy jakie polecenia moŜemy
do nich posyłać?
O tym decyduje definicja klasy samochodów, którą nasz program musi albo skądś pobrać
albo sam dostarczyć.
Klasa - to opis takich cech grupy podobnych obiektów, które są dla nich niezmienne (np.
zestaw atrybutów i usług, ktore mogą świadczyć)
MoŜna by więc symbolicznie zapisać coś takiego:
Klasa Samochod
atrybuty:
cięŜar
wysokość
akualna prędkość
usługi - operacje:
włącz_się_do_ruchu
zatrzymaj_się
zwiększ_prędkość
skręć_w_lewo
Dopiero teraz będziemy wiedzieć co charakteryzuje kaŜdy obiekt-samochód w naszym
programie i co moŜemy z kaŜdym takim obiektem robić.
Nie naleŜy myśleć, Ŝe np. definicja klasy samochodów jest "naturalnie" ustalona, jedyna, dana
raz na zawsze.
Konkretne obiekty samochody moŜemy przecieŜ w naszych programach opisywać bardzo
róŜnie w zaleŜności od tego jaki problem ma do rozwiązania nasz program.
Np. w przypadku symulacji ruchu ulicznego nie będzie pewnie nas interesować taka cecha
samochodu jak kolor (zatem ten atrybut nie znajdzie się w definicji klasy jako wspólna cecha
wszystkich obiektów samochodów).
Ale być moŜe gdyby nasz program zajmował się zagadnieniem sprzedaŜy samochodów, to
cecha "kolor" znalazłaby się jako istotny atrybut w definicji klasy. A zamiast operacji: włącz
się_do ruchu itp. potrzebne byłyby całkiem inne operacje na obiektach (np. sprzedaj).
W przypadku konkretnych, fizycznych obiektów to wszystko jest chyba dość zrozumiałe. A
co z obiektami abstrakcyjnymi, co to takiego, po co w ogóle mogą być nam potrzebne?
Przypomnijmy sobie pary liczb całkowitych z poprzedniego wykładu.
Niewątpliwie taka para liczb jest obiektem abstrakcyjnym (bowiem nie istnieje fizycznie).
W naszym programie odzwierciedlamy właściwości tych abstrakcyjne obiektów za pomocą
definicji klasy par liczb całkowitych.
Atrybuty pary są naturalne: pierwsza liczba i druga liczba pary.
W poprzednim wykładzie wykorzystywaliśmy tylko dwie z moŜliwych operacji na parze:
ustalenie jej wartości (tzn. wartości pierwszego i drugiego składnika pary) oraz pokazanie
pary (wyprowadzenie na konsolę obu liczb). Mogą być i inne operacje np.: dodawanie par,
odejmowanie par. Zestaw moŜliwych operacji moŜemy więc - taka samo jak w przypadku
odzwierciedlania obiektów fizycznych - dostosowywać do potrzeb naszych programów
poprzez odpowiednią definicję klasy.
Na przykład:
Klasa Para
atrybuty:
pierwsza_liczba_pary
druga_liczba_pary
usługi - operacje:
set // ustal wartość pary
add // dodaj pary
substract // odejmij pary
show // pokaŜ parę
Uwaga: to Ŝe stosujemy angielskie słowa do nazywania operacji na parach nie ma Ŝadnego
specjalnego znaczenia; ogólnie jednak lepiej jest stosować w programach identyfikatory w
języku angielskim, gdyŜ w ten sposób tekst programu wygląda bardziej naturalnie (słowa
kluczowe i tak są słowami języka angielskiego) i staje się powszechnie zrozumiały. Tę zasadę
będziemy wprowadzać w naszych przykładowych programach stopniowo, aby teksty były
bardziej naturalne i zrozumiałe dla nie znających języka angielskiego, a jednocześnie powoli
pojawiały się w nich waŜne w informatyce słowa angielskie (np. add czy show).
Znowu: ta definicja nie określa wartości cech pojedynczego obiektu. MoŜemy mieć wiele
obiektów par-liczb całkowitych, kaŜdy z których ma podane atrybuty (ale np. róŜne ich
wartości) oraz nad kaŜdym z których moŜemy wykonywać podane operacje (set,add itd). Przy
czym niekiedy moŜemy zdefiniować klasę Para w taki sposób, Ŝe dopuszczalne jest
odejmowanie par; a innym razem ta operacja akurat będzie nam niepotrzebna - i wtedy
definicja klasy nie będzie jej zawierać.
Zobaczmy teraz na co w ogóle moŜe się przydać definicja klasy Para. Wyobraźmy sobie, Ŝe w
programie mamy zapisać dodawanie par liczb całkowitych. MoŜemy to oczywiście zrobić,
korzystając z pierwotnych typów danych:
int a1 = 1;
int a2 = 2;
int b1 = 3;
int b2 = 4;
int c1;
int c2;
c1 = a1 + b1;
c2 = a2 + b2;
Jednak mając definicję klasy Para moŜemy zapisać tę operację w duŜo prostszy i bardziej
zrozumiały sposób:
Para a = new Para(); // przypomnienie: w Javie obiekty
tworzymy za pomoca wyraŜenia
Para b = new Para(); // new, o którym za chwilę dowiemy się
wszystkiego
a.set(1, 1);
b.set(3, 4)
Para c = a.add(b);
Zatem nie tylko opis fizycznych właściwości obiektów (które nie mają Ŝadnych
odpowiedników w składni języka) za pomocą definicji klas moŜe być przydatny; równieŜ
klasy obiektów abstrakcyjnych (czasem łatwiej opisywalnych za pomocą danych typów
pierwotnych) są w programowaniu bardzo przydatne.
Porównując powyŜsze fragmenty kodów warto zwrócić uwagę na pewną róŜnicę: oto wartości
zmiennych całkowitych (a1, a2, b1, b2) inicjowaliśmy przy ich deklaracji, natomiast obiektypary (a i b) - tworzyliśmy, ale zamiast inicjacji uŜywana była operacja set.
Czy nie moŜna by "załatwić" takiej inicjacji przy okazji tworzenia obiektu?
AleŜ tak - o ile tylko definicja klasy to przewiduje.
KaŜda klasa moŜe zdefiniować specjalną operację inicjacji obiektu, której moŜna uŜyć "w
trakcie" jego tworzenia.
Wyobraźmy sobie, Ŝe operacja taka zdefiniowana jest w klasie Para, a jej uŜycie polega na
podaniu w wyraŜeniu new po napisie Para - w nawiasach okrągłych argumentów, których
wartości zostaną przypisane składnikom pary - nowotworzonego obiektu.
Zadanie dodania dwóch par (1,2) i (3,4) moŜna by wtedy zapisać jeszcze prościej:
Para a = new Para(1,2);
Para b = new Para(3,4);
Para c = a.add(b);
Podkreślmy: to, Ŝe akurat moŜna uŜyć takiego zapisu - zaleŜy od definicji klasy Para.
Zatem:
Definicja klasy określa:
•
•
•
zestaw cech (atrybutów) obiektów klasy,
zestaw operacji, które moŜna wykonywac na obiektach klasy
specjalne operacje, które pozwalają na inicjowanie obiektów przy ich tworzeniu.
W wielu językach obiektowych (w tym w Javie):
•
•
wspólne cechy (atrybuty) obiektów nazywają się polami klasy
operacje (polecenia) - nazywają się metodami,
•
specjalne operacje inicjacji - nazywają się konstruktorami
Definicja klasy stanowi zatem definicję:
•
•
•
pól
metod
i konstruktorów
Klasę winniśmy traktować jako swoisty wzorzec, szablon opisujący powstawanie obiektów
(konstruktory), ich cechy (pola) oraz sposób komunikowania się z obiektami (metody).
W Javie do definiowania klas uŜywa się słowa kluczowego class. Samą definicję umieszcza
się w następujących po nim nawiasach klamrowych. Kod definicji (pomiędzy nawiasami
klamrowymi) nazywa się ciałem klasy.
[ public ] class NazwaKlasy
{
// definicje pól
// definicje konstruktorów
// definicje metod
}
gdzie:
•
•
słowo kluczowe public jest nieobowiązkowe (dlatego w nawiasach kwadratowych) i
określa dostępność klasy dla innych programów (klasa zdefiniowana ze słowem
public jest dostępna zewsząd)
nazwa klasy musi spełniać ograniczenia dotyczące identyfikatorów i (zgodnie z
konwencjami nazewniczymi) powinna zaczynać się od duŜej litery i być pisana w
notacji węgierskiej
Przykłady "szablonów" definicji klas:
public class Para {
// ciało klasy
}
public class Car
// ciało klasy
}
class TestPara {
// ciało klasy
}
{
/ definicja klasy par liczb całkowitych
Pola i metody klasy nazywają się składowymi klasy.
Składowe klasy = pola + metody
2. Pola
Pola klasy określają z jakich elementów będą składać się obiekty tej klasy.
Na przykład obiekty-pary liczb całkowitych składają się z dwóch liczb całkowitych.
W definicji klasy Para trzeba to jakoś zapisać. Naturalnym sposobem jest zadeklarowanie
zmiennych odpowiednich typow
public class Para {
int a;
int b;
// dalej będą następować definicje konstruktorów i metod
klasy....
}
Taki zapis oznacza, Ŝe kaŜdy z obiektów klasy Para będzie zawierał dwie liczby całkowite.
Będzie się składał z dwóch elementów - liczb całkowitych. Identyfikatory zmiennych (a i b)
są oczywiście dowolne, a potrzebne są po to, by do tych liczb móc odwoływać się w
metodach klasy.
Pamiętamy, Ŝe jedną z waŜnych cech programowania obiektowego jest hermetyzacja. Polega
ona (między innymi) na tym, Ŝe działając na obiektach jakiejś klasy powinniśmy wyłącznie
posługiwac się dostępnymi dla nas jej metodami, a nie grzebać w "środku obiektów".
Dlatego pola klasy deklaruje się zwykle ze specyfikatorem dostępu private, co oznacza, Ŝe
dostęp do nich moŜliwy jest tylko z wnętrza danej klasy (m.in z jej metod), a odwołania spoza
klasy są niedopuszczalne.
Definiowanie pól klasy
[public] class NazwaKlasy {
[ specyfikator_dostępu ] nazwa_typu nazwa_zmiennej [
inicjator ];
//....
}
uwaga:
•
•
•
nawiasy kwadratowe oznaczają opcjonalność elementów definicji
specyfikator dostępu to zwykle private
inicjator ma znaną nam postać wyraŜenia po znaku =; więcej na ten temat w
podpunkcie dotyczącym jawnych inicjacji
Na przykład:
public class Para {
private int a;
private int b;
// ...
}
Polami klasy mogą być zmienne obiektowe (zmienne oznaczające obiekty; ściślej powiemy:
zmienne typów referencyjnych). Zobaczmy jak mógłby wyglądać fragment definicji klasy
Book, która opisuje ksiąŜki:
public class Book {
private String author;
private String title;
private double price;
// ....
}
// autor
// tytuł
// cena
Zmienne typu String są referencjami, będą wskazywać na odpowiednie obiekty - łańcuchy
znakowe.
Pojęcie pola dotyczy klasy, pojęcie elementu - dotyczy obiektu. Dla uproszczenia będziemy
jednak czasem mówić "pole obiektu".
NaleŜy wyraźnie dostrzegać róŜnicę pomiędzy definicją pól klasy, a elementami obiektów.
Zestaw pól klasy określa jakie elementy mogą mieć obiekty tej klasy. Elementy są natomiast
konkretnymi obszarami pamięci alokowanymi "w środku" konkretnych obiektów.
Np. definicja klasa Para mówi o tym, Ŝe kaŜdy jej obiekt zawiera dwa elementy - liczby
całkowite. Po utworzeniu obiektu i jego inicjacji (bądŜ np. uŜyciu metody set, ustalającej
wartość pary) obiekt będzie zawierał dwa elementy - liczby całkowite o konkretnych
wartościach. Inny obiekt klasy Para będzie teŜ zawierał dwie liczby całkowite, ale (być
moŜe) o innych wartościach niŜ ten pierwszy.
Co się stanie, jeśli - ani za pomocą konstruktora, ani w inny sposób - nie ustalimy przy
tworzeniu obiektu wartości jego elementów?
Elementy obiektu będą miały wartości domyślne.
Domyślnie, przy tworzeniu obiektów, pola klasy otrzymują wartość ZERO, co
•
•
•
•
dla typów całkowitych oznacza liczbę całkowitą 0,
dla typów rzeczywistych oznacza wartość rzeczywistą 0.
dla typu boolean oznacza wartość false,
dla typów referencyjnych oznacza wartość null (referencja nie wskazuje na Ŝaden
obiekt)
Np. przy takiej definicji klasy:
class Person {
private String name;
private int age;
private boolean isEmployee;
}
po utworzeniu obiektu tej klasy, jego elementy odpowiadające polom name, age i isEmployee
będą miały wartości - odpowiednio - null, 0 i false.
3. Metody
Zestaw operacji na obiektach określany jest przez definicję metod klasy.
Pojęcie metody zbliŜone jest do znanego juŜ nam pojęcia funkcji.
Metoda - tak samo jak funkcja - to wyodrębniony zestaw czynności, zapisywany
jednorazowo w postaci fragmentu kodu, który moŜe być wywoływany wielokrotnie z innych
miejsc programu.
Metody słuŜą głównie (ale nie tylko i niekoniecznie) do wykonywania operacji na obiektach.
Zatem - w odróŜnieniu od funkcji - metody zwykle wywoływane są na rzecz konkretnych
obiektów.
Wywołania "na rzecz" obiektu (jak juŜ widzieliśmy) dokonuje się za pomocą "operatora"
kropka. Np. jeśli p - oznacza obiekt klasy Para (czyli jest referencją do obiektu klasy Para), a
w klasie tej zdefiniowano metodę show, to wywołanie tej metody na rzecz tego obiektu
zapisujemy jako:
p.show();
"Wywołanie na rzecz obiektu" oznacza to samo, co "posłanie polecenia do obiektu" lub
"komunikatu do obiektu" lub "wykonanie operacji na obiekcie".
W tym przypadku (dla metody show):
wywołanie metody show na rzecz obiektu p
=
posłanie komunikatu/polecenie show do obiektu p
=
wykonanie operacji uwidocznienia obiektu p
Schematyczna postać definicji metody jest następująca:
[specyfikator_dostępu] typ_wyniku nazwa_metody(
lista_parametrów ) {
// ... instrukcje wykonywane po wywołaniu metody
}
Uwagi:
•
•
nawiasy kwadratowe oznaczają opcjonalność
kod zawarty pomiędzy nawiasami klamrowymi nazywany jet ciałem metody
Specyfikator dostępu określa czy metoda moŜe być wywołana spoza klasy, w której jest
zdefiniowana. W szczególności:
•
•
specyfikator public mówi o tym, Ŝe dana metoda moŜe być wywołana z dowolnej
innej klasy
a private - oznacza, Ŝe metoda moŜe być wywołana tylko w tej klasie, w której została
zdefiniowana
Nazwę metody zaczynamy od małej litery, stosując dalej notację węgierską, np. count,
setPrice, getAuthor
Te metody, które chcemy udostępnić jako ogólniedostępne operacje na obiektach oznaczamy
słowem public; metody "robocze", które mają znaczenie tylko dla nas (twórców klasy) i nie
powinny być dostępne dla innych uŜytkowników klasy - oznaczamy słowem private.
Lista parametrów zawiera rozdzielone przecinkami deklaracje parametrów, które metoda
otrzymuje przy wywołaniu. Lista moŜe być pusta (brak argumentów).
Metoda moŜe zwracać wynik (wtedy w jej definicji musimy podać konkretny typ wyniku, a
zakończenie działania metody powinno następować na skutek instrukcji return zwracającej
dane podanego typu). Jeśli metoda nie zwraca Ŝadnego wyniku to jej typ wyniku określamy
słowem kluczowym void, a metoda moŜe skończyć działanie na skutek dobiegnięcia do
zamykającego nawiasu klamrowego lub wykonania instrukcji return bez argumentów
Instrukcja return ma postać:
return [ wyraŜenie ];
Np. metoda zwracająca sumę dwóch liczb całkowitych moŜe wyglądać tak:
int suma(int x, int y) {
int z = x + y;
return z;
}
lub tak
int suma(int x, int y) {
return x + y;
}
Przy wywołaniu metoda suma uzyskuje dwa przekazane jej argumenty jako parametry x i y.
Jej działanie polega na dodaniu obu wartości parametrów i zwróceniu (do miejsca wywołania)
wyniku. Obowiązkowo, w definicji metody trzeba było podać typ zwracanego wyniku.
Przykład innej metody:
void say(String s) {
System.out.println(s);
}
Wywołanie metody say spowoduje wyprowadzenie na konsolę przekazanego jako argument
napisu. Metoda nie zwraca Ŝadnego wyniku, mimo to trzeba było określić typ wyniku słowem
kluczowym void (dokładnie "nie dotyczy", znaczy - brak wyniku).
W Javie argumenty przekazywane są metodom wyłącznie przez wartość.
Oznacza to, Ŝe w samej metodzie odwołujemy się nie do faktycznego argumentu, ale do jego
kopii. Zatem zmiany przekazanego metodzie argumentu są lokalne, dotyczą wyłącznie kopii i
nie dotykają oryginału.
Np. po wywołaniu metody:
void incr(int x) {
++x;
}
ze zmienną z = 1 jako arguementem w samej metodzie zmienna (parametr) x uzyska wartość
2, ale po zakończeniu działania metody i powrocie sterowania do punktu wywołania zmienna
z będzie miała nadal wartość 1.
To samo dotyczy typów obiektowych. Pamiętamy: zmienne oznaczające obiekty zawierają
referencje, a nie same obiekty.
Zatem np. w ew. metodzie przestawPary:
void przestawPary(Para p1, Para p2) {
Para temp = p1;
p1 = p2;
p2 = temp;
}
nie uzyskamy zamierzonego rezultatu, bowiem metoda otrzymuje tylko wartości referencji, a
nie odniesienia do nich i wszelkie operacje na tych referencjach dotyczą kopii oryginałów.
Nie znaczy to jednak, Ŝe w metodach nie moŜemy działać na obiektach. Referencje przecieŜ
na nie wskazują: trzeba zatem - poprzez nie - odwoływać się do pól i metod klasy i za ich
pomocą (jeśli jest to moŜliwe) zmieniać obiekty. O czym dalej.
W klasie mogą być definiowane metody o tej samej nazwie, ale róŜniące się liczbą i/lub
typami argumentów.
Nazywa się to przeciąŜaniem metod.
Po co taka moŜliwość?
Wyobraźmy sobie, Ŝe na obiektach klasy par liczb całkowitych chcielibyśmy wykonywać
operacje:
•
•
•
dodawania innych obiektów-par
dodawania (do składników pary) kolejno dwóch podanych liczb cłakowitych
dodawania (do kaŜdego składnika pary) jednej i tej samej podanej liczby całkowitej
Gdyby nie było przeciąŜania metod musielibyśmy dla kaŜdej operacji wymyślać inną nazwę
metody. A przecieŜ istota operacji jest taka sama (wystarczy więc nazwa add), a jej uŜycie
powinno być jasne z kontekstu (określanego przez argumenty).
Dzięki przeciąŜaniu moŜna w klasie Para np. zdefiniować metody:
void add(Para p) //
wywołano metodę, parę
dodaje do pary, na rzecz której
// podaną jako argument
void add(int i)
// do obu składników pary dodaje podaną
liczbę
void add(int i, int k) // pierwszą podaną liczbę dodaje do
pierwszego składnika pary
// a drugą - do drugiego
i uŜyć - gdzie indziej - w naturalny sposób:
Para p;.
Para jakasPara;
....
p.add(3);
// wybierana jest ta metoda, która
pasuje (najlepiej) do argumentów
p.add(1,2);
p.add(jakasPara);
Identyfikatory metod definiowanych w klasie muszą być od siebie róŜne.
Wyjątkiem od tej reguły są metody przeciąŜone tj. takie, które mają tę samą nazwę
(identyfikator), ale róŜne typy i/lub liczbę argumentów
4. Konstruktor
Specjalną operacją jest operacja tworzenia obiektu.
Jak wiemy, wykonywana jest ona za pomocą wyraŜenia new.
Okazuje się, Ŝe to co w nim zapisujemy oznacza wywołanie konstruktora klasy.
Konstruktor słuŜy (głównie) do inicjowania pól obiektów.
O konstruktorze moŜna myśleć jako o specjalnej metodzie, która:
•
zawsze ma nazwę taką samą jak nazwa klasy
•
•
nie ma Ŝadnego typu wyniku (nawet void!)
ma listę parametrów (w szczególności moŜe być pusta)
Podobnie jak przy definicji metod - w definicji konstruktora moŜemy podać specyfikator
dostępu, który określa czy konstruktor moŜe być wywołany spoza klasy.
Postać definicji konstruktora:
[ public] class nazwa_klasy {
// Definicja konstruktora
[ specyfikator_dostępu ] nazwa_klasy(lista_parametrów) {
// czynności wykonywane przez konstruktor
}
}
W klasie Para moŜemy mieć np. takie konstruktory:
public class Para {
private int a;
private int b;
public Para(int x, int y) {
a = x;
b = y;
}
...
// Nadaje polom a i b wartości
// przekazane konstruktorowi jako
// argumenty
}
albo:
public class Para {
private int a, b;
public Para(int x) {
a = x;
b = x;
}
...
}
// Konstruktor ma jeden parametr:
// oba pola są nim inicjowane
MoŜemy teŜ w tej samej klasie mieć kilka konstruktorów, które róŜnią się listą parametrów
(np. oba w/w konstruktory w klasie Para). Jest to jak gdyby odpowiednik przeciąŜania metod.
Mając tak zdefiniowane dwa konstruktory w klasie Para, moŜemy teraz łatwo tworzyć
obiekty-pary o zadanych wartościach np.
Para p1 = new Para(10,11);
Para p2 = new Para(2);
// para 10, 11
// para 2, 2
Konstruktory zawsze są wywoływane za pomocą wyraŜenia new
Szczególnym rodzajem konstruktora jest konstruktor bezparametrowy.
Jest on automatycznie dodawany do definicji klasy, gdy nie zdefiniowano Ŝadnego
konstruktora (przy czym jego ciało jest puste). Zatem jeśli nie dostarczymy w klasie Ŝadnego
konstruktora, to przy tworzeniu obiektu zostanie wywołany automatycznie dodany
konstruktor bezparametrowy (który nie robi nic).
Uwaga: konstruktor bezparametrowy nie jest dodawany, gdy w klasie zdefiniowano
jakikolwiek konstruktor.
5. Przykład
Jako podsumowanie powyŜszych rozwaŜań przeanalizujemy pełny przykład definicji klasy.
Mimo, Ŝe juŜ od dwóch wykładów posługujemy się fragmentami (róŜnych) definicji klasy
Para (są one zawarte w programach przykładowych do wykładów w katalogu samples) dokładne poznanie tej klasy pozostawimy do następnego wykładu, bowiem nadaje się ona
doskonale do sczegółowego prześledzenia co dzieje się przy tworzeniu obiektów i
wywoływaniu metod. Ta szczegółowa analiza potrzebna jest dla pełnego zrozumienia przede
wszystkim właśnie sposobu poslugiwania się metodami.
Na razie potrzebne jest nam dosyć intuicyjne rozumienie tych wszystkich kwestii, a podany
dalej przykład powinien je ugruntować.
Zobaczymy przy okazji, Ŝe definiowanie klas jest bardzo łatwe, czasem nawet trochę
nudnawe, choć moŜe być teŜ i zabawne.
Wyobraźmy sobie, Ŝe prowadzimy księgarnię. Księgarnia zajmuje się sprzedaŜą publikacji
(ksiąŜek, czasopism, płyt CD itp.). Zatem głównym obiektem naszego zainteresowania będą
publikacje.
ZauwaŜmy, Ŝe budując klasę publikacji, staramy się znaleźć wspólne atrybuty wszystkich
publikacji. Zatem np. właściwość "autor" zostaje tu pominięta, bo nie wszystkie publikacje
(np. czasopisma) mają autorów
O kaŜdej publikacji powinniśmy wiedzieć:
jaki jest jej tytuł,
kto ją wydał,
rok wydania,
jaki jest jej numer identyfikacyjny (ISBN, ISSN, jakiś inny),
jaka jest cena (powiedzmy hurtowa).
ile egzemplarzy tej publikacji posiada księgarnia.
Te wszystkie atrybuty - w naturalny sposób - będą stanowić pola klasy.
public class Publication {
private String title;
private String publisher;
private int year;
private String ident;
private double price;
private int quantity;
...
}
KaŜda publikacja moŜe pojawić się jako obiekt w naszym programie, gdy uŜyjemy wyraŜenia
new. Obiekt ten powinien być jakoś zainicjowany - dlatego musimy dostarczyć odpowiedni
konstruktor, który będzie inicował podanymi argumentami elementy obiektu.
public class Publication {
private
private
private
private
private
private
String title;
String publisher;
int year;
String ident;
double price;
int quantity;
public Publication(String t, String pb, int y,
String i, double pr, int q)
{
title = t;
// pole title uzyskuje wartość parametru t
publisher = pb;
// pole publisher uzyskuje wartość parametru pb
year = y;
ident = i;
// itd...
price = pr;
quantity = q;
}
...
}
Teraz - w innej klasie ( np. w metodzie main umieszczonej w innej klasie) moŜemy stworzyć
obiekt - ksiąŜkę pt. "Psy", wydaną przez wydawnictwo "Dog & Sons", o cenie 21 zł. Na razie
nie mamy jeszcze Ŝadnego egzemplarza tej ksiąŜaki.
Publication b = new Publication("Psy", "Dog & Sons", 2002,
"ISBN6789", 21.0, 0);
Co moŜemy robić z publikacjami?
MoŜemy je kupować, moŜemy sprzedawać, moŜemy wreszcie uzyskać informacje o kaŜdej
publikacji: jej dane bibliograficzne (tytuł, wydawca, rok, identyfikator), jej aktualną cenę,
liczbę egzemplarzy, znajdujących się w księgarni. MoŜe się takŜe okazać, Ŝe cena publikacji
uległa zmianie, musimy zatem mieć jakiś sposob by zmienić ten element obiektu- publikacji.
Te wszystkie "operacje" na publikacjach zdefiniujemy jako metody klasy.
public class Publication {
...
// Metody klasy
// Zwraca tytuł
public String getTitle() {
return title;
}
// Zwraca wydawcę
public String getPublisher() {
return publisher;
}
// Zwraca rok wydania
public int getYear() {
return year;
}
// Zwraca numer identyfikacyjny
public String getIdent() {
return ident;
}
// Zwraca cenę
public double getPrice() {
return price;
}
// Zmienia cenę
public void setPrice(double p) {
price = p;
}
// Zwraca liczbę egzemplarzy
public int getQuantity() {
return quantity;
}
// Zakup n egzemplarzy
public void buy(int n) {
quantity += n;
}
// SprzedaŜ n egzemplarzy
public void sell(int n) {
quantity -= n;
}
}
Mając gotową klasę Publikacji moŜemy przetestować jej działanie.
Powiedzmy, Ŝe testowanie odbywać się będzie w klasie TestPub (tradycyjnie w metodzie
main, zawartej w tej klasie)
class PubTest {
public static void main(String[] args) {
// Tworzenie obiektu - publikacji
Publication b = new Publication("Psy", "Dog & Sons", 2002,
"ISBN6789", 21.0, 0);
int n = 10;
b.buy(n);
// kupimy n = 10 egzemplarzy
// łatwo policzyć koszt zakupu
double koszt = n * b.getPrice();
System.out.println("Na zakup " + n + " publikacji:");
System.out.println(b.getTitle());
System.out.println(b.getPublisher());
System.out.println(b.getYear());
System.out.println(b.getIdent());
System.out.println("---------------\nwydano: " + koszt);
// teraz sprzedamy 4 egzemplarze i zobaczymy ile zostało
b.sell(4);
System.out.println("Po sprzedaŜy zostało " + b.getQuantity() + "
pozycji");
}
}
Na zakup 10 publikacji:
Psy
Dog & Sons
2002
ISBN6789
--------------wydano: 210.0
--------------Po sprzedaŜy zostało 6 pozycji
6. Dziedziczenie
Dziedziczenie polega na przejęciu właściwości i funkcjonalności obiektów innej klasy i
ewentualnej ich modyfikacji i/lub uzupełnieniu w taki sposób, by były one bardziej
wyspecjalizowane.
Omawiana wyŜej klasa Publication opisuje właściwości publikacji, które kupuje i sprzedaje
księgarnia. ZauwaŜmy, Ŝe za pomocą tej klasy nie moŜemy w pełni opisać ksiąŜek. KsiąŜki
są szczególną, "wyspecjalizowaną" wersją publikacji, oprócz tytułu, wydawcy, ceny itd mają jeszcze jedną właściwość - autora (lub autorów).
Gdybyśmy w programie chcieli opisywać zakupy i sprzedaŜ ksiąŜek - to powinniśmy
stworzyć nową klasę opisującą ksiąŜki o nazwie np. Book.
Moglibyśmy to robić od podstaw (definiując w klasie Book pola author, title, ident, price i
wszystkie metody operujące na nich, jak równieŜ metody sprzedaŜy i kupowania).
Ale po co? PrzecieŜ klasa Publication dostarcza juŜ większość potrzebnych nam pól i metod.
Odziedziczymy ją zatem w klasie Book i dodamy tylko te nowe właściwości (pola i metody),
których nie ma w klasie Publication, a powinny charakteryzować ksiąŜki.
Słowo kluczowe extends słuŜy do wyraŜenia relacji dziedziczenia jednej klasy przez drugą.
Piszemy:
class A extends B {
...
}
co oznacza, Ŝe klasa B dziedziczy (rozszerza) klasę A.
Mówimy:
•
•
klasa A jest bezpośrednią nadklasą, superklasą, klasą bazową klasy B
klasa B jest bezpośrednią podklasą, klasą pochodną klasy A
Zapiszmy zatem:
public class Book extends Publication {
// definicja klasy Book
}
Co naleŜy podać w definicji nowej klasy?
Takie właściwości jak tytuł, wydawca, rok wydania, identyfikator, cena, liczba publikacji "na
stanie", metody uzyskiwania informacji o tych cechach obiektów oraz metody sprzedaŜy i
zakupu - przejmujemy z klasy Publication. Zatem nie musimy ich na nowo definiować.
Pozostało nam tylko zdefiniować nowe pole, opisujące autora (niech nazywa się author) oraz
metodę, która umoŜliwia uzyskanie informacji o autorze (powiedzmy getAuthor()).
class Book extends Publication {
private String author;
public String getAuthor() {
return author;
}
}
Czy to wystarczy?
Nie, bo jeszcze musimy powiedzieć w jaki sposób mają być inicjowane obiekty klasy Book.
Aha, potrzebny jest konstruktor.
Naturalnie, utworzenie obiektu-ksiąŜki wymaga podania:
•
•
•
•
•
•
•
autora,
tytułu,
wydawcy,
roku wydania,
identyfikatora (numeru ISBN),
ceny,
liczby ksiąŜek aktualnie "na stanie".
Czyli konstruktor powinien mieć postać:
public Book(String aut, String tit, String pub, int y, String id,
double price, int quant) {
....
}
Zwróćmy jednak uwagę: pola tytułu, wydawcy, roku, identyfikatora, ceny i ilości - są
prywatnymi polami klasy Publication. Z klasy Book nie mamy do nich dostępu. Jak je
zainicjowac?
Pola nadklasy (klasy bazowej) inicjujemy za pomocą wywołania z konstruktora klasy
pochodnej konstruktora klasy bazowej (nadklasy)
UŜycie w konstruktorze następującej konstrukcji składniowej:
super(lista_argumentów);
oznacza wywołanie konstruktora klasy bazowej z argumentami lista_argumentów .
Jeśli występuje - MUSI być pierwszą instrukcją konstruktora klasy pochodnej.
Jeśli nie występuje - przed utworzeniem obiektu klasy pochodnej zostanie wywołany
konstruktor bezparametrowy klasy bazowej.
Konstruktor klasy Book musi więc wywołać konstruktor nadklasy, po to by zainicjować jej
pola, a następnie zainicjować pole author.
// Konstruktor klasy Book
// argumenty: aut - autor, tit - tytuł, pub - wydawca, y - rok wydania
//
id - ISBN, price - cena, quant - ilość
public Book(String aut, String tit, String pub, int y, String id,
double price, int quant) {
super(tit, pub, y, id, price, quant);
author = aut;
}
Teraz moŜna podać juŜ pełną definicję klasy Book.
public class Book extends Publication {
private String author;
public Book(String aut, String tit, String pub, int y, String id,
double price, int quant) {
super(tit, pub, y, id, price, quant);
author = aut;
}
public String getAuthor() {
return author;
}
}
Zwróćmy uwagę: wykorzystanie klasy Publication (poprzez jej odziedziczenie) oszczędziło
nam wiele pracy. Nie musieliśmy ponownie definiować pól i metod z klasy Publication w
klasie Book.
Przy tak zdefiniowanej klasie Book moŜemy utworzyć jej obiekt:
Book b = new Book("James Gossling", "Moja Java", "WNT", 2002,
"ISBN6893", 51.0, 0);
Ten obiekt zawiera:
•
•
elementy określane przez pola klasy dziedziczonej (Publication) - czyli: title,
publisher, year, ident, price, quantity
element określany przez pole klasy Book - author
Podkreślmy: jest to jeden obiekt klasy Book.
Wiemy na pewno, Ŝe moŜemy uŜyć na jego rzecz metody z klasy Book - getAuthor().
Ale poniewaŜ klasa Book dziedziczy klasę Publication to obiekty klasy Book mają równieŜ
wszelkie właściwości obiektów klasy Publication , a zatem moŜemy na ich rzecz uŜywać
równieŜ metod zdefiniowanych w klasie Publication.
Nic zatem nie stoi na przeszkodzie, by napisać taki program:
class TestBook {
public static void main(String[] args) {
Book b = new Book("James Gossling", "Moja Java", "WNT", 2002,
"ISBN6893", 51.0, 0);
int n = 100;
b.buy(n);
double koszt = n * b.getPrice();
System.out.println("Na zakup " + n + " ksiąŜek:");
System.out.println(b.getAuthor());
System.out.println(b.getTitle());
System.out.println(b.getPublisher());
System.out.println(b.getYear());
System.out.println(b.getIdent());
System.out.println("---------------\nwydano: " + koszt);
b.sell(90);
System.out.println("---------------");
System.out.println("Po sprzedaŜy zostało " + b.getQuantity() + "
pozycji");
}
}
Na zakup 100 ksiąŜek:
James Gossling
Moja Java
WNT
2002
ISBN6893
--------------wydano: 5100.0
--------------Po sprzedaŜy zostało 10 pozycji
który skompiluje się i wykona poprawnie dając w wyniku pokazany listing.
MoŜemy powiedzieć, Ŝe obiekty klasy Book są równieŜ obiektami klasy Publication
(w tym sensie, Ŝe mają wszelkie właściwości obiektów klasy Publication)
Dzięki temu referencje do obiektów klasy Book moŜemy przypisywać zmiennym,
oznaczającym obiekty klasy Publication (zawierającym referencje do obiektów klasy
Publication). Np.
Book b = new Book(...);
Publication p = b;
Nazywa się to referencyjną konwersją rozszerzającą (ang. widening reference conversion).
Słowo konwersja oznacza, Ŝe dochodzi do przekształcenia z jednego typu do innego typu
(np. z typu Book do typu Publication). Konwersja jest rozszerzająca, bowiem,
przekształcamy typ "pochodny" (referencja do obiektu podklasy) do typu "wyŜszego"
(referencja do obiektu nadklasy). A poniewaŜ chodzi o typy referencyjne - mówimy o
referencyjnej konwersji rozszerzającej,
Nieco mniej precyzyjnie, ale za to podkreślając, Ŝe chodzi o operowanie na obiektach,
będziemy mówić o takich konwersjach jako o obiektowych konwersjach rozszerzających
(ang. "upcasting" - up - bo w górę hierarchii dziedziczenia).
Obiektowe konwersje rozszerzające dokonywane są automatycznie przy:
•
•
•
przypisywaniu zmiennej-referencji odniesienia do obiektu klasy pochodnej,
przekazywaniu argumentów metodzie, gdy parametr metody jest typu "referencja do
obiektu nadklasy argumentu",
zwrocie wyniku, gdy wynik podstawiamy na zmienną będącą referencją do obiektu
nadklasy zwracanego wyniku
Ta zdolność obiektów Javy do "stawania się" obiektem swojej nadklasy jest niesłychanie
uŜyteczna.
Wyobraźmy sobie np. Ŝe oprócz klasy Book - z klasy Publication wyprowadziliśmy jeszcze
klasę Journal (czasopisma)
Klasa Journal dziedziczy klasę Publication i dodaje do niej - zamiast pola, opisującego autora
- pola opisujące wolumin i numer wydania danego czasopisma.
Być moŜe będziemy mieli jeszcze inne rodzaje publikacji - np. muzyczne, wydane na płytach
CD (powiedzmy klasę CDisk, znowu dziedziczącą klasę Publication, i dodającą jakieś
właściwe dla muzyki informacje, np. czas odtwarzania).
MoŜemy teraz np. napisać uniwersalną metodę pokazującą róŜnicę w dochodach ze sprzedaŜy
wszystkich zapasów dowolnych dwóch publikacji.
public double incomeDiff(Publication p1, Publication p2) {
double income1 = p1.getQuantity() * p1.getPrice();
double income2 = p2.getQuantity() * p2.getPrice();
return income1 - income2;
}
i wywoływać ją dla dowolnych (róŜnych rodzajów) par publikacji
Book b1 =
Book b2 =
Journal j
CDisk cd1
CDisk cd2
double
diff =
diff =
diff =
new Book(...);
new Book(...);
= new Journal(...);
= new CDisk(...);
= new CDisk(...);
diff = 0;
incomeDiff(b1, b2);
incomeDifg(b1, j);
inocmeDiff(cd1, b1);
Gdyby nie było obiektowych konwersji rozszerzających, to dla kaŜdej mozliwej kombinacji
"rodzajowej" par - musielibyśmy napisać inną metodę incomeDiff np.
double incomeDiff(Book, Book), double incomeDiff(Book, Journal), double
incomeDiff(Book, CDisk) itd.
Zwróćmy uwagę, Ŝe w przedstawionej metodzie incomeDiff moŜna wobec p1 i p2 uŜyć
metod klasy Publication (bo tak są zadeklarowane parametry), ale nie moŜna uŜywać metod
klas pochodnych, nawet wtedy, gdy p1 i p2 wskazują na obiekty klas pochodnych. Np.
....
{
Book b1 = new Book(...);
Book b2 = new Book(...);
jakasMetoda(b1,b2);
....
}
void jakasMetoda(Publication p1, Publication p2) {
String autor = p1.getAuthor(); // Błąd kompilacji niezgodność typów
...
// na rzecz
obiektu klasy Publication
...
// nie wolno
uŜyć metody getAuthor()
}
metody nie ma w klasie Publication
// bo takiej
Więcej na temat konwersji dowiemy się w przyszłych wykładach, a jeśli chodzi o pełne
zrozumienia znaczenia dziedziczenia i roli konwersji referencyjnych - to uzyskamy je w
drugim semestrze, gdzie zagadnienia obiektowości będą szczególnie akcentowane.
Na koniec krótkiego, wstępnego, mającego raczej instrumentalny dla dalszych wykładów tego
semestru charakter, wprowadzenia do dziedziczenia, naleŜy zaznaczyć bardzo waŜną
właściwość Javy.
W Javie kaŜda klasa moŜe bezpośrednio odziedziczyć tylko jedną klasę.
Ale pośrednio moŜe mieć dowolnie wiele nadklas, co wynika z hierarchii dziedziczenia.
Ta hierarchia zawsze zaczyna się na klasie Object (której definicja znajduje się w zestawie
stanardowych klas Javy).
Zatem w Javie wszystkie klasy pochodzą pośrednio od klasy Object.
Jeśli definiując klasę nie uŜyjemy słowa extends (nie zaŜądamy jawnie dziedziczenia), to i tak
nasza klasa domyślnie będzie dziedziczyć klasę Object (tak jakbyśmy napisali class A extends
Object).
Wobec tego hierarchia dziedziczenia omawianych tu klas wygląda następująco:
Z tego wynika, Ŝe:
referencję do obiektu dowolnej klasy moŜna przypisać zmiennej typu Object (zawierającej
referencję do obiektu klasy Object).
Z właściwości tej korzysta wiele "narzędziowych" metod zawartych w klasach standardu
Javy.
Zadania i ćwiczenia
1. Zdefiniować pola klasy określającej obiekty typu "prostokąt". Podać konstruktor,
który inicjuje wszystkie pola klasy.
2. Zdefiniować metodę, która otrzymuje jako argumenty trzy liczby całkowite i zwraca
ich sumę, poprzedzoną napisem "Suma liczb = ".
3. W klasie PubTest wprowadzić kilka innych publikacji, oprócz juŜ podanej ("Psy") i
pokazać identyczne dane na ich temat.
4. W klasie Publication wprowadzić dwa pola cen: cene zakupu i cenę sprzedaŜy.
Dostarczyć metod pobierania i ustalania wartości tych pól. Przetestowac działanie,
pokazując w programie PubTest jaki dochód (przychód - koszty) uzyskała księgarnia
na sprzedaŜy kilku publikacji .
5. Stworzyć klasę Pracownik, w której oprócz danych osobowych będzie równieŜ
zdefiniowane pole, określające pensję i dostarczyć metodę zmiany pensji pracownika.
Przetestować klasę, pokazując informacje o róŜnych pracownikach przed i po zmianie
pensji.
6. Zdefiniować klasy Book, Journal i CDisk, dziedziczące klasę Publication. W innej
klasie przetestować ich działanie.
Opracowane na podstawie wykładu:
"Podstawy programowania w Javie", Krzysztof Barteczko.