Część 3.4. Klasy i obiekty
Wstęp W rzeczywistym świecie spotykamy wiele obiektów. Obiektem jest człowiek, dom, komputer, itd. Każde z nich posiada indywidualne cechy takie jak kolor, wymiary, itd. Każde z nich może też realizować pewne zadania.Podobnie w ujęciu programistycznym obiekt posiada zbiór cech, które nazywamy właściwościami (polami danych) oraz potrafi wykonać pewne czynności, które nazywamy metodami. Uogólniając: obiekt jest to zestaw właściwości i możliwych do wykonania zadań. Przykładem obiektu może być na przykład okienko wyświetlające komunikat. Posiada ono cechy: wysokość, szerokość, tytuł, położenie na ekranie, kolor tła, ramkę itd. Do jego metod należą: możliwość zmiany rozmiaru okienka, przesunięcie, odświeżenie, zamknięcie, reakcja na przesuwanie lub kliknięcie myszki, wyświetlenie komunikatu, itp. Konkretny człowiek lub budynek jest obiektem. Potrafimy jednak mówić o człowieku czy domu w ogóle: podając, że każdy posiada określone cechy i zachowania. Np. każdy człowiek posiada wzrost, wagę, kolor włosów i oczu, wiek, nazwisko, itd. Stanowi to zbiór właściwości człowieka. Z jego możliwych działań możemy wymienić: poruszanie się, czytanie, pisanie, praca, itd. Taki ogólny zbiór cech i właściwości charakteryzuje każdego człowieka, stanowi jego wzorzec. W programowaniu obiektowym zbiór takich ogólnych cech nazywamy klasą. Innymi słowy klasa stanowi zbiór wszystkich możliwych właściwości i zachowań (metod) każdego utworzonego na jej podstawie egzemplarza czyli obiektu: klasa definiuje potencjalne właściwości i metody możliwego zbioru obiektów. Dopiero utworzenie (zadeklarowanie) obiektu danej klasy tworzy programową "rzeczywistość": zmienna obiektową o określonych właściwościach zdolną wykonywać pewne zadania. Widać z tego, że klasa stanowi pojęcie abstrakcyjne i istnieje tylko w czasie projektowania i pisania programu, obiekt zaś stanowi fizyczne wystąpieniem klasy i istnieje tylko w czasie wykonywania programu. Programowanie obiektowe w dużym uproszczeniu polega na tworzeniu klas, powoływaniu do "życia" obiektów tych klas oraz obsłudze możliwych zdarzeń związanych z tym obiektem. Takie podejście pozwala programiście skupić się na tym, co dzieje się w obrębie danego obiektu i jak ma on reagować na występujące zdarzenia. Obiekty mogą istnieć równolegle i działać niezależnie od siebie. Tyle tytułem wstępu. Jeżeli chcesz poznać dokładniej terminologie związana z klasami i obiektami, to zajrzyj na stronę terminologia obiektowa, gdzie pojęcia klasy i obiektu omówione są obszerniej. Każdy program w Javie składa z klas i tylko z klas. Poza klasami nic innego w programie nie występuje. W klasach deklarowane są wszystkie używane zmienne (pola danych) oraz zawarty jest cały kod wykonywalny programu, który realizuje jego zadania. W dotychczasowych przykładach była to tylko jedna klasa o nazwie Przyklad. Definiowała ona pewien typ, który do "życia" powoływany był w momencie uruchomienia programu. Dotychczas deklarowaliśmy klasę w jeden ustalony sposób. Pełna definicja klasy może mieć następującą składnię: [modyfikatory] class NazwaKlasy [extends NazwaKlasyBazowej] [implements NazwyKlasInterfejsów] { // Ciało klasy: // definicje pól danych , metod i klas wewnętrznych definiowanej klasy } Gdzie modyfikatory deklarują dostępność klasy (np.: public, abstract lub final static), NazwaKlasy określa nadaną przez programistę nazwę deklarowanej klasy, NazwaKlasyBazowej jest nazwą klasy nadrzędnej, a NazwyKlasInterfejsów to lista nazw klas interfejsów implementowanych przez deklarowana klasę. Ciało klasy stanowią definicje jej pól danych, metod i klas wewnętrznych. Po bardziej szczegółowe informacje o deklarowaniu klas zajrzyj do klasy jako typu danych. Składowymi klasy są pola i metody. Pola to zmienne, w których możemy przechowywać związane z daną klasą. Metody to kod wykonywalny, który realizuje "zachowania" klasy.Aby to zobrazować, utwórzmy klasę Prostokat, która będzie opisywała dowolny prostokąt poprzez określenie współrzędnych jego wierzchołków: public class Prostokat { int X1; // współrzędna X lewego górnego rogu int Y1; // współrzędna Y lewego górnego rogu int X2; // współrzędna X prawego dolnego rogu int Y2; // współrzędna Y prawego dolnego rogu } Zadeklarujmy teraz zmienną typu Prostokat: Prostokat mojProstokat; Jak widać deklaracja jest identyczna, jak deklaracja zmiennych typów podstawowych: najpierw podajemy typ zmiennej, a następnie nazwę deklarowanej zmiennej. W ten sposób utworzyliśmy referencje (odwołanie) do zmiennej (obiektu) typu Prostokąt. Skorzystanie z tego obiektu wymaga jednak utworzenia samego obiektu. Służy do tego operator new, który ma postać: new NazwaKlasy(); W naszym przypadku możemy to zrobić na 1 z dwu możliwych sposobów. Utworzyć obiekt w momencie jego deklarowania: Prostokat mojProstokat = new Prostokat(); lub zdeklarować referencję do obiektu, a dopiero później utworzyć sam obiekt: Prostokat mojProstokat; ..... mojProstokat = new Prostokat(); Klasa Prostokąt zawiera tylko 4 pola danych: X1, Y1, X2, Y2. Ponieważ już utworzyliśmy obiekt typu Prostokat, to możemy już nadawać tym polom wartości i odczytywać ich aktualna zawartość. W naszym przypadku mogłoby to wyglądać następująco: ..... Prostokat mojProstokat = new Prostokat(); ..... mojProstokat.X1 = 1; mojProstokat.Y1 = 2; mojProstokat.X2 = 5; mojProstokat.Y2 = 10; ..... int x1 = mojProstokat.X1; int y1 = mojProstokat.Y1; int x2 = mojProstokat.X2; int y2 = mojProstokat.Y2; ..... W tej chwili nasza klasa służy jedynie do przechowywania współrzędnych wierzchołków prostokąta i nie potrafi ich w żaden sposób wykorzystać. Aby klasa mogła zacząć "działać", musimy dopisać do niej metody, czyli funkcje, które będą wykonywały pewne działania (szczegóły deklaracji metod). Dopiszmy do naszej klsy metody, które umożliwią jej obliczenie pola i obwodu prostokąta: public class Prostokat { int X1; // współrzędna X lewgo górnego rogu int Y1; // współrzędna Y lewgo górnego rogu int X2; // współrzędna X prawgo dolnego rogu int Y2; // współrzędna Y prawgo dolnego rogu int obwod () // metoda obliczajaca obwód { int o = 2 * Math.abs(X2 - X1) + 2 * Math.abs(Y2 - Y1); return o; // wartość zwracana przez metodę } int pole () // metoda obliczajaca pole { int p = Math.abs(X2 - X1) * Math.abs(Y2 - Y1); return p; // wartość zwracana przez metodę } } W powyższym przykładzie użyliśmy metody Math.abs(...), aby obliczyć wartość bezwzględna z wyrażenia. Jest to konieczne, aby wynik był poprawny niezależnie od wartości współrzędnych prostokąta. Obecnie obiekt klasy Prostokat może już nie tylko pamiętać współczynniki wierzchołków, ale także obliczyć obwód i pole zapamiętanego prostokąta. Możemy z tego skorzystać w naszym programie: ..... Prostokat mojProstokat = new Prostokat(); ..... mojProstokat.X1 = 1; mojProstokat.Y1 = 2; mojProstokat.X2 = 5; mojProstokat.Y2 = 10; ..... System.out.println ("Pole = " + mojProstokat.pole();); System.out.println ("Obwód = " + mojProstokat.obwod();); ..... Najwyższa już pora na pokazanie jak wygląda cały program z wykorzystaniem utworzonej przez nas klasy Prostokat. Poniższy kod zapisz w jednym pliku o nazwie Przyklad.java, a następnie skompiluj i uruchom ten program. public class Przyklad { public static void main (String args[]) { Prostokat mojProstokat = new Prostokat(); System.out.println ("Pole = " + mojProstokat.pole()); System.out.println ("Obwód = " + mojProstokat.obwod()); } } class Prostokat { int X1=10; int Y1=10; int X2=1; int Y2=1; int obwod () { int o = 2 * Math.abs(X2 - X1) + 2 * Math.abs(Y2 - Y1); return o; } int pole () { int p = Math.abs(X2 - X1) * Math.abs(Y2 - Y1); return p; } } Efektem programu powinno być wyświetlenie 2 wierszy: Jak działa nasz program. Wyjaśnijmy to krok po kroku:
Pozostańmy jeszcze przy naszym przykładzie. Wszystkie zmienne publiczne służące do zapamiętania wierzchołków prostokąta zainicjowaliśmy konkretnymi wartościami. Co jednak by się stało, gdybyśmy tego nie zrobili i pozostawili ich deklaracje bez inicjacji? public class Prostokat { int X1; int Y1; int X2; int Y2; } Otóż w takim przypadku Java zainicjowałaby wszystkie te zmienne wartością zero. Jest to istotna różnica między deklaracją zmiennych prywatnych i publicznych: niezainicjowana zmienna prywatna powoduje błąd wykonania programu podczas próby jej użycia, natomiast wszystkie zmienne publiczne są zawsze inicjowane wartościami domyślnymi (zerem dla zmiennych numerycznych i wartością false dla zmiennych logicznych). W powyższy sposób sama Java rozwiązała nam problem inicjacji zmiennych w naszej klasie Prostokat. W praktyce jednak na ogół nie mamy do czynienia z prostokątami, które mają współrzędne wszystkich wierzchołków równe zero. Pozostaje więc żmudny proces "ręcznego" przypisywania wartości każdej ze współrzędnych: ..... Prostokat mojProstokat = new Prostokat(); mojProstokat.X1 = 1; mojProstokat.Y1 = 2; mojProstokat.X2 = 5; mojProstokat.Y2 = 10; ..... Aby tego uniknąć, możemy do naszej klasy dodać metodę, która ustawi od razu wartości wszystkich współrzędnych wierzchołkowych. Nazwijmy ją ustawWspolrzene: void ustawWspolrzene (int x1, int y1, int x2, int y2) { X1 = x1; Y1 = y1; X2 = x2; Y2 = y2; } Teraz możemy sobie uprościć ustawianie współrzędnych w naszym programie: Prostokat mojProstokat = new Prostokat(); mojProstokat.ustawWspolrzene (1, 2, 5, 10); ..... Możemy również stwierdzić, że w naszym przypadku najczęściej używanymi współrzędnymi są (1,1) i (10,10). Co zrobić, aby uniknąć każdorazowego ich ustawiania? Możemy do tego użyć specjalnej metody zwanej konstruktorem. Konstruktor, to metoda, która wykonywana jest zawsze podczas tworzenia obiektu danej klasy. Są one zawsze publiczne, nie posiadają typu i muszą mieć nazwę identyczną z nazwą klasy. Każda klasa może mieć wiele konstruktorów różniących się między sobą liczbą parametrów: podczas tworzenia obiektu klasy wykonany będzie ten, którego ilość parametrów zgodna jest z ilością podaną podczas tworzenia obiektu. W naszej klasie Prostokat utworzymy konstruktory, które umożliwią przyjęcie wartości typowych lub podanych podczas tworzenia obiektu: public Prostokat ( ) { X1 = 1; Y1 = 1; X2 = 10; Y2 = 10; } public Prostokat (int x1, int y1, int x2, int y2) { X1 = x1; Y1 = y1; X2 = x2; Y2 = y2; } Przerobimy teraz nasz ostatni program tworząc dwa prostokąty: pierwszy z domyślnymi warościami współrzędnych i drugi - z podanymi przez nas w momencie jego tworzenia. Następnie wyświetlimy pola i obwody obu utworzonych prostokątów: public class Przyklad { public static void main (String args[]) { Prostokat pr1 = new Prostokat(); Prostokat pr2 = new Prostokat(7, 6, 11, 13); System.out.println ("Pole domyślnego prostokąta = " + pr1.pole()); System.out.println ("Obwód domyślnego prostokąta = " + pr1.obwod()); System.out.print ("\n"); System.out.println ("Pole zdefiniowanego prostokąta = " + pr2.pole()); System.out.println ("Obwód zdefiniowanego prostokąta = " + pr2.obwod()); } } class Prostokat { int X1; int Y1; int X2; int Y2; public Prostokat ( ) { X1 = 1; Y1 = 1; X2 = 10; Y2 = 10; } public Prostokat (int x1, int y1, int x2, int y2) { X1 = x1; Y1 = y1; X2 = x2; Y2 = y2; } int obwod () { int o = 2 * Math.abs(X2 - X1) + 2 * Math.abs(Y2 - Y1); return o; } int pole () { int p = Math.abs(X2 - X1) * Math.abs(Y2 - Y1); return p; } } Wynik działania tego programu powinien wyglądać następująco: Użycie konstruktorów w klasach czyni ją bardziej elastyczną i wygodną w użyciu. Pozwala ponadto zdefiniować pewne właściwości obiektu już w momencie jego tworzenia. Jak widać z poprzedniego programu używanie konstruktorów nie jest konieczne. W wielu wypadkach ułatwia jednak znacznie pisanie programów i korzystanie z klas. W poprzednich przykładach spotkałeś się z tym, że klasy, właściwości i metody były poprzedzone modyfikatorami. Użycie ich związane jest z pojęciem hermetyzacji. Hermetyzacja polega na zamknięciu w jednej jednostce syntaktycznej danych i metod oraz ukrywanie informacji, które nie powinny być widoczne dla użytkownika. Innymi słowy od użycia odpowiedniego modyfikatora zależy czy dana klasa jest dostępna wszędzie, tylko dla określonej grupy klas, czy jej właściwości i metody mogą być używane w dowolnym miejscu programu, czy tylko w klasach potomnych lub tylko w samej klasie.Dla klas mamy do dyspozycji następujące modyfikatory dostępu:
Wyjaśnijmy to na przykładzie. Mamy napisać program, który będzie obliczał obwody prostokątów i trójkątów na podstawie wprowadzonych współrzędnych wszystkich wierzchołków oraz pola tych figur. Można tworzyć osobne klasy dla trójkątów i dla prostokątów, i w nich metody na obliczanie obwódów i pól. Ma to sens dla obliczania powierzchni pola, gdyż obie figury maja różne wzory na ich obliczanie. Obwód jednak zawsze liczymy sumując długości boków. Możemy więc zaoszczędzić sobie trochę pracy tworząc klasę Boki i na jej bazie budując dopiero nasze klasy Prostokat i Trojkat. Oto przykładowy kod takiego programu: public class Przyklad { public static void main (String args[]) { // deklaracja obiektów z domyślnymi współrzędnymi wierzchołków Prostokat pr1 = new Prostokat(); Trojkat tr1 = new Trojkat(); // deklaracja obiektów ze zdefiniowanymi współrzędnymi wierzchołków Prostokat pr2 = new Prostokat(0, 0, 0, 3, 3, 3, 3, 0); Trojkat tr2 = new Trojkat(1, 1, 5, 5, 10, 1); // wyświetlenie wszystkich obwodów i pól System.out.println ("Obwod domyslnego prostokata = " + pr1.obwod()); System.out.println ("Pole domyslnego prostokata = " + pr1.pole()); System.out.print ("\n"); System.out.println ("Obwod domyslnego trojkata = " + tr1.obwod()); System.out.println ("Pole domyslnego trojkata = " + tr1.pole()); System.out.print ("\n"); System.out.println ("Obwod zdefiniowanego prostokata = " + pr2.obwod()); System.out.println ("Pole zdefiniowanego prostokata = " + pr2.pole()); System.out.print ("\n"); System.out.println ("Obwod zdefiniowanego trojkata = " + tr2.obwod()); System.out.println ("Pole zdefiniowanego trojkata = " + tr2.pole()); } } // deklaracja klasy Boki, jako klasy bazowej dla Prostokat i Trojkat class Boki { private int n; // wszystkie zmienne są deklarowane private int B[][]; // jako prywatne, gdyż będą używane private int ile; // tylko wewnątrz tej klasy // konstruktor klasy, parametr podaje ilość boków figury public Boki(int x) { n = x; // zapamiętanie ilości boków B = new int[n][2]; // utworzenie tablicy dla współrzędnych wierzchołków ile = 0; // zerowanie ilości podanych wierzchołków } // metoda do zapamiętywania współrzędnych kolejnych wierzchołków figury void dodajWierzcholek(int x, int y) { if (ile == n) return; // jeżeli podano już wszystkie, to przetrwij B[ile][0] = x; // zapamiętanie współrzędnych B[ile][1] = y; ile++; // zwiększenie licznika zapamiętanych współrzędnych } // metoda służąca do obliczania obwodu figury // z tej metody będą korzystały klasy Prostokat i Trojkąt double obwod() { double o = 0; for (int i=1 ; i<=n ; i++) o += bok(i); return o; } // metoda obliczająca długość i-tego boku figury wg wzoru: // pierwiasztek z wyrażenia: (x2-x1)2 + (y2-y1)2 // z tej metody będą korzystały klasy Prostokat i Trojkąt double bok(int i) { double o = 0; if (i > n) return 0; if (i == n) o = Math.sqrt((B[0][0] - B[n-1][0])*(B[0][0] - B[n-1][0]) + (B[0][1] - B[n-1][1])*(B[0][1] - B[n-1][1])); else o = Math.sqrt((B[i-1][0] - B[i][0])*(B[i-1][0] - B[i][0]) + (B[i-1][1] - B[i][1])*(B[i-1][1] - B[i][1])); return o; } } // klasa Prostokat, która bazuje na klasie Boki class Prostokat extends Boki // konstruktor z nadamiem wartości wierzchołkom prostokąta { public Prostokat (int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4) { super(4); super.dodajWierzcholek (x1, y1); super.dodajWierzcholek (x2, y2); super.dodajWierzcholek (x3, y3); super.dodajWierzcholek (x4, y4); } // konstruktor domyślny klasy public Prostokat ( ) { super(4); super.dodajWierzcholek (1, 1); super.dodajWierzcholek (1, 2); super.dodajWierzcholek (2, 2); super.dodajWierzcholek (2, 1); } // metoda obliczająca pole prostokąta double pole () { return (super.bok(1) * super.bok(2)); } } // klasa Trojkat, która bazuje na klasie Boki class Trojkat extends Boki // konstruktor z nadamiem wartości wierzchołkom trójkąta { public Trojkat (int x1, int y1, int x2, int y2, int x3, int y3) { super(3); super.dodajWierzcholek (x1, y1); super.dodajWierzcholek (x2, y2); super.dodajWierzcholek (x3, y3); } // konstruktor domyślny klasy public Trojkat ( ) { super(3); super.dodajWierzcholek (0, 0); super.dodajWierzcholek (0, 4); super.dodajWierzcholek (3, 0); } // metoda obliczająca pole trójkąta double pole () { double p = super.obwod() / 2; double pole = Math.sqrt(p * (p - super.bok(1)) * (p - super.bok(2)) * (p - super.bok(3))); return pole; } } Tak powinien wyglądać wynik działania programu: Czymś zupełnie nowym w tym programie jest pojawienie się słowa super. Służy ono do odwołania się w klasie potomnej do metody lub właściwości klasy bazowej. Jest to jedyna metoda bezpośredniej łączności klasy potomnej z klasą bazową. W powyższym programie zwróć szczególną uwagę na kilka istotnych rzeczy:
Sądzę, że dokładne przeanalizowanie tego programu wyjaśni Ci mechanizm dziedziczenia klas.
|