Uvod u programiranje

Maja Čić


Nasljeđivanje, polimorfizam i abstraktne klase


Klasa predstavlja skup objekata sa zajedničkom građom i ponašanjem. Klasa određuje strukturu objekta navođenjem varijabli koje su sadržane u svim instancama klase i određuje ponašanje objekata preko metode instance koje izražavaju ponašanje objekata. Ovo je moćna ideja, ali nešto poput ovog se može postići u većini programskih jezika. Glavna novost objektno orijentiranog programiranja u odnosu na tradicionalno je da klase mogu izražavati sličnosti između objekata koji imaju zajedničke neke, ali ne sve dijelove strukture i ponašanja. Ovakve sličnosti mogu biti izražene pomoću nasljeđivanja (inheritance) i polimorfizma.


Naziv nasljeđivanje odnosi se na činjenicu da jedna klasa može naslijediti dio ili svu strukturu i ponašanje od druge klase. Klasa koja nasljeđuje zove se podklasa (subclass) klase od koje nasljeđuje. Ako je klasa B podklasa klase A onda ja klasa A nadklasa (superclass) klase B. Podklasa može nadopunjavati strukturu i ponašanje klase koju nasljeđuje, a može i zamijeniti ili izmijeniti naslijeđeno ponašanje, ali ne i naslijeđenu strukturu. Odnos između podklase i nadklase je često prikazan kao dijagram u kojem je podklasa prikazana ispod i povezana na nadklasu.

U Javi se može prilikom stvaranja nove klase daklarirati da je nova klasa podklasa postojeće klase. U slučaju klase B koja se definira kao podklasa klase A to se zapisuje ovako:

        class B extends A {
            .
            .  // dodaci na, i izmijene
            .  // onog što je naslijeđeno od klase A
            .
        }


Više se klasa može deklarirati kao podklase iste nadklase. Podklase (mogu se zvati i "srodne klase") imaju zajedničke dijelove strukture i ponašanja - one koje naslijeđuju od zajedničke nadklase. Nadklasa izražava ove zajedničke strukture i ponašanja. Na slici lijevo, klase B, C i D su srodne klase. Naslijeđivanje se može protegnuti preko nekoliko generacija klasa, što je prikazano na slici gdje je klasa E podklasa klase D koja je, opet, podklasa klase A. U ovom slučaju, klasa E smatra se podklasom klase A iako joj nije izravna podklasa.


Razmotrimo primjer programa koji prati registracije motornih vozila. Program koristi klasu Vehicle koja predstavlja sva motorna vozila, a uključuje varijable instance poput registrationNumber, owner i metode instance poput transferOwnership(). Ove su varijable i metode zajedničke svim vozilima. Tri podklase klase Vehicle: Car, Truck i Motorcycle sadržavaju varijable i metode vezane uz određene vrste vozila. Klasa Car može dodati varijablu instance numberOfDoors, Truck klasa može dodati numberOfAxels, a Motorcycle klasa može imati logičku varijablu hasSidecar.

Deklaracija ovih klasa bi u Javi u grubim crtama izgledala ovako:

     class Vehicle {
        int registrationNumber;
        Person owner;  // (uz pretpostavku da je klasa Person već definirana)
        void transferOwnership(Person newOwner) {
            . . .
        }
        . . .
     }
     class Car extends Vehicle {
        int numberOfDoors;
        . . .
     }
     class Truck extends Vehicle {
        int numberOfAxels;
        . . .
     }
     class Motorcycle extends Vehicle {
        boolean hasSidecar;
        . . .
     }

Pretpostavimo da je myCar varijabla tipa Car deklarirana i inicijalizirana izrazom:

     Car myCar = new Car();

Uz ovakvu deklaraciju, program bi mogao pozivati myCar.numberOfDoors jer je numberOfDoors varijabla instance u klasi Car. Budući da klasa Car nasljeđuje klasu Vehicle, Car ima strukturu i ponašanje Vehiclea, to znači da također postoje i myCar.registrationNumber, myCar.owner i myCar.transferOwnership().

U stvarnom svijetu automobili, kamioni i motocikli su stvarno vozila, kao i u programu. Znači, objekt tipa Car, Truck ili Motorcycle je automatski i objekt tipa Vehicle. To nas vodi do važnog zaključka:

Varijabla koja u sebi može sadržavati poziv na objekt klase A,
može sadržavati i poziv na objekt koji pripada bilo kojoj od podklasa klase A.

Stvarni učinak ovog u našem primjeru je da objekt tipa Car može biti pridjeljen varijabli tipa Vehicle, tj. možemo pisati:

     Vehicle myVehicle = myCar;

ili čak:

     Vehicle myVehicle = new Car();

Nakon bilo kojeg od ovih izraza varijabla myVehicle sadrži poziv na Vehicle objekt koji je instanca podklase Car. Objekt "pamti" da je zapravo Car, a ne samo Vehicle. Podatak o stvarnoj klasi objekta je spremljen kao dio tog objekta. Čak je moguće ispitati pripada li objekt danoj klasi koristeći instanceof operator:

     if (myVehicle instanceof Car) ...

ovaj test određuje da li je objekt na kojeg pokazuje myVehicle stvarno Car.

Sa druge strane, ako je myVehicle varijabla tipa Vehicle, izraz pridjeljivanja:

     myCar = myVehicle;

nije dozvoljen jer myVehicle može pokazivati na druge tipove iz klase Vehiclea a ne samo iz klase Car. Slično kao kad računalo ne dozvoljava pridjeljivanje int vrijednosti short varijabli, jer svaki int nije nužno i short. Rješenje ovog slučaja je opet pretvaranje tipova. Ako slučajno znamo da se myVehicle uistinu odnosi na Car, možemo pretvoriti tip izrazom:

     myCar = (Car)myVehicle;

a čak se može pozvati i

     ((Car)myVehicle).numberOfDoors.

Pokažimo to na primjeru ispisivanja važnih podataka o vozilu:

     System.out.println("Podaci o vozilu:");
           System.out.println("Registracijski broj:  "
							 + myVehicle.registrationNumber);
           if (myVehicle instanceof Car) {
              System.out.println("Tip vozila:  Car");
              Car c = (Car)myVehicle;
              System.out.println("Broj vrata:  " + c.numberOfDoors);
           }
           else if (myVehicle instanceof Truck) {
              System.out.println("Tip vozila:  Truck");
              Truck t = (Truck)myVehicle;
              System.out.println("Broj osiju:  " + t.numberOfAxels);
           }
           else if (myVehicle instanceof Motorcycle) {
              System.out.println("Tip vozila:  Motorcycle");
              Motorcycle m = (Motorcycle)myVehicle;
              System.out.println("Ima:    " + m.hasSidecar);
           }

Uočite da pri izvršavanju programa računalo provjerava da li je pretvaranje tipova ispravno, tako ako se myVehicle odnosi na objekt tipa Truck, pretvaranje tipa (Car)myVehicle će proizvesti pogrešku.


Za još jedan primjer, razmotrimo program koji radi s oblicima prikazanim na slici: pravokutnicima (Rectangle), elipsama (Oval) i zaobljenim pravokutnicima (Rounded Rectangle) raznih boja.

Klase Rectangle, Oval, i RoundRect predstavljaju tri vrste oblika. Ove tri klase imaju zajedničku nadklasu Shape koja predstavlja zajedničke osobine svih triju elemenata. Klasa Shape može sadržavati varijable instance koje predstavljaju boju, položaj i veličinu oblika. Osim toga može sadržavati metode instance za mijenjanje boje, položaja i veličine oblika. Radnja poput promjene boje sastoji se od promjene varijable instance i ponovnog iscrtavanja oblika u novoj boji:

     class Shape {
       
           Color color;   // Boja oblika.  (Sjetite se da je klasa Color
                          // definirana u paketu java.awt. Pretpostavimo
                          // da je ova klasa uvedena.)
                          
           void setColor(Color newColor) {
                 // Metoda za izmjenu boje obliku.
              color = newColor; // promijeni vrijednost varijable instance
              redraw(); // ponovno iscrtaj oblik, novom bojom
           }
           
           void redraw() {
                 // metoda za iscrtavanje oblika
              ? ? ?  // koje naredbe bi stavili ovdje?
           }

           . . .          // još varijabli instance i metoda instance

       } // kraj klase Shape

Problem s metodom redraw() je u tome što se svaki oblik crta različito. Metoda setColor() može biti pozvana za bilo koji oblik, kako računalo može znati koji oblik treba nacrtati koji oblik treba nacrtati kad izvršava metodu redraw()? Mogli bi reći da se redraw() metoda izvršava tako da se od oblika traži da ponovo nacrta sam sebe jer svaki objekt zna kako to uraditi.

Zapravo, to znači da svaki oblik ima vlastitu redraw() metodu:

     class Rectangle extends Shape {
          void redraw() {
             . . .  // naredbe za iscrtavanje pravokutnika
          }
          . . . // moguće, još metoda i varijabli
       }
       class Oval extends Shape {
          void redraw() {
             . . .  // naredbe za iscrtavanje elipse
          }
          . . . // moguće, još metoda i varijabli
       }
       class RoundRect extends Shape {
          void redraw() {
             . . .  // naredbe za iscrtavanje pravokutnik zaobljenih kuteva
          }
          . . . // moguće, još metoda i varijabli
       }

Ako je oneShape varijabla tipa Shape, mogla bi se odnositi na objekt bilo kojeg od tipova Rectangle, Oval, ili RoundRect. Kako se vrijednost varijable oneShape mijenja u vrijeme izvršavanja programa tako se može odnositi na objekte različitih tipova. Prilikom svakog izvršavanja izraza

     oneShape.redraw();

poziva se ona redraw() metoda koja odgovara tipu objekta na koji se oneShape odnosi. Iz koda programa ne mora biti očito koji će oblik ova metoda iscrtati, jer to ovisi o objektu na kojeg se metoda u tom trenutku odnosi. Zanimljiv je i slučaj ako se izraz poput oneShape.redraw() nalazi u petlji, jer ako se oneShape mijenja za vrijeme izvršavanja petlje, taj će izraz pozivati različite metode i crtati različite oblike. Kažemo da je metoda redraw() polimorfna. Metoda je polimorfna ako radnja koju metoda obavlja ovisi o objektu na kojeg se primjenjuje. Polimorfnost je jedna od važnih osobina koje izdvajaju objektno orijentirano programiranje.

Možda će biti jasnije drugačije objašnjenje: Kod objektnog programiranja, pozivanje metode se može smatrati i slanjem poruke objektu. Objekt odgovara izvršavanjem prikladne metode. Dakle, izraz "oneShape.redraw()" je poruka objektu na kojega pokazuje oneShape. Buduću da objekt zna koje je vrste, onda zna i kako reagirati na tu poruku. S ove točke gledanja, objekti su aktivne jedinice koje primaju i šalju poruke, a polimorfnost je prirodni, zapravo i neophodni dio ovog načina programiranja. Polimorfnost znači samo da različiti objekti mogu različito reagirati na istu poruku.


Pri svakom pozivu redraw() metode, izvršava se metoda iz odgovarajuće klase, ovisno o vrsti objekta. Postavlja se pitanje: Što radi redraw() metoda u klasi Shape i kako je definirati?

Odgovor je: Treba je ostaviti praznu! Činjenica je da Shape predstavlja neodređeni oblik i da nema načina na koji bi se takav oblik mogao nacrtati. Moguće je nacrtati samo određene likove poput pravokutnika ili elipsi. Potrebno je da metoda redraw() bude definirana u Shape klasi da bi je se moglo pozivati u setColor() metodi klase Shape ili da bi se uopće moglo izvršiti izraz "oneShape.redraw()", jer je oneShape varijabla tipa Shape.

Zapravo, redraw() metoda iz klase Shape neće nikad biti pozvana, a i nema razloga kreirati objekte tipa Shape. Varijable tipa Shape mogu postojati, ali će objekti na koje će se odnositi uvijek pripadati nekoj od podklasa klase Shape. Kažemo da je Shape apstraktna klasa. Apstraktne klase su one koje ne služe stvaranju objekata, nego samo kao osnova za stvaranje podklasa, a služe samo da bi se izrazile zajednička svojstva svih podklasa.

Slično tome, može se reći da je redraw() metoda u klasi Shape apstraktna metoda, jer nije zamišljena da je se ikad pozove. Zapravo, ona i nema što raditi, jer se svo crtanje obavlja redraw() metodama podklasa Shape. Ona govori da svi Shape objekti razumiju redraw() poruku i određuje zajedničko sučelje za sve redraw() metode u potklasama Shape.

Klasa Shape i njena redraw() metoda su po svom značenju apstraktne, što se može zapisati i dodavanjem modifikatora "abstract" u njihovu definiciju. Kod apstraktnih metoda blok naredbi koje inače opisuju metodu zamijenjen točka-zarezom (;), a njihova primjena mora biti zadana u svim podklasama apstraktne klase. Apstraktna klasa Shape izgleda ovako:

     abstract class Shape {
       
           Color color;   // color of shape. 
                                     
          void setColor(Color newColor) {
                 // Metoda za izmjenu boje obliku.
              color = newColor; // promijeni vrijednost varijable instance
              redraw(); // ponovno iscrtaj oblik, novom bojom
           }
           

           abstract void redraw();
                 // apstraktna metoda -- mora biti definirana u odgovarajućoj podklasi

           . . .          // još varijabli i metoda instance

       } // kraj klase Shape

Kod ovakve definicije klase, postaje zabranjeno stvaranje bilo kakvih objekata koji pripadaju ovoj klasi, a ako bi pokušali računalo bi prijavilo grešku.


Svaka klasa u Javi ima svoju nadklasu, a ako ona nije definirana, automatski se pridjeljuje nadklasa Object. Klasa Object je predefinirana klasa iz paketa java.lang i jedina je klasa koja nema nadklasu.

Stoga su zapisi:

     class myClass { . . .

i

     class myClass extends Object { . . .

sasvim jednakog značenja.

Sve klase su, izravno ili neizravno, podklase klase Object. To znači da objekt bilo koje klase može biti pridjeljen varijabli tipa Object. Klasa Object predstavlja najopćenitija svojstva koja imaju svi objekti, bez obzira kojoj klasi pripadaju. Object je najapstraktnija od svih klasa.

Klasa Object koristi se kod rada s vrlo općenitim objektima. Na primjer, java ima standardnu klasu java.util.Vector koja predstavlja listu objekata tipa Object. Klasa Vector je vrlo pogodna jer Vector može sadržavati neograničen broj objekata i raste prema potrebi. Budući su objekti u listi tipa Object, lista zapravo može sadržavati objekte bilo kojeg tipa.

Program koji prati objekte tipa Shape koji su nacrtani na ekranu može spremiti te oblike u Vector. Recimo da se Vector zove listOfShapes. Oblik oneShape može biti dodan na kraj liste pozivanjem metode instance "listOfShapes.addElement(oneShape)", a uklonjen s liste pozivom "listOfShapes.removeElement(oneShape)". Broj oblika u listi se može dobiti pozivom "listOfShapes.size()", a i-ti objekt se poziva izrazom "listOfShapes.elementAt(i)". Potrebno je voditi računa o tome da ovaj poziv vraća tip Object a ne Shape. Budući je poznat tip objekata, moguće je izlazni tip objekta pretvoriti u Shape izrazom poput:

     oneShape = (Shape)listOfShapes.elementAt(i). 

Razmotrimo primjer ponovnog iscrtavanja svih olika u listi pomoću for petlje, u kojemu je lijepo prikazana polimorfnost i objektno orijentirano programiranje:

     for (int i = 0; i < listOfShapes.size(); i++) {
            Shape s;  // i-ti element liste, smatra se da je Shape
            s = (Shape)listOfShapes.elementAt(i);
            s.redraw();
         }

Korisno je razmotriti i aplet koji koristi apstraktnu klasu Shape i Vector u kojemu je spremljena lista oblika:

Sorry, your browser doesn't
support Java.

Pritiskom na jednu od tipki u dnu apleta, odgovarajući oblik će biti dodan u gornjem lijevom kutu apleta. Boja je određena izbornikom u donjem desnom kutu. Oblik se može pomicati po ekranu, a pri tom će zadržavati odnos naprijed-nazad s obzirom na druge oblike na ekranu. Oblik se može izvući ispred ostalih tako da se drži pritisnita tipka shift prilikom izabiranja tog oblika.

Jedina upotreba klase oblika je prilikom iscrtavanja oblika na ekranu. Nakon iscrtavanja, oblikom se upravlja kao s apstraktnim oblikom. Potprogram koji omogućava pomicanje oblika radi s varijablama tipa Shape. Kako se oblik pomiče, potprogram poziva metodu za iscrtavanje iz klase Shape, pa ne mora znati ni kako se iscrtava taj oblik ni kojeg je oblik tipa. Objekt sam odgovara za svoje iscrtavanje. Prilikom dodavanja novog oblika bilo bi dovoljno definirati novu podklasu klase Shape i dodati tipku na aplet, nikakve druge promjene u programu ne bi bile potrebne.

Pogledajte izvorni kod prikazanog apleta ShapeDraw.



[ prethodna stranica | Početak | sljedeća stranica ]