što bi značilo da drugi generički argument mora biti klasa koja nasleđuje prvi generički argument. 7.2.2. Džokerski parametri Generička klasa može da bude tip parametra neke metode. Tada se, međutim, javlja jedan problem. Posmatrajmo neku metodu m koja za parametar treba da ima objekat generičke klase GenClass opisane u prethodnoj tački. Ako bismo metodu zadali sa tip_metode m(GenClass
par) { ... }
NE!
za P se ne bi moglo ustanoviti da li se radi o generičkom parametru ili generičkom argumentu tj. o konkretnoj klasi koja se zove P. U takvim situacijama koristi se tzv. džokerski parametar označen simbolom ?, koji se stavlja na mesto generičkog parametra. Dakle, tip_metode m(GenClass> par) { ... }
DA!
Sada metoda m može za argument da primi objekat bilo koje klase koja konkretizuje klasu GenClass. Džokerski parametri mogu se ograničiti i odozgo i odozdo, s obzirom na relaciju nasleđivanja. U metodi tip_metode n(GenClass extends Q> par) { ... } kao argument pri pozivu n može se naći objekat klase koja konkretizuje klasu GenClass ili GenClass gde je X neka od klasa izvedenih iz Q. Slično, argument metode tip_metode p(GenClass super R > par) { 66
Radi se o tzv. "omotačkim klasama" (engl. wrapper), videti sledeću glavu.
7. Polimorfizam. Specijalne klase
179
... } može biti objekat klase GenClass gde je Y neka od natklasa klase R, s tim da se R isključuje (tj. ne može biti objekat klase GenClass). 7.2.3. Generičke metode Sve metode, nestatičke i statičke (uključujući i konstruktore), pored toga što slobodno raspolažu generičkim parametrima klase, mogu imati sopstvene generičke parametre. Odmah napominjemo da generički parametri pojedinačnih metoda nemaju nikakve veze sa generičkim parametrima klase, čak i ako se poklapaju po oznaci (što, dakle, treba izbegavati). Takođe, metode mogu biti generičke i u slučaju da sama klasa nije! Sintaksna oznaka generičnosti stavlja se ispred tipa metode. Na primer, metodu public static int genMethod(T x, V[] y) iz neke klase K pozivamo sa NE K.genMethod(a,b) K.genMethod(a,b) gde argument a ima tip NatKlasa, b je niz objekata tipa PotKlasa i PotKlasa nasleđuje NatKlasu. Uočimo:
prilikom poziva generičke metode generički argumenti se ne navode!
Generički argumenti određuju se automatski, na osnovu tipa argumenata pri pozivu metode. 7.2.4. Sirovi tipovi i brisanje Pod pojmom sirovog tipa (engl. raw type) podrazumeva se, u stvari, stari način za realizaciju generičnosti opisan na početku ovog odeljka (direktna primena polimorfizma). S obzirom na to da je realizacija generičnosti unutar jave ostala nepromenjena po uvođenju generičkih klasa, generičnost se može ostvariti na oba načina. Generičku klasu GenClass s početka tačke 7.2.1 možemo instancirati bez navođenja generičkih argumenata, dakle sa GenClass obs = new GenClass(“ovo je string”); s tim da se, naravno, prilikom korišćenja mora upotrebiti kast: String str = (String)obs.getOb(); Objašnjenje načina funkcionisanja generičkih klasa i njihove veze sa sirovim tipovima pruža postupak koji nosi naziv brisanje (engl. erasing). Da bi bio očuvan osnovni mehanizam za realizaciju generičnosti (inkluzioni+koercitivni polimorfizam), u postupak prevođenja uključen je segment koji modifikuje izvorni kod, tako da generički parametri i argumenti uopšte ne dospevaju u bajt-kod. Pomenuti deo postupka prevođenja jeste brisanje. U osnovi67 , radi se o jednostavnom postupku zamene generičkih parametara konkretnim klasama iz javine hijerarhije koji se izvodi tako što se generički parametar zamenjuje gornjom granicom ako je ima, odnosno klasom Object ako gornja granica nije navedena. Posmatrajmo jednostavnu klasu oblika public class K1
{ void met1(P x) { ... } P met2() { ... } 67
ali ne i u pojedinostima
7. Polimorfizam. Specijalne klase
180
}
U toku prevođenja ova klasa se biti modifikovana u public class K1 extends java.lang.Object { void met1(java.lang.Object x) { ... } java.lang.Object met2() { ... } }
i kao takva biće pretvorena u bajt-kod. Iz ovog primera vidimo i zašto svaka klasa nasleđuje klasu Object, direktno ili indirektno. Slično se razrešava i klasa sa generičkim parametrom koji je ograničen natklasom: public class K2 { void met3(Q x) { ... } Q met4() { ... } }
koja se razrešava sa public class K2 extends java.lang.Object { void met3(java.lang.String x) { ... } java.lang.String met4() { ... } }
Druga važna aktivnost je dodavanje eksplicitne konverzije u svim izrazima gde se pojavljuju generički argumenti. Recimo da je klasa K1 bila instancirana sa generičkim argumentom ConcreteClass gde je ConcreteClass konkretna klasa: K1 ob = new K1();
i da se u kodu nalazi naredba ConcreteClass z = ob.met2();
Ova naredba biće razrešena kastom ConcreteClass z = (ConcreteClass)ob.met2();
i to zato što metoda m2 vraća, ustvari, tip Object. 7.2.5. Generičke klase i nasleđivanje Generičke klase mogu nasleđivati i biti nasleđivane. Pritom, generička klasa može naslediti generičku klasu ili konkretnu klasu. Ono što, naravno, nije moguće jeste izvesti konkretnu klasu iz generičke iz prostog razloga što tada nema generičkog argumenta neophodnog natklasi. Prilikom nasleđivanja generički argument se prosleđuje natklasi (ako je generička). Evo jednostavnog primera: //Genericka natklasa public class GenSuperClass { protected T field; public void set(T field) { this.field= field; } public T get() { return field; }
7. Polimorfizam. Specijalne klase
181
} //Genericka potklasa public class GenSubClass extends GenSuperClass { public void showField() { System.out.println("Field value: "+field); } }
U potklasu je dodata metoda showField. Kako vidimo, nema nekih bitnih razlika u odnosu na nasleđivanje konkretnih klasa. Relacija instanceof važi i za generičke klase, s tim što se može proveravati samo pripadnost tipu generičke klase, što nije moguće za pripadnost tipu konkretizovane generičke klase. Neka je obsuper objekat natklase sa generičkim argumentom String, a obsub isto za potklasu konkretizovanu istim generičkim argumentom, tj. GenSuperClass obsuper = new GenSuperClass(); GenSubClass obsub = new GenSubClass(); Sledeće provere su moguće: obsub instanceof GenSuperClass> //vrednost je true obsub instanceof GenSubClass> //vrednost je true obsuper instanceof GenSubClass> //vrednost je false Primena instanceof na konkretizovanu generičku klasu nije moguća, zbog brisanja koje eliminiše generički argument: obsub instanceof GenSubClass //NE MOZE! I ostali mehanizmi vezani za nasleđivanje - posebno inkluzioni i koercitivni polimorfizam normalno funkcionišu sa generičkim klasama. 7.2.6. Ograničenja Korišćenje generičkih klasa podleže određenim ograničenjima. Ima ih više, a ovde ćemo se osvrnuti samo na najvažnija. Jedno smo već pomenuli:
statički članovi klase ne mogu koristiti generičke parametre, ali statičke metode mogu imati sopstvene generičke parametre
Takođe,
generički parametri ne mogu se instancirati
što je, uostalom, i za očekivanje jer se ne znaju parametri konstruktora. Ostala važnija ograničenja vezana su za nizove, koji i inače predstavljaju problem još od kada su se prvi put pojavili u procedurnom fortranu. 1. Ne može se kreirati niz konkretizovanih generičkih objekata. Neka je GenClass generička klasa. Ono što se ne može uraditi jeste GenClass g[] = new GenClass[100] //NE! ali se zato može pisati
7. Polimorfizam. Specijalne klase
182
GenClass> = new GenClass>[100] //DA
2.
jer je to isto što i sirovi tip. Niz generičkih vrednosti može se definisati, ali se ne može instancirati.
Ova druga stavka itekako može da pravi probleme za slučajeve generičkih nizova koji su polja ili lokalni objekti u metodama. Pošto je problem isti i iziskuje ista rešenja posmatraćemo slučaj generičkog niza koji je polje. Vratimo se na primer generičkog steka iz primera u odeljku 7.2.1. Ono što bismo želeli jeste Stack.java
Može! Definisanje reference „body“ na generički niz
//Genericki stek public class Stack { private int top, capacity; private T[] body;
Ne! Pokušaj instanciranja generičkog niza body
//Konstruisanje praznog steka public Stack(int capacity) { this.body = new T[capacity]; this.capacity= capacity; top= -1; } ... itd.
Za rešavanje ovog problema na raspolaganju su dve tehnike. Jedna je
realizovati generički niz kao parametar metode (u ovom slučaju konstruktora). Prilikom poziva dodeliti referencu na niz koji je argument (i čiji se, dakle, tip zna) referenci na generički niz.
U našem primeru, to bi izgledalo ovako: //Genericki stek public class Stack { private int top, capacity; private T[] body;
može
//Konstruisanje praznog steka public Stack(T[] body) { this.body= body; this.capacity= body.length; top= -1; } ... itd.
Drugo rešenje predstavlja kombinaciju generičke realizacije i eksplicitnog korišćenja inkluzionog i koercitivnog polimorfizma. Iako deluje rizičnije (u nekim slučajevima i jeste, jer nema provere tipa), ovo rešenje dobro funkcioniše kad god je generički niz skriven od klijenta. Ovu tehniku koristili smo u verziji datoj ranije: Stack.java //Genericki stek public class Stack { private int top, capacity; private T[] body; inkluzioni + koercitivni polimorfizam
7. Polimorfizam. Specijalne klase
183
//Konstruisanje praznog steka public Stack(int capacity) { this.body = (T[])new Object[capacity]; this.capacity= capacity; top= -1; } ... itd.
Slično bismo postupili i u slučaju da je generički niz lokalan za metodu m klase čiji je generički parametar P: public void m() { P[] gniz = (P[]) new Object[duzina]; ... itd. } Primer 7.4. Jedna od najvažnijih struktura podataka jeste tzv. red (red čekanja, engl. queue). U pitanju je linearna struktura u kojoj se pristupa prvom po redu elementu, uklanja se prvi, a dodaje se iza poslednjeg, slika 7.10.
pristup
uklanjanje
dodavanje
Slika 7.10. Red je dobio naziv red čekanja zbog jedinstvene osobine da se ranije uklanja onaj element koji je ranije i ušao u red. Ova osobina nosi naziv FIFO od First In First Out. Redovi se koriste kod prenosa podataka, u operativnim sistemima i u diskretnoj računarskoj simulaciji, u stvari svugde gde je neophodna njihova FIFO osobina. U ovom primeru napravićemo klasu SequentialQueue koja predstavlja sekvencijalno realizovan red čekanja. "Sekvencijalno realizovan" znači da se elementi upisuju u sukcesivne memorijske lokacije, u okviru unapred zauzetog segmenta memorije, zadatog kapaciteta. To znači da se ovakav red programski realizuje kao niz. Da bi se postigla generičnost, i to takva da u red može da se unese ma koji objekat, elementi niza, nazvanog elements, su tipa Object. Elementi se ređaju od nižih indeksa ka višim, tako da se očitava i briše prvi zauzeti element, a uklanja poslednji. Neka je dužina niza size. Ova realizacija ima jednu manjkavost koja bi, ako je ne uklonimo, bila dovoljna da se odustane od sekvencijalne realizacije. Radi se o pojavi tzv. "lažne prepunjenosti" reda. Posmatrajmo stanje reda prikazano na slici 7.11 gore. 0
1
2
size-1
...
a
b
getPosition
0
1
putPosition
2
size-1
...
a
getPosition
Slika 7.11.
b
c putPosition
7. Polimorfizam. Specijalne klase
184
Oznaka getPosition odnosi se na indeks prvog elementa, a putPosition na mesto iza poslednjeg elementa koje je predviđeno za upis pri izvođenju operacije dodavanja. Kako se vidi na istoj slici, na donjem crtežu, već posle jednog dodavanja elementa npr. c, poslednja raspoloživa lokacija u nizu je zauzeta, te bi novi pokušaj dodavanja rezultovao porukom o prepunjenosti memorijskog prostora! To, očigledno, nije tačno jer na drugom kraju reda ima mesta, samo se ona ne mogu popunjavati elementarnim algoritmom za dodavanje. Da bi se ovaj problem rešio, red se realizuje cirkularno (kružno) tako da se, kada se stigne do kraja memorijskog prostora, sa dodavanjem nastavlja na početku (naravno, ako ima mesta). Cirkularno rešenje prikazano je na slici 7.12 levo. Nažalost, ovo nije dovoljno jer se pojavljuje novi problem: (stvarno) popunjen red ne može se razlikovati od praznog reda jer u oba slučaja važi getPosition = putPosition. Ova situacija prikazana je na slici 7.12 u sredini i desno..
putPosition
pun red
getPosition
cirkularni red
prazan red
Slika 7.12.
Za ovaj problem ima više rešenja, a ovde ćemo opisati jedno, u svoje vreme prilično originalno. Ideja se sastoji u tome da se žrtvuje jedna lokacija u koju nije dozvoljen upis, što znači da je memorijski prostor reda za 1 lokaciju veći od stvarnog kapaciteta. Na taj način ostvaruje se razlika između praznog i punog reda. Dok je kod praznog reda i dalje getPosition=putPosition, pun red prepoznaje se po tome što su pozicije getPosition i putPosition susedne (u cirkularnom smislu), slika 7.13.
pun red
prazan red
Slika 7.13. Programski kôd klase ima sledeći izgled: SequentialQueue.java //Sekvencijalno realizovan red public class SequentialQueue { protected protected protected protected
T[] int int int
elements; size; getPosition; putPosition;
//Elementi //Velicina memorijskog prostora reda //Indeks sa kojeg se cita //Indeks na koji se upisuje
//Konstruktor SequentialQueue(int capacity) { size= capacity+1; elements= (T[])new Object[size];
7. Polimorfizam. Specijalne klase
185
getPosition= putPosition= 0; } //Upis elementa u red na mesto putPosition public void put(T element) { elements[putPosition]= element; putPosition= (putPosition+1)%size; } //Citanje elementa sa mesta getPosition public T get() { if(isEmpty()) return null; return elements[getPosition]; } //Uklanjanje elementa sa pocetka reda public T remove() { if(isEmpty()) return null; T el= elements[getPosition]; getPosition= (getPosition+1)%size; return el; } //Praznjenje reda public void clear() { getPosition= putPosition; } //Da li je red prazan? public boolean isEmpty() { return getPosition==putPosition; } //Da li je red pun? public boolean isFull() { return getPosition==(putPosition+1)%size; } //Kapacitet reda (za 1 manji od memorijskog prostora) public int capacity() { return size-1; } }
Klasa SequentialQueue realizovan je kao generička klasa, parametrizovana tipom (klasom) elemenata reda T. Pošto su elementi definisani tako da, posle brisanja, pripadaju klasi Object koja je bazna klasa svake klase u javi, standardne ili programirane, u red se može upisati objekat bilo koje klase K. Usput, videćemo u nastavku da se od baznih tipova int, double itd. mogu napraviti objekti, što znači da se u ovakve redove mogu upisati doslovno svi objekti i vrednosti svih tipova koji postoje u javi. Neka je obK objekat (ma koje) klase K. Nek aje q red konstruisan naredbom q = new SequentialQueue(100);
Upis objekta obK klase K vrši se direktnom primenom metode put, oblika q.put(obK);
7. Polimorfizam. Specijalne klase
186
Očitavanje elementa iz reda takođe se vrši direktno: obK= q.get();
7.2.7. Parametarski polimorfizam metoda Generičnost nije jedina vrsta parametarskog polimorfizma. Parametarski polimorfizam - inače, najstarija konstatovana vrsta polimorfizma - uočen je kod potprograma koji podešavaju ponašanje ne samo prema vrednosti argumenta nego i prema njegovom tipu. Inače, parametarski polimorfne metode (i potprogrami uopšte) više nisu deo onog što bismo nazvali visokokvalitetnim programiranjem. Primer 7.5. Pokazaćemo sada kako se na javi realizuje (parametarski) polimorfna metoda. Napravićemo metodu, baziranu na hijerarhiji trouglova iz primera 5.1, koja će biti u stanju da prepozna klasu argumenta (Trougao, PravougliTrougao, RavnostraniTrougao) i da uskladi svoje ponašanje sa tim. Pre nego što pređemo na realizaciju, valja napomenuti da mnogi autori ovakve metode i potprograme uopšte, ne smatraju delom dobrog stila programiranja, te ovu mogućnost ne treba preterano intenzivno koristiti. Metoda koju ćemo realizovati nosi naziv prikazTrougla, statička je i ima za zadatak da na ekranu prikaže osnovne podatke o trouglu-argumentu, pri čemu se ti osnovni podaci razlikuju u zavisnosti od vrste trougla. Za klasu Trougao to su dužine stranica, za pravougli trougao katete i hipotenuza, a za ravnostrani trougao stranica i visina. Parametarski polimorfizam ove metode postiže se kombinovanjem inkluzionog polimorfizma koji omogućuje slanje sve tri vrste argumenata, koercitivnog polimorfizma u vidu kasta, kao i relacionog operatora instanceof što omogućuje prepoznavanje klase argumenta koji je prosleđen metodi. Pošto metoda objašnjava samu sebe navodimo njen tekst bez suvišnog komentarisanja. public static void prikazTrougla(Trougao t) { if(t instanceof PravougliTrougao) System.out.println("PRAVOUGLI TROUGAO."+ " Kateta 1: "+((PravougliTrougao)t).kateta1()+ " Kateta 2: "+((PravougliTrougao)t).kateta2()+ " Hipotenuza: "+((PravougliTrougao)t).hipotenuza()); else if(t instanceof RavnostraniTrougao) System.out.println("RAVNOSTRANI TROUGAO."+ " Stranica: "+((RavnostraniTrougao)t).stranica()+ " Visina: "+((RavnostraniTrougao)t).visina()); else if(t instanceof Trougao) System.out.println("TROUGAO."+ " Stranica a: "+t.a()+ " Stranica b: "+t.b()+ " Stranica c: "+t.c()); System.out.println("Obim: "+t.obim()+ " Povrsina: "+t.povrsina()); }
Inače, bolje rešenje u objektnom smislu bilo bi da se, umesto jedne statičke metode, u svaku klasu uključi metoda za prikazivanje. Time bismo omogućili da se prikažu i objekti klasa koji će u budućnosti biti uključeni u hijerarhiju što nije moguće u navedenom primeru bez promene izvornog koda metode prikazTrougla.
7.3. APSTRAKTNE KLASE Neka je dat skup klasa geometrijskih figura u ravni, koji sadrži sledeće klase: Trougao, PravougliTrougao, RavnostraniTrougao, Pravougaonik, Kvadrat i Krug, slika 7.14.
7. Polimorfizam. Specijalne klase
Trougao
187
Pravougaonik
+obim():real +povrsina():real
+obim():real +povrsina():real
PravougliTrougao
RavnostraniTrougao
+obim():real +povrsina():real
+obim():real +povrsina():real
Krug +obim():real +povrsina():real
Kvadrat +obim():real +povrsina():real
Slika 7.13. Kako vidimo, neke među klasama su u vezi nasleđivanja, a neke nisu. Takođe - a to je ključno – sve imaju metode za računanje obima i površine (imaju, naravno, još metoda, ali ove su bitne za naše razmatranje). Neka je potrebno napraviti metodu koja za argument može da prihvati objekat iz bilo koje od navedenih klasa i koja manipuliše isključivo metodama obim i povrsina. To može, recimo, da bude metoda koja će na ekranu prikazati obim i površinu figure prosleđene kao argument. Pre svega, to apsolutno ima smisla, jer sve klase predstavljaju geometrijske figure u ravni i sve imaju metode za računanje obima i površine. Nažalost, već na prvom koraku, pri pokušaju da napišemo zaglavlje metode, nailazimo na prepreku: inkluzioni polimorfizam nije moguć jer klase nisu uređene u hijerarhiju sa korenom koji bi mogao da bude parametar naše metode. S druge strane, sasvim je logično da takvu klasu uvedemo, pošto klase sa slike 7.13 jesu srodne i sve predstavljaju geometrijske figure! Uvedimo, dakle, klasu Figura koja je bazna za klase Trougao, Pravougaonik i Krug, pa samim tim postaje koren hijerarhije, slika 7.14. Ovim ipak nismo rešili problem jer pri pokušaju da realizujemo klasu Figura opet nailazimo na prepreku, ovog puta znatno ozbiljniju s obzirom na sredstva kojima trenutno raspolažemo. Radi se o činjenici da metode obim i povrsina u klasi Figura ne možemo realizovati jer ne postoji formula za obim ili površinu proizvoljne geometrijske figure. Postoje samo formule za konkretne figure: trougao, pravougaonik, krug itd. Jedna od mogućnosti za prevazilaženje poteškoće mogla bi biti takva realizacija pomenutih metoda u kojoj se nalazi naredba za momentalno prekidanje programa, koja bi sprečila da se nepostojeća metoda izvrši. Ovo rešenje je moguće, ali nije dobro jer se greška pokazuje tek u toku izvršenja programa. Na sreću, java raspolaže boljim rešenjem68. Radi se o tzv. apstraktnim metodama i apstraktnim (Mejer ih naziva nepotpunim) klasama. Prvo, apstraktne metode su metode koje nemaju realizaciju te se, shodno tome, ne mogu aktivirati. Deklarišu se zaglavljem u kojem se među modifikatorima nalazi i modifikator abstract
Tako, apstraktne metode obim i povrsina u klasi Figura deklarišemo sa public abstract double obim(); public abstract double povrsina();
Sasvim logično, apstraktne metode ne mogu da se aktiviraju jer nema šta da se aktivira. One moraju biti redefinisane (upotrebljava se izraz operacionalizovane) što ukazuje na činjenicu da klasa koja sadrži apstraktne metode mora biti nasleđena. Takođe, klasa sa apstraktnim metodama ne sme se instancirati, jer bi odgovarajući objekat, zbog apstraktnih metoda, bio neupotrebljiv. Upravo od te činjenice se i polazi kada se u javi, dosta originalno, apstraktna klasa definiše kao 68
Nije jedina. Raspolažu i drugi razvijeni objektni programski jezici.
7. Polimorfizam. Specijalne klase
188
klasa koja se ne može instancirati.
Figura {abstract} +obim():real {abstract} +povrsina():real {abstract}
Trougao
Pravougaonik
+obim():real +povrsina():real
+obim():real +povrsina():real
PravougliTrougao
RavnostraniTrougao
+obim():real +povrsina():real
+obim():real +povrsina():real
Krug +obim():real +povrsina():r
Kvadrat +obim():real +povrsina():real
Slika 7.14. Treba odmah uočiti da apstraktne klase ne moraju imati ni jednu apstraktnu metodu. Dovoljno je u okviru naredbe class među modifikatore uvrstiti modifikator abstract
Apstraktne klase mogu imati kontrolu pristupa public ili default. Dakle, kostur apstraktne klase je oblika pristup abstract ime_klase { . . . . . }
Suština apstraktnih klasa sastoji se u njihovoj nameni. Apstraktna klasa, naime, služi za to da "prikupi" zajedničke osobine više srodnih klasa, ma i po cenu sadržavanja apstraktnih metoda, i da odigra ulogu korene klase hijerarhije u koju strukturišemo pomenuti skup klasa. Kao i sve druge klase, pored toga što može biti nasleđivana, može i nasleđivati i to bez ikakvih ograničenja. Apstraktne metode se u izvedenim klasama operacionalizuju, ali i konkretne metode mogu biti redefinisane apstraktnim. Apstraktne klase se u UML prikazuju tako što se uz naziv dopisuje tekst {abstract}, a isto važi i za svaku apstraktnu metodu ponaosob (videti klasu Figura na slici 7.14). Prikazaćemo programski kod klase Figura. Sticajem okolnosti, a ne zato što je to često, u klasi Figura nema polja i nema konkretnih metoda. Klasa Figura izgleda ovako: Figura.java public abstract class Figura { public abstract double obim(); public abstract double povrsina(); }
7. Polimorfizam. Specijalne klase
189
Čitalac može sam realizovati ostale, konkretne, klase sa slike 7.14. Primenom klase Figura u stanju smo da izradimo metodu za prikaz na ekranu obima i površine ma koje figure predstavljene hijerarhijom klasa sa slike 7.14 i to bez primene koercitivnog polimorfizma (kasta). public static void prikazObimPovrsina(Figura f) { System.out.println("Obim: "+f.obim()+ "\nPovrsina: "+f.povrsina); }
Još jedan primer mogao bi da bude računanje zbira površina figura smeštenih u niz figura sa nazivom nizFigura: public double zbirPovrsina(Figura nizFigura[]) { double s; for(s=0,int i=0;i
Apstraktna klasa ne može se instancirati, ali se sme koristiti referenca na nju, dakako u sadejstvu sa inkluzionim i koercitivnim polimorfizmom. Nekoliko primera vezanih za geometrijske figure: Figura f = new Figura(); //Ne sme! Klasa Figura je apstraktna Figura fig1,fig2; //Moze fig1 = new Trougao(3,5,6); //Moze. Dodela potomka pretku Kvadrat kv = new Kvadrat(10); fig2= kv; //Referenca fig2 pokazuje na kvadrat System.out.println(((Kvadrat)fig2).obim()); //Kast Pravougaonik pr= (Pravougaonik)fig2; //Moze
Primer 7.6. U primerima 6.2, 7.3 i 7.3A (CD i DVD plejeri i kasetofon) klase sa uređajima i klase sa medijumima uređene su u hijerarhije sa korenim klasama Player i Medium. Ove klase zaštićene su nivoom default da se ne bi mogle instancirati iz klijenta. To, međutim, znači da njihovi identifikatori nisu dostupni, pa je samim tim klijent lišen mogućnosti upotrebe polimorfizma. Ako bi, iz ma kojeg razloga, trebalo napraviti npr. niz medijuma sve tri vrste, to bi bilo neizvodljivo s obzirom na neophodnost upotrebe nedostupnog identifikatora Medium u definiciji tog niza. Mnogo je bolje omogućiti upotrebu referenci na obe pomenute klase, a istovremeno sprečiti njihovo instanciranje. Kao prirodno rešenje, u ovom slučaju, nudi se korišćenje apstraktnih klasa. Klase Player i Medium treba proglasiti apstraktnim (uočimo: nemaju apstraktnih metoda!) čime se omogućava primena referenci, ali ne i instanciranje. Delovi korigovanog koda klasa Player i Medium imaju sledeći oblik (prikazan je samo izmenjeni deo) //Korena klasa za plejere package players; public abstract class Player { protected boolean on; protected boolean loaded; protected boolean playing; //Korenaprotected klasa za medijume boolean paused; package players; public abstract class Medium { //Ukljuciti private String name; public void switchOn() { public Medium(String name) { on= true; this.name= name; }
7. Polimorfizam. Specijalne klase
190
Primer 7.7. U liniji nasleđivanja Kicmenjak Ptica Orao ZlatniOrao sve klase osim poslednje nužno su apstraktne, pošto instanca klase npr. Ptica (tj. objekat koji bi bio samo ptica i ništa više) u prirodi ne postoji. Postoje samo konkretni orlovi, nojevi, pingvini itd. Pri tom, u klasi Kicmenjak nalaze se zajedničke osobine svih kičmenjaka (npr. dužina kičme), u klasi Ptica osobine koje nemaju svi kičmenjaci, a imaju sve ptice (recimo, osobine vezane za perje) itd.
7.4. INTERFEJSI Interfejs je komponenta koja se prividno gotovo poklapa sa apstraktnom klasom bez ijedne konkretne metode. Međutim, semantički, a naročito pragmatski (tj. u pogledu primene) interfejs se znatno razlikuje od apstraktne klase. Pre svega, interfejs – nije klasa. Kao što i sam naziv sugeriše, interfejs je komponenta jave koja služi isključivo za spregu klase sa klijentom, te možemo usvojiti radnu definiciju: Interfejs je komponenta jave koja služi za spregu klijenta sa jednom ili više klasa povezanih sa njim specijalnom vrstom relacije. Dakle, interfejs je semantički nesamostalan ili, još preciznije, interfejs sam za sebe nema nikakvu funkcionalnost. Funkcionalnost, pa i semantika uopšte, ostvaruje se tek u klasi ili klasama za koje je interfejs vezan. Ma kako se na prvi pogled činilo, ova osobenost toliko je bitna da interfejse ni formalno ne ubrajamo u klase. Osobine interfejsa su sledeće: 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.
Nema nijednu konkretnu (operacionalizovanu) metodu. Svi članovi interfejsa su public, bez obzira na to da li to eksplicitno piše ili ne. Ako i imaju polja (retko) ta polja tretiraju se kao static final. Sami interfejsi, kao i klase, mogu imati nivo zaštite public ili default. Kao ni apstraktne klase, ne mogu se instancirati (što bi bilo posebno besmisleno budući da nemaju nikakvu semantiku). Omogućuju polimorfno pridruživanje u svim oblicima, što je, sa pragmatskog stanovišta, najvažnija osobina. Interfejsi se međusobno mogu vezivati relacijom nasleđivanja extends, ali između klasa i interfejsa ovu relaciju nije moguće uspostaviti. Interfejs se, poput klase, može upotrebiti kao desni operand relacionog operatora instanceof. Levi operand je i dalje objekat. Klase se dovode u vezu sa interfejsima specijalnom vrstom relacije, sa nazivom implements, koja podseća na nasleđivanje, ali nije ista. Interfejs može biti generički
U UML interfejs se prikazuje simbolom klase sa dodatim stereotipom <> koji se navodi iznad naziva interfejsa, slika 7.15. <> naziv ..... .....
Slika 7.15. Interfejs se definiše posebnom naredbom koja nije class, baš da bi se podvuklo da se ne radi o klasi. Naredba se zove interface i, osim po nazivu, ne razlikuje se od naredbe class: modifikatori interface naziv { //polja (retko)
7. Polimorfizam. Specijalne klase
191
//zaglavlja metoda }
Klasa se vezuje za interfejs vezom operacionalizacije (implementacije), sa nazivom implements, koja ima sintaksni oblik kao veza nasleđivanja extends, a u UML se prikazuje prema slici 7.16. <> I
K
Slika 7.16.
Ako klasa sa nazivom K ima ovu vezu sa interfejsom I (kaže se "implementira interfejs") na javi se piše modifikatori class K implements I { //opis klase }
Broj klasa koje mogu da implementiraju isti interfejs je proizvoljan. Međutim, a za razliku od nasleđivanja, dozvoljava se da klasa implementira proizvoljan broj interfejsa. Uopšte uzev, data klasa (konkretna ili apstraktna) može da nasledi najviše jednu klasu i implementira više interfejsa, što je dato na slici 7.17. K1
<> I1
...
<> In
K2
Slika 7.17. Šema veza sa slike 7.17 se na javi prikazuje ovako: class K2 extends K1 implements I1,...,In { . . . . . }
Kako funkcioniše jedan interfejs? Pre svega, on može da funkcioniše samo u zajednici sa konkretnom klasom što implementira sve metode interfejsa. Posmatrajmo par koji čine klasa K2 i bilo koji interfejs Ij sa slike 7.17. Neka interfejs čine apstraktne metode m1,...mk. Definišimo referencu na interfejs sa nazivom refIj tako što ćemo joj dodeliti objekat klase K2: Ij refIj = new K2();
U daljem, referenca refIj ponaša se u skladu sa pravilima inkluzionog polimorfizma koji, kako smo rekli, važi i za relaciju implements podjednako kao za relaciju extends. Preko reference mogu se aktivirati samo metode m1,...,mk čija su zaglavlja u interfejsu Ij pri čemu se aktiviraju
7. Polimorfizam. Specijalne klase
192
verzije iz klase K2. Sve u svemu, postigli smo to da se kroz referencu refIj "vide" samo metode m1,...,mk deklarisane u interfejsu Ij. U nastavku, razmotrićemo nekoliko tipičnih situacija u kojima se primenjuju interfejsi. 7.3.1. Višestruka realizacija istog modela klase U programiranju je vrlo čest slučaj da se jedna klasa, posmatrana na nivou modela, realizuje na više načina, gde svaki ima neke prednosti i nedostatke. Na primer, klasa koja modeluje red (videti primer 7.4) može se realizovati sekvencijalno kao u pomenutom primeru, ali i spregnuto gde elementi nisu na susednim memorijskim lokacijama nego su povezani putem adresa (referenci). Veza se uspostavlja tako što se u dati element reda, pored njegovog redovnog sadržaja, upisuje i referenca na sledeći element u redu, tako da lokacije na kojima se nalaze elementi više nisu od interesa jer se veza ostvaruje softverski, slika 7.18.
Slika 7.18. Ovako realizovan red ima prednost nad sekvencijalnim utoliko što se ne može prepuniti jer se memorijski prostor ne zauzima unapred i smatra se, s obzirom na kapacitet operativne memorije, da mesta za novi element uvek ima. Nedostatak je u tome što su u ovakvom redu operacije dodavanja i uklanjanja nešto sporije. Bez obzira na realizaciju, sekvencijalnu ili spregnutu, red ima iste operacije: get, put, remove, isEmpty i isFull. Razlika je samo u njihovoj realizaciji. Da bi se to postiglo, treba primeniti šemu veza prema slici 7.19, gde je Queu interfejs, zatim SequentialQueue malo modifikovana verzija klase iz primera 7.4 i LinkedQueue spregnuta realizacija koja će biti detaljno prikazana u odeljku 7.3.2. Celu strukturu treba smestiti u paket sa nazivom queues. <> queues::Queue
+put(element:T) +get():T +remove():T +clear() +isEmpty():boolean +isFull():boolean
queues::SequentialQueue
queues::LinkedQueue
Slika 7.19. Programski tekst interfejsa Queue izgleda ovako: Queue.java package queues; public interface Queue { void put(T element); //public se podrazumeva
7. Polimorfizam. Specijalne klase
193
T get(); T remove(); void clear(); boolean isEmpty(); boolean isFull(); }
U interfejsu se nalaze sve metode koje su relevantne za obe realizacije. Metoda za očitavanje maksimalnog broja elemenata za spregnutu realizaciju ima vrednost "proizvoljno velik" te se zato ne nalazi u zajedničkom interfejsu. Metoda isFull u klasi LinkedQueue uvek vraća vrednost false. Metoda put u sekvencijalnoj realizaciji može da se izvrši samo ako red nije pun i zato vraća indikaciju o uspehu. U spregnutoj realizaciji (klasa LinkedQueue) tih ograničenja nema i zato u spregnutoj varijanti metoda uvek treba da vrati rezultat true. Pošto su izmene u klasi SequentialQueue minorne (samo uključivanje u paket queues), dok ćemo klasu LinkedQueue razmatrati kasnije, dajemo sasvim kratak tekst realizacije: package queues; public class LinkedQueue implements Queue ... package queues; public class SequentialQueue implements Queue { protected T[] elements; protected int size; protected int getPosition; protected int putPosition; . . . . .
Redovi se instanciraju preko interfejsa naredbama tipa (K je konkretna klasa) Queue qseq = new SequentialQueue(100); //Sekvencijalni red Queue qlink = new LinkedQueue() //Spregnuti red
Sledeći segment u kojem se pojavljuje red q uopšte ne zavisi od toga da li je q sekvencijalni ili spregnuti red: if(!q.isFull()) q.put(e1); x= q.get(); q.remove();
//el je tipa K
7.3.2. Podešavanje interfejsa klase (nasleđivanje implementacije) Pomoću komponente interface može se podešavati interfejs već izrađene klase čiji izvorni kod nije na raspolaganju. Ovaj postupak se, inače, zove i nesleđivanje implementacije). Da bismo izbegli nesporazum podvucimo da interfejs kao javina komponenta i interfejs klase nisu isto: dok je prvo komponenta jave sa nazivom interface, interfejs klase je i dalje ono što smo ranije definisali: skup članova klase koji su dostupni klijentu. Dakle, interfejs ne spada u sadržaj klase, a interfejs klase jeste esencijalni deo tog sadržaja! Neka je data neka klasa K za koju izvorni kod nije na raspolaganju. Neka je, dalje, potrebno podesiti interfejs klase tako da sadrži metode m1,...,mn od kojih su neke dodate, a neke redefinisane. Ovo se, naravno, bez poteškoća rešava nasleđivanjem, tako što se formira klasa K1 extends K. Šta se, međutim, dešava ako klasa K sadrži metode koje u klasi K1 nisu potrebne? Java - a ni drugi objektni jezici vredni pomena - nema mogućnost odbacivanja (nepreuzimanja) metoda, jer bi to stvorilo velike probleme zbog inkluzionog polimorfizma.
7. Polimorfizam. Specijalne klase
194
Ostaje drugo rešenje: sprečiti pristup metodama koje su višak ili, još bolje, propisati precizno koje metode se mogu koristiti. To se ostvaruje definisanjem interfejsa I što sadrži samo potrebne metode i vezivanjem klase K1 za taj interfejs, slika 7.20. <> I +m1(...) +m2(...) ..... +mn(...)
K
K1
Slika 7.20. Kada definišemo referencu I refK = new K1();
preko reference refK mogu se aktivirati samo metode m1,...mn sadržane u interfejsu I, tj. dostupno je samo ono što treba i ništa više. Primer 7.8. Pokazaćemo kako se primenom upravo opisane tehnike, "u tri poteza" može napraviti jednostruko spregnuti red iz primera 7.4. Naime, u standardnom paketu jave sa nazivom java.util postoji gotova generička klasa LinkedList što sadrži sve metode potrebne za realizaciju spregnutog reda i još mnogo više. Metode od interesa za naš zadatak jesu sledeće:
metoda getFirst() za očitavanje prvog elementa liste metoda addLast() za dodavanje iza poslednjeg elementa liste metoda removeFirst() za uklanjanje prvog elementa liste metoda clear() za pražnjenje liste metoda isEmpty() za proveru da li je lista prazna.
Na slici 7.21 dat je dijagram klasa za realizaciju spregnutog reda čekanja. <> queues::Queue
java.util::LinkedList
+put(T element) +get():T +remove():T +clear() +isEmpty():boolean +isFull():boolean
queues::SequentialQueue
queues::LinkedQueue
Slika 7.21. Interfejs Queue.java dat je u odeljku 7.3.1. Klasa LinkedQueue izgleda ovako (uočimo primenu metoda iz klase LinkedList za realizaciju metoda u redu): LinkedQueue.java // Klasa red.
7. Polimorfizam. Specijalne klase
195
// Realizovan spregnuto nasledjivanjem klase LinkedList // i implementacijom interfejsa Queue. package queues; import java.util.LinkedList; public class LinkedQueue extends LinkedList implements Queue { public void put(T el) { addLast(el); } public T get() { return isEmpty() ? null : getFirst(); } public T remove() { return removeFirst(); } //clear() je vec implementirana u LinkedList! //isEmpty() je vec implementirana u LinkedList! public boolean isFull() { return false; } }
7.3.3. Više pogleda na istu klasu Ovde se radi o klasi za koju je vezano više interfejsa što omogućavaju više pogleda na istu klasu. Pokažimo odmah na primeru kako to izgleda. Već pomenuta klasa LinkedList iz paketa java.util može, istovremeno sa spregnutim redom, da implementira i interfejs Stack čime se, jednostavno kao i u slučaju reda, realizuje spregnuti stek. Za realizaciju spregnutog steka trebaju metode:
metoda getFirst() za očitavanje prvog elementa liste metoda push() za dodavanje ispred prvog elementa liste metoda removeFirst() za uklanjanje prvog elementa liste metoda clear() za pražnjenje liste metoda isEmpty() za proveru da li je lista prazna.
Što se tiče druge implementacije, spregnuto realizovanog reda, programski tekst je isti kao u primeru 7.8. Na slici 7.22 prikazan je dijagram klasa za spregnute varijante reda i steka. Iza slike, dat je kôd interfejsa Stack i klase LinkedStack. Stack.java // Interfejs za ma kakav stek. public interface Stack { void push(T element); T top(); T pop(); void clear(); boolean isEmpty(); boolean isFull(); }
7. Polimorfizam. Specijalne klase
<> queues::Queue
196
<> Stack
+put(element:T) +get():T +remove():T +clear() +isEmpty():boolean +isFull():boolean
java.util::LinkedList
+push(element:T) +top():T +pop():T +clear() +isEmpty():boolean +isFull():boolean
queues::LinkedQueue
LinkedStack
Slika 7.22. LinkedStack.java // Klasa stek. // Realizovan spregnuto // nasledjivanjem klase LinkedList i interfejsa Stack. import java.util.LinkedList; public class LinkedStack extends LinkedList implements Stack { //push(T element) je vec implementirana u LinkedList! public T top() { return getFirst(); } public T pop() { return removeFirst(); } //clear() je vec implementirana u LinkedList! //isEmpty() je vec implementirana u LinkedList! public boolean isFull() { return false; } }
7.3.4. Interfejs kao zamena za enumeraciju Kao čist objektni jezik, java nema ni jedan izvedeni tip podataka, pa tako ni enumeraciju. Iako potreba za ovim tipom u javi nije velika, ako se ukaže može biti zadovoljena interfejsom sa poljima koja odgovaraju konstantama enumeracije. Polja imaju istu prirodu kao simboličke konstante u C/C++, tj. predstavljaju tzv. "compile time" konstante što se još u toku prevođenja zamenjuju pridruženim vrednostima. Podsetimo se, sva polja u interfejsu su uvek public final, te se to može, a ne mora, napisati. Evo jednostavnog primera interfejsa u ulozi enumeracije sa nekoliko boja iza kojih stoje celobrojne vrednosti. Boja.java //Interfejs u ulozi enumeracije public interface Boja { int CRVENA =0;
7. Polimorfizam. Specijalne klase
int int int int int
PLAVA ZELENA ZUTA CRNA BELA
197
=1; =2; =3; =4; =5;
}
Vrednosti se koriste kao i sve statičke konstante, npr. Boja.CRVENA ili Boja.ZUTA. Inače, počev od verzije J2SE5, u javu je uvedena nova komponenta, koja nije klasa i nije interfejs, nosi naziv enum i ima osobine enumeracije snabdevene nekim metodama. 7.3.5. Interfejs kao filter Neka je m neka metoda koja treba da bude primenljiva na tačno određen skup klasa K1,...Kn koje međusobno nisu vezane relacijom nasleđivanja. Na koji način obezbediti da metoda m za argument prihvati sve objekte navedenih klasa i samo njih? Jedno rešenje bilo bi uvesti neku apstraktnu klasu koja je bazna za sve klase K1,...,Kn. Šta, međutim, ako neka od ovih klasa već eksplicitno nasleđuje? Pošto java nema višestrukog nasleđivanja, ovo rešenje u opštem slučaju nije moguće. Stoga ćemo, umesto nasleđivanja klase, upotrebiti implementaciju interfejsa gde se, videli smo, ne propisuje broj interfejsa koji se nasleđuju. Napravićemo, jednostavno, interfejs I (može biti i prazan, bez metoda) i u svaku klasu Kj koja pretenduje na to da bude argument metode m uključiti kvalifikator implements I
tako da se svaki objekat klase Kj ponaša i kao instanca interfejsa I. S druge strane, u parametre metode m uključićemo parametar tipa I, tako da ma koja instanca klase Kj j=1,...,n može da bude upotrebljena kao argument. Primer 7.9 Neka je Printer klasa koja modeluje sasvim jednostavan štampač sa samo dve metode: jedna, sa nazivom printString štampa string. Druga metoda sa imenom print treba da prikaže objekat koji je pripremljen za štampu time što obavezno sadrži metodu format za formatiranje izlaza (pretvaranjem u string). Kao filter što propušta u metodu print samo objekte klasa sa metodom format iskoristićemo interfejs Printable sa jednom metodom format (). Dijagram klasa prikazan je na slici 7.23 (uočiti vezu korišćenja između Printer i Printable. Trougao
<> Printable
<