1. Do czego służą klasy?
W programowaniu obiektowym posługujemy się obiektami.
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 posyłać)
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.
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, które 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ć w programie.
Ale
skąd się biorą obiekty-samochody w naszym programie?
Otóż
musimy je tworzyć.
Obiekty
tworzymy za pomocą wyrażenia
new
Wyrażenie new ma postać:
new
NazwaKlasy(
parametry)
gdzie: parametry (wyrażenia, rozdzielone
przecinkami) zazwyczaj określają inicjalne wartości wszystkich lub
wybranych atrybutów.
Skąd
wiadomo jakie parametry i w jakiej kolejności podać? Otóż każda klasa
może zdefiniować specjalne operacje inicjacji obiektu, których można
użyć w trakcie jego tworzenia.
Załóżmy, że w klasie
Samochod jest
zdefiniowana taka operacja, która nadaje atrybutom obiektów
kolejne - przekazane przez parametry - wartości (ciężar, wysokość,
aktualna prędkość).
Możemy więc
stworzyć obiekty:
samA = new
Samochod(500, 1.5, 0);
samB = new
Samochod(1000, 2.2, 60);
Teraz mamy dwa
obiekty-samochody, oznaczane przez zmienne samA i samB.
Wartości
ich atrybutów zostały zainicjowane i mają następująca postać:
Samochód A (oznaczony w programie samA)
| Samochód B (oznaczony w programie samB)
|
ciężar = 500
wysokość = 1.5
aktualna prędkość = 0
| ciężar
= 1000
wysokość = 2.2
aktualna prędkość = 60 |
Na
obiektach samochodach możemy wykonywać operacje, posyłać do nich
polecenia.
Do
obiektów posyłamy polecenia za pomocą kropki.
Na
przykład:
samA.włącz_się do_ruchu();
samB.zatrzymaj_się();
Warto zwrócić uwagę,
że dzięki
zastosowaniu klas mamy możliwość programowania w języku problemu (np.
symulacji ruchu samochodów). Klasy pozwalają nam wprowadzać do języka
nowe typy danych (takie jak Samochod) z właściwymi dla nich zestawami
dopuszczalnych wartości (tu: możliwe wartości atrybutów, takich jak
ciężar, wysokość, aktualna prędkość jazdy) i dopuszczalnymi
operacjami.
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).
Korzyść
w przypadku odzwierciedlania rzeczywistych obiektów jest oczywista:
piszemy program w języku problemu, który mamy rozwiązać.
A co
z obiektami abstrakcyjnymi?
Rozważmy np. pary liczb
całkowitych.
Niewątpliwie para liczb całkowitych 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. Taka
definicja określa atrybuty pary oraz operacje, które na parach
można wykonywać.
Klasa
Para
atrybuty:
pierwsza_liczba_pary
druga_liczba_pary
usługi -
operacje:
operacja_inicjacji
// zainicjuj parę dwoma podanymi liczbami
set
// ustal wartość pary na podstawie
wartości innej
pary
add
// dodaj do pary inną
parę
show
// pokaż parę
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 na każdym z których możemy wykonywac
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 calkowitych.
Możemy
to oczywiście zrobić, korzystając z prostych typów danych:
// Niech a1 i a2 oznaczają składniki pierwszej pary
// b1 i b2 - składniki drugiej pary
// c1 i c2 - składniki pary wynikowej (sumy par)
int a1 = 1;
int a2 = 2;
int b1 = 3;
int
b2 = 4;
int c1;
int c2;
c1 = a1 + b1;
c2 = a2 + b2;
System.out.println(c1 + " " + c2);
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(1,2); // utwórz i zainicjuj parę dwoma podanymi liczbami
Para b = new Para(3,4); // i drugą też
Para c = a.add(b); // dodaj do siebie dwie pary; wynikowa para będzie oznaczana przez zmienną c
c.show(); // pokaż wynik
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:
- slowo kluczowe public jest nieobowiązkowe (dlatego
w nawiasach
kwadratowych)
i określa dostępność klasy z innych klas (klasa
zdefiniowana ze słowem
public jest dostępna zewsząd). Uwaga: w pliku źródłowym może występować
tylko jedna klasa publiczna, i jeśli występuje - to plik musi miec
dokładnie taką samą nazwę jak ta klasa.
- nazwa
klasy musi spelniać 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 { // definicja klasy par liczb
całkowitych
// cialo klasy
}
public class Car {
// ciało klasy
}
class TestPara {
// ciało klasy
}
Pola i metody
klasy nazywają się
składowymi klasy.
Składowe klasy = pola + metody
2. Definiowanie pól
Pola klasy określają
(zazwyczaj) 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 typów.
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 zmiennych móc odwoływać się w metodach klasy.
Jedną z waznych 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
] [static] nazwa_typu
nazwa_zmiennej [ inicjator
];
//....
}
uwagi:
- nawiasy kwadratowe oznaczają opcjonalność elementów
definicji,
- o specyffikatorach dostępu zob. rozdział "Programowanie obiektowe I",
- znaczenie słowa static zostanie wyjasnione w następnym rozdziale,
- inicjator ma znaną nam postać wyrażenia po znaku
=, nadającego incjalną wartość zmiennej.
Na przykład:
public class Para {
private int a;
private int b;
// ...
}
Polami klasy mogą być zmienne obiektowe (zmienne oznaczające
obiekty). Zobaczmy jak mógłby wyglądać
fragment
definicji klasy Book, która opisuje książki:
public class Book {
private String author; // autor
private String title;
// tytuł
private double price; // cena
// ....
}
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 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.
Nie
każde pole klasy określa elementy zawarte w obiektach. Pola deklarowane
ze specyfikatorem static
są polami statycznymi - nie są zawarte w obiektach, dla każdej zmiennej
statycznej
wydzielany jest tylko jeden obszar pamięci na całą klasę. Więcej na
temat składowych statycznych - w dalszej części tekstu.
Przy
tworzeniu obiektów pola klasy zawsze uzyskują inicjalne, domyślne
wartości (powiemy: mają zagwarantowaną inicjację). Ogólnie są to
wartości ZERO (np. dla liczb całkowitych - całkowite zero, dla typu
boolean - wartość false).
3. Definiowanie metod
Zestaw operacji na obiektach określany jest przez definicję metod klasy.
Pojęcie metody zbliżone jest do znanego z innych języków programowania
pojęcia funkcji lub procedury.
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, 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:
class NazwaKlasy {
// ...
[
specyfikator_dostępu]
[static]
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 ktorej została zdefiniowana
Nazwę metody zaczynamy od malej 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;
}
Przykład wywołania metody suma:
int sum = suma(10,11);
Przy wywolaniu metoda suma
uzyskuje dwa przekazane jej argumenty (10 i 11)
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. Po wywołaniu zmienna sum
będzie miała wartość 21.
Przyklad 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
(dokladnie "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. mając metodę:
void incr(int x) {
++x;
}
i wywołując ją w następującym kontekście:
int z = 1;
incr(z);
System.out.println(z);
uzyskamy następujący efekt:
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 i ta wartość zostanie wyprowadzona na konsolę.
Metody
statyczne (definiowane ze specyfikatorem static) nie są wywoływane na
rzecz obiektów, mogą więc być wywoływane nawet wtedy gdy nie istnieje
żaden obiekt. Więcej na temat składowych statycznych w dalszej części
tekstu.
4. Definiowanie konstruktorów
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) { // Nadaje polom a i b wartości
a = x; // przekazane konstruktorowi jako
b = y; // argumenty
}
...
}
albo:
public class Para {
private int a, b;
public Para(int x) { // Konstruktor ma jeden parametr:
a = x; // oba pola są nim inicjowane
b = x;
}
...
}
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).
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 10, 11
Para p2 = new Para(2); //
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
wyowlany
automatycznie dodany konstruktor bezparametrowy( który nie robi nic ).
Uwaga: konstruktor bezparametrowy nie jest dodawany, gdy w
klasie zdefiniowano jakikolwiek konstruktor.
5. Przykład definiowania klasy
Jako podsumowanie powyższych rozważań przeanalizujemy
pełny przykład definicji klasy.
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 String title;
private String publisher;
private int year;
private String ident;
private double price;
private 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ążki (wartość pola quantity będzie równa 0).
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ś
sposób, 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).
public 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; // kupimy n = 10 egzemplarzy
b.buy(n);
// ł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");
}
}
Zobacz demo programu.
Program wyprowadzi na konsolę
następujące dane.
Na zakup 10 publikacji:
Psy
Dog & Sons
2002
ISBN6789
---------------
wydano: 210.0
---------------
Po sprzedaży zostało 6 pozycji
6. Składowe statyczne
Jak pamiętamy, pola i metody klasy mogą
być statyczne
i niestatyczne. Dotąd poznaliśmy tylko jeden rodzaj składowych -
składowe
niestatyczne.
Składowe niestatyczne zawsze wiążą się z
istnieniem jakiegoś obiektu
(pola – reprezentują dane obiektu, metody muszą być wywoływane na rzecz
obiektu, określają polecenia wysyłane do obiektu)
Składowe statyczne
(pola i metody):
- nie dotyczą obiektów, ale całej klasy - są wspólne dla wszystkich obiektów tej klasy,
- są deklarowane przy
użyciu specyfikatora static,
- mogą
być używane nawet wtedy, gdy nie istnieje żaden obiekt klasy.
Do statycznych
składowych możemy
odwoływać się za pomocą konstrukcji:
NazwaKlasy.NazwaSkładowej
Już od samego początku mieliśmy do czynienia z
metodą statyczną, mianowicie metodą main.
Ta
metoda musi być statyczna, ponieważ od niej zaczyna się wykonanie
aplikacji Javy, a w tym momencie (na samym początku( - nie istnieje
jeszcze żaden obiekt.
Inny przykład: w klasie System
(dostarczanej
jako klasa standardowa w dystrybucji Java) jest statyczna metoda
exit(int) kończąca działanie aplikacji i zwracająca
podany kod powrotu (wynik działania aplikacji).
Zatem, aby zakończyć działanie aplikacji z kodem powrotu 0
piszemy:
System.exit(0);
W klasie System jest też stała statyczna o nazwie
out, która określa obiekt –
konsolę (standardowe wyjście). Temu obiektowi możemy wydać polecenie
println,
co spowoduje wyprowadzenie podanego argumentu jako napisu na konsolę
(println
jest metodą z klasy PrintStream, a out oznacza obiekt-konsolę, który
jest
obiektem klasy PrintStream).
Zobaczmy. Schematyczna deklaracja
stałej statycznej out w klasie System wygląda tak:
public class System {
...
public
final static PrintStream out;
...
}
W klasie PrintStream (też dostępnej jako jedna ze
standardowych klas Javy)
zdefiniowano metody println, które wypisują do strumienia wyjściowego
(PrintStream)
podane argumenty. Np.
public class PrintStream {
...
public
void println(String s) {
...
}
}
Zatem, po kolei:
- System.out - jest statycznym polem klasy
System o identyfikatorze out;
zmienna ta zawiera referencję do standardowego wyjścia - konkretnego
obiektu
klasy PrintStream,
- println - oznacza użycie metody
na rzecz tego obiektu.
W sumie: System.out.println(...).
Ze specyfikatorem
static można
deklarowac tylko zmienne, będące polami
klasy
Bardzo ważną kwestią jest uświadomienie sobie różnicy pomiędzy
składowymi
statycznymi i niestatycznymi.
Można powiedzieć, że skladowe statyczne stanowią właściwości całej
klasy, a nie poszczególnych jej obiektów.
W przypadku pól rozróżnienie jest następujące:
- pola niestatyczne istnieją (jako elementy) w każdym
obiekcie klasy,
- pola statyczne - nie są zawarte w
obiektach, dla każdej zmiennej statycznej
wydzielany jest tylko jeden obszar pamięci na całą klasę.
Na przykład, w klasie opisującej dyski będziemy mieli pole statyczne
vat
(stawka vat) oraz pola niestatyczne model (opis modelu dysku), capacity
(pojemność
w GB), price (cenę netto). Każdy dysk będzie się różnił modelem,
pojemnością
i ceną netto (zatem te elementy będą stanowiły zawartość obiektów),
natomiast
stawka vat dla wszystkich dysków jest taka sama - nie ma więc sensu
zapisywać
jej w każdym obiekcie, będzie ona wspólną właściwością klasy, a jako
zmienna
statyczna - będzie zajmować tylko 8 bajtów na całą klasę, niezależnie
od
tego czy w programie stworzymy 1 czy 10000 obiektów - dysków.
public class Disk {
private static double vat;
private String model;
private int capacity;
private double price;
public Disk(String m, int c, double p) {
model = m;
capacity = c;
price = p;
}
public String getDescription() {
return model + ", " + capacity + " GB";
}
public double getBruttoPrice() {
return price * (1 + vat / 100);
}
public static void setVat(double v) {
vat = v;
}
}
class Test {
public static void main(String[] args) {
Disk.setVat(22.0);
Disk d1 = new Disk("Seagate Barracuda", 500, 200.0);
System.out.println(d1.getDescription() + " - cena "
+ d1.getBruttoPrice() + " zł");
}
}
Seagate Barracuda, 500 GB - cena 244.0 zł
Zwróćmy uwagę, że metodę setVat wywołaliśmy
przed stworzeniem jakiegokolwiek obiektu i pole vat uzyskalo wartość,
mimo,
że żaden obiekt nie istniał.
Ze statycznych metod nie
wolno odwoływać się do niestatycznych składowych
klasy (obiekt może nie istnieć). Możliwe są natomiast odwołania do
innych
statycznych składowych, np. innych metod statycznych.
Zauważmy też, że z metod niestatycznych możemy
w sposób naturalny odwoływac się do składowych (pól i metod)
statycznych,
co widac wyraźnie w metodzie getBruttoPrice().
7. Przeciążanie metod i konstruktorów
W klasie (i/lub jej klasach pochodnych) możemy zdefiniować metody o tej samej
nazwie, ale różniące się liczbą i/lub typami parametrów.
Nazywa się to przeciążaniem metod.
Po co istnieje taka możliwość?
Wyobraźmy sobie, że na obiektach klasy par liczb całkowitych (znanej nam
z poprzednich rozdziałów) chcielibyśmy wykonywać operacje:
- dodawania innych obiektów-par
- dodawania (do składników pary) kolejno dwóch podanych liczb całkowitych
- 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) // dodaje do pary, na rzecz której wywołano metodę, parę
// 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);
Innym
przykładem przeciążonej metody jest metoda println(...). Ma ona bardzo
wiele wersji - z argumentami różnych typów (m.in. wszystkich prostych,
String i Object). I bardzo dobrze, bo w przeciwnym przypadku
musielibyśmy pisać np. printInt(3) i printString("Ala"), aby
wyprowadzić odpowiednio liczbę całkowitą i napis.
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
Z przeciążaniem metod związany jest pewien problem. Otóż dopasowanie wywołania
do odpowiedniej wersji metody jest dokonywane przez kompilator na podstawie
liczby i typów argumentów. Musimy przy tym uważać, bowiem kiedy liczba parametrów
jest w różnych wersjach metody przeciążonej taka sama, a ich typy zbliżone
- to może się okazać, że źle interpretujemy działanie programu. Co się np
stanie, jeśli mamy dwie metody o nazwie show, pierwsza z parametrem typu
short, a druga z parametrem typu int, i wywołujemy metodę show z argumentem
typu char ? Powiedzieliśmy przed chwilą, że zostanie wywołana metoda, której
parametry najlepiej pasują do argumentów wywołania. Ponieważ typ short jest
"bliższy" typowi char niż typ int - mogłoby się wydawać, że zostanie wywołana
metoda show(short).
Tymczasem - jak pamiętamy - wykonywana jest promocja argumentu
typu char do typu int i będzie wywołana metoda show(int).
Jeżeli nie znamy dobrze mechanizmów automatycznych konwersji, to w metodach
przeciążonych specyfikujmy różną liczbę parametrów lub radykalnie różne typy
parametrów
Ogólnie, algorytm wyboru przez kompilator odpowiedniej metody
przeciążonej jest dość skomplikowany i może mieć zaskakujące,
nieintuicyjne konsekwencje (zob. specyfikację języka: JLS 15.12.2.5).
Również z tego powodu należy wyraźnie różnicować liczbę/typy argumentów.
Zwróćmy też uwagę, że przeciążanie "rozciąga się" na różne rodzaje metod.
Dwie metody - statyczną i niestatyczną - o tej samej nazwie, ale o różnych
typach/liczbie argumentów są przeciążone.
Podobnie jak w przypadku metod, możemy przeciążać konstruktory, Znaczy
to, że w jednej klasie możemy mieć kilka wersji konstruktorów z różnymi parametrami.
W ten sposób udostępniamy różne sposoby inicjacji obiektów klasy.
W takim
przypadku, po to by nie powtarzać wspólnego kodu w różnych konstruktorach,
wygodna okazuje się możliwość wywoływania konstruktora z innego konstruktora.
Do takiego wywołania stosujemy słowo kluczowe this, co ilustruje poniższy
fragment składni: