Programiranje je zapisovanje
navodil računalniku, ki so potrebna za izvajanje kakega opravila. Dandanes je
programska oprema vse obsežnejša in vse bolj zahtevna ter njen razvoj zmeraj
dražji. Od programske opreme zahtevamo veliko zanesljivost, saj programi
krmilijo zelo zapletene in drage sisteme, kjer je vsaka napaka lahko usodna. V
reševanju teh problemov moramo iskati motive za nastanek vedno novih možnosti
programiranja med katere spada objektno programiranje. To je danes v izjemnem
razmahu, ker s svojim modelom zmanjšuje kompleksnost nalog ter z zmožnostjo
ponovne uporabe že napisane programske kode omogoča hitrejši in cenejši razvoj
ter večjo zanesljivost.
Prednosti objektnega
programiranja so:
-
večja kakovost programov,
-
lažje vzdrževanje programov,
-
večja zmožnost spreminjanja programov in
-
ponovna uporaba programov.
objektno orientiran
jezik = objekti + razredi + dedovanje
|
Ključna pojma v objektnem
programiranju sta objekt (ang. object) in razred (ang. class). Če
pogledamo naokoli (pa naj bo to kar računalniška miza), lahko vidimo več
objektov. Pred vami je vaš delovni računalnik, torej objekt. Računalnik med
drugim vsebuje miško in tipkovnico, torej dva objekta. Tipkovnica je
sestavljena iz množice tipk, torej množice objektov...
Na vsako konkretno zadevo, na
vsak predmet, na vsako bitje lahko gledamo, kakor da je objekt.
Skupna značilnost vsem tem
objektom je, da imajo:
-
stanje,
-
obnašanje oz. vedenje.
V objektnem programskem jeziku
stanje predstavimo s spremenljivkami, obnašanje pa z metodami (funkcije in
procedure).
|
Za primer računalnika bi lahko
imeli naslednjo tabelo:
Stanje:
-
hitrost procesorja
-
količina RAM pomnilnika
-
prižgan ali ugasnjen računalnik
|
Obnašanje:
-
prižiganje računalnika
-
ugašanje računalnika
-
tiskanje dokumenta
|
To sedaj pretvorimo v spremenljivke
in metode:
Stanje bi ponazorili z
naslednjimi spremenljivkami:
int hitrostProcesorja; // hitrost procesorja v megahercih
int kolicinaPomnilnika; // količina RAM pomnilnika v megabajtih
boolean prizgan; // true, če je računalnik prižgan, sicer false
Obnašanje pa bi ponazorili z naslednjimi
metodami:
void prizgiRacunalnik(); // prižiganje računalnika
void ugasniRacunalnik(); // ugasni računalnik
void natisniDokument(imeDokumenta); // natisni dokument na tiskalnik
Zapis v javi:
class Racunalnik
{
// stanje (objektne
spremenljivke)
int hitrostProcesorja; // hitrost procesorja
v megahercih
int kolicinaPomnilnika; // količina RAM
pomnilnika v megabajtih
boolean prizgan; // true, če je
računalnik prižgan, sicer false
//
obnašanje (metode)
void PrizgiRacunalnik() // prižiganje
računalnika
{
prizgan=true;
}
void UgasniRacunalnik() // ugasni računalnik
{
prizgan=false;
}
}
Podan primer obnašanja in metode
pa niso vse, kar se definira v razredu. Običajno mu dodamo še metode s katerimi
povprašujemo po stanju:
int vrniHitrostProcesorja()
{
return(HitrostProcesorja);
}
stanje spreminjamo:
void NastaviHitrostProcesorja(int novaHitrost)
{
hitrostProcesorja=novaHitrost;
}
in metodo toString, ki pretvori objekt v niz (za predstavitev objekta):
String toString()
{
return
hitrostProcesorja+" "+kolicinaPomnilnika+" prizgan
"+prizgan;
}
Poleg tega ima vsak razred tudi
posebno metodo, ki jo imenujemo konstruktor.
Ta metoda ima ime enako imenu razreda. Namen konstruktorja je, da določi
vrednosti objektnim spremenljivkam (ne nujno vsem, ampak kar pač potrebujemo).
Ločimo osnovni konstruktor (brez
kakršnih parametrov) in dodatne konstruktorje. Konstruktorjev je lahko poljubno
mnogo, le razlikovati se morajo med seboj v parametrih, ki jih sprejmejo.
Primer osnovnega
konstruktorja za računalnik bi recimo bil naslednji:
Racunalnik() // osnovni
konstruktor (konstruktor brez parametrov)
{
hitrostProcesorja=2000;
kolicinaPomnilnika=512;
prizgan=false;
}
Iz zapisa je razvidno, da je ime
metode, ki predstavlja konstruktor enako imenu razreda, ta konstruktor določi
vrednosti spremenljivkam hitrostProcesorja, kolicinaPomnilnika in prizgan.
Te vrednosti se torej določijo
avtomatsko in nanje ne moremo vplivati. Lahko pa poskrbimo, da pri kreiranju
objekta vplivamo na določene vrednosti spremenljivk. Tiste, na katere želimo
vplivati, naštejemo kot parametre konstruktorja (torej parametre metode).
Dodatni konstruktor bi recimo
bil:
Racunalnik(int hitProc,
int kolPom) // dodatni konstruktor
{
//
parameter konstruktorja je hitrost procesorja in količina pomnilnika
hitrostProcesorja=hitProc;
kolicinaPomnilnika=kolPom;
prizgan=false;
}
Po definiciji konstruktor ne
vrača vrednosti.
Če konstruktorja ne definiramo
ga avtomatično kreira prevajalnik sam.
|
Deklaracija razreda Racunalnik ne ustvari objekta temveč je
le neke vrste šablona zanj (lahko bi tudi rekli nek novi "podatkovni"
tip). Spremenljivkam, ki po tipu ustrezajo takemu razredu pravimo objekti tega
razreda.
Objekte kreiramo z operatorjem new, ki mu posredujemo tip objekta in
morebiti še parametre s katerimi inicializiramo objektne spremenljivke. Po
dodelitvi prostora in inicializaciji operator new vrne referenco na objekt:
Racunalnik pentium=new Racunalnik(); // ustvarimo
objekt pentium
Racunalnik amd=new Racunalnik(2600, 512); // ustvarimo objekt amd

Faze življenjskega cikla java objektov so torej naslednje:
-
deklaracija
|
ImeRazreda imeObjekta;
|
-
kreiranje z new
|
new ImeRazreda(seznamParametrov);
|
-
deklaracijo in kreiranje lahko združimo
|
ImeRazreda
imeObjekta=new ImeRazreda(seznamParametrov);
|
-
uporaba objekta in proženje metod
|
imeObjekta.imeMetode(parametri);
|
-
uničenje (garbage collector)
|
|
objekt = podatki
+ metode za delo s podatki
|
Razred Racunalnik
class Racunalnik
{
// stanje
int hitrostProcesorja; // hitrost procesorja
v megahercih
int kolicinaPomnilnika; // količina RAM
pomnilnika v megabajtih
boolean prizgan; // true, če je
računalnik prižgan, sicer false
Racunalnik() // osnovni
konstruktor
{
hitrostProcesorja=2000;
kolicinaPomnilnika=512;
prizgan=false;
}
Racunalnik(int
hitProc, int kolPom) // dodatni konstruktor
{
hitrostProcesorja=hitProc;
kolicinaPomnilnika=kolPom;
prizgan=false;
}
//
obnašanje (metode)
void PrizgiRacunalnik() // prižiganje
računalnika
{
prizgan=true;
}
void UgasniRacunalnik() // ugasni računalnik
{
prizgan=false;
}
}
// Primer uporabe razreda
class Primer
{
public static void main
(String[] args)
{
Racunalnik ibm=new
Racunalnik(); //
ustvarimo novi objekt
ibm.hitrostProcesorja=1000;
ibm.kolicinaPomnilnika=256;
ibm.PrizgiRacunalnik(); // vključi
računalnik
// izpis
System.out.println("Hitrost
CPU: "+ ibm.hitrostProcesorja); // 1000
System.out.println("Hitrost
CPU: "+ ibm.kolicinaPomnilnika); // 256
// ustvarimo nov objekt z uporabo dodatnega konstruktorja
Racunalnik compaq=new
Racunalnik(2600, 512);
System.out.println("Hitrost
CPU: "+ compaq.hitrostProcesorja);
// 2600
System.out.println("Hitrost
CPU: "+ compaq.kolicinaPomnilnika); // 512
}
}
Izdelajmo razred Oseba, ki
naj predstavlja neko osebo z naslednjimi podatki: ime, priimek in spol ( "m"
naj predstavlja moški, "ž"
za ženski spol).
class Oseba
{
String ime;
String priimek;
String spol;
public Oseba() // osnovni
konstruktor
{
ime="";
priimek="";
spol="";
}
public Oseba(String i,
String p, String s) // dodatni konstruktor
{
ime=i;
priimek=p;
spol=s;
}
}
// Primer uporabe razreda Oseba
public class Primer
{
public static void
main(String args[])
{
Oseba a=new Oseba();
a.ime="Janez"; // vnos podatkov
a.priimek="Novak";
a.spol="m";
System.out.println(a.ime+"
"+a.priimek+" "+a.spol);
// izpis podatkov
// podatke vnesemo v dodatnem konstruktorju
Oseba b=new
Oseba("Petra", "Petrovič", "ž");
System.out.println(b.ime+"
"+b.priimek+" "+b.spol);
// izpis podatkov
}
}
Če dobro premislimo lahko hitro
ugotovimo, da ima razred Oseba majhno nedorečenost – kaj se bo namreč
zgodilo če za spol namesto znakov m oz. ž vnesemo katerikoli drug
znak?
Nič - saj razred ne preverja
vrednosti, ki jih vnesemo.
Razred bi lahko
"popravili" tako, da bi pri vnosu spola preverjali, če je ta
veljaven. Kako to naredimo bomo spoznali v naslednjem poglavju (javnost /
privatnost).
Razred Oseba bi običajno
uporabili v programu, kjer bi želeli obdelovati celo množico oseb. Za tak
primer bi lahko uporabili tabelo objektov:
class Oseba
{
String ime;
String priimek;
String spol;
public Oseba() // osnovni
konstruktor
{
ime="";
priimek="";
spol="";
}
public Oseba(String i,
String p, String s) // dodatni konstruktor
{
ime=i;
priimek=p;
spol=s;
}
}
// Primer uporabe tabele objektov
public class Primer
{
public static void
main(String args[])
{
Oseba[] a=new Oseba
[3];
// rezervirali smo pomnilnik za 3 objekte Oseba.
//
s tem objektov še nismo ustvarili, ampak le reference na njih
a[0]=new Oseba (); // ustvarimo novi
objekt
a[0].ime="Janez";
a[0].priimek="Novak";
a[0].spol="m";
a[1]=new Oseba
("Petra", "Kosec", "ž"); // ustvarimo novi
objekt
a[2]=new Oseba
("Nataša", "Petek", "ž"); // ustvarimo novi
objekt
// izpis vrednsoti vseh treh objektov
for
(int i=0; i<3; i++)
System.out.println(a[i].ime+"
"+a[i].priimek+" "+a[i].spol);
}
}
Ko enkrat določimo kaj so
objektne spremenljivke in kaj metode, se odločamo še o temu, kakšno naj bo
skrivanje elementov razreda oziroma kaj je javnega (public) in kaj
privatnega (private) značaja. Za nekatere spremenljivke ali metode
namreč želimo, da se lahko uporabljajo samo znotraj metod in na točno določenih
mestih, ne pa popolnoma poljubno.
Recimo da bi našemu razredu o
računalniku dodali metodo, kjer prižgemo računalnik:
void prizgiRacunalnik()
{
vklopiNapajalnik();
resetirajMaticnoPlosco();
preberiPodatkeIzBIOSa();
prizgan=true;
}
V metodi bi torej, če bi zares
prižgali računalnik, poklicali še tri metode (vklopiNapajalnik, resetirajMaticnoPlosco in preberiPodatkeIzBIOSa), ki pa imajo popolnoma privaten značaj. Z
drugimi besedami, če bodo te metode že kdaj poklicane, želimo da se to zgodi
samo v tem primeru, ne pa tudi recimo, da bi nekdo poklical metodo za
resetiranje matične plošče po tiskanju dokumenta.
Enako velja za spremenljivke; kar
ne želimo, da bi nekdo uporabljal izven metod, določimo da je privatnega
značaja.
Javnost oz.
privatnost spremenljivk in metod določimo z naslednjimi določili:
-
public tako deklarirane spremenljivke in metode so
dostopne vsepovsod,
-
private dostopne so samo v razredu, ki jih vsebuje,
-
protected dostopne so v razredu, ki jih vsebuje in v
vseh podrazredih.
Dopolnjen razred Racunalnik, v katerem upoštevamo javnost
oz. privatnost spremenljivk in metod, bi izgledal tako:
class Racunalnik
{
private boolean prizgan; // true, če je
računalnik prižgan, sicer false
public Racunalnik() // konstruktor
{
prizgan=false;
}
//
metode
private void vklopiNapajalnik()
{
...;
}
private void resetirajMaticnoPlosco()
{
...;
}
private void preberiPodatkeIzBIOSa()
{
...;
}
public void prizgiRacunalnik() // prižiganje računalnika
{
vklopiNapajalnik(); // klic privatne
metode
resetirajMaticnoPlosco();
preberiPodatkeIzBIOSa();
prizgan=true;
}
public void ugasniRacunalnik() // ugasni računalnik
{
prizgan=false;
}
}
Uporabimo sedaj razred
na primeru:
class Primer
{
public static void main (String[] args)
{
Racunalnik ibm=new Racunalnik();
ibm.prizgan=true; // !!! napaka !!!
}
}
Program
bo javil napako. Ker je spremenljivka prizgan
privatna jo lahko uporabljamo samo znotraj razreda v katerem je definirana. Če
bi bila javna (public)
bi jo v glavnem programu lahko spremenili na true, takšen objekt pa ne bi deloval tako,
kot smo predvideli v razredu Racunalnik.
Naš razred namreč zahteva, da se pri vklopu prvo izvršijo določene metode šele
nato se prizgan
postavi na true.
Torej računalnik lahko pravilno "prižgemo" samo z javno metodo prizgiRacunalnik().
Enako
kot za spremenljivke velja tudi za metode. Če želimo, da določene metode niso
vidne zunaj razreda, jih deklariramo kot privatne (private).
Definirajmo razred Cas, ki naj predstavlja trenutni čas
(ure in minute):
class Cas
{
private int ura;
private int minuta;
// osnovni konstruktor
public Cas()
{
ura=0;
minuta=0;
}
}
Ker
sta obe objektni spremenljivki privatni, sta za naše programe nedosegljivi. Če
želimo torej tak razred uporabljati mu moramo dodati tako imenovane get/set
metode. To so javne (public) metode s katerimi vračamo (get)
vrednosti privatnih objektnih spremenljivk ali jih nastavljamo (set). S
temi metodami običajno tudi preverjamo, če so podatki v mejah, ki jih
zahtevamo. (Npr.: ure 0..23, minute 0..59 itd.)
class Cas
{
private int ure;
private int minute;
public Cas() // konstruktor
{
ure=0;
minute=0;
}
// get metode
public int vrniUre()
{
return ure;
}
public int vrniMinute()
{
return minute;
}
// set metode
public void nastaviUre(int u)
{
if (u>=0 && u<24)
ure=u;
else
// vnos je napačen => vrnemo
napako ali ignoriramo vrednost...
}
public void nastaviMinute(int m)
{
if (m>=0 && m<60)
minute=m;
else
// vnos je napačen => vrnemo
napako ali ignoriramo vrednost...
}
}
Z uporabo
privatnih spremenljivk in metod v razredih (enkapsulacija) dosežemo, da se
naši objekti obnašajo tako, kot smo to predvideli v razredu.
|
Pri
objektih se pogosto srečamo s prekrivanjem metod (ang. overloading) - kar pomeni, da imamo več metod, ki imajo enako ime,
vendar različno signaturo (metode med seboj razlikujejo po tipih parametrov, ki
jih prejmejo. (Podobno zadevo smo srečali že pri konstruktorjih, ki imajo ravno
tako vsi enaka imena, razlikujejo se pa po parametrih, ki jih prejmejo).
public int sestej(int
a, int b)
{
return (a+b);
}
public int sestej(int
a, int b, int c)
{
return (a+b+c);
}
public double
sestej(double a, double b)
{
return (a+b);
}
Če
v razredu uporabimo več metod z enakimi imeni (prekrivanje) se morajo te
razlikovati med seboj po številu parametrov in/ali tipu parametrov, ki jih
prejmejo.
Tip,
ki ga metode vračajo, pri prekrivanju metod, ne igra nobene vloge.
|
Razredne
spremenljivke označimo s static in predstavljajo skupne podatke za vse
primerke določenega razreda, njihova inicializacija pa se izvrši samo enkrat.
Rezervirano
besedo static uporabljamo tudi za označevanje razrednih metod. Te lahko
uporabimo oziroma prožimo, četudi ne obstaja niti en primerek razreda, saj se
lahko na njih sklicujemo s pomočjo imena razreda.
Razredu
Oseba bomo dodali števec vseh ustvarjenih objektov (oseb).
class Oseba
{
String ime, priimek, spol;
static int st_oseb=0;
// razredna spremenljivka število oseb
public Oseba() // osnovni konstruktor
{
ime=""; priimek=""; spol="";
st_oseb++; // ob vsakem klicu konstruktorja povecamo število oseb za 1
}
public Oseba(String i, String p, String s) // dodatni
konstruktor
{
ime=i; priimek=p; spol=s;
st_oseb++;
}
// razredna metoda
static int getSteviloOseb()
{
return st_oseb;
}
}
//
Primer uporabe
public class Primer
{
public static void main(String args[])
{
// izpis števila oseb - na metodo se
sklicujemo kar s pomočjo imena razreda
System.out.println("Trenutno stevilo oseb: "+Oseba.getSteviloOseb()); // 0
Oseba b=new Oseba("Petra", "Petrovič",
"ž");
System.out.println(b.ime+" "+b.priimek+"
"+b.spol); // izpis podatkov
System.out.println("Trenutno stevilo oseb:
"+Oseba.getSteviloOseb()); // 1
}
}
Dedovanje
je eden od pomembnejših principov objektnega programiranja. V implementacijah
se nam lahko pojavi množica razredov, ki se med seboj le malo razlikujejo
(dopolnjujejo v kodi ali pa drugače implementirajo del kode) in jih je smiselno
zaradi urejenosti in pregleda ustrezno hierarhično urediti. Običajno se
uporabljajo postopki dedovanja (ang. inheritance),
kjer izpeljani razredi (in temu primerno potem tudi objekti) podedujejo
nekatere lastnosti, podatke in operacije od drugih razredov.
Na
primer, imamo nek razred A, predstavljen z določeno množico spremenljivk in
metod, zatem pa želimo napisati nov razred B, ki je v skoraj vsem enak, le da
ima še eno dodatno spremenljivko. Namesto, da bi ponovno pisali isto kodo in le
dodali eno vrstico zaradi te dodatne spremenljivke, lahko preprosteje določimo,
da ima razred B vse lastnosti in metode enake kot razred A, razlika je le
dodatna spremenljivka. Pravimo, da smo razred B izpeljali iz razreda A oziroma,
da je razred A nadrazred (superrazred) razredu B.
Primer
Spomnimo
se razreda Oseba, ki smo ga definirali na začetku in je predstavljal
neko osebo (ime, priimek in spol). Sedaj bi želeli ustvariti nov razred Student,
ki bo predstavljal podatke o poljubnem študentu: ime, priimek, spol ter zraven
še vpisno številko. Kot vidimo se razreda med seboj le malo razlikujeta, zato
je smiselno, da razred Student ustvarimo tako, da le razširimo razred Oseba,
ki je že narejen od prej:
class Oseba
{
String ime, priimek, spol;
public Oseba() // osnovni konstruktor
{
ime="";
priimek="";
spol="";
}
public Oseba(String i, String p, String s) // dodatni konstruktor
{
ime=i;
priimek=p;
spol=s;
}
}
class Student extends
Oseba
{
int vpisna_st;
Student(String i, String p, String s, int vs) // dodatni
konstruktor
{
super(i, p, s); // kličemo konstruktor nadrazreda Oseba
this.vpisna_st=vs;
}
}
//
Primer uporabe razreda Student
public class Primer
{
public static void main(String args[])
{
Student b=new Student("Petra", "Petrovič",
"ž", 12345);
// izpis podatkov
System.out.println(b.ime+" "+b.priimek+"
"+b.spol+" "+b.vpisna_st);
}
}
Razred
Student smo definirali tako, da smo v dodatnem konstruktorju prvo z
besedo super poklicali konstruktor nadrazreda Oseba s podatki
ime, priimek in spol; nato pa še vnesli podatek o vpisni številki v razred Student.
V javi izpeljavo iz
nadrazreda določimo z besedo extends
|
S
pomočjo rezerviranih besed this in super lahko dostopamo in se sklicujemo
na nadrazred ali pa na objekt sam in sicer:
-
this pomeni nanašanje na trenutni
primerek tega razreda
|
this.imeSpremenljivke;
this.imeMetode();
|
-
super pomeni nanašanje na nadrazred
|
super.ImeSpremenljivke;
super.ImeMetode();
|
Vsak
podrazred avtomatsko kliče osnovni konstruktor svojega nadrazreda, zato klic super
v konstruktorjih podrazreda uporabimo le, če kličemo dodatne konstruktorje
nadrazreda (tiste z argumenti).
|
V
javi so vsi razredi izpeljani iz razreda
java.lang.Object
|
Izpeljani
razredi lahko torej spremenljivkam in metodam, ki so jih podedovali, dodajo še
svoje lastne. Lahko pa tudi nadomestijo (ang. override) podedovane
metode tako, da jih na novo realizirajo.
Primer
class Krog
{
private double polmer; // polmer kroga
public Krog(double r) // konstruktor
{
polmer=r;
}
public void nastaviPolmer(double polmer)
{
this.polmer=polmer; // ker ima argument
metode isto ime kot objektna
} // spremenljivka (polmer) uporabimo besedo this
public double vrniPolmer()
{
return polmer;
}
public double vrniPloscino()
{
return 3.14*polmer*polmer; // vrne
ploščino kroga
}
}
class Odsek extends Krog
{
public Odsek(double r) // konstruktor
{
super(r); // kličemo konstruktor nadrazreda Krog
}
public double vrniPloscino(double kot) // Nadomestimo (overriding) metodo
{ // vrniPloscino
iz nadrazreda
return super.vrniPloscino()*kot/360;
// ploščina krožnega izseka - ploščino
} // izračuna
metoda iz nadrazreda Krog
}
Pri
dedovanju se večkrat srečamo s pojmom abstraktni razredi. Z abstraktnimi
razredi definiramo samo del implementacije in omogočimo, da izpeljani razredi
izdelajo specifično implementacijo. Uporabljamo jih predvsem pri posplošitvah,
kjer iz več razredov izpeljemo skupen nadrazred.
Npr.
geometrijski liki imajo vsi definirano ploščino, vendar za abstraktni pojem
lika ploščine ne moremo izračunati oz. definirati.
Primer:
abstract class Lik
{
abstract public double
ploscina(); // abstraktnaa metoda za izracun ploscine
}
class Kvadrat extends Lik // izpeljemo iz razreda Lik
{
private double a;
public Kvadrat (double a) // konstruktor
{
this.a=a;
}
public double ploscina() // konkretna metoda za izracun ploscine
{
return a*a;
}
}
class Test // testni program
{
public static void main(String[] args)
{
Lik
kv=new Kvadrat(5.5);
System.out.println("Ploscina je: "+kv.ploscina());
}