1. Obiektowość Javy I
(repetytorium i synteza)
Wykład poświęcony jest powtórzeniu podstawowych pojęć, związanych
z programowaniem obiektowym w Javie. Przedstawione koncepcje będą ilustrowane
na przykładzie budowy konkretnej klasy, opisującej pojazdy.
1.1. Podstawowe definicje
Java jest językiem obiektowym. Języki obiektowe posługują się pojęciem obiektu i klasy.
Obiekt – to konkretny lub abstrakcyjny byt, wyróżnialny w modelowanej rzeczywistości,
posiadający określone granice i atrybuty (właściwości) oraz mogący świadczyć
określone usługi.
Usługa – to określone działanie (zachowanie) obiektu, które jest on zobowiązany przejawiać.
Obiekty współdziałają ze sobą wymieniając komunikaty.
Komunikat - to wąski i dobrze określony interfejs, opisujący współzależność działania obiektów.
Komunikaty zwykle żądają od obiektów wykonania określonych (właściwych im) usług.
Klasa - to opis takich cech grupy podobnych obiektów, które są dla nich
niezmienne (np. zestaw atrybutów i metod czyli usług, które mogą świadczyć)
Definicje powyższe stanowią abstrakcyjne odzwierciedlenie cech rzeczywistości.
Na podstawie:
Kazimierz Subieta. Słownik pojęć obiektowych , FAQ na liście pl.comp.object
, P. Coad, E.Yourdon. Analiza obiektowa, Oficyna Wydawnicza READ ME, W-wa
1994 P. Coad, E.Yourdon. Projektowanie obiektowe, Oficyna Wydawnicza - READ
ME, W-wa 1994
1.2. Abstrakcja obiektowa. Klasy i obiekty
Gdybyśmy mieli w języku programowania podobne pojęcia, to moglibyśmy ujmować
projekt rozwiązania rzeczywistego problemu i jego oprogramowanie w języku
adekwatnym do problemu. I to zapewniają języki obiektowe. Jest to ich bardzo
ważna cecha – abstrakcja obiektowa, znacznie ułatwiająca tworzenie oprogramowania.
Programowanie polega na przetwarzaniu danych. Dane zawsze są określonych
typów, a typ to nic innego jak rodzaj danych i działania które na nich można
wykonać.
Z pragmatycznego punktu widzenie możemy więc powiedzieć, że klasa to typ,
jej definicja opisuje właściwości typu danych (również funkcjonalne tzn.
jakie są dostępne operacje na danych tego typu).
Języki obiektowe pozwalają na definiowanie własnych klas – własnych typów
danych, co właśnie oznacza programowanie w języku problemu.
O obiektach możemy myśleć jako o egzemplarzach określonych klas.
Możemy mieć np. klasę pojazdów o następujących atrybutach: szerokość, wysokość,
długość, ciężar, właściciel, stan (stoi, jedzie, zepsuty itp.) oraz udostępniających
usługi: ruszania, zatrzymywania, zmiany właściciela (sprzedaż pojazdu) itp
.
Gdy mamy np. dwa obiekty – egzemplarze klasy pojazdów, oznaczane przez zmienne
a i b, to możemy symulować w programie sekwencję działań: uruchomienie pojazdu
a, uruchomienie pojazdu b, zatrzymanie obu pojazdów - za pomocą komunikatów
posyłanych do obiektów, np.:
a.start(); // komunikat do pojazdu a: ruszaj!
b.start(); // komunikat do pojazdu b: ruszaj!
a.stop(); // komunikat do pojazdu a: zatrzymaj się!
b.stop(); // komunikat do pojazdu b: zatrzymaj się!
Mówi się również: a.start() - to użycie metody start() na rzecz obiektu oznaczanego przez zmienną a.
1.3. Hermetyzacja
Oprócz odzwierciedlenia w programie "języka problemu" abstrakcja obiektowa
ma jeszcze jedną ważną przewagę nad ujęciami nieobiektowymi.
Mianowicie, atrybuty obiektu nie powinny być bezpośrednio dostępne. W programie
z obiektami "rozmawiamy" za pomocą komunikatów, obiekty same "wiedzą najlepiej"
jak zmieniać swoje stany. Dzięki temu nie możemy nic nieopatrznie popsuć,
co więcej nie możemy zażądać od obiektu usługi, której on nie udostępnia.
Dane (atrybuty) są ukryte i są traktowane jako nierozdzielna całość z usługami.
Nazywa się to hermetyzacją i oznacza znaczne zwiększenie odporności programu na błędy.
Sama koncepcja klasy jako zestawu pól i metod już zapewnia określony poziom hermetyzacji.
Nie możemy np. do obiektów klasy Vehicle posłać komunikatu sing(), bowiem metoda sing() nie występuje jako składowa tej klasie.
Dodatkowo języki obiektowe (w tym Java) pozwalają ukrywać dane (i metody) przed powszechnym dostępem.
Dostęp do składowych klasy regulują tzw. specyfikatory dostępu, których używamy w deklaracjach zmiennych, stałych i metod.
Każda składowa klasy może być:
- prywatna – dostępna tylko w danej klasie (specyfikator private),
- zaprzyjaźniona – dostępna ze wszystkich klas danego pakietu; mówi się tu też o dostępie pakietowym
lub domyślnym - domyślnym dlatego, iż ten rodzaj dostępności występuje wtedy,
gdy w deklaracji składowej nie użyjemy żadnego specyfikatora,
- chroniona lub zabezpieczona – dostępna z danej klasy i wszystkich klas ją dziedziczących (specyfikator protected),
- publiczna – dostępna zewsząd (specyfikator public).
Po co jest prywatność?
1. Ochrona przed zepsuciem (pola powinny być prywtane)
- użytkownik klasy nie ma dostępu do prywatnych pól i nic nie popsuje (nieświadomie).
2. Zapewnienie właściwego interfejsu (metody "robocze" winny być prywatne)
- użytkownik klasy ma do dyspozycji tylko niezbędne (klarowne) metody, co ułatwia mu korzystanie z klasy.
3. Ochrona przed konsekwencjami zmiany implementacji
- twórca klasy może zmienić zestaw i implementację prywatnych metod,
nie zmieniając interfejsu publicznego: wszystkie programy napisane przy wykorzystaniu
tego interfejsu nie będą wymagały żadnych zmian.
Mamy też w Javie pojęcie klas publicznych i pakietowych (klasy w Javie są
albo publiczne albo pakietowe).Klasa pakietowa jest dostępna tylko z klas
pakietu. Klasa publiczna jest dostępna zewsząd (z innych pakietów).Klasę
publiczną deklarujemy ze specyfikatorem public
Stosując regułę ukrywania danych i specyfikatory dostępu możemy teraz przedstawić
przykładową definicję klasy Person, a następnie zmodyfikować definicję klasy
Vehicle.
Atrybuty obiektów klasy Person przedstawiono jako pola prywatne.
Spoza klasy nie ma do nich dostępu.
Przy tworzeniu obiektu jego elementy odpowiadające tym polom są inicjalizowane
za pomocą wywołania konstruktora. Później zmiany tych elementów danych nie
są już możliwe, możemy tylko uzyskać dane za pomocą publicznych metod getName()
i getPesel().
Przypomnienie
|
- programowanie w Javie polega na posługiwaniu się obiektami
- przed użyciem obiekty muszą być tworzone
- do tworzenia obiektów służy wyrażenie new
- po słowie new podajemy odwołanie do konstruktora z odpowiednimi argumentami
- wyrażenie new zwraca referencję (odniesienie, swoisty adres) do nowoutworzonego obiektu
- posługiwanie się obiektami w Javie polega wyłącznie na operowaniu na referencjach
|
Terminologia
"Adres" obiektu nazywa się odniesieniem. Zmienna, która zawiera odniesienie nazywa się odnośnikiem.
Często nie rozgranicza się tych dwóch pojęć i mówi się o odniesieniu i odnośniku
jako o referencji. Jest to czasem wygodniejsze, szczególnie, gdy różnica
treści zawsze jasna jest z kontekstu.
Dla wygody używać będziemy także skrótu myślowego mówiąc o obiektach, gdy
naprawdę chodzi o referencje np. zamiast "metoda zwraca referencję do obiektu
klasy Color" – "metoda zwraca obiekt" lub nawet "zwraca kolor". Pamiętajmy
jednak zawsze, że w Javie operuje się wyłącznie na referencjach.
|
Modyfikując i rozbudowując klasę Vehicle w myśl reguł hermetyzacji:
- uczynimy atrybuty pojazdów prywatnymi.
- dostarczymy – jako publicznego – tylko niezbędnego interfejsu.
- ukryjemy "roboczą" metodę zmieniającą stan obiektu.
- wprowadzimy publiczne stałe oznaczające stany pojazdu oraz odpowiadające
im nazwy stanów (które uczynimy prywatnymi, by ew. zmiany nazw nie wpływały
na oprogramowanie wykorzystujące klasę Vehicle) [ UWAGA: stałe specyfikujemy za pomocą final ].
public class Vehicle {
// stany
public final int BROKEN = 0, STOPPED = 1, MOVING = 2;
private final String[] states = { "ZEPSUTY", "STOI", "JEDZIE" };
private int width, height, length, weight;
private Person owner;
private int state;
public Vehicle(Person p, int w, int h, int l, int ww) { // konstruktor
owner = p; width = w; height = h;
length = l; weight = ww; state = STOPPED;
}
public void start() { setState(MOVING); }
public void stop() { setState(STOPPED); }
// Prywatna metoda robocza wykorzystywana w metodach start() i stop().
// Arbitralne ustalenie stanu spoza klasy nie jest możliwe
private void setState(int newState) {
if (state == newState || state == BROKEN)
System.out.println("Nie jest mozliwe przejscie ze stanu " +
states[state] + " do stanu " + states[newState]);
else state = newState;
}
public void repair() {
if (state == MOVING)
System.out.println("Nie można reperować jadącego pojazdu");
else if (state != BROKEN)
System.out.println("Pojazd sprawny");
else state = STOPPED;
}
// Sprzedaż pojazdu
public void sellTo(Person p) {
owner = p;
}
public String toString() {
return "Pojazd, właścicielem którego jest "
+ owner.getName() + " - " + states[state];
}
}
Zwróćmy uwagę:
- atrybuty pojazdu (wysokość, waga etc) nadawane są przy tworzeniu obiektu i żadna inna klasa nie może ich później zmienić.
- stan pojazdu jest zmieniany tylko na skutek wywołania publicznych metod
start(), stop(), repair() przy czym "sam obiekt" dba, by nie dopuścić do
niewłaściwych zmian (zob. definicje tych metod).
- metoda toString ma ważną właściwość:
jest wywoływana, gdy referencja do obiektu klasy, w której ją zdefiniowano,
pojawi się w wyrażeniu konkatenacji; wynik zastępuje w tym wyrażeniu wspomnianą
referencję. A zatem jeśli v jest obiektem klasy Vehicle, i użylismy wobec
niego metody start(), to gdy napiszemy System.out.println(" => " + v);
to na konsolę zostaie wyprowadzony napis "=> Pojazd, właścicielem którego
jest ... - JEDZIE".
1.4. Statyczne składowe klasy
Stałe (oznaczające stany i ich nazwy) są teraz zawarte w każdym tworzonym
obiekcie klasy Vehicle. Niewątpliwie jest to marnotrawstwo: powinniśmy móc
zdefiniować je jako właściwość raczej całej klasy obiektów, a nie każdego
obiektu z osobna (zawsze są przecież takie same). Na pomoc przychodzi koncepcja
składowych statycznych.
Składowe klasy mogą być
statyczne i niestatyczne.
Niestatyczne zawsze wiążą się z istnieniem jakiegoś obiektu (pola
- odpowiadają elementom obiektu, metody muszą być wywoływane na rzecz obiektu,
są komunikatami do obiektu)
Składowe statyczne (pola i metody):
- są deklarowane przy użyciu specyfikatora static
- mogą być używane nawet wtedy, gdy nie istnieje żaden obiekt klasy
Uwaga: 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.
Spoza klasy do jej statycznych składowych możemy odwoływać się na dwa sposoby:
- NazwaKlasy.NazwaSkładowej
- gdy istnieje jakiś obiekt (dodatkowo): tak samo jak do niestatycznych składowych
Uczynimy zatem stałe zdefiniowane w klasie Vehicle statycznymi, dostarczymy
też metody pozwalającej pokazać możliwe nazwy stanów pojazdów, nawet jeśli
nie ma jeszcze żadnych obiektów klasy Vehicle. Skorzystamy też z koncepcji
składowych statycznych po to, by każdemu pojazdowi tworzonemu w naszym programie
nadawać unikalny numer (powiedzmy od jednego), a także zawsze mieć rozeznanie
ile obiektów typu Vehicle dotąd utworzyliśmy (co – uwaga – nie znaczy, że
wszystkie jeszcze istnieją!).
public class Vehicle {
public final static int BROKEN = 0, STOPPED = 1, MOVING = 2;
private final static String[] states = { "ZEPSUTY", "STOI", "JEDZIE" };
private static int count; // ile obiektów dotąd utworzyliśmy
private int currNr; // bieżący numer pojazdu
//...
public Vehicle(Person p, int w, int h, int l, int ww) {
// ....
// Każde utworzenie nowego obiektu zwiększa licznik o 1
// bieżąca wartość licznika nadawana jest jako numer pojazdu
// numer pojazdu jest niestatycznym polem klasy, a więc elementem obiektu
currNr = ++count;
}
//.....
// zwraca unikalny numer pojazdu
public int getNr() { return currNr; }
// zwraca liczbę dotąd utworzonych obiektów
// metoda jest statyczna, by móc zwrócić 0
// gdy nie ma jeszcze żadnego obiektu
static int getCount() { return count; }
// zwraca tablicę nazw stanów
// Metoda jest statyczna, bo dopuszczalne stany
// nie zależą od istnienia obiektów
static String[] getAvailableStates() {
return states;
}
// ...
}
Wykorzystanie:
// jakie są dopuszczalne nazwy stanów
// pojazdów?
String[] stany = Vehicle.getAvailableStates();
// ile obiektów dotąd utworzyliśmy?
int n =Vehicle.getCount()
1.5. Inicjacje
Przy tworzeniu obiektu:
- pola klasy mają zagwarantowaną inicjację na wartości ZERO (0, false
– dla typu boolean, null – dla referencji, co oznacza referencję "pustą",
nie odnosząca się do żadnego obiektu),
- zwykle w konstruktorze dokonuje się reinicjacji pól,
- ale można również posłużyć się jawną inicjacją przy deklaracji pól.
Obiekt klasy Vehicle składa się z następujących elementów (odpowiadają one polom klasy):
private int currNr;
private int width, height, length, weight;
private Person owner;
private int state;
W trakcie tworzenia obiektu ( new Vehicle(...) ) uzyskają one wartości 0
(dla elementów typu int) oraz null dla elementu, odpowiadającego referencji
owner.
Następnie zostanie wywołany konstruktor, w którym dokonujemy właściwej inicjacji.
Można by było napisac np.
private int state = STOPPED; // jawna inicjacja pola
i usunąć z konstruktora instrukcję state = STOPPED.
A co z polami statycznymi? I jaka jest kolejność inicjacji?
Reguły inicjacji
- Każde pierwsze odwołanie do klasy inicjuje najpierw pola statyczne.
Pierwszym odwołaniem do klasy może być odwołanie do składowej statycznej
(np. Vehicle.getCount()) lub utworzenie obiektu ( np. Vehicle v – new Vehicle());
- Tworzenie obiektu (new) inicjuje pola niestatyczne, po czym wykonywany jest konstruktor,
- Kolejność inicjacji pól - wg ich kolejności w definicji klasy (w podziale na statyczne i niestatyczne, najpierw statyczne)
|
Gdybyśmy np. zmienili interpretację statycznego pola count (niech będzie
to teraz pierwszy dopuszczalny numer identyfikacyjny) i jawnie je zainicjowali
wielkością 100, to w kontekście:
class Vehicle {
private int currNr = ++count;
private static count = 100;
...
public int getNr() { return currNr; }
public static int getCount() { return count; }
}
po:
Vehicle.getCount()
Vehicle v = new Vehicle(...);
wyrażenie:
v.getNr()
miałoby wartość 101.
Z inicjacją wiąże się również pojęcie bloków inicjacyjnych.
Jak pamiętamy - w Javie w zasadzie nie można używać instrukcji wykonywalnych
(m.in. sterujących) poza ciałami metod. Od tej zasady istnieją jednak dwa wyjątki:
- użycie niestatycznego bloku inicjacyjnego,
- oraz użycie statycznego bloku inicjacyjnego.
Niestatyczny blok inicjacyjny wprowadzamy ujmując kod wykonywalny w nawiasy
klamrowe i umieszczając taką konstrukcję w definicji klasy poza cialem jakiejkolwiek
metody. Kod bloku zostanie wykonany na etapie inicjacji obiektu, czyli przy
tworzeniu obiektu, przed wywołaniem konstruktora.
Taka możliwość może okazać się przydatna, gdy mamy kilka konstruktorów (o
wielu różnych konstruktorach definiowanych w jednej klasie - zob. następny
punkt) i chcemy wyróżnić pewien kod, który będzie inicjował obiekt niezależnie
od użytego konstruktora i przed użyciem jakiegikolwiek z nich.
Np. możemy w bloku inicjacyjnym wyodrębnić nieco bardziej zaawansowaną postać
inicjacji zmiennej state, opisująceje stan pojazdu.
public class Vehicle {
// ...
private int state;
// Niestatyczny blok inicjacyjny
// -- w niedzielę wszystkie samochody inicjalnie stoją
// -- w poniedziałek te o parzystuych numerach inicjalnie jadą,
// inne - stoją
// -- w pozostałe dni tygodnia: pierwszy stworzony inicjalnie stoi,
// inne jadą
{ // początek bloku
int dayOfWeek = Calendar.getInstance().get(Calendar.DAY_OF_WEEK);
switch (dayOfWeek) {
case Calendar.SUNDAY : state = STOPPED; break;
case Calendar.MONDAY : state = (currNr % 2 == 0 ? MOVING : STOPPED);
break;
default : state = (currNr == 1 ? STOPPED : MOVING);
break;
}
} // koniec bloku
// Konstruktory i metody klasy
public Vehicle(...) {
// ...
}
// ...
}
Uwaga: dla uzyskania aktualnego dnia tygodnia stosujemy klasę Calendar.
W pokazanym przypadku takie wyodrębnienie niestatycznego bloku inicjacyjnego ma pewien sens (kod konstruktorów jest
może bardziej czytelny), ale zazwyczaj nie jest to zbyt interesujące rozwiązanie,
i - jeśli już - należy go stosować z wyjątkowym umiarem (bowiem może ono
oznaczać trudniej czytelny kod - rozproszony po wielu miejscach, zamiast
skupiony w naturalnym miejscu inicjacji - konstruktorach).
Ciekawszym rozwiązaniem wydaje się statycznyblok inicjacyjny
. Czasem pojawia się potrzeba wykonania jakiegoś kodu jeden raz, przy pierwszym
odwołaniu do klasy. Przy inicjacji pól statycznych możemy skorzystać z dowolnych
wyrażeń, składających się ze zmiennych i stalych statycznych oraz z wywołań
statycznych metod, ale - oczywiście - nie sposób użyć instrukcji wykonywalnych
(np. sterujących).
Statycznym blok inicjacyjny wprowadzamy słowem kluczowym static z
następującym po nim kodem ujętym w nawiasy klamrowe. Kod ten będzie wykonywany
jeden raz przy pierwszym odwołaniu do klasy (np. użyciu metody statycznej
lub stworzeniu pierwszego obiektu). Oczywiście, z takiego bloku możemy odwoływać
się wyłącznie do zmiennych statycznych (obiekt jeszcze nie istnieje).
Przykład:
wyobraźmy sobie, że numeracja pojazdów zależy od domyślnej lokalizacji aplikacji
(inna jest jeśli lokalizacją jest USA - inną dla, powiedzmy, Włoch). Będziemy
więc inicjować zmienną statyczną count w zależności od lokalizacji (w tym
przypadku oczywiście count nie będzie oznaczać liczby utworzonych w programie
obiektów, ale inicjalny numer pojazdu). Taki problem można rozwiązac właśnie
za pomocą statycznego bloku inicjacyjnego.
public class Vehicle {
public final static int BROKEN = 0, STOPPED = 1, MOVING = 2;
private final static String[] states = { "ZEPSUTY", "STOI", "JEDZIE" };
private static int count;
// Statyczny blok inicjacyjny
// za jego pomocą inicjujemy zmienną count w taki sposób,
// by numery pojazdów zaczynały się w zależności
// od domyślnej lokalizacji aplikacji
// np. jeśli aplikacja jest wykonywana w lokalizacji włoskiej
// numery zaczynają się od 10000.
static {
// Locale.setDefault(Locale.ITALY);
Locale[] loc = { Locale.UK, Locale.US, Locale.JAPAN, Locale.ITALY, };
int[] begNr = { 1, 100, 1000, 10000, };
count = 300; // jeżeli aplikacja działa w innej od wymienionych
// w tablicy lokalizacji, zaczynamy numery od 300
Locale defLoc = Locale.getDefault(); // jaka jest domyślna lokalizacja?
for (int i=0; i < loc.length; i++)
if (defLoc.equals(loc[i])) {
count = begNr[i];
break;
}
} // koniec bloku
// pola niestatyczne
private int currNr = ++count;
private int width, height, length, weight;
// ...
// Konstruktory i metody
// ...
} // koniec klasy
1.6. 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 argumentów.
Nazywa się to przeciążaniem metod.
Tak samo możemy mieć kilka wersji konstruktorów:
Klasa wcale nie musi mieć jawnie zdefiniowanego konstruktora.
W tym przypadku do definicji klasy automatycznie dodawany jest konstruktor domyślny
, bezparametrowy (tj. nie mający żadnych parametrów). Konstruktor domyślny
"nic nie robi" (jego ciało jest puste), a wtedy niestatyczne pola klasy są
inicjowane według podanych w ich deklaracjach inicjatorów lub (gdy inicjatorów
brak) na gwarantowane wartości ZERO (0, false, null).
Jeśli zdefiniowaliśmy jakikolwiek konstruktor, to konstruktor bezparametrowy
NIE jest automatycznie dodawany i musimy go sami zdefiniować
Przypomnienie
|
Słowo kluczowe this w ciele konstruktorów i metod niestatycznych oznacza
referencję do danego obiektu (tzn. tego, który jest inicjowany przez konstruktor
lub tego na rzecz którego użyto metody).
Zwykle nie używamy this: odwołania do składowych klasy wewnątrz konstruktora lub metody są jednoznaczne – dotyczą tego obiektu (inicjowanego przez konstruktor lub na rzecz którego wywołano metodę). Np.
public void start() { setState(MOVING); }
=
public void start() { this.setState(Vehicle.MOVING); }
i
public int getState() { return state; }
=
public int getState() { return this.state; }
|
Czasem musimy użyć słowa this.
1. Przy przesłonięciu nazw zmiennych oznaczających pola.
Np.:
class Vehicle {
private int width, height, length, weight;
private Person owner = null;
private int state;
public Vehicle(Person owner, int width, int height, int length, int weight) {
this.owner = owner; this.width = width; this.height = height;
this.length = length; this.weight = weight;
}
2. Drugi - częsty - przypadek koniecznego użycia słowa this występuje
wtedy, gdy metoda musi zwrócić TEN obiekt (na rzecz którego została wywołana).
Możemy np. zdefiniować metodę repair() tak, by zwracała ten pojazd (który został nareperowany)
Vehicle repair() {
// ...
return this;
}
i pisać szybko:
Vehicle v .... ;
...
v.repair().start();
3. Trzeci przypadek użycia słowa this - to wywołanie z konstruktora innego konstruktora (o czym mowa była przed chwilą).
1.7. Pełny przykład klasy Vehicle i jej wykorzystania
public class Vehicle {
public final static int BROKEN = 0, STOPPED = 1, MOVING = 2; // stany
private final static String[] states = { "ZEPSUTY",
"STOI",
"JEDZIE" }; // nazwy stanów
private static int count; // liczba dotąd utworzonych obiektów,
// inicjalnie 0
private int currNr = ++count; // przy tworzeniu obiektu count zwiększa się o 1
// i staje się numerem pojazdu
private int width, height, length, weight; // atrybuty pojazdu
private Person owner; // ... właściciel
private int state = STOPPED; // ... stan
public Vehicle() { } // konstruktor bezparametrowy
// konstruktor; tworzy pojazdy bez właściciela
public Vehicle(int w, int h, int l, int ww) {
this(null, w, h, l, ww);
}
// pełny konstruktor
public Vehicle(Person p, int w, int h, int l, int ww) {
owner = p; width = w;
height = h; length = l;
weight = ww;
}
public void start() { // start pojazdu
setState(MOVING);
}
public void stop() { // zatrzymanie pojazdu
setState(STOPPED);
}
// Robocza metoda zmiany stanu, wykorzystywana w stop() i start()
// Uwzględnia możliwe zmiany stanów (sprawdza to)
// Jest prywatna: arbitralna zmiana stanu pojazdu
//nie jest możliwa spoza klasy
private void setState(int newState) {
if (state == newState || state == BROKEN)
System.out.println("Nie jest mozliwe przejscie ze stanu "
+ states[state] + " do stanu " + states[newState]);
else state = newState;
}
// Reperowanie pojazdu:
// zawsze zakończone sukcesem, jeśli tylko można przystąpić do naprawy :-)
public Vehicle repair() {
if (state == MOVING)
System.out.println("Nie można reperować jadącego pojazdu");
else if (state != BROKEN) System.out.println("Pojazd sprawny");
else state = STOPPED;
return this;
}
// zwraca stan jako liczbę;
// można ją porównać ze stałymi Vehicle.BROKEN, Vehicle.STOPPED itp.
public int getState() { return state; }
// Zwraca nazwę stanu podanego jako argument
public static String getState(int state) { return states[state];}
// czy pojazd stoi?
public boolean isStopped() { return state == STOPPED; }
// Kolizja: w wyniku kolizji oba pojazdy w niej uczestniczące
//stają się niesprawne
// Nie może być kolizji, jeśli oba pojazdy stoją
public void crash(Vehicle v) {
if (state != MOVING && v.state != MOVING)
System.out.println("Nie ma kolizji");
else {
setState(BROKEN);
v.setState(BROKEN);
}
}
public void sellTo(Person p) { // sprzedaż pojazdu osobie p
owner = p;
}
public int getNr() { return currNr; } // zwraca unikalny numer pojazdu
// Czy pojazd może przejechać pod konstrukcją o podanej wysokości?
public boolean isTooHighToGoUnder(int limit) {
return height > limit ? true : false;
}
// Metoda toString uwzględnia teraz,
// że pojazd może jeszcze nie mieć właściciela
public String toString() {
String s = (owner == null ? "sklep" : owner.getName());
return "Pojazd " + currNr + " ,właścicielem którego jest "
+ s + " - " + states[state];
}
// ile pojazdów utworzyliśmy dotąd?
static int getCount() { return count; }
// zwraca tablicę nazw dostępnych stanów pojazdu
static String[] getAvailableStates() {
return states;
}
}
class VehTest { // klasa testująca
// Metoda raportująca stan tablicy pojazdów
// przekazanych jako argument
static void report(String msg, Vehicle[] v) {
System.out.println(msg);
for (int i=0; i < v.length; i++)
System.out.println("" + v[i]);
}
public static void main(String[] args) {
System.out.println("Dopuszczalne stany pojazdow");
String[] states = Vehicle.getAvailableStates();
for (int i = 0; i < states.length; i++) System.out.println(states[i]);
System.out.println("W programie mamy teraz pojazdow: " + Vehicle.getCount());
Vehicle[] v = { new Vehicle(new Person("Jan Piesio", "010268246"),
200, 150, 500, 900),
new Vehicle(new Person("Stefan Kot", "010262241"),
210, 250, 800, 1900),
new Vehicle(200, 230, 300, 600),
};
System.out.println("W programie mamy teraz pojazdow: " + Vehicle.getCount());
report("Na początku", v);
System.out.println("Pojazd 3 zostaje sprzedany Ambrożemu");
v[2].sellTo(new Person("Ambroży", "000000"));
for (int i=0; i < v.length; i++) v[i].start();
report("Po wyruszeniu", v);
// Czy mogą przejechać pod wiaduktem 220 cm?
for (int i=0; i < v.length; i++)
if (v[i].isTooHighToGoUnder(220)) v[i].stop();
report("Komu udało się przejechać pod wiaduktem ?", v);
// Zatrzymane wyruszaja w objazd
for (int i=0; i < v.length; i++)
if (v[i].isStopped()) v[i].start();
report("Po wyruszeniu w objazd", v);
// Kolizja
v[0].crash(v[1]);
report("Po kolizji", v);
System.out.println("Piesio próbuje ruszyć");
v[0].start();
System.out.println("Piesio sprzedaje pojazd Krówce. Ta go reperuje i jedzie");
v[0].sellTo(new Person("Anna Krówka", "121212908"));
v[0].repair().start();
report("Ostatecznie",v);
System.out.println("Stan pojazdu 3 " + Vehicle.getState(v[2].getState()));
}
}
Po uruchomieniu programu otrzymamy wydruk:
Dopuszczalne stany pojazdow
ZEPSUTY
STOI
JEDZIE
W programie mamy teraz pojazdow: 0
W programie mamy teraz pojazdow: 3
Na początku
Pojazd 1 ,właścicielem którego jest Jan Piesio - STOI
Pojazd 2 ,właścicielem którego jest Stefan Kot - STOI
Pojazd 3 ,właścicielem którego jest sklep - STOI
Pojazd 3 zostaje sprzedany Ambrożemu
Po wyruszeniu
Pojazd 1 ,właścicielem którego jest Jan Piesio - JEDZIE
Pojazd 2 ,właścicielem którego jest Stefan Kot - JEDZIE
Pojazd 3 ,właścicielem którego jest Ambroży - JEDZIE
Komu udało się przejechać pod wiaduktem ?
Pojazd 1 ,właścicielem którego jest Jan Piesio - JEDZIE
Pojazd 2 ,właścicielem którego jest Stefan Kot - STOI
Pojazd 3 ,właścicielem którego jest Ambroży - STOI
Po wyruszeniu w objazd
Pojazd 1 ,właścicielem którego jest Jan Piesio - JEDZIE
Pojazd 2 ,właścicielem którego jest Stefan Kot - JEDZIE
Pojazd 3 ,właścicielem którego jest Ambroży - JEDZIE
Po kolizji
Pojazd 1 ,właścicielem którego jest Jan Piesio - ZEPSUTY
Pojazd 2 ,właścicielem którego jest Stefan Kot - ZEPSUTY
Pojazd 3 ,właścicielem którego jest Ambroży - JEDZIE
Piesio próbuje ruszyć
Nie jest mozliwe przejscie ze stanu ZEPSUTY do stanu JEDZIE
Piesio sprzedaje pojazd Krówce. Ta go reperuje i jedzie
Ostatecznie
Pojazd 1 ,właścicielem którego jest Anna Krówka - JEDZIE
Pojazd 2 ,właścicielem którego jest Stefan Kot - ZEPSUTY
Pojazd 3 ,właścicielem którego jest Ambroży - JEDZIE
Stan pojazdu 3 JEDZIE
1.8. Podsumowanie
W wykładzie przypomniano pojęcia:
- obiektu
- klasy
- metody
- pola
- składowych statycznych
- incicjacji obiektów
- przeciążania metod i konstruktorów
pokazując ich zastosowanie przy budowie i wykorzystaniu klasy, opisującej pojazdy.
1.9. Zadania i ćwiczenia
Zadanie 1. Woda
Gromadzimy wodę mineralną. Są trzy rodzaje butelek: o pojemności 2 l (duże)
, średnie - o pojemności 1 l oraz małe o pojemności 0.5 l.
Stworzyć klasę MyWater z metodami:
void addLarge(int) - dodaje do zapasu wody podaną jako argument liczbę dużych butelek,
void addMedium(int) - dodaje do zapasu wody podaną jako argument liczbę średnich butelek.
void addSmall(int) - dodaje do zapasu wody podaną jako argument liczbę małych butelek
oraz z metodami umożliwiającymi uzyskanie informacji o tym ile jest każdego
rodzaju butelek oraz jaka jest łączną pojemność zgromadzonej wody.
Pojemności butelek (dużych, średnich, małych) przedstawić jako pola statyczne klasy.
Dostarczyć metod, pozwalających uzyskiwac informacje o tych pojemnościcach oraz je zmieniać.
Klasę przetestować dodając do zapasu różne liczby butelek wody i wyprowadzając potem komunikat w rodzaju:
Mam teraz 6.5 litrów wody
dużych butelek: 2
średnich butelek: 1
małych butelek: 3
Zadanie 2 - pudełka
Zdefiniować klasę Box, której obiekty będą stanowić pudełka o zadanych
rozmiarach (szerokość, wysokość).
Dostarczyć w tej klasie specjalnych metod lączenia pudełek:
-
diagonalnie (zwraca pudełko o rozmiarach równych sumie szerokości i wysokości
łączonych pudełek) - nazwa metody joinDiagonal,
-
poziomo (zwraca pudełko o rozmiarach równych sumie szerokości łączonych
pudełek i wysokości równej maksimum z wysokości pudełek,
-
pionowo (zwraca pudelko o rozmiarach równych sumie wysokości łączonych
pudełek i szerokości równej maksimum z szerokości pudełek).
Przetestować klasę w następujący sposób:
-
użytkownik programu w dialogu wejściowym wprowadza dowolną liczbę szerokości
pudełek
-
wysokość tych pudelek równa jest 2*szerokość
-
wyprowadzić na konsolę wielkość (szerokość, wysokość) każdego z pudełek
-
połączyć otrzymane pudełka w kombinacje diagonalne, wertyklane i horyzontalne
i wyprowadzić na konsole ich rozmiary
Zadanie 3 (wektory)
Stworzyć klasę Wektor, której obiekty będą stanowić wektory (tablice) liczb
rzeczywistych i dostarczyć w niej następujących konstruktorów i metod:
- konstruktor tworzący wektor zerowy o rozmiarze n (n - argument konstruktora)
- konstruktor tworzący wektor z podanej jako argument tablicy liczb rzeczywistych,
- konstruktor tworzący wektor z podanego jako argument napisu, ktorego kolejne słowa będą stanowić elementy wektora,
- metoda get, która zwraca i-ty lement wektora,
- metoda set, która ustala i-ty element wektora na podaną wartość,
- metoda max, która zwraca maksymalny element wektora,
- me
toda min, która zwraca minimalny element wektora,
- metoda size, która zwraca liczbę elementów wektora.
- metoda add, która zwraca wektor, będący sumą poelementową dwóch wektorów.
- metoda toString, która zwraca łańcuch znakowy przedstawiający elementy wektora,
- statyczna metoda show(double[]), która wyprowadza na konsoli w jednym wierszu elementy przekazanej jako argument tablicy,
- metoda sort, która zwraca posortowany wektor w p
orządku rosnącym lub malejącym (to zależy od argumentu).
Etapy wykonania (wzrastająca skala trudności):
A. należy wykonać punkty 1, 2, 4-8 i uzyskać prawidlowy wynik części poniższego programu zaznaczonej na czerwono.
B. należy dodatkowo wykonać punkty 9-11, a także dodatkowo zapewnić prawidłowe wykonanie części programu zaznaczonej na brązowo.
C. należy dodatkowo wykonać punkty 3 i 12, przy czym w punkcie 3 należy uwzględnić, że :
- jeżeli argumentem wywołania konstruktora jest null lub pusty łańcuch
znakowy lub napis zawierający same spacje, to ma być zgłoszony wyjątek IllegalArgumentException
(jest to gotowa i dostępna klasa wyjątku - można jej spokojnie użyć),
- jeżeli w podanym jako argument łańcuchu znakowym występuje napis JEDEN
(jakkolwiek pisany, gdy idzie o wielkość liter), to odpowiednią warością
elementu wektora ma być 1,
- jeżeli w łańcuchu znakowym występują słowa "a", "b", ... "f", to mają
być traktowane jako liczby 10, 11, .. 15, i takie wartości należy przypisać odpowiednim elementom wektora,
- w każdym innym przypadku jeżeli słowo łańcucha znakowego nie daje się zinterpretować jako liczba
rzeczywista, to odpowiedni element wektora będzie miał wartość 0.
W punkcie 12 należy przyjąć, że argumentem metody sort jast napis oznaczający
porządek sortowania: ASC - rosnąco, DESC - malejąca. Litery w napisie mogą
być małe lub duże.
Program, który ma działać i wytwarzać pokazane wyniki:
public class Test {
// tu zapewne znajdzie się metoda increase
public static void main(String[] args) {
double[] d = { 1, 2, -7, 3, 4 };
Wektor w1 = new Wektor(d);
Wektor w2 = new Wektor(5);
w2.set(1, 12);
w2.set(3, -1);
System.out.println(w1.min() + " " + w1.max() + " " +
w2.min() + " " + w2.max());
/*---------------------------------------------------*/
Wektor w3 = w1.add(w2);
Wektor.show(d);
System.out.println(w1.toString());
System.out.println(w2.toString());
System.out.println(w3.toString());
// Wywolana poniżej metoda increase ma zwiększać wartość każdego elementu
// podanego wektora (tu w1)
// o podaną wielkośc (tu 1)
// dopóki suma elementów nie przekroczy podanej wartości (tu 5)
increase(w1, 1, 5);
System.out.println("Teraz w1 = " + w1);
/*------------------------------------------------------*/
// w4 i w5 oznaczają obiekty klasy Wektor
// ...
try {
w4 = new Wektor("2 7 a jeden z");
w5 = new Wektor(" ");
} catch (IllegalArgumentException exc) {
System.out.println(exc.toString());
}
w5 = w4.sort("Asc"); // sortowanie w porządku rosnącym
System.out.println(w4.toString());
System.out.println(w5.toString());
w5 = w4.sort("DESC"); // sortowanie w porządku malejącym
System.out.println(w5.toString());
}
}
Wyniki:
-7.0 4.0 -1.0 12.0
[ 1.0 2.0 -7.0 3.0 4.0 ]
[ 1.0 2.0 -7.0 3.0 4.0 ]
[ 0.0 12.0 0.0 -1.0 0.0 ]
[ 1.0 14.0 -7.0 2.0 4.0 ]
Teraz w1 = [ 2.0 3.0 -6.0 3.0 4.0 ]
java.lang.IllegalArgumentException: Wadliwy argument konstruktora Wektor(String)
[ 2.0 7.0 10.0 1.0 0.0 ]
[ 0.0 1.0 2.0 7.0 10.0 ]
[ 10.0 7.0 2.0 1.0 0.0 ]
Zadanie 4 (liczba wystąpień słów)
Stworzyć klasę SortedWordsFromFile, której obiekty będą posortowanymi
tablicami wszystkich słów z pliku tekstowego, którego nazwę
podajemy przy tworzeniu obiektu jako argument konstruktora.
Dostarczyć w tej klasie metody getWords(), która zwraca posortowaną tablicę wszystkich słów oraz metody getUniqueWordsCount(), która zwraca tablicę obiektów klasy WordsCount, każdy z których reprezentuje parę: unikalne słowo - liczbe jego wystąpień w pliku.
Łańcuchowa reprezentacja takich obiektów, uzyskiwana metodą toString winna mieć formę: słowo liczba_wystąpień.
W klasie WordsCount dostarczyć statycznej metody sortowania tablic elementów
tego typu według liczby wystąpień słowa - sortByCount(WordsCount[]).
Dodatkowe warunki, które ma spelniać program:
- tablica unikalnych słów musi mieć rozmiary dokładnie takie jak liczba unikalnych słów,
- nie wolno dokonywać żadnych realokacji tablic,
- nie wolno używać żadnych klas kolekcyjnych,
- algorytmy sortowania należy napisac samemu, ale nie muszą być stabilne,
ani szybkie, wystarczą najprostsze - typu "selection sort"
- metoda getUniqueWordsCount zwraca tablicę elementów posortowaną w alfabetycznym porządku słów,
- słowa mają być identyfikowane możliwie najpelniej, tak by dość sensowne
było przetwarzanie tekstów (uwzględnić odpowiedni zestaw separatorów),
- zliczając wystąpienia, słów słowa różniące się tylko wielkością liter traktujemy jako takie same,
- tekst programu nie może przekraczać 110 wierszy i ma zawierać poniższą
metodę main, która daje prawidłowy wynik dla przytoczonego poniżej
przypadku testowego.
public static void main(String[] args) {
SortedWordsFromFile swf = new SortedWordsFromFile(args[0]);
WordsCount[] wc = swf.getUniqueWordsCount();
System.out.println("Wszystkich słów jest: " + swf.getWordsCount());
System.out.println("Unikalnych: " + wc.length);
for (int i=0; i<wc.length; i++) System.out.println(wc[i].toString());
System.out.println("Posortowane wg liczby wystąpień od największej:");
WordsCount.sortByCount(wc);
for (int i=0; i<wc.length; i++) System.out.println(wc[i].toString());
}
Zawartość pliku:
ala ma kota
kot ala pies pies ala
i "co tam, co tam" Ala! Kot? Pies;i kot
Wszystkich słów jest: 18
Unikalnych: 8
ala 4
co 2
i 2
Kot 3
kota 1
ma 1
pies 3
tam 2
Posortowane wg liczby wystąpień od największej:
ala 4
Kot 3
pies 3
tam 2
i 2
co 2
ma 1
kota 1