Polimorfizm, interfejsy i klasy wewnętrzne
Materiał przedstawia przede wszystkim istotne w
programowaniu obiektowym pojęcia polimorfizmu, klas abstrakcyjnych,
interfejsów oraz klas wewnętrznych. Dyskusja ilustrowana będzie
praktycznymi przykładami użyteczności tych koncepcji.
1. Metody wirtualne i polimorfizm
W klasie Car z poprzedniego wykładu 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[] allveh = getAllVehiclesToStart();
for (Vehicle v : allveh) v.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 jest formalnego typu Vehicle – powinny być wywołane
metody start() z każdej z odpowiednich podklas klasy Vehicle (właściwa metoda start() dla danego rodzaju pojazdu).
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ą metody 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, natomiast 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".
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 z innych metodach danej klasy nie
są polimorficzne, bo metody prywatne nie mogą być przedefiniowane).
2. 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 omawianych tu treści).
class Zwierz {
String name = "bez imienia";
Zwierz() { }
Zwierz(String s) { name = s; }
String getTyp() { return "Jakis zwierz"; }
String getName() { return name; }
String getVoice() { return "?"; }
// Metoda speak symuluje wydanie głosu poprzez wypisanie odpowiedniego komunikatu
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".
public 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 bez imienia mówi ?
Jakis zwierz bez imienia mówi ?
----------------------------------------
Pies Kuba mówi HAU, HAU!
Pies Reksio mówi HAU, HAU!
----------------------------------------
Pies Kuba mówi HAU, HAU!
Kot bez imienia mówi Miauuuu...
----------------------------------------
Pies Reksio mówi HAU, HAU!
Pies bez imienia 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.
Nie
jest to kwestia jakichś wymyślonych przykładów. Z praktyczną
użytecznością polimorfizmu stykamy się w Javie od samego
początku. Jak już wielokrotnie wspominano, metoda println z
argumentem Object wyprowadza tekst opisujący przekazany obiekt. Tekst
ten dostaje poprzez odwołanie do metody toString() za pośrednictwem
statycznej metody valueOf(Object) z klasy String:
W klasie String:
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
W klasie PrintStream (lub PrintWriter):
public void println(Object obj) {
String txt = String.valueOf(x);
// wypisanie tekstu txt
}
Metoda
toString() jest po raz pierwszy zdefiniowana w klasie Object - jej
wynikiem jest tam napis w postaci:
nazwa_klasy@unikalny_identyfikator_obiektu. Klasę Object dziedziczą
wszystkie klasy (pośrednio lub bezpośrednio). W klasach tych można więc
zawsze przedefiniować metodę
toString(). A przedefiniowane metody wołane są polimorficznie - zawsze
więc uzyskamy właściwy opis obiektu (określony w danej klasie), lub -
jeśli nie zdefiniowano w niej metody toString - opis z pierwszej
nadklasy, w której jest ona zdefiniowana.
Na przykład, mając taką hierarchię dziedziczenia jak na rysunku:
i używając programu testowego:
package tostring;
public class TestToString {
public static void main(String[] args) {
Object[] elts = { new Para(3, 5),
new Vehicle(
new Person("Stefan", "65021123456"),
100, 100, 100, 100
),
new Car("AA7897",
new Person("Janek", "78011222457"),
100, 100, 100, 500, 50
),
new MojaKlasa(), new Bicycle() };
// Iterujemy po tablicy obiektów
// dla każdego zostanie wywołana metoda toString() z jego klasy
// albo z pierwszej jego nadklasy, która ją definiuje
// (w ostateczności z klasy Object)
for (Object elt : elts) {
System.out.println(elt);
}
}
}
uzyskamy następujący wynik:
( 3, 5 )
Pojazd 1, właścicielem którego jest Stefan - STOI
Samochód nr rej AA7897 - STOI
tostring.MojaKlasa@1fb8ee3
Pojazd 3, właścicielem którego jest sklep - STOI
Pamietamy,
że przedefiniowanie metody wymaga, aby miała ona identyczną
sygnaturę i identyczny (lub kowariantny) typ wyniku jak metoda z
nadklasy, a także by nie ograniczała jej widzialności (dostepu).
Problemy ze specyfikatorami dostępu wykryje kompilator. Np. w kontekście :
class A {
String toString() {
return "Obiekt klasy A";
}
}
uzyskamy komunikat o błędzie:
Cannot reduce the visibility of the inherited method from Object
bo
metoda String toString() ma w klasie Object specyfikator dostępu
public, ai nie podając żadnego specyfikatora dla tej metody w klasie A
ograniczono dostęp z publicznego do pakietowego.
Gorzej, gdy popełnimy błędy w pisowni nazwy metody lub podamy niewłaściwe argumenty.
Może
się wydawać, że metoda jest przedefiniowana, gdy wcale tak nie jest. A
jak nie ma przedefiniowania - to nie ma polimorficznych odwołań.
Z przykładowego programu:
package tostring;
class A {
public String ToString() {
return "Obiekt klasy A";
}
}
class B {
public String toString() {
return "Obiekt klasy B";
}
}
class C extends B {
public String toString(String ... myMsg) {
String s = "Obiekt klasy C";
if (myMsg.length == 1) s += myMsg[0];
return s;
}
}
public class UseOverride {
public static void main(String[] args) {
Object[] arr = { new A(), new B(), new C() };
for (Object o : arr) {
System.out.println(o);
}
}
}
uzyskamy - zapewne wbrew intencjom - następujące wyniki:
tostring.A@a90653 // błąd w nazwie metody - wołana jest toString() z klasy Object
Obiekt klasy B // tu dobrze
Obiekt klasy B // ale tu metoda toString() jest przeciążona w klasie C, a nie przedefiniowana
// dlatego dostajemy napis z metody toString z klasy B
Aby
uniknąć tego rodzaju błędów w codziennym programowaniu warto
przyzwyczaić się i stosować konwencje nazewnicze Javy (wtedy nigdy nie
napiszemy nazwę jako metody ToString). Oprócz tego pomocna jest
adnotacja @Override, o której była już mowa w punkcie o
przedefiniowaniu metod.
Za pomoca tej adnotacji informujemy
kompilator, że naszą intencją jest przedefiniowanie metody. Jeśli
warunki przedefiniowania nie są spełnione (nie ma metody o identycznej
sygnaturze i wyniku w nadklasach) - kompilator poinformuje o błędzie.
Użycie @Override w poprzednim przykładzie ilustruje rysunek.
Przedefiniowując
metody warto używać adnotacji @Override. Dzięki temu można łatwo
uniknąć błędów, szczególnie gdy intencją jest polimorficzne wywołanie
metod.
Polimorfizm działa również sprawnie dla kowariantnych
typów wyników. Przypomnijmy sobie klasy Liczba, Cala i Rzecz z
poprzedniego wykładu, obrazujące kowariancję typów wyniku przy
przedefiniowaniu metod.
class Liczba {
private Number n;
public Liczba() {
}
public Liczba(Integer i) {
n = i;
}
public Liczba(Double d) {
n = d;
}
public Number getNumber() {
return n;
}
}
class Cala extends Liczba {
public Cala(int n) {
super(n);
}
public Integer getNumber() {
return super.getNumber().intValue();
}
}
class Rzecz extends Liczba {
public Rzecz(double n) {
super(n);
}
public Double getNumber() {
return super.getNumber().doubleValue();
}
}
Padło wtedy pytanie o sens. Otóż możliwość stosowania kowariancji wyników powoduje, że poniższy program testowy:
public static void main(String[] args) {
Liczba[] liczby = { new Cala(1), new Rzecz(2.1), new Rzecz(3.1), new Cala(3) };
for (Liczba l : liczby) {
System.out.println(l.getNumber());
}
}
wyprowadzi właściwe wyniki:
1
2.1
3.1
3
Metoda getNumber nie tylko jest wołana polimorficznie, ale zwraca
też odpowiednio zawężony wynik, co widać na wydruku, gdzie liczby
całkowite nie mają kropek (separatorów miejsc dziesiętnych).
Gdyby
nie było kowariancji wyników, metody getNumber() w klasach Cala i
Rzecz musielibyśmy zdefiniować z typem wyniku Double. Oczywiście,
byłyby one wołane polimorficznie, ale wynik byłby inny - liczby
całkowite wyprowadzane byłyby z kropką.
3. 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 (definicja w klasie kodu metody) może być
bardzo różna, w zależności od konkretnego rodzaju obiektów, które
opisuje dana klasa.
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 = "bez imienia";
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 = "bez imienia";
Zwierz() {}
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 "?").
4. Interfejsy
Interfejs klasy – to sposób komunikowania się z jej obiektami.
Inaczej - zestaw jej dostępnych do użycia z poziomu innej klasy metod.
W odróżnieniu od interfejsu -
implementacja określa konkretne definicje metod interfejsu oraz zestaw niestatycznych pól klasy.
W Javie słowo interfejs ma też dodatkowe, specjalne "techniczne" znaczenie,
odwołujące się zresztą do ogólnego pojęcia interfejsu.
Rozważmy przykład.
Nie wszystkie zwierzęta wydają głos. Zatem umieszczanie (abstrakcyjnej) metody
getVoice() oraz metody speak() w klasie Zwierz nie jest "czystym rozwiązaniem".
Co więcej nie tylko zwierzęta mówią.
Chciałoby się więc mieć klasę obiektów wydających głos, którą móglby dziedziczyć np. Wodospad i Pies.
Ale Pies jest Zwierzem (dziedziczy Zwierza) i nie może odziedziczyć klasy
obiektów "wydających głos". W Javie nie ma bowiem wielodziedziczenia klas: każda
klasa może dziedziczyć bezpośrednio tylko jedną klasę.
Unikanie wielodziedziczenia klas w Javie wynika z niejednoznaczności, które
związane są z niektórymi postaciami wielodziedziczenia (np. z tzw. problemem
diamentu lub rombu wielodziedziczenia, która to nazwa wynika z kształtu relacji
pomiędzy klasami - zob. rysunek).
Wyobraźmy sobie na chwilę, że w Javie jest wielodziedziczenie klas i spróbujmy
zbudować klasę Child, które niewątpliwie dziedziczy po Father i po Mother,
które to z kolei klasy są swoistymi "specjalizacjami" klasy Man.
Niech w klasie Man znajduje się abstrakcyjna metoda String getSex(), a
w klasach Father i Mother zdefiniujemy tę metodę tak, by zwracała
właściwą
dla każdego z rodziców płeć - np. "kobieta" lub "mężczyzna".
class Man {
public abstract String getSex();
}
class Father extends Man {
public String getSex() { return "male"; }
}
class Mother extends Man {
public String getSex() { return "female"; }
}
class Child extends Mother, Father { // hipotetyczne wielodziedziczenie
// klasa Child nie przedefiniowuje metody getSex() !
}
Pytanie: jaki wynik dla obiektu-dziecka (klasy Child) zwróci wywołanie metody getSex() ?
Oczywiście, w tej konkretnej sytuacji - nie wiadomo.
Również pola niestatyczne (i dostępne z poziomu innych klas, np.
publiczne, chronione lub pakietowe) sprawiają kłopot przy
wielodziedziczeniu. Zobaczmy.
class A {
public int a;
}
class B extends A { // ma pole a
}
class C extends A { // ma pole a
}
class D extends B i C { // hipotetyczne wielodziedziczenie
}
Obiekt d z klasy D ma element definiowany przez pole a. Jeden czy dwa?
Jesli jeden, to który? Czy ten który należy do obiektu B czy pochodzący z obiektu C, czy
może to jest ten sam element, definiowany przez pole a klasy A? I jak rozumieć
naturalne odwołanie d.a ?
Oczywiście, zawsze można przyjąć jakieś arbitralne ustalenia. Zabronić kompilacji
programu z nieszczęsnym Dzieckiem bez określonej płci (bez przedefiniowania
metody getSex() w klasie Child w naszym przykładzie). Albo ustalić, że
klasa D z drugiego przykładu ma pole a, które jest polem klasy A.
Ale Java tego nie rozstrzyga: po prostu unika wielodziedziczenia klas.
Pies jest Zwierzem (dziedziczy właściwości Zwierza), w Javie nie może dodatkowo
odziedziczyć funkcjonalności klasy "obiektów wydających głos".
Ale byłoby to bardzo wskazane.
Pewne rozwiązanie tego problemu uzyskano w Javie wprowadzając (jako element skladni języka) pojęcie interfejsu
, jakby biedniejszej klasy, nie zawierającej pól, ale tylko metody (i/lub
stałe statyczne). Uniknięto w ten sposób niejasności związanych z polami,
zachowując - jak zobaczymy dalej - pewne zalety wielodziedziczenia klas.
Interfejs (deklarowany za pomocą słowa kluczowego
interface) to:
- zestaw publicznych abstrakcyjnych metod
- i/lub publicznych statycznych stałych
Implementacja interfejsu w klasie - to zdefiniowanie w tej klasie wszystkich metod interfejsu.
To że klasa ma implementować interfejs X oznaczamy słowem kluczowym
implements.
Np. interfejs określający abstrakcyjną funkcjonalność "wydającego głos" mógłby wyglądać tak:
public interface Speakable {
int QUIET = 0; // <- publiczne stałe statyczne
int LOUD = 1; // domyślnie public static final
String getVoice(int voice); // <- metoda abstrakcyjna;
// ponieważ w interfejsie mogą być
// tylko publiczne metody abstrakcyjne,
// specyfikatory public i abstract niepotrzebne
}
a jego implementacja w klasie Wodospad:
public class Wodospad implements Speakable {
public String getVoice(int voice) { // metody interfejsu są zawsze publiczne!
if (voice == LOUD) return "SZSZSZSZSZSZ....";
else if (voice == QUIET) return "szszszszszsz....";
else return "?"
}
}
Klasa, w definicji której zaznaczono (za pomocą slowa implements),
że ma implementowac interfejs musi zdefiniować wszystkie metody tego interfejsu,
albo być deklarowana jako klasa abstrakcyjna.
W przeciwnym razie wystąpi błąd w kompilacji.
Podsumujmy:
- Interfejs zawiera deklaracje publicznych metod abstrakcyjnych oraz ew. publicznych stałych statycznych.
- Każda klasa implementująca interfejs musi zdefiniować WSZYSTKIE jego metody albo będzie klasą abstrakcyjną.
Rozważmy teraz interfejs, opisujący obiekty zdolne się poruszać:
public interface Moveable {
void start();
void stop();
}
Ta funkcjonalność dotyczy zarówno Psa, jak i Samochodu (np. obiektów klasy Car), jak również innych pojazdów.
Weźmy Psa:
- Pies jest Zwierzem,
- Pies potrafi mówić,
- Pies może się poruszać.
Mamy trzy właściwości.
Właściwość bycia Zwierzem zrealizujemy przez dziedziczenie klasy Zwierz
(jej zmodyfikowana postać nie zawiera metod getVoice(0 i speak(),
ponieważ nie wszystkie zwierzęta mówią), pozostałe dwie właściwości przez implementację interfejsów.
W Javie klasa (oprócz dziedziczenia innej klasy) może implementować dowolną liczbę interfejsów.
Zatem w Javie nie ma wielodziedziczenia implementacji, ale za to jest możliwe wielodziedziczenie interfejsów.
Możemy więc napisać:
public abstract class Zwierz {
private String name = "bez imienia";
public Zwierz() {}
public Zwierz(String s) {
name = s;
}
public abstract String getTyp();
public String getName() {
return name;
}
}
public class Pies extends Zwierz implements Speakable, Moveable {
public Pies() {
}
public Pies(String s) {
super(s);
}
public String getTyp() {
return "Pies";
}
public String getVoice(int voice) {
if (voice == LOUD)
return "HAU... HAU... HAU... ";
else
return "hau... hau...";
}
public void start() {
System.out.println("Pies " + getName() + " biegnie");
}
public void stop() {
System.out.println("Pies " + getName() + " stanął");
}
}
i użyć np. tak :
Pies kuba = new Pies("Kuba");
kuba.start();
System.out.println(kuba.getVoice(Speakable.LOUD));
kuba.stop();
co da:
Pies Kuba biegnie
HAU... HAU... HAU...
Pies Kuba stanął
W tym fragmencie programu zmienna kuba jest typu Pies, a to znaczy, że jest również typu Zwierz ORAZ typu Speakable i Moveable, ponieważ:
- klasa Pies dziedziczy klasę Zwierz - zatem kuba jest typu Zwierz,
- klasa Pies implementuje interfejs Speakable - zatem kuba jest typu Speakable,
- klasa Pies implementuje interfejs Moveable - zatem kuba jest typu Moveable
Interfejsy - podobnie jak klasy - wyznaczają typy danych
Możemy więc robić konwersje w górę do typu wyznaczanego przez implementowany interfejs, jak również używać operatora instanceof, by stwierdzić czy obiekt jest obiektem klasy implementującej dany interfejs.
Dlatego warto wprowadzić subtelną zmianę w definicji omawianej wcześniej klasy Vehicle, dodając:
class Vehicle implements Moveable {
...
}
Wszystkie klasy pochodne wobec Vehicle – prawem dziedziczenia – także będą implementować ten interfejs.
Teraz np. jeśli mamy klasy Car, Rower, Pies i Kot implementujące interfejs Moveable
oraz aplikację Wyscig z metodą wyscig:
public class Wyscig {
static void wyscig(Moveable ... moveables) {
for (Moveable m : moveables) {
m.start();
if (m instanceof Vehicle) System.out.println(m);
}
}
public static void main(String[] args) {
wyscig(new Pies("Kuba"),
new Car("WB4545", new Person("Janek", "9012102567"),100, 100, 100, 100, 100),
new Kot("Mruczek"),
new Bicycle(new Person("Ala", "7011122347"),100, 100, 100, 100)
);
}
}
to po uruchomieniu tej aplikacji moglibyśmy otrzymać np. taką informację:
Pies Kuba biegnie
Samochód nr rej WB4545 - JEDZIE
Kot Mruczek się skrada
Pojazd 2, właścicielem którego jest Ala - JEDZIE
Zwróćmy baczną uwagę na ten przykład.
Oto - dzięki koncepcji interfejsów - uzyskaliśmy swoiste poszerzenie polimorfizmu.
Polimorficzne odwołania są teraz możliwe nie tylko "wzdłuż" hierarchii dziedziczenia
klas, ale również "w poprzek" tych hierarchii.
No, tak - przecież klasy Pies i Kot należą do innej hierarchii dziedziczenia
niż klasa Car i Rower. Dzięki interfejsom jednak (dzięki temu, że wszystkie
te klasy implementują interfejs Moveable) dla wszystkich tych klas uzyskujemy
możliwość polimorficznych odwołań do metody start().
Oczywiście, możemy też w naszych przykładach zastosować konwersje zawężające.
Przypomnijmy: jeśli mamy referencję do obiektu typu Zwierz na którą podstawiono odniesienie
do obiektu typu Pies, to możemy zrobić konwersję "w dół" hierarchii dziedziczenia.
Pies p = new Pies();
Zwierz z = p;
Pies p1 = (Pies) z; // Konwersja z typu Zwierz do typu Pies
Mówiąc obrazowo (ale pamiętając o tym, że mamy do czynienia z konwersjami
referencyjnymi i tak naprawdę jest tu tylko jeden obiekt, który po prostu,
w kolejnych przekształceniach referencyjnych, traktujemy inaczej):
Pies pochodzi od Zwierza, możemy więc z Psa uzyskać Zwierza, a później z tego Zwierza z powrotem Psa.
Mechanizm konwersji zawężających w równym stopniu dotyczy interfejsów (interfejsy,
tak samo jak klasy, też określają typy, przy czym można dziedziczyć, czy
raczej rozszerzać interfejsy; ten temat pozostawimy jednak na boku).
Rozważmy jeszcze dwa krótkie przykłady.
Załóżmy, że klasa Pies ma dodatkową własną metodę:
void merda() { System.out.println("Merda ogonem"); )
Można napisać metodę:
static void info(Zwierz z) {
say(z.getTyp() + z.getName()); // say własna metoda = System.out.println
if (z instanceof Speakable) {
Speakable zs = (Speakable) z;
say(zs.getVoice(Speakable.LOUD));
}
if (z instanceof Pies) ((Pies) z).merda();
}
Co wywołane dla obiektu klasy Kot (implementującej interfejs Speakable):
Kot mruczek = new Kot("mruczek");
info(mruczek)
może wypisać:
Kot Mruczek
Miauuu....
bo:
- Przy przekazywaniu argumentu następuje konwersja: Kot -> Zwierz,.
- Polimorficznie jest wołana metoda getTyp() (z jest Zwierz, ale Java wie, że w tym Zwierzu siedzi Kot).
- Ponieważ w z siedzi Kot, który implementuje interfejs
Speakable, można zrobić konwersję do typu Speakable i odwołać się polimorficznie
do getVoice().
- Ponieważ Kot nie jest Psem - nie merda. ogonem.
dla obiektu hipotettycznej klasy Ryba - info( new Ryba()). możemy dostać w wyniku tylko:
Ryba
bo wartość wyrażenia z instanceof Speakable jest false (Ryba nie implementuje interfejsu Speakable) i oczywiście nie jest też Psem.
natomiast dla Psa kuby (kuba = new Pies("Kuba") po info(kuba) dostaniemy pewnie:
Pies Kuba
HAU... HAU... HAU...
Merda ogonem
I jeszcze jeden krótki przykład.
Mając metodę:
void run(Moveable m) {
m.start();
if (m instanceof Pies) {
System.out.println(" ... i .... " );
((Pies) m).merda();
}
}
i wołając ją z argumentem typu Pies, otrzymamy wynik:
Pies biegnie
... i ....
merda ogonem
bo uzyskaliśmy:
- Konwersję Psa do typu Moveable (w górę),
- Polimorficzne wywołanie metody start() na rzecz obiektu oznaczanego przez m (formalnego typu Moveable, który jednak faktycznie jest typu Pies),
- I jeżeli m odnosi się do Psa (a odnosi się), to po jawnej konwersji z typu Moveable do Psa możemy na rzecz przekształconego m wywołać "indywidualną" metodę merda z klasy Pies.
Zauważmy,
że usuwając z klasy Zwierz właściwość "mówienia" - metodę speak() -
ograniczyliśmy jej funkcjonalność, dziedziczoną w podklasach. A
przecież to metoda speak() (jej ew. skomplikowana postać, np.
wykorzystująca multimedia) służyła nam z początku za argument na rzecz
polimorfizmu (metoda napisana raz, działająca właściwie dla różnych
nawet nie istniejących jeszcze klas zwierząt).
Nic jednak nie stoi na przeszkodzie, aby przywrócić to wygodne ponowne użycie.
Oczywiście nie możemy przywrócić jej poprzedniej wersji:
void speak() {
System.out.println(getTyp()+" "+getName()+" mówi "+getVoice());
}
bo wystąpi błąd w kompilacji, gdyż w klasie Zwierz nie ma metody getVoice().
Jest
ona deklarowana w interfejsie Speakable - więc na pewno potrzebne jest
rzutowanie referencji na rzecz której wywołano metodę speak() do typu
Speakable.
Jednak taki zapis nie wystarczy:
void speak() {
System.out.println(getTyp()+" "+getName()+" mówi "+((Speakable) this).getVoice(Speakable.QUIET));
}
bo
gdyby metoda speak(0 została wywołana dla Ryby, która nie wydaje głosu
i wobec tego nie implementuje interfejsu Speakable, to wystąpiłby
wyjątek ClassCastException:
java.lang.ClassCastException: cannot cast from Ryba to Speakable
Ostatecznie metodę speak() winniśmy zapisać tak (dodajmy jeszcze możliwość wyboru natężenia głosu):
public abstract class Zwierz {
// ...
public void speak(int ... v) {
int vol = Speakable.QUIET;
if (v.length == 1) vol = v[0];
String voice;
if (this instanceof Speakable) voice = ((Speakable) this).getVoice(vol);
else voice = "... (cisza) ...";
System.out.println(getTyp()+" "+getName()+ " mówi " + voice);
}
}
a klasa testująca:
public class Dialog {
public static void main(String[] arg) {
Pies kuba = new Pies("Kuba"), reksio = new Pies("Reksio");
Kot kot = new Kot("Mruczek");
Ryba ryba = new Ryba();
animalDialog(kuba, reksio);
animalDialog(kuba, kot);
animalDialog(reksio, ryba);
}
static void animalDialog(Zwierz z1, Zwierz z2) {
z1.speak();
z2.speak();
System.out.println("--------------------------------------");
}
}
da w wyniku:
Pies Kuba mówi Hau... Hau...
Pies Reksio mówi Hau... Hau...
--------------------------------------
Pies Kuba mówi Hau... Hau...
Kot Mruczek mówi Miau...
--------------------------------------
Pies Reksio mówi Hau... Hau...
Ryba bez imienia mówi ... (cisza) ...
--------------------------------------
5. Użyteczność interfejsów: przykłady "for-each" i formatowania
W
standardzie Javy jest bardzo dużo interfejsów. Są one wykorzystywane
przez wiele klas i bardzo ułatwiają programowanie, bowiem umożliwiają
pisanie uniwersalnych, elastycznych kodów. Sztandarowym przykładem mogą
być klasy kolekcyjne Javy - w następnym wykładzie zobaczymy jak
interfejsy sprzyjają bardziej niezawodnemu i uniwersalnemu
oprogramowaniu operacji na kolekcjach.
Wiele interfejsów
wprowadzonych do Javy umożliwia też proste rozszerzanie funkcjonalności
budowanych przez nas klas. Rozpatrzymy przykłady.
Po zaznajomieniu się z interfejsami można podać dokładną definicję składni rozszerzonej instrukcji for.
Składnia
for ( Typ id : expr )
stmt
gdzie:
- expr - wyrażenie, którego typem jest Iterable
(np. wszystkie kolekcje są
Iterable) albo wyrażenie, którego typem jest typ tablicowy,
- Typ
- nazwa typu elementów zestawu danych (np.
int albo String)
- id - identyfikator zmiennej, na
którą będzie
podstawiany kolejny element
zestawu danych; do tej zmiennej mamy dostęp w stmt (czyli instrukcji
wykonywanej
w każdym kroku for).
Iterable jest interfejsem, który zawiera jedną metodę iterator(),
zwracającą obiekt-iterator służący do iterowania po zestawie danych.
Obiekt-iterator z kolei jest obiektem klasy implementującej interfejs Iterator, w którym znajdują się metody hasNext(), next() i remove().
Możemy
więc za pomocą "for-each" iterować po tablicach i po kolekcjach, ale na
tym się nie ogranicza użyteczność rozszerzonego for: można go użyć dla
obiektów dowolnych klas implementujących interfejs Iterable.
Ogólny mechanizm działania jest taki:
Iterable<Typ> classObject = ....;
....
for ( Iterator<Typ> $i =
classObject.iterator(); $i.hasNext(); ) {
Typ id = $i.next();
stmt
}
Warto
zuwazyć, że oba interfejsy Iterable i Iterator są sparametryzowane
(przy ich definiowaniu powinniśmy podać w nawiasach kątowych typ
zwracanej przez metodę next() wartości). Jest to podobne do
omawianej wczesniej parametryzacji kolekcji i jak już wiemy pozwala
wykrywać błędy typów w fazie kompilacji i unikać referencyjnych
konwersji zawężających w kodzie programu (więcej o parametryzacji typów
już za chwilę).
Dla przykładu stworzymy klasę FromTo, która
okresla przedizał dni (od daty do daty), a wobec jej obiektów można
stosować rozszerzone for, iterujace po datach przedziału.
Klasa ta może być uzyta np. tak:
public class CalendarForEach {
public static void main(String[] args) {
Calendar from, to;
(from = Calendar.getInstance()).set(2008, 9, 1);; // 1 października
(to = Calendar.getInstance()).set(2008, 9, 10); // 10 pażdziernika
FromTo timeInterval = new FromTo(from, to);
for (Calendar day : timeInterval) {
System.out.printf("%tF%n", day);
}
}
}
co da w wyniku:
2008-10-01
2008-10-02
2008-10-03
2008-10-04
2008-10-05
2008-10-06
2008-10-07
2008-10-08
2008-10-09
2008-10-10
Aby for-each działało klasa FromTo musi implementować Iterable.
import java.util.*;
public class FromTo implements Iterable<Calendar> {
private Calendar from = Calendar.getInstance(),
to = Calendar.getInstance();
public FromTo(Calendar f, Calendar t) {
from.setTime(f.getTime());
to.setTime(t.getTime());
}
public Iterator<Calendar> iterator() {
return new CalendarIterator(from, to);
}
}
Implementacja w klasie metody iterator() tego interfejsu zwraca obiekt typu Iterator<Calendar>.
Jest
to konkretnie obiekt naszaej klasy CalendarIterator, ale ponieważ
implementuje ona interfejs Iterator<Calendar> możemy podać
własnie ten typ wyniku.
Klasa iteratora pokaauje wydruk.
import java.util.*;
import static java.util.Calendar.*;
public class CalendarIterator implements Iterator<Calendar> {
private Calendar to; // data końca
private Calendar current = Calendar.getInstance(); // bieżąca data iteratora
CalendarIterator(Calendar from, Calendar to) {
current.setTime(from.getTime());
current.add(DATE, -1); // ustawiamy iterator przed początkiem zakresu
this.to = to; // koniec zakresu
}
public boolean hasNext() {
return current.before(to);
}
public Calendar next() {
if (!hasNext()) throw new NoSuchElementException();
current.add(DATE, 1);
return current;
}
// Metoda remove() nic nie robi,
// ale musi być bo implementujemy interfejs Iterator
public void remove() {
}
}
Drugi przykład wykorzystania dostępnych interfejsów Javy dotyczy formatowania.
W
wykładzie "Liczby, daty, algorytmy" była mowa o tym, że dla
symbolu konwersji s lub S jeżeli klasa drugiego argumentu metody format
na to pozwala, to do formatowania zostanie użyta metoda formatTo.
Jest
to możliwe tylko wtedy gdy klasa argumentu "do sformatowania"
implementuje interfejs Formattable i definiuje jego jedyną metodę:
public void formatTo(Formatter formatter, int flags, int width, int precision)
Jeśli teraz za pomocą jakiegoś formatora następuje formatowanie obiektu z użyciem symbolu konwersji s lub S, to:
- wywoływana jest metoda formatTo z klasy obiektu,
- metodzie formatTo jest przekazywany formator (jako parametr formatter), flagi formatowania (parametr flags), szerokość pola (parametr width) i precyzja (parametr precision),
- w
metodzie formatTo możemy sprawdzić wartości przekazanych informacji
(np. locale formatora, flagi, szerokość precyzje) i na tej podstawie
podjąć odpowiednie działania przetwarzające obiekt do
wynikowego napisu,
- przed zwróceniem sterowania z metody formatTo wywołujemy metodę format przekazanego formatora,
- sformatowany napis będzie przez formator zapisany do odpowiedniej destynacji (np. wyprowadzony na standardowe wyjście).
Sprawdzić
jakich użyto flag formatowania możemy za pomocą porównywania ze stałymi
statycznymi klasy java.util.FormattableFlags. Dostępne są stałe o
następujących nazwach:
- ALTERNATE - oznacza, że należy użyć alternatywnej formy sformatowania obiektu (w formacie użyto znaku #),
- LEFT_JUSTIFY - sformatowany napis będzie wyrównany w polu do lewej (w formacie użyto znaku -),
- UPPERCASE - zamiana liter na duże (użyto wielkiej litery w symbolu konwersji np. S, zamiast s).
Przykładowy
program pokazuje zastosowanie metody formatTo do łatwej zmiany sposobu
wyprowadzania informacji o obiektach klasy Person. W trybie
normalnym wyprowadzane jest nazwisko, w trybie alternatywnym
(zastosowana flaga #) - nazwisko i imię. W metodzie formatTo, na
podstawie przekazanych informacji wybieramy formę formatowania,i
budujemy napis formatu na podstawie przekazanej informacji, po czym za
jego pomocą formatujemmy obiekt, używając przekazanego formatora.
import java.util.*;
import static java.util.FormattableFlags.*;
public class Person implements Formattable{
private String fname;
private String lname;
public Person(String fname, String lname) {
this.fname = fname;
this.lname = lname;
}
@Override
public void formatTo(Formatter formatter, int flags, int width, int precision) {
String txt = lname;
if ((flags & ALTERNATE) == ALTERNATE) txt += ' ' + fname;
String fs = "%";
if ((flags & LEFT_JUSTIFY) == LEFT_JUSTIFY) fs += '-';
if (width >= 0) fs += width;
if (precision >= 0) fs += "."+precision;
fs += ((flags & UPPERCASE) == UPPERCASE) ? "S" : "s";
formatter.format(fs, txt);
}
public static void main(String[] args) {
Person e = new Person("Jan", "Kowalski");
System.out.printf("%#s\n", e);
System.out.printf("%20s\n", e);
System.out.printf("%#30S\n", e);
System.out.printf("%#.10S\n", e);
}
}
Wynik działania programu:
Kowalski Jan
Kowalski
KOWALSKI JAN
KOWALSKI J
6. Klasy wewnętrzne
6.1. Pojęcie klasy wewnętrznej
Klasa wewnętrzna – to klasa zdefiniowana wewnątrz innej klasy.
class A {
....
class B {
....
}
....
}
Klasa B jest klasą wewnętrzną w klasie A.
Klasa A jest klasą otaczającą klasy B.
Klasa wewnętrzna może:
|
- być zadeklarowana ze specyfikatorem private (normalne klasy nie!), uniemożliwiając wszelki dostęp spoza klasy otaczającej,
- odwoływać się do niestatycznych składowych klasy otaczającej (jeśli nie jest zadeklarowana ze specyfikatorem static),
- być zadeklarowana ze specyfikatorem static (normalne klasy nie!),
co powoduje, że z poziomu tej klasy nie można odwoływać się do składowych
niestatycznych klasy otaczającej (takie klasy nazywają się zagnieżdżonymi,
ich rola sprowadza się wyłacznie do porządkowania przestrzeni nazw i ew.
lepszej strukturyzacji kodu)
- mieć nazwę (klasa nazwana),
- nie mieć nazwy (wewnętrzna klasa anonimowa),
- być lokalna – zdefiniowana w bloku (metodzie lub innym bloku np. w bloku po instrukcji if),
- odwoływać się do zmiennych lokalnych (o ile jest lokalna, a zmienne są deklarowane ze specyfikatorem final).
|
Uwaga.
Zawarcie klasy wewnętrznej w klasie otaczającej NIE OZNACZA, że obiekty klasy
otaczającej zawierają elementy (pola) obiektów klasy wewnętrznej.
Obiekt
niestatycznej klasy wewnętrznej zawiera referencję do obiektu klasy otaczającej, co umożliwia odwoływanie się do jej wszystkich składowych.
Między obiektami
statycznej klasy wewnętrznej a obiektami klasy otaczającej nie zachodzą żadne związki.
Po co są klasy wewnętrzne? W jakim celu są używane?
- Klasy wewnętrzne mogą być ukryte przed innymi klasami pakietu (względy bezpieczeństwa).
- Klasy wewnętrzne pozwalają unikać kolizji nazw (np. klasa wewnętrzna
nazwana Vector nie koliduje nazwą z klasą zewnętrzną o tej samej nazwie).
- Klasy wewnętrzne pozwalają (czasami) na lepszą, bardziej klarowną strukturyzację
kodu, bo można odwoływać się z nich do składowych (nawet prywatnych) klasy
otaczającej, a przy tym zlokalizować pewne działania.
- Klasy wewnętrzne (w szczególności anonimowe) są intensywnie używane przy implementacji standardowych interfejsów Javy.
- Anonimowe
klasy wewnętrzne pozwalają na traktowanie fragmentów kodu do wykonania
(ściślej: metod przedefiniowywanych w tych klasach) jak obiektów, a
wobec tego np. umieszczanie ich w tablicach, kolekcjach, czy
przekazywanie innym meteodom jako argumentów. Można to traktować jako
bardzo częściową ( ograniczoną co do semantyki i nieco żmudną w składni)
realizację koncepcji domknięć (closures).
Rozważmy przykładowe zastosowanie klas wewnętrznych do ulepszenia znanej nam klasy Car,
Problem.
Jadąc, samochody zużywają paliwo. Zatem w klasie Car należałoby dostarczyć
mechanizmu symulującego zużycie paliwa i ew. tego skutki (zatrzymanie pojazdu).
Mechanizm ten nie powinien być w żaden sposób dostępny z innych klas, powinien
być dobrze zlokalizowany i odseparowany. Jednocześnie, musi odwoływać się
do prywatnej zmienej klasy Car, obrazującej bieżącą ilość paliwa w baku (fuel).
Koncepcja rozwiązania:
prywatna klasa wewnętrzna.
Przyjęte założenie symulacji:
w każdej jednostce czasu jazdy (1 sek czasu programu) zużywany jest podana ilość paliwa.
Dodatkowe szczegóły realizacyjne:
Do symulacji wykorzystamy klasę Timer z pakietu javax.swing. Uruchomiony
(metodą start()) obiekt tej klasy z zadaną częstotliwością (pierwszy argument
konstruktora klasy Timer) wywołuje metodę actionPerformed(...) z klasy i
na rzecz obiektu podanego jako drugi argument konstruktora. Klasa drugiego
argumentu implementuje interfejs ActionListener i definiuje jego jedyną metodę
void actionPerformed(ActionEvent e).
Rozwiązanie.
import javax.swing.*;
import java.awt.event.*;
public class Car extends Vehicle {
private String nrRej;
private int tankCapacity;
private int fuel;
// Klasa wewnętrzna. Prywatna - nie możemy jej użyć poza klasą Car
// Dostarcza definicji metody actionPerformed(...), wywoływanej przez Timer
private class FuelConsume implements ActionListener {
public void actionPerformed(ActionEvent e) {
if (getState() != MOVING) fuelTimer.stop(); // nie zużywaj paliwa, gdy nie jedziesz
else {
fuel -= 1; // odwolanie do pryw. składowej klasy otaczajĄcej
if (fuel == 0) stop();
}
}
}
// Timer będzie co sekundę wywoływać metodę actionPerformed(...)
// z klasy obiektu podanego jako drugi argument konstruktora
// ( obiekt klasy FuelConsume)
// w rezultacie co sekunde czasu komputerowego bedzie zuzywany 1 l paliwa
private Timer fuelTimer = new Timer(1000, new FuelConsume());
public Car(String nr, Person owner, int w, int h, int l,
int weight, int tankCap) {
super(owner, w, h, l, weight);
nrRej = nr;
tankCapacity = tankCap;
}
public void fill(int amount) {
if (getState() == MOVING)
System.out.println("Nie moge tankowac w ruchu");
else {
fuel += amount;
if (fuel > tankCapacity) fuel = tankCapacity;
}
}
public void start() {
if (fuel > 0) {
super.start();
fuelTimer.start(); // start Timera
}
else System.out.println("Brak benzyny");
}
public void stop() {
super.stop();
fuelTimer.stop(); // zatrzymanie Timera
}
public String toString() {
return "Samochód nr rej " + nrRej + " - " + getState(getState());
}
}
Podana na wydruku metoda main()
public static void main(String[] args) throws InterruptedException {
Car c = new Car("aaa", new Person("x", "c"), 100, 100, 100, 100, 100);
c.fill(7);
c.start();
for (int i = 1; i <= 9; i++) {
Thread.sleep(1000);
System.out.println("Po " + i + " sek. - " + c);
}
}
wyprowadzi pokazane wyniki:
Po 1 sek. - Samochód nr rej aaa - JEDZIE
Po 2 sek. - Samochód nr rej aaa - JEDZIE
Po 3 sek. - Samochód nr rej aaa - JEDZIE
Po 4 sek. - Samochód nr rej aaa - JEDZIE
Po 5 sek. - Samochód nr rej aaa - JEDZIE
Po 6 sek. - Samochód nr rej aaa - JEDZIE
Po 7 sek. - Samochód nr rej aaa - JEDZIE
Po 8 sek. - Samochód nr rej aaa - STOI
Po 9 sek. - Samochód nr rej aaa - STOI
Uwaga:
otrzymywane wyniki mogą się lekko różnić w zalezności od okolicznosci,
bowiem mamy tu sytuację równoległego wykonania dwóch fragmentów kodu
(metody actionPerformed() wołanej co sekundę przez Timer i metody
toString() wołanej w main) wspóldzielących tę samą zmienną (state).
Więcej na ten temat przy okazji omawiania programowania
współbieżnego w następnym semestrze.
Oczywiście klasy wewnętrzne nie muszą być prywatne,
Wtedy możliwe jest odwoływanie się do nich spoza kontekstu klasy otaczającej.
Takie odwołanie ma formę:
NazwaKlasyOtaczającej.NazwaKlasyWewnętrznej
Tworzenie obiektu niestatycznej klasy wewnętrznej wymaga zawsze istnienia obiektu klasy otaczającej. Mówi się, że obiekt klasy wewnętrznej opiera się na obiekcie klasy otaczającej .
Gdyby zatem nasza klasa FuelConsume była publiczna, to moglibyśmy spoza klasy Car odwoływać się do niej i tworzyć jej obiekty
Jeżeli niepotrzebna nam zmienna car, moglibyśmy zapisać to szybciej:
Car.FuelConsume cfc = new Car().new FuelConsume();
6.2. Anonimowe klasy wewnętrzne
Anonimowe klasy wewnętrzne nie mają nazwy.
Jeśli tak, to jakiego typu będą referencje do obiektów tych klas i po co taka możliwość?
Otóż, najczęściej tworzymy klasy wewnętrzne po to, by przedefiniować jakieś
metody klasy dziedziczonej przez klasę wewnętrzną bądź zdefiniować metody
implementowanego przez nią interfejsu na użytek jednego obiektu. Referencję
do tego obiektu chcemy traktować jako typu klasy dziedziczonej lub typu implementowanego
interfejsu. Nazwa klasy wewnętrznej jest więc nam niepotrzebna i nie chcemy
jej wymyślać. Wtedy stosujemy anonimowe klasy wewnętrzne.
Definicję anonimowej klasy wewnętrznej dostarczamy w wyrażeniu new.
new
NazwaTypu(
parametry ) {
// pola i metody klasy wewnętrznej
}
gdzie:
- NazwaTypu – nazwa nadklasy (klasy dziedziczonej w klasie wewnętrznej) lub implementowanego przez klasę wewnętrzną interfejsu,
- parametry – argumenty przekazywane konstruktorowi nadklasy;
w przypadku gdy Typ jest nazwą interfejsu lista parametrów jest oczywiście
pusta (bo chodzi o implementację interfejsu).
Np. jeśli mamy klasę DBase, zawierają zestaw metod działania na bazie danych
(m.in metodę void add(Record r), dodającą nowy rekord do bazy) i chcemy
w naszym programie utworzyć jeden obiekt tej klasy (operujemy na jednej bazie),
a jednocześnie uzupełnić działanie metody add (w stosunku do jej definicji zawartej w klasie DBase), to możemy użyć anonimowej klasy wewnętrznej:
i wykorzystać utworzony obiekt anonimowej klasy wewnętrznej, na który wskazuje referencja db np.
Record jakisRekord;
//....
db.add(jakisRekord);
W przypadku zużycia paliwa w samochodzie (poprzedni przykład) również możemy (i powinniśmy) użyć anonimowej klasy wewnętrznej
Po co nam nazwa klasy - FuelConsume? Potrzebujemy tylko jednego obiektu tej
klasy, przy czym interesuje nas jego funkcjonalność jako ActionListenera.
Tak naprawdę potrzebujemy więc jednego obiektu typu ActionListener, a ponieważ
ActionListener jest interfejsem i musimy zdefiniować jego metodę actionPerformed(...),
to powinniśmy zrobić to w anonimowej klasie wewnętrznej.
class Car extends Vehicle {
private ActionListener fuelConsumer = new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (getState() != MOVING) fuelTimer.stop();
else {
fuel -= 1;
System.out.println("Fuel "+ fuel);
if (fuel == 0) stop();
}
}
};
private Timer fuelTimer = new Timer(1000, fuelConsumer);
....
}
Możemy jeszcze bardziej uprościć sobie życie. Zauważmy, że wyrażenie new
zwraca referencję do nowoutworzonego obeiktu. Wszędzie tam, gdzie może wystąpić
referencja może wystąpić wyrażenie new. Może zatem wystąpić jako drugi argument
wyrażenia new, tworzacego timer.
class Car extends Vehicle {
private Timer fuelTimer = new Timer(1000, new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (getState() != MOVING) fuelTimer.stop();
else {
fuel -= 1;
if (fuel == 0) stop();
}
}
} // nawias zamykający definicję klasy wewnętrznek
); // nawias zamykający new Timer(...),
// i średnik kończący instrukcję deklaracyjną
...
}
Uwagi:
- anonimowe klasy wewnętrzne nie mogą mieć konstruktorów (bo nie mają nazwy),
- za pomocą anonimowej klasy wewnętrznej można stworzyć tylko jeden obiekt, bo definicja klasy podana jest w wyrażeniu new
czyli przy tworzeniu obiektu, a nie mając nazwy klasy nie możemy potem tworzyć
innych obiektów; jeśli jednak to wyrażenie new umieścimy np. w pętli – to
oczywiście stworzone zostanie tyle obiektów ile razy wykona się pętla,
- definiowanie klas wewnętrznych implementujących interfejsy stanowi
jedyny dopuszczalny przypadek użycia nazwy interfejsu w wyrażeniu new (nie
wolno na przykład pisać ActionListener al = new ActionListener();, ale możemy
użyć new ActionListener() wtedy, gdy zaraz potem następuje definicja anonimowej
klasy wewnętrznej implementującej interfejs ActionListener),
- anonimowe klasy wewnętrzne są kompilowane do plików .class o nazwach
automatycznie nadawanych przez kompilator (nazwa składa się z nazwy klasy
otaczającej i jakiegoś automatycznie nadawanego identyfikatora np. Car$1.class)
6.3. Wewnętrzne klasy lokalne
Klasy wewnętrzne (nazwane i anonimowe)
mogą być definiowane w blokach lokalnych (np. w ciele metody). Będziemy je
krótko nazywać
klasami lokalnymi.
Ma to dwie zalety.
Wewnętrzne klasy lokalne są doskonale odseparowane (nie ma do nich żadnego dostępu spoza bloku, w którym są zdefiniowane), a mogą odwoływać się do składowych klasy otaczającej oraz zmiennych lokalnych zadeklarowanych w bloku (pod warunkiem, że są one zadeklarowane ze specyfikatorem final, o czym dalej).
Poza tym możliwość definiowania wewnętrznych klas lokalnych umożliwia umieszczenie odpowiedniego kodu w miejscu jego wykorzystania.
Możemy
teraz zmodyfikować poprzedni przykłąd iterowania po datach
(implementacja interfejsu Iterable w klasie FromTo). Odrębna klasa dla
kalendarzowego iteratora nie jest nam wcale potrzebna. Definicję
klasy iteratora możemy umieściś bezpośrednio w metodzie iterator() - w
instrukcji return, która ma zwrócić obiekt-iterator.
import static java.util.Calendar.*;
public class FromTo implements Iterable<Calendar> {
private Calendar from = Calendar.getInstance(),
to = Calendar.getInstance();
public FromTo(Calendar f, Calendar t) {
from.setTime(f.getTime());
to.setTime(t.getTime());
}
@Override
public Iterator<Calendar> iterator() {
// użycie lokalnej anonimowej klasy wewnętrznej
return new Iterator<Calendar>() {
Calendar current = Calendar.getInstance();
// Przydatność bloku inicjacyjnego
{
current.setTime(from.getTime());
current.add(DATE,-1);
}
@Override
public boolean hasNext() {
return current.before(to);
}
@Override
public Calendar next() {
if (!hasNext()) throw new NoSuchElementException();
current.add(DATE,1);
return current;
}
@Override
public void remove() {
}
};
}
}
Uwagi:
- w
definicji klasy wewnętrznej możemy wprowadzać deklaracje i inicjacje
pól, ale jeśli klasa jest anonimowa, to nie możemy podać konstruktora;
dlatego przydatny do inicjacji obiektu takiej klasy może okazać się
niestatyczny blok inicjacyjny,
- zastosowanie lokalnej anonimowej
klasy wewnętrznej w klasie FromTo pozwoliło na zmniejszenie liczby klas
w programie i długości kodu (wewnętrzna klasa iteratora ma dostęp do
pól from i to z klasy otaczającej).
Rozpatrzmy inny przykład: metody wypisywania zawartości katalogu.
Obiekty plikowe (pliki i katalogi) są obiektami klasy File z pakietu java.io. Wobec katalogu można użyć metody list z klasy File, która zwraca tablicę nazw plików (i podkatalogów) w nim zawartych. Używając metody list z argumentem typu FilenameFilter możemy określić kryteria filtrowania
wyniku wg nazw (np. otrzymać tylko listę plików o rozszerzeniu .java).
FilenameFilter jest interfejsem, w którym zawarto jedną metodę boolean accept(File dir, String filename).
Musimy zatem mieć obiekt klasy implementującej FilenameFilter, w której to
klasie zdefiniujemy metodę accept i podać referencję do tego obiektu jako
argument metody list. Metoda accept będzie wtedy wywoływana dla każdego obiektu
plikowego, zawartego w katalogu z argumentami – katalog, nazwa pliku lub
podkatalogu.
Powinniśmy ją zdefiniować w taki sposób, by zwracała true tylko wtedy, gdy
nazwa spełnia wymagane przez nas kryteria, a w przeciwnym razie false.
Naturalnym sposobem oprogramowania jest tu umieszczenie definicji anonimowej
klasy wewnętrznej implementującej FilenameFilter w wyrażeniu new podanym
jako argument metody list. A ponieważ listowanie umieszczamy w jakiejś metodzie,
to ta anonimowa klasa będzie lokalną klasą wewnętrzną.
Np.
void listJavaFiles(String dirName) { // argument - nazwa katalogu
File dir = new File(dirName); // katalog jako obiekt typu File
// listowanie z filtrowaniem nazw
// kryteria wyboru nazw podajemy za pomocę
// implementacji metody accept
// w lokalnej anonimowej klasie
String[] fnames = dir.list( new FilenameFilter() {
public boolean accept(File directory, String fname) {
return fname.endsWith(".java");
}
});
for (int i=0; i < fnames.length; i++) { // lista -> stdout
System.out.println(fnames[i]);
}
}
Jednak gdybyśmy chcieli określić rozszerzenie listowanych plików w jakiejś
zmiennej lokalnej metody (np. ext), to tę zmienną lokalną musielibyśmy zadeklarowac
ze specyfikatorem final.
Przypomnijmy, że słowo kluczowe final oznacza, że wartość zmiennej może być ustalona tylko raz i nie może potem ulegać zmianom.
Dlaczego takie wymaganie przy lokalnych klasach wewnętrznych?
Zauważmy: obiekt klasy wewnętrznej jest odrębnym bytem. Ma dostęp do pól
klasy otaczającej (elementów obiektu, na którym się opiera), ale tylko dlatego,
że wewnątrz zawiera referencję do tego obiektu. Przy tworzeniu obiektu klasy
wewnętrznej ta referencja jest zapisywana w "jego środku".
Jedynym sposobem by zapewnić dostęp do zmiennych lokalnych jest – analogicznie
– skopiowanie ich wartości "do środka" obiektu klasy wewnętrznej.
Gdyby więc wartości zmiennych lokalnych, do których odwołuje się klasa wewnętrzna
mogły się zmieniać, to mogłaby powstać niespójność pomiędzy kopią i oryginałem.
Dlatego zmiany są zabronione i konieczny jest specyfikator final.
Należy podkreślić, że parametry metody są również zmiennymi lokalnymi i wobec nich stosuje się tę samą regułę.
// Metoda listuje pliki z rozszerzeniem podanym jako drugi argument,
// z podanego jako pierwszy argument katalogu
void listFilesWithExt(String dirName, final String ext ) {
File dir = new File(dirName);
String[] fnames = dir.list( new FilenameFilter() {
public boolean accept(File dir, String fname) {
return fname.endsWith(ext);
}
});
for (int i=0; i < fnames.length; i++) {
System.out.println(fnames[i]);
}
}
Zmienne lokalne używane w anonimowych klasach wewnętrznych muszą być deklarowane jako
final
6.4. Anonimowe klasy wewnętrzne a domknięcia i tablice funkcji
Domknięcie
jest fragmentem kodu (zwykle funkcją), który może zawierać
swobodne zmienne oraz ma dostęp do środowiska, wiążącego te zmienne z
ich właściwym leksykalnym zakresem.
Między innymi domknięcia
pozwalają na traktowanie funkcji jak "normalnych obiektów" czyli np.
przekazywanie ich jako argumentów innym funkcjom (metodom). Przy tym
kod domknięcia ma efektywny dostęp do wszystkich zmiennych np.
zmiennych lokalnych bloku, w którym domknięcie jest deklarowane i pól
otaczającej klasy.
Domknięcia są dostępne w wielu
językach (np. Groovy lub Ruby) i znacząco ułatwiają
programowanie. W dotychczasowych wersjach Javy domknięć jeszcze nie ma,
ale Neal Gafter mocno już zaawansował projekt ich implementacji w Open
JDK i jest szansa, że pojawią się one w którejś z kolejnych wersji
Javy, wydawanych przez Sun.
Pewną namiastką domknięć może być jednak
w dotychczasowej Javie implementacja interfejsów w anonimowych
klasach wewnętrznych.
Zobaczmy to na przykładzie fragmentów kodów działających na tablicach (listach) napisów.
W
języku Groovy możemy napisać następujący kod, wypisujący wszystkie
napisy dłuższe od 2 znaków, następnie wszystkie napisy przekształcone
do dużych liter.
String[] napisy = ["Ala", "ma", "kota", "i", "psa" ];
napisy.each { txt -> if (txt.size() > 2) println(txt) };
napisy.each { txt -> println(txt.toUpperCase()); };
Tutaj
korzystamy z metody each, która iteruje po zestawie danych na rzecz
którego została wywołana, a jako argument ma domknięcie, stosowane
wobec każdego elementu zestawu. Domknięcie podajemy w nawiasach
klamrowych i ma ono postać { argument -> kod_do_wykonania }.
Argumentem przekazywanym domknięciu jest (w każdej iteracji) kolejny
element zestawu napisów.
W tym prostym przypadku ten sam efekt
możemy uzyskać w Javie stosując anonimową klasę wewnętrzną
implementująca interfejs, określający - jako metodę - kod do wykonania.
Obiekt takiej klasy będzie praktycznie swego rodzaju funkcją i obiekt
taki podamy jako argument własnej metody each (nie ma takiej w Javie,
ale zrobimy ją sobie w klasie dziedziczącej listę napisów).
interface StringOp {
void execute(String s);
}
class MyStringList extends ArrayList<String> {
public MyStringList(String ...args) {
for(String s : args) this.add(s);
}
public void each(StringOp func) {
for(String s : this) {
func.execute(s);
}
}
}
public class QuasiClosures {
public static void main(String[] args) {
MyStringList list = new MyStringList("Ala", "ma ", "kota", "i", "psa");
list.each( new StringOp() { // kod do wykonania podajemy jako obiekt anonimowej klasy wewnętrznej
@Override
public void execute(String s) {
if (s.length() > 2) System.out.println(s);
}
});
list.each( new StringOp() { // a tu inny kod, stworzony ad hoc
@Override
public void execute(String s) {
System.out.println(s.toUpperCase());
}
});
}
Wynik:
Ala
kota
psa
ALA
MA
KOTA
I
PSA
Oczywiście,
jest tu więcej pisania niż przy prawdziwych domknięciach, kod jest też
mniej elegancki. Niestety, zastosowanie anonimowych klas wewnętrznych
charakteryzuje się również licznymi restrykcjami (i to nie tylko
związanymi z dostępem do zmiennych lokalnych). Na prawdziwe domknięcia
w Javie przyjdzie więc jeszcze poczekać.
W sumie jednak już
teraz zastosowanie anonimowych klas wewnętrznych, implementujących
interfejsy, do przekazywania innym metodom fragmentu kodu do wykonania
jest w Javie użyteczne i nieraz wykorzystywane. Przykładem może być
metoda invokeLater z klasy SwingUtilities, która otrzymuje jako
argument obiekt klasy implementującej metodę run() z interfejsu
Runnable i zapewniająca wykonanie kodu tej metody w tzw. wątku obsługi
zdarzeń.
Na koniec dodac warto, że w wielu językach
programowania są dostępne tablice funkcji. W Javie można je symulować
za pomoca - a jakże - anonimowych klas wewnętrznych, Oto przykład.
public class FuncTab {
interface Func {
void func();
}
private void testFuncTab() {
// Tablica funkcji
Func[] tabfunc = {
new Func() {
public void func() {
System.out.println("Funkcja 1");
}
},
new Func() {
public void func() {
System.out.println("Funkcja 2");
}
},
new Func() {
public void func() {
System.out.println("Funkcja 3");
}
},
};
// Wywołanie funkcji z tablicy
tabfunc[0].func();
tabfunc[1].func();
tabfunc[2].func();
}
public static void main(String[] args) {
new FuncTab().testFuncTab();
}
}
Wynik:
Funkcja 1
Funkcja 2
Funkcja 3
7. Zaawansowane użycie wyliczeń (enum)
Typy wyliczeniowe (enum), wstępnie opisane w wykładzie "Klasy i obiekty
II", mają szereg ciekawych, dodatkowych możliwości. Opierają się one na
tym, że tak naprawdę wyliczenia są klasami, dziedzicżącymi
specjalną klasę z pakietu java.lang o nazwie Enum.
A zatem:
- można dodawać dowolną niemal funkcjonalność do
enum - to jest klasa i może mieć dodatkowe pola, konstruktory, metody,
- w definicji enum ze stałymi można wiązać
wywołania metod poprzez niejawne użycie anonimowych klas wewnętrznych i przedefiniowanie w nich metod,
- istnieją efektywne implementacje
zbioru elementów wyliczeniowych (EnumSet)
i mapy, w której kluczami są elementy enumeracji (EnumMap);
implemementacje
te są realizowane jako wektory bitowe (na typie long) albo tablice typu int[] i
w
większości przypadków dają dużą poprawę efektywności wobec normalnych
zbiorów
i map (w których zresztą też możemy używać elementów enumeracji)
Przykłady
Przykładowe programy pokazują jak można rozszerzyć
enumerację, traktując ją niemal jak zwykłą klasę.
Najpierw dodamy konstruktor, który wiąże ze stałymi wyliczeniowymi
(gazety) ich ceny.
Uwaga. Obiektów enumeracji nie można tworzyć za pomocą
wyrażenia new.
package kiosk1;
public enum Gazety {
Głos(1), Polityka(4.5), Gazeta(2.5);
Gazety(double p) { price = p; }
public double getPrice() { return price; }
private final double price;
}
Następny kod - przy okazji - pokazuje, że przy pisaniu nieco
większych programów
należy korzystać z komentarzy dokumentacyjnych - i jak z nich korzystać.
package kiosk1;
import java.util.*;
import static kiosk1.Gazety.*; // musi być nazwany pakiet!
import static java.lang.System.out;
public class Kiosk {
// Mapa gazet dostępnych w kiosku: klucz - gazeta, wartość - liczba egzemplarzy
private EnumMap<Gazety, Integer> map =
new EnumMap<Gazety, Integer>(Gazety.class);
public Kiosk() {
supply(Głos, 20);
supply(Polityka, 20);
supply(Gazeta, 20);
// co jest w kiosku:
// Jak łatwo i elegancko!!!
for (Gazety g : Gazety.values())
out.println(g + " - liczba egzemplarzy " + map.get(g));
// Sprzedajemy trochę
double income = 0;
income += sale(Głos, 2);
income += sale(Polityka, 10);
income += sale(Gazeta, 5);
income += sale(Głos, 2);
// Teraz w kiosku zostało
for (Gazety g : Gazety.values())
out.println(g + " - liczba egzemplarzy " + map.get(g));
// a uzyskany dochód ze sprzedaży
out.println("Dochód: " + income);
}
/**
* Dostawa q sztuk gazety g
* @param g - konkretna gazeta
* @param q - liczba sztuk w dostawie
*/
public void supply(Gazety g, int q) {
// metoda containsKey() zwraca true jesli w mapie jest podany klucz
if (map.containsKey(g)) q += map.get(g);
map.put(g, q);
}
/**
* sprzedaż q sztuk gazety g
* @param g - sprzedawana gazeta
* @param q - liczba sprzedanych sztuk
* @return wartość transakcji
*/
public double sale(Gazety g, int q) {
if (!map.containsKey(g)) return 0;
int n = map.get(g);
if (q > n) q = n;
map.put(g, n-q);
return q*g.getPrice();
}
public static void main(String[] args) {
new Kiosk();
}
}
Wynik działania programu:
Głos - liczba
egzemplarzy 20
Polityka - liczba egzemplarzy 20
Gazeta - liczba egzemplarzy 20
Głos - liczba egzemplarzy 16
Polityka - liczba egzemplarzy 10
Gazeta - liczba egzemplarzy 15
Dochód: 61.5
I bardziej zaawansowany przykład, korzystający z możliwości
znaczących rozszerzeń
funkcjonalności klas enum (w tym - z wiązania ze stałymi wyliczenia
przedefiniowanych
metod, które będą wywoływane na ich rzecz).
W nowej wersji "gazet" (konstruktor ma dwa parametry - cenę
hurtową i detaliczną),
z konkretnymi stałymi (gazetami) wiążemy także przedefiniowane metody
toString(),
tak by wydruk był jeszcze bardziej informacyjny.
package kiosk2;
public enum Gazety {
// W konstruktorze użyjemy dwóch parametrów
// Ze stałymi wyliczenia zwiążemy przedefiniowane metody toString
// tak, by wydruk był ładniejszy nieco
Głos(0.75, 1) {
public String toString() { return "Tygodnik \"Głos\""; }
},
Polityka(4, 4.5) {
public String toString() { return "Tygodnik \"Polityka\""; }
},
Gazeta(2, 2.5) {
public String toString() { return "\"Gazeta Wyborcza\""; }
};
Gazety(double wp, double rp) {
wholesalePrice = wp;
retailPrice = rp;
}
public double getRetailPrice() { return retailPrice; }
public double getWholesalePrice() { return wholesalePrice; }
private final double wholesalePrice;
private final double retailPrice;
}
Wprowadzamy nową enumerację, opisującą transakcje: dostawa
(SUPPLY) i sprzedaż (SALE).
package kiosk2;
import java.util.*;
// Dosyć funkcjonalna enumeracja
// opisuje możliwe transakcje (dostawa, sprzedaż gazet)
// a zarazem prowadzi rejestr magazynu i rejestr sprzedanych gazet
public enum Transaction {
// Elementy wyliczenia (konkretne operacje)
// Wiązemy z nimi ciała podklas, w których
// implementujemy metodę perform,
// która jest zadeklarowana na końcu jako abstrakcyjna
// Uwaga: w klasach enum elementy wyliczenia winny
// poprzedzać wszelkie rozszerzenia funkcjonalności
SUPPLY {
public void perform(Gazety g, int n) {
if (volume.containsKey(g)) n += volume.get(g);
volume.put(g, n);
}
},
SALE {
public void perform(Gazety g, int n) {
if (!volume.containsKey(g)) return;
int k = volume.get(g);
if (n > k) n = k;
volume.put(g, k-n);
sold.put(g, n);
}
};
public static int getVolume(Gazety g) {
if (!volume.containsKey(g)) return 0;
return volume.get(g);
}
public static int getSold(Gazety g) {
if (!sold.containsKey(g)) return 0;
return sold.get(g);
}
// Gazety - stan magazynowy
private static final EnumMap<Gazety, Integer> volume =
new EnumMap<Gazety, Integer>(Gazety.class);
// Gazety - sprzedane
private static final EnumMap<Gazety, Integer> sold =
new EnumMap<Gazety, Integer>(Gazety.class);
// Metoda perform musi być zsdeklarowana jako abstrakcyjna
// ona własnie jest w różny sposób implemetowana w ciałach podklas
// przypisanych stałym wyliczenia
public abstract void perform(Gazety g, int n);
}
No i nowa klasa Kiosk, która w pełni korzysta z dobrodziejstw obu
enumeracji
i w łatwy sposób uzyskuje sporą funkcjonalnośc pod względem
obliczeniowym
i prezentacyjnym.
package kiosk2;
import static kiosk2.Gazety.*;
import static kiosk2.Transaction.*;
import static java.lang.System.out;
public class Kiosk {
public Kiosk() {
// Sprowadzamy gazety
// możemy użyć dalej zdefiniowanej metody trans (może coś jeszcze będzie robić)
trans(SUPPLY, Głos, 20);
trans(SUPPLY, Polityka, 20);
trans(SUPPLY, Gazeta, 20);
// ale możemy też pisać tak:
SUPPLY.perform(Gazeta, 10);
SUPPLY.perform(Polityka, 10);
// co jest w kiosku i ile wydano na sprowadzenie gazet:
out.println("Po dostawie w kiosku są następujące gazety:");
double cost = 0;
for (Gazety g : Gazety.values()) {
int n = getVolume(g);
cost += g.getWholesalePrice() * n;
out.println(g + " - liczba egzemplarzy " + n );
}
out.println("Wydano: " + cost);
// Sprzedajemy trochę
SALE.perform(Polityka, 15);
SALE.perform(Głos, 10);
SALE.perform(Gazeta, 20);
// Co sprzedano i ile uzyskano?
out.println("Sprzedano gazety: ");
double income = 0;
for (Gazety g : Gazety.values()) {
int n = getSold(g);
income += g.getRetailPrice() * n;
out.println(g + " - liczba egzemplarzy " + n );
}
out.println("Dochód " + income);
out.println("Zarobek : " + (income - cost));
out.println("Zostały do sprzedaży:");
for(Gazety g : Gazety.values())
out.println(g + " - liczba egzemplarzy: " + getVolume(g));
}
/**
* Ew. można taką metodę sobie dodac
* Transakcja - zakup gazet do kiosku lub ich sprzedaż
* @param t - rodzaj transakcji (SUPPLY lub SALE)
* @param g - jaka gazeta
* @param n - ile sztuk
*/
public void trans(Transaction t, Gazety g, int n) {
t.perform(g, n);
}
public static void main(String[] args) {
new Kiosk();
}
}
Wynik działania programu:
Po dostawie w kiosku są następujące gazety:
Tygodnik "Głos" - liczba egzemplarzy 20
Tygodnik "Polityka" - liczba egzemplarzy 30
"Gazeta Wyborcza" - liczba egzemplarzy 30
Wydano: 195.0
Sprzedano gazety:
Tygodnik "Głos" - liczba egzemplarzy 10
Tygodnik "Polityka" - liczba egzemplarzy 15
"Gazeta Wyborcza" - liczba egzemplarzy 20
Dochód 127.5
Zarobek : -67.5
Zostały do sprzedaży:
Tygodnik "Głos" - liczba egzemplarzy: 10
Tygodnik "Polityka" - liczba egzemplarzy: 15
"Gazeta Wyborcza" - liczba egzemplarzy: 10