W2




Wykład 2







2. Obiektowość Javy II.
Dziedziczenie i polimorfizm



Zaczynając od koncepcji ponownego wykorzystania klas
poprzez kompozycję i dziedziczenie - przyjrzymy się tu przede wszystkim
bardzo ważnej koncepcji polimorfizmu. Można powiedzieć, że jest ona sednem
programowania obiektowego. Jest też intrygująca, a ponadto ma duże praktyczne
znaczenie dla umiejętnego stosowania językowych konstrukcji Javy.
Większość materiału dotyczącego dziedziczenia oraz obiektowych konwersji
rozszerzających jest ujęta syntetycznie, gdyż stanowi powtórzenie (ale z
innymi ilustracyjnymi przykładami) z poprzedniego semesteru.


2.1. Dziedziczenie
Podejście obiektowe umożliwia ponowne wykorzystanie już gotowych klas przy
tworzeniu klas nowych, co znacznie oszczędza pracę przy kodowaniu, a także
chroni przed błędami.

Istnieją dwa sposoby ponownego wykorzystania klas:

kompozycja,
dziedziczenie

Z koncepcyjnego punktu widzenia kompozycja oznacza, że "obiekt jest
zawarty w innym obiekcie" . Jest to relacja "całość
część" ( B "zawiera"
A). Np. obiekty typu Lampa zawierają obiekty typu Żarówka.

Dziedziczenie polega na przejęciu własności i funkcjonalności innego
obiektu i ewentualnej ich modyfikacji w taki sposób, by były one bardziej
wyspecjalizowane.
Jest to relacja, nazywana generalizacją-specjalizacją: B "jest typu" A,
"B jest A", a jednocześnie B specjalizuje A. A jest generalizacją B.

Przede wszystkim jednak dziedziczenie umożliwia posługiwanie się bardzo ważną
koncepcją programowania obiektowego, jaką jest polimorfizm (o czym za chwilę).

Jest to również odzwierciedlenie rzeczywistych sytuacji.
Np.obiekty klasy samochodów przejmują wszystkie właściwości obiektów klas
pojazdów, dodatkowo dostarczając jakichś własnych specyficznych cech.

Projektując klasę samochodów (klasę Car) możemy skorzystać z gotowej klasy
Vehicle (nie musimy na nowo pisać metod, definiować pól etc). Skupiamy się
na specyficznych cechach samochodów, ich cechy jako pojazdów "w ogóle" przejmując
z klasy Vehicle.

Przyjmijmy, że wyróżniającymi cechą samochodów są:

numer rejestracyjny
użycie paliwa (przy braku paliwa samochód nie może ruszyć)

i

dodajmy odpowiednie pola do klasy Car oraz odpowiedni konstruktor,
dodajmy metodę fill() pozwalającą tankować paliwo,
przedefiniujmy metodę start(), tak, by bez paliwa samochód nie mógł ruszyć




Przedefiniowanie metody nadklasy w klasie pochodnej oznacza dostarczenie
w klasie pochodnej definicji metody z taką samą sygnaturą jak sygnatura metody
nadklasy, ale inną definicją ciala metody (innym kodem, który jest wykonywany
przy wywołaniu metody)

W klasie Car przedefiniowano metodt start() i toString() z klasy Vehicle.

Obiekt klasy Car składa się z elementów zdefiniowanych przez pola klasy Vehicle
oraz elementów zdefiniowanych przez pola klasy Car.

Wobec obiektów klasy Car możemy używać:

wszystkich nieprywatnych (i nieprzedefiniowanych) metod klasy Vehicle,
przedefiniowanych w klasie Car metod klasy Vehicle,,
własnych metod klasy Car.

Pokazuje to poniższy program:

class Cars21 {

static void say(Car c) { System.out.println(c.toString()); }

public static void main(String[] args) {
Car c = new Car("WA1090",
new Person("Janek", "0909090"),
100, 100, 100, 100, 50),
d = new Car("WB7777", new Person("Zbyszek", "0909090"),
100, 100, 100, 100, 50);
c.start();
say(c);
c.fill(30);
c.start();
say(c);
d.fill(40);
d.start();
say(d);
c.stop();
say(c);
d.crash(c);
say(c);
say(d);
}

}



który wyprowadzi:
Brak benzyny
Samochód nr rej WA1090 - STOI
Samochód nr rej WA1090 - JEDZIE
Samochód nr rej WB7777 - JEDZIE
Samochód nr rej WA1090 - STOI
Samochód nr rej WA1090 - ZEPSUTY
Samochód nr rej WB7777 - ZEPSUTY







Odwołania do przesłoniętych składowych nadklasy realizowane są za pomocą konstrukcji:


super.odwołanie_do_składowej

czyli np.:

super.x // odwołanie do pola z nadklasy,
// które ma taki sam identyfikator
// jak pole w klasie

super.show() // wywołanie metody z nadklasy,
// której nazwa w danej klasie
// jest przesłonięta
// (metoda jest przedefiniowana)



W konstruktorze

użycie wyrażenia:

super(argumenty)

oznacza wywołanie konstruktora klasy bazowej z argumentami "argumenty".

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.







Przy budowaniu obiektów klas pochodnych podstawową regułą jest, iż najpierw muszą być zainicjowane pola klasy bazowej.

Sekwencja inicjowania obiektu klasy pochodnej jest następująca:

Wywoływany jest konstruktor klasy pochodnej.
Jeśli pierwszą instrukcją jest super(args) wykonywany jest konstruktor klasy bazowej z argumentami args.
Jeśli nie ma super(...) wykonywany jest konstruktor bezparametrowy klasy bazowej.
Wykonywane są instrukcje wywołanego konstruktora klasy pochodnej.

Proszę przeanalizować poniższy przykład.

Przykład.


class s { // ułatwienie dla wypisywania
static void ay(String s) { System.out.println(s); } }

class A {
A() { s.ay("Konstruktor A"); }
A(String t) {
s.ay("Konstruktor A z parametrem String = " + t);
}
}

class B extends A {
B() { s.ay("Konstruktor B"); }
B(int i) { s.ay("Konstruktor B z parametrem int = " + i); }
B(String t) {
super(t);
s.ay("Konstruktor B z parametrem String = " + t);
}
}

class C extends B {
}

class Test {
public static void main(String[] arg) {
s.ay("Tworzenie obiektu B - B():");
new B();
s.ay("Tworzenie obiektu B - B(int)");
new B(1);
s.ay("Tworzenie obiektu B - (Strng)");
new B("Ala");
s.ay("Tworzenie obiektu C:");
new C();
}
}

Wynik działania programu:
Tworzenie obiektu B - B():
Konstruktor A
Konstruktor B
Tworzenie obiektu B - B(int)
Konstruktor A
Konstruktor B z parametrem int = 1
Tworzenie obiektu B - (Strng)
Konstruktor A z parametrem String = Ala
Konstruktor B z parametrem String = Ala
Tworzenie obiektu C:
Konstruktor A
Konstruktor B

Java ma pewne szczególne cechy w porównaniu z innymi językami obiektowymi:

nie ma w niej wielodziedziczenia (nie można dziedziczyć wielu klas),
a hierarchia dziedziczenia wszyskich klas zaczyna się w jednym miejscu.


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 z pakietu java.lang.

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).


2.2. Obiektowe konwersje rozszerzające
Referencje do każdego obiektu możemy przekształcić do typu referencja do jego (dowolnej) nadklasy.
Nazywa się to referencyjną konwersją rozszerzającą, albo obiektową konwersją
rozszerzającą, "upcasting" (up
bo w górę hiererachii dziedziczenia).

Obiektowe konwersje rozszerzające dokonywane są automatycznie (nie musimy stosować operatora konwersji) przy:

przypisywaniu zmiennej-referencji odniesienia do obiektu klasy pochodnej,
przekazywaniu argumentów metodom, gdy parametr jest typu refrencja do nadklasy argumentu ,
zwrocie wyniku, gdy wynik podstawiamy na zmienną będącą referencją do obiektu nadklasy zwracanego wyniku.

Przykład:




W powyższym przykładzie metoda reportState() może być wywołana z dowolną
liczbą argumentów, które są referencjami do dowolnych podklas klasy Vehicle.

Dzięki obiektowym konwersjom rozszerzającym możemy pisać uniwersalne metody.
Gdyby nie było obiektowych konwersji rozszerzających musielibyśmy dostarczyć
odrębnej metody reportState() dla każdego typu pochodnego od Vehicle.




2.3. Polimorfizm. Metody wirtualne
W klasie Car przedefiniowaliśmy metodę start() z klasy Vehicle (dla samochodów
sprawdza ona czy jest paliwo by ruszyć, nie robi tego dla pojazdów "w ogóle").
Przedefiniowaliśmy też metodę toString() (dla obiektów klasy Car zwraca ona
inne napisy niż dla ogólniejszych obiektów klasy Vehicle).

Jeżeli teraz:


Car c = new Car(...); // utworzymy nowy obiekt klasy Car
Vehicle v = c; // dokonamy obiektowej konwersji rozszerzającej

to jaki będzie wynik użycia metod start() i toString() wobec obiektu oznaczanego v:

v.start();
System.out.println(v.toString());


Czy zostaną wywołane metody z klasy Vehicle (formalnie metody te są wywoływane
na rzecz obiektu klasy Vehicle) czy z klasy Car (referencja v formalnego
typu "referencja na obiekt Vehicle" faktycznie wskazuje na obiekt klasy Car)
?

Rozważmy przykład "z życia" zapisany w programie, a mianowicie schematyczną symulację wyścigu pojazdów.
Uczestnicy: rowery (obiekty klasy Rower), samochody (obiekty klasy Car), rydwany (obiekty klasy Rydwan).
Wszystkie klasy są pochodne od Vehicle.
Każda z tych klas inaczej przedefiniowuje metodę start() z klasy Vehicle
(np. Rower może w ogóle jej nie przedefiniowywać, Car
tak jak w poprzednich
przykładach, Rydwan
w jakiś inny sposób).
Sygnał do startu wszystkich pojazdów daje starter.
W programie moglibyśmy to symulować poprzez:

uzyskanie tablicy wszystkich startujących w wyścigu pojazdów (np. getAllVehiclesToStart()),
przebiegnięcie przez wszystkie elementy tablicy i posłanie do każdego
z obiektów, przez nie reprezentowanych, komunikatu start()

przykładowo:

Vehicle[] v = getAllVehiclesToStart();
for (int i = 0; i <v.length; i++) v[i].start();

Jeżeli nasz program ma odwzorowywać rzeczywistą sytuację wyścigu (sygnał
startera, po którym wszystkie pojazdy
jeśli mogą
ruszają), to oczywiste
jest, że
mimo, iż v[i] są formalnego typu Vehicle
powinny być wywołane
metody start() z każdej z odpowiednich podklas klasy Vehicle.

I tak jest rzeczywiście w Javie.




Ale jak to jest możliwe?
Z punktu widzenia.łączenia przez kompilator odwołań do metody (np. start())
oraz jej definicji (wykonywalnego kodu) sytuacja jest następująca:

kompilator wie tylko, że start() jest komunikatem do obiektu typu Vehicle,
powinien więc związać odwołanie v.start() z definicją start() z klasy Vehicle

Jakże inaczej? Przecież wartość v może zależeć od jakichś warunków występujących
w trakcie wykonania programu (nieznanych kompilatorowi).

Np. mając dwie klasy dziedziczące klasę Vehicle, Car i Rydwan możemy napisać:

public static void main(String args[]) {
Car c = new Car(...);
Rydwan r = new Rydwan(...);
Vehicle v;
if (args[0].equals("Rydwan")) v = r;
else v = c;
v.start();
}

Kompilator nie może wiedzieć jaki konkretnie jest typ obiektu wskazywanego przez v (czy Car czy Rydwan). I nie wie!

W jaki sposób zatem uzyskujemy opisany wcześniej (zgodny z życiowym doświadczeniem)
efekt, czyli np. wywołanie metody start() z klasy Car, jeśli v wskazuje
na obiekt klasy Car i wywołanie metody start() z klasy Rydwan, jeśli v wskazuje
na obiekt klasy Rydwan ?

Otóż metoda start() z klasy Vehicle jest metodą wirtualną, a dla takich metod wiązanie odwołań z kodem następuje w fazie wykonania, a nie w fazie kompilacji.

Nazywa się to "dynamic binding" lub "late binding" i technicznie realizowane jest poprzez wykorzystanie ukrytego w obiekcie pola, wskazującego na jego prawdziwy typ.

Mówi się, że odwołania do metod wirtualnych są polimorficzne, a słowo
"polimorficzne" używane jest w tym sensie, iż konkretny efekt odwołania może
przybierać różne kształty, w zależności od tego jaki jest faktyczny typ obiektu
na rzecz którego wywołano metodę wirtualną.
Istotnie, jak widzieliśmy:
v,start() raz może oznaczać start samochodu, a innym razem start rydwanu, czy roweru.

Wszystkie metody w Javie są wirtualne, za wyjątkiem:

metod statycznych (bo przecież nie dotyczą obiektów),
metod deklarowanych ze specyfikatorem final (co oznacza, że postać
metody jest ostateczna i nie może być ona przedefiniowana w klasie pochodnej,
a jak nie ma przedefiniowania, to niepotrzebna jest wirtualność),
metod prywatnych (do których odwołania w innych metodach danej klasy nie są polimorficzne).




2.4. Znaczenie polimorfizmu

Rozważmy pewną hierarchię dziedziczenia, opisującą takie właściwości różnych
zwierząt jak nazwa rodzaju, sposób komunikowania się ze światem oraz imię.
Dzięki odpowiedniemu określeniu bazowej klasy Zwierz przy definiowaniu klas
pochodnych (takich jak Pies czy Kot) mamy całkiem niewiele roboty.
(uwaga: dla ustalenia uwagi w dalszych przykładach pomijamy specyfikatory dostępu, bowiem nie mają one znaczenia dla omaiwanych tu treści).

class Zwierz {
String name = "nieznany";
Zwierz() { }
Zwierz(String s) { name = s; }
String getTyp() { return "Jakis zwierz"; }
String getName() { return name; }
String getVoice() { return "?"; }
void speak() {
System.out.println(getTyp()+" "+getName()+" mówi "+getVoice());
}
}

class Pies extends Zwierz {
Pies() { }
Pies(String s) { super(s); }
String getTyp() { return "Pies"; }
String getVoice() { return "HAU, HAU!"; }
}

class Kot extends Zwierz {
Kot() { }
Kot(String s) { super(s); }
String getTyp() { return "Kot"; }
String getVoice() { return "Miauuuu..."; }
}

W klasie Main wypróbujemy naszą hierarchię klas zwierząt przy symulowaniu
rozmów pomiędzy poszczególnymi osobnikami. Rozmowę symuluje statyczna funkcja
animalDialog, która ma dwa argumenty
obiekty typu Zwierz, oznaczające aktualnych
"dyskutantów".


class Main {
public static void main(String[] arg) {
Zwierz z1 = new Zwierz(),
z2 = new Zwierz();
Pies pies = new Pies(),
kuba = new Pies("Kuba"),
reksio = new Pies("Reksio");
Kot kot = new Kot();

animalDialog(z1, z2);
animalDialog(kuba, reksio);
animalDialog(kuba, kot);
animalDialog(reksio, pies);
}

static void animalDialog(Zwierz z1, Zwierz z2) {
z1.speak();
z2.speak();
System.out.println("----------------------------------------");
}
}


Uruchomienie tej aplikacji da następujący wynik:
Jakis zwierz nieznany mówi ?
Jakis zwierz nieznany mówi ?
----------------------------------------
Pies Kuba mówi HAU, HAU!
Pies Reksio mówi HAU, HAU!
----------------------------------------
Pies Kuba mówi HAU, HAU!
Kot nieznany mówi Miauuuu...
----------------------------------------
Pies Reksio mówi HAU, HAU!
Pies nieznany mówi HAU, HAU!
----------------------------------------


Cóż jest ciekawego w tym przykładzie? Otóż dzięki wirtualności metod getTyp()
i getVoice() metoda speak(), określona w klasie Zwierz prawidłowo działa
dla różnych zwierząt (obiektów podklas klasy Zwierz).
Jest to nie tylko ciekawe, ale i wygodne: jedna definicja metody speak()
załatwiła nam wszystkie potrzeby (dotyczące dialogów różnych zwerząt). Co
więcej
będzie ona tak samo użyteczna dla każdej nowej podklasy Zwierza,
którą kiedykolwiek w przyszłości wprowadzimy!


2.5. Metody i klasy abstrakcyjne

Metoda abstrakcyjna nie ma implementacji (ciała) i winna być zadeklarowana ze specyfikatorem abstract.

abstract int getSomething(); // nie ma ciała - tylko średnik

Klasa w której zadeklarowano jakąkolwiek metodę abstrakcyjną jest klasą abstrakcyjną i musi być opatrzona specyfikatora abstract.

Np.

abstract class SomeClass {
int n;
abstract int getSomething();
void say() { System.out.println("Coś tam");
}

Po co są metody abstrakcyjne?
Metody abstrakcyjne to takie, co do których nie wiemy jeszcze jaka może być
ich konkretna implementacja (lub nie chcemy tego przesądzać), ale wiemy,
że powinny wystąpić w zestawie metod każdej konkretnej klasy dziedziczącej klasę abstrakcyjną.
Konkretna implementacja może być bardzo różna, w zależności od konkretnego rodzaju obiektów, które opisuje dana klaas.

Klasa abstrakcyjna nie musi mieć metod abstrakcyjnych.
Wystarczy zadeklarować ją ze specyfikatorem abstract.

Abstrakcyjność klasy oznacza, iż nie można tworzy jej egzemplarzy (obiektów).

Moglibyśmy więc zadeklarować klasę Zwierz ze specyfikatorem abstract:


abstract class Zwierz {
String name = "nieznany";
Zwierz() { }
Zwierz(String s) { name = s; }
String getTyp() { return "Jakis zwierz"; }
String getName() { return name; }
String getVoice() { return "?"; }
void speak() {
System.out.println(getTyp()+" "+getName()+" mówi "+getVoice());
}
}

powiadając w ten sposób:
nie chcemy bezpośrednio tworzyć obiektów klasy Zwierz.
Cóż to jest Zwierz?
To dla nas jest - być może - czysta abstrakcja.
Abstrakcyjna klasa Zwierz może być natomiast dziedziczona przez klasy konkretne
np. Pies czy Kot. albo może Tygrys, co daje im już pewne zagwarantowane cechy
i funkcjonalność.
Dopiero z tymi konkretnymi typami zwierząt możemy się jakoś obchodzić, a
zestaw metod wprowadzonych w klasie Zwierza daje nam po temu ustalone środki.

Skoro Zwierz jest abstrakcyjny, to zestaw jego metod (tu: do jakiegoś stopnia) może być też abstrakcyjny:

abstract class Zwierz {
String name;
Zwierz() { name = "nieznany" }
Zwierz(String s) { name = s; }
abstract String getTyp();
abstract String getVoice();
String getName() { return name; }
void speak() {
System.out.println(getTyp()+" "+getName()+" mówi "+getVoice());
}
}


Metody getTyp() i getVoice() są abstrakcyjne (nie dostarczyliśmy ich implementacji, bowiem zależy ona od konkretnego Zwierza).

Więcej są - jak domyślnie wszystkie metody w Javie - wirtualne.

Wirtualne - znaczy o możliwych różnych definicjach przy konkretyzacji.
Wirtualne - o nieznanym (jeszcze) dokładnie sposobie działania.
Wirtualne - niekoniecznie już istniejące.

W tym kontekście metoda speak() staje się jeszcze ciekawsza/.
Oto używamy w niej nieistniejących jeszcze metod!
Możemy się odwoływać do czegoś co być może powstanie dopiero w przyszłości.
Co może mieć wiele różnorodnych konkretnych kształtów, teraz nam jeszcze nie znanych.

Konkretyzacje następują w klasach pochodnych, gdzie implementujemy (definiujemy) abstrakcyjne metody getTyp i getVoice.

Klasa dziedzicząca klasę abstrakcyjną musi zdefiniować wszystkie abstrakcyjne
metody tej klasy, albo sama będzie klasą abstrakcyjną i wtedy jej definicja
musi być opatrzona specyfikatorem abstract.

Zatem po to, byc móc tworzyć i posługiwać się obiektami klas Pies i Kot musimy
zdefiniować w tych klasach abstrakcyjne metody klasy Zwierz.

class Pies extends Zwierz {
Pies() { }
Pies(String s) { super(s); }
String getTyp() { return "Pies"; }
String getVoice() { return "HAU, HAU!"; }
}

class Kot extends Zwierz {
Kot() { }
Kot(String s) { super(s); }
String getTyp() { return "Kot"; }
String getVoice() { return "Miauuuu..."; }
}



Możliwość deklarowania metod abstrakcyjnych można też traktować jako pewne
pragmatyczne ulatwienie. Nie musimy oto wymyślać (i zapisywać) sztucznej
funkcjanalności, sztucznego działania na zbyt abstrakcyjnym poziomie (jak
np. return "jakiś zwierz" czy return "?".).


2.6. Podsumowanie
Przedstawiono tu przede wszystkim ważną koncepcję polimorfizmu. Jej zrozumienie,
a także umiejętnośc posługiwania się polimorfizmem jest - praktycznie - warunkiem
sprawnego programowania w Javie.


2.7. Zadania i ćwiczenia
Przedstawione zadania są istotnym uzupełnieniem treści wykładu, a ich wykonanie
powinno owocować jej dobrym zrozmieniem. Oprócz tego zawarto tu informacje
pomocnicze, ulatwiające wykonanie zadań, a także rozszerzające nieco wiedzę
o sposobach programowania w Javie.

Odnośniki:

Zadanie 1. Samochody i autobusy (bardzo łatwe)

Zadanie 2. Sklep owocowy (latwe, nie wymaga korzystania z polimorfizmu)

Zadanie 3. Sklep owocowy (trudniejsze, wymaga użycia polomorfizmu oraz dla prawnego wykonania pewnych nowych elementów, o których mowa w POMOCY

Zadanie 4. Modyfikacja 3 (bardzo łatwe, jeśli wykoanno zadanie 3)

POMOC (informacje pomocnicze)






Zadanie 1. Samochody i autobusy
Posługując się przykładem z wykładu (klasa Vehicle i klasa Car) zdefiniować
klasę EngineVehicle (pojazd silnikowy), dziedziczącą klasę Vehicle.
Wszystkie pojazdy silnikowe mają następujące właściwości:


mają numer rejestracyjny,

są napędzane paliwem (np. benzyną), a zatem mają bak o zadanej pojemności,
paliwo w baku (jakaś ilość) oraz umożliwiają tankowanie (powiedzmy metoda
void fill(int) - wlanie do baku n litrów paliwa.

Używając zdefiniowanej klasy EngineVehicle zmodyfikować klasę Car z
wykładu i zdefiniować nową klasę Bus (autobus).

Autobusy będą miały dwie dodatkowe właściwości:


liczba miejsc

liczba wolnych miejsc.


Dostarczyć metod startVehicles(EngineVehicle[]) i reportState(EngineVehicle[]),
tak, by następujący program wyprowadził podane po nim napisy:



class Veh1 {

static void reportState(EngineVehicle[] v) {
// ...
}


static void startEngineVehicles(EngineVehicle[] v) {
// ...
}

public static void main(String[] args) {
Bus a = new Bus("BUS1", new Person("MZA", "xxx"),
100, 100, 100, 100, 500, 20),
b = new Bus("BUS2", new Person("MZA", "xxx"),
100, 100, 100, 100, 500, 25);

Car c = new Car("WA1090", new Person("Janek", "xxx"),
100, 100, 100, 100, 50),
d = new Car("WB7777", new Person("Zbyszek", "xxx"),
100, 100, 100, 100, 50);

a.fill(100);
b.fill(100);
c.fill(30);
d.fill(50);

// proszę zwrócić uwagę na inicjację tablicy allVeh!
EngineVehicle[] allVeh = new EngineVehicle[] { a, b, c, d };

startEngineVehicles( allVeh );
reportState( allVeh );
}
}

Program wyprowadza napisy:

Pojazd silnikowy BUS1 - JEDZIE
Pojazd silnikowy BUS2 - JEDZIE
Pojazd silnikowy WA1090 - JEDZIE
Pojazd silnikowy WB7777 - JEDZIE

Następnie spróbować uzupełnić funkcjonalność obiektów klasy Bus o wsiadanie/wysiadanie pasażerów
na przystankach.



Zadanie 2. Sklep owocowy
Napisać aplikację, która symuluje zakupy w sklepie z owocami.
Aplikacja wymaga zdefiniowania kilku klas i umiejętnego ich użycia, w taki
sposób by następujący program działał poprawnie.Uwaga: w pokazanym tekście programu występują odwołania do klas:
Cennik, Koszyk, Truskawki, Banany, Agrest, Mandarynki, Kasa, Torba,
ale nie występuje jeszcze (co najmniej!) jedna ważna klasa potrzebna do spełnienia
wymagań postawionych przed programem.

class Sklep {
public static void main(String[] args) {

// Na poczatku dnia ustalany jest cennik

Cennik cennik = new Cennik();
cennik.set("Truskawki", 5); // ceny owoców w zł
cennik.set("Banany", 6);
cennik.set("Agrest", 7);

// Wchodzimy do sklepu i bierzemy koszyk
Koszyk koszyk = new Koszyk();

// Dodajemy do koszyka kilo truskawek
// pol kilo bananow, cwierc kilo agrestu
// i 2 kilo mandarynek

koszyk.add(new Truskawki(1));
koszyk.add(new Banany(0.5));
koszyk.add(new Agrest(0.25));
koszyk.add(new Mandarynki(2)); // dziwnym trafem sprzedawca zapomnial
// ustalic ich ceny, ale my o tym nie wiemy

// podchodzimy do kasy nr 1,
// pokazujemy zawartość koszyka
// a kasa nas rozlicza wg obowiązującego cennika

Kasa kasa = new Kasa(1);
koszyk.showContent(); // pokazuje zawartość koszyka
kasa.printBill(koszyk, cennik); // wydruk rachunku

// placimy (czego program juz nie pokazuje)
// i przekladamy zawartosc koszyka do torby

Torba torba = new Torba();
torba.loadFrom(koszyk); // przelozenie owocow z koszyka do torby

// Przychodzimy do domu i pokazujemy co kupilismy

torba.showContent(); // program pokazuje zawartosc torby

}
}

Powyższy program powinien wyprowadzić coś w rodzaju następującego listingu:

Zawartość koszyka:
Truskawki 1.0 kg
Banany 0.5 kg
Agrest 0.25 kg
Mandarynki 2.0 kg
Kasa nr 1 (rachunek)
Truskawki 1.0 kg * 5.0 zl/kg = 5.0 zl
Banany 0.5 kg * 6.0 zl/kg = 3.0 zl
Agrest 0.25 kg * 7.0 zl/kg = 1.75 zl
Razem: 9.75 zl
Zawartość torby

Truskawki 1.0 kg

Banany 0.5 kg

Agrest 0.25 kg
Wymaganie:dodanie do powyższego programu zakupów innych rodzajów owoców (np. Winogron)
ma byc bardzo łatwe.
Potrzeba tylko :

zdefiniowac nową klasę np. Winogrona, przy czym ma to być jak najbardziej
oszczędna definicja (kilka wierszy kodu)dodać odpowiedni kod do funkcji main (np. cennik.set("Winogrona", 8);
koszyk.add(new Winogrona(0.5)); )

Przy dodaniu nowego rodzaju owoców nie wolno modyfikować żadnych innych
klas programu.
UWAGA. W sklepie mogą być owoce, których zapomniano dodać do cennika. Wtedy
przy kasie są one usuwane z naszego koszyka.
Pomoc - jak można zrobić cennik?




Zadanie 3. Sklep owocowy B

Napisać aplikację, która symuluje zakupy w sklepie z owocami.
Aplikacja wymaga zdefiniowania kilku klas i umiejętnego ich użycia, w taki
sposób by następujący program działał poprawnie.Uwaga: w pokazanym tekście programu występują odwołania do klas:
Cennik, Koszyk, Truskawki, Banany, Agrest, Mandarynki, Kasa, Torba,
ale nie występuje jeszcze dwie ważne klasy potrzebne do spełnienia wymagań
postawionych przed programem.

class Sklep {

private Kasa kasa;

Sklep() {
// Na poczatku dnia ustalany jest cennik
// i otwierana jest kasa
Cennik cennik = Cennik.getCennik();
cennik.set("Truskawki", 5);
cennik.set("Banany", 6);
cennik.set("Agrest", 7);
kasa = new Kasa();
}

public void zakupyDemo(String osoba) {

// Podana osoba wchodzi do sklepu i bierze koszyk
// Każdy koszyk ma swój numer
// Liczba koszyków jest nieograniczona

Koszyk koszyk = new Koszyk();
System.out.println(osoba + " bierze " + koszyk);


// Dodaje do koszyka kilo truskawek
// pol kilo bananow, cwierc kilo agrestu
// i 2 kilo mandarynek
koszyk.add(new Truskawki(1));
koszyk.add(new Banany(0.5));
koszyk.add(new Agrest(0.25));
koszyk.add(new Mandarynki(2)); // dziwnym trafem sprzedawca zapomnial
// ustalic ich ceny, ale kupujacy o tym nie wie

// podchodzi do kasy
// pokazuje zawartość koszyka
// a kasa go rozlicza wg ustalonego cennika

koszyk.showContent();
kasa.printBill(koszyk); // wydruk rachunku

// placi i ...
// przeklada zawartosc koszyka do torby
Torba torba = new Torba(osoba);
torba.loadFrom(koszyk);

// Przychodzi do domu i pokazuje co kupil(a)
torba.showContent();

}
}

// Klasa tesująca klasę Sklep
class Test {
public static void main(String[] args) {
Sklep s = new Sklep();
s.zakupyDemo("Janek");
}
}


Wydruk programu:
Janek bierze koszyk sklepowy nr 1

Zawartość pojemnika "koszyk sklepowy nr 1" :

Truskawki 1.0 kg

Banany 0.5 kg

Agrest 0.25 kg

Mandarynki 2.0 kg

Kasa - rachunek za [ koszyk sklepowy nr 1 ] :

Truskawki 1.0 kg * 5.0 zl/kg = 5.0 zl

Banany 0.5 kg * 6.0 zl/kg = 3.0 zl

Agrest 0.25 kg * 7.0 zl/kg = 1.75 zl

Razem: 9.75 zl

Zawartość pojemnika "torba [ właściciel: Janek ] " :

Truskawki 1.0 kg

Banany 0.5 kg

Agrest 0.25 kg


Wymagania:

dodanie do powyższego programu zakupów innych rodzajów owoców (np.
Winogron) ma byc bardzo łatwe; należy tylko dodać kilkuwierszową definicję
nowej klasy owoców, ustalenie cen oraz ew. zakup tych owoców.należy wykorzystać klasy abstrakcyjne i polimorfizmnależy zminimalizować kod klas Koszyk i Torba - pomoc
należy zdefiniować klasę Cennik, tak by zawsze w programie istniał nie więcej niż jeden obiekt tej klasy (taka klasa nazywa się singleton). Uzasadinienie: jest tylko jeden cennik. Pomoc
.


UWAGA. W sklepie mogą być owoce, których zapomniano dodać do cennika. Wtedy
przy kasie są one usuwane z koszyka.


Zadanie 4. Modyfikacja zadania 3


Po wykonaniu zadania 2 usunąć metodę zakupyDemo(...) z klasy Sklep, a zamiast
niej zdefiniować metodę void zakupy(...), tak by następujący kod:

class Sklep {

private Kasa kasa;

Sklep() {
// Na poczatku dnia ustalany jest cennik
// i otwierana jest kasa
Cennik cennik = Cennik.getCennik();
cennik.set("Truskawki", 5);
cennik.set("Banany", 6);
cennik.set("Agrest", 7);
kasa = new Kasa();
}

// tu definicja metody zakupy(....)

}

class Test {
public static void main(String[] args) {
Sklep s = new Sklep();
s.zakupy("Janek",
new Owoce[] { new Truskawki(1), new Banany(0.5),
new Agrest(0.25), new Mandarynki(1) }
);

s.zakupy("Małgosia",
new Owoce[] { new Truskawki(5), new Banany(3) } );
}
}

dał w wyniku wydruk:
Janek bierze koszyk sklepowy nr 1

Zawartość pojemnika "koszyk sklepowy nr 1" :

Truskawki 1.0 kg

Banany 0.5 kg

Agrest 0.25 kg

Mandarynki 1.0 kg

Kasa - rachunek za [ koszyk sklepowy nr 1 ] :

Truskawki 1.0 kg * 5.0 zl/kg = 5.0 zl

Banany 0.5 kg * 6.0 zl/kg = 3.0 zl

Agrest 0.25 kg * 7.0 zl/kg = 1.75 zl

Razem: 9.75 zl

Zawartość pojemnika "torba [ właściciel: Janek ] " :

Truskawki 1.0 kg

Banany 0.5 kg

Agrest 0.25 kg

Małgosia bierze koszyk sklepowy nr 2

Zawartość pojemnika "koszyk sklepowy nr 2" :

Truskawki 5.0 kg

Banany 3.0 kg

Kasa - rachunek za [ koszyk sklepowy nr 2 ] :

Truskawki 5.0 kg * 5.0 zl/kg = 25.0 zl

Banany 3.0 kg * 6.0 zl/kg = 18.0 zl

Razem: 43.0 zl

Zawartość pojemnika "torba [ właściciel: Małgosia ] " :

Truskawki 5.0 kg

Banany 3.0 kg

Pomoc




Pomoc - informacje pomocnicze


Pomoc 1 (Hashtable)
Przy ustalaniu cen można skorzystać z klasy Hashtable, która umożliwia łatwe
odnajdywanie informacji wg kluczy.
Do Hashtable dodajemy za pomocą metody put pary (klucz, wartość),
które są odniesieniami do obiektów typu Object.
Np. jeśli chcemy dodać pod kluczem "Ala" wartość typu double, to musimy z
double zrobić obiekt (do czego służy klasa Double):
Hashtable ht = new Hashtable();
double d = 1.7;
String s = "Ala";
ht.put(s, new Double(d)); // dodanie wartości
// (obiekt
Double, który zawiera wartośc double 1.7)
// pod kluczem s ("Ala")
Z tablicy uzyskujemy wartości (które są odniesieniami do obiektów formalnego
typu Object) "po kluczu" za pomocą odwołania get(key) np. aby uzyskać
wartość zapisaną uprzednio (w wierszach wyżej) pod kluczem "Ala":
Object o = ht.get(s); // s wskazuje na "Ala" i jest kluczem // jeżeli w tablicy nie będzie klucza s
("Ala") to metoda zwraca null
Double val = (Double) o; // obowiązkowa konwersja,
// by na uzyskanym obiekcie
stosować metody klasy Double
double dv = val.doubleValue(); // uzyskujemy wartość typu double,
// zapisaną
w obiekcie typu Double


Pomoc 2 (Vector)
Dodawanie do koszyka i torby można oprogramować wykorzystując klasę Vector.

Obiekt klasy Vector to dynamiczna (zmieniająca swoje rozmiary w trakcie wykonania)
tablica dowolnych obiektów (typ Object).
Metody:


add(Object o) - dodaje referencję do obiektu o jako nowy element wektora,


Object get(int i) - zwraca referencję do obiektu, która zapisana jest w i-ym
lemencie wektora

remove(Object o) - usuwa referencję do obiektu o z wektora

int size() - zwraca aktualną liczbę elementów wektora


Pomoc 3 (dla realizacji zadania 2 -- singleton)
Singleton - to klasa, którą można wykorzystać do stworzenia (w danym programie)
tylko jednego obiektu.
Typowa realizacja:

prywatny konstruktor,metoda statyczna get...(), która zwraca JEDYNY utworzony obiekt tej
klasy (jeśli jeszcze nie został utworzony, to go tworzy)


Pomoc 4 (przypomnienie - dla realizacji zadania 3)

Wykorzystać symulację wywołania funkcji (metody) ze zmienną liczbą argumentów.


Wywołanie jest pokazane - przypomnieć sobie co oznacza ono dla definicji funkcji
(metody).





Wyszukiwarka

Podobne podstrony:
MB w2
zj w2
w2 2
SD przykłady do w2
DROGI w2 w3 tyczenie
w2
W2?
metody numeryczne i w2
W2
W2 Opadanie czastek cial stalych w plynach
NB NST 10 W2 KORA MOZGOWA,?ekty uszkodzen
DROGI w2 w2 tyczenie
admin w2
w2
W2
w2
nw asd w2

więcej podobnych podstron