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.