3. Java objektinis programavimas
Pereitame skyriuje susidūrėme su Java objektais kalbėdami apie kintamuosius ir klases. Programavimo objektinio orientavimo savybė yra tiek svarbi, kad net rašant paprasčiausias Java programas tenka minimaliai susipažinti su Java objektiniu programavimu.
Pradėsime nuo objektinio programavimo esmės išaiškinimo ir kokius privalumus tai teikia Javai. Mums teks susitarti dėl objektinio programavimo aprašyme naudojamos terminijos. Naudojant techninius objektinio programavimo terminus galėsime naujoje šviesoje nužvelgti pereitame skyrelyje aprašytas programas.
Objektinio programavimo privalumai.
Pastaruoju metu objektinis programavimas tapo bene plačiausiai vartojamu terminu programuotojų pasaulyje. Tačiau šio termino turinys toli gražu nevienareikšmiai suprantamas. Tarkime mes anksčiau sakėme, kad objektinis programavimas yra metodologija padedanti lengviau programuoti. Tačiau toks apibrėžimas primena paprastą ir plačiai naudojamą reklaminį triuką stengiantis įpiršti savo produktą. Pakartokime ką sužinojome pereitame skyrelyje apie Java objektinį programavimą.
Klasės yra Java objektinio programavimo pagrindas. Kiekviena klasė apibrėžia naują programuotojo sukurtą tipą, kuriame yra naudojami kintamieji ir apibrėžiami metodai. Klasė apibrėžia tipą, o to tipo (klasės) kintamuosius vadiname objektais. Objektai skiriasi nuo įprastų kintamųjų tuo, kad juose apibrėžiami metodai aprašantys kintamųjų keitimo taisykles. Neobjektinių programavimo kalbų paprogramės neturi lankstaus mechanizmo kaip reguliuoti paprogramės kintamųjų keitimą kitoms paprogramėms.
3-1 lentelėje pateikiami objektiniame programavime naudojami terminai.
Terminas | Apibrėžimas |
---|---|
Paketas | Vienetas apjungiantis Java klases į vieną grupę. |
Klasė | Duomenų tipo apibrėžimas, kuriame yra duomenys ir metodai (paprogramės). |
Metodas | Javoje vartojamas vardas vadinti paprogramės egzempliorių. |
Konstravimas | Programos vykdymo metu klasės kintamojo kūrimas. |
Egzempliorius, objektas, realizacija | Klasės tipo kintamasis, kuris buvo realizuotas. |
Panaudojimo modifikatorius | Aprašo kokia klasių aibė turi teisę keisti duotąjį klasės narį. |
Ar prisimenate private panaudojimo modifakatorių, kuris pasirodė pereitame skyrelyje? Šis modifikatorius apsaugoja kintamąjį nuo kitų klasių. Jei kintamasis yra private, jį gali pakeisti tik tos pačios klasės metodai. Kada atsiranda prasmė tokiai kintam0jo apsaugai? Pasižiūrėkime kokia kartais paradoksali situacija susidaro procedūriniame programavime.
Jums tikriausiai teko skaityti procedūrinio programavimo literatūroje, kad nerekomenduojama piktnaudžiauti globaliais kintamaisiais. Jei kintamasis paskelbtas global yra tikėtina, kad jis bus šaltinis sunkiai aptinkamos programos vykdymo klaidos. Reikalas tame, kad sunku atsekti kuriuo momentu ir kas pakeitė global tipo kintamojo reikšmę jums netinkamu būdu. Tokią klaidą dar sunkiau aptikti, jei programinį produktą jūs kuriate ne vienas ir atskirus modulius kuria keli programuotojai.
Tarkime programoje yra aštuonios paprogramės ir trys iš jų naudoja tą patį kintamąjį. Jeigu paskelbsite šį kintamąjį global, jis bus prieinamas visoms aštuonioms paprogramėms ir netyčia kurioje nors iš likusių penkių galėsite paskelbti global kintamąjį tuo pačiu vardu ir jį nepageidautinai likusioms trims pakeisti. Būtų žymiai natūraliau, jei šis kintamasis būtų global tik atžvilgiu trijų ir negalėtų būti paskelbtas global likusioms penkioms.
Java atveju mes tiesiog patalpiname šį kintamąjį į klasę, paskelbiame jį private ir toje pačioje klasėje aprašome tris metodus, kurie gali naudotis šiuo kintamuoju. Naudodamasis kas nors kitas ar mes patys po ilgo laiko būsime pamiršę programos detales, tačiau private kintamasis bus apsaugotas nuo neapgalvotų naujų programuotojų ir mūsų veiksmų.
Duomenų apsaugojimo mechanizmas remiasi hierarchija tarp kintamųjų panaudojimo. Keičiant apsaugotus kintamuosius tenka kreiptis į specialiai tuo tikslu parašytą klasės metodą. Tokia kintamųjų apsauga vadinama duomenų apgauba (angl. encapsulation).
Tarkime pavyzdžio paprastumo dėlei yra tik vienas apgaubtas duomuo. tegul jis yra sveikasis skaičius, nurodantis kiek atspausdinti tuščių eilučių. Jei rašytume analogišką paprogramę procedūrinėje kalboje, tektų tikrinti ar tas skaičius nėra neigiamas. Java atveju mes apgaubiame kintamąjį ir metodą vienoje klasėje ir dabar nereikia pergyventi, kad kintamasis taps neigiamu. Dalykas tas, kad tik klasės metodai gali keisti apgaubtą kintamąjį ir belieka tik parašyti metodus taip, kad jie neleistų kintamajam tapti neigiamu sveikuoju skaičiumi. Konkreti aprašytos situacijos realizacija galėtų būti tokia:
public class printLines { private int linesToPrint=0; public void printSomeLines() { for (int i=0;i<=linesToPrint;i++) { System.out.println("");} } public void setLinesToPrint(int j) { if (j>0) { linesToPrint=j;} } }
Analizuojant programą matyti, kad tik setLinesToPrint metodas gali keisti linesToPrint kintamojo reikšmę. Šis metodas ir pasirūpina, kad neigiamos reikšmės nebūtų priskiriamos.
Pasižiūrėkime kitą pavyzdį. Tarkime kokį nors masyvą naudoja keli metodai ir mums reikia saugoti kaip kito specialus einamąją masyvo elemento poziciją nurodantis indeksas. Taigi kiekvieną kartą perduodant masyvą jums reikės perduoti ir einamojo elemento indekso reikšmę. Paimkime labiau konkretų pavyzdį. Tarkime metodui perduodamas simbolių masyvas ir reikia pakeisti eilinėje einamojoje pozicijoje rastą simbolį c nauju simboliu d. Kreipiantis kitą kartą į tą patį metodą naujo simbolio c paiešką reikia pradėti nuo prieš tai rastos pozicijos. Procedūrinėje kalboje turėsite vargo su šiuo uždaviniu, nes sunku bus aprašyti masyvo ir einamojo indekso būseną. Javoje tai darysite paprastai:
public class replaceChars { private char myArray[]; private int curPos=0; public replaceChars(char someArray[]) { myArray=someArray;} public boolean replaceNextChar(char c, char d) { if (newPositionSet(c)) { myArray[curPos]=d; return true;} else {return false;} } private boolean newPositionSet(char c) { int i=curPos; while (i<myArray.length) { if (c==myArray[i]) { curPos=i; return true;} else {i++;} } return false; } public boolean atEnd() { return(curPos==myArray.length-1);} //subtract 1 because positions in an array //start at zero }
Taigi Java programos kodas gana trumpas. Atkreipkite dėmesį, kad newPositionSet metodas pažymėtas private. Tai primena analogišką paprastų kintamųjų apsaugą. Dabar mes apsaugojame vietoje kintamojo metodą. Tokia metodo apsauga garantuoja, kad kiti objektai negalės pasinaudoti newPositionSet metodu ir todėl niekas nesugadins einamojo indekso reikšmės.
Kokios problemos kiltų sprendžiant šį uždavinį su procedūrine kalba? Pirma - procedūrinėje kalboje nebūtų galimybės paskelbti myArray[] private tipo ir todėl juos galėtų sugadinti kitos procedūros. Antra - ta pati pastaba galioja ir einamojo masyvo elemento indeksui; kadangi procedūrinėje kalboje negalima apsaugoti kintamojo nuo pakeitimo, mes negalime būti tikri, kad kas nors neįsigilinęs į šią programos dalį sugadins tikrąją curPos reikšmę.
Taigi šis paprastas uždavinys procedūrinėje kalboje sunkiai sprendžiamas, o Java kalboje mes reikalingą kintamąjį ir metodą apgaubėme vienoje klasėje ir tai padėjo išspręsti problemą. Java privalumai dar labiau išryškėja, kai sprendžiame uždavinį kaip atlikti aprašytą simbolių pakeitimo operaciją vienu metu keliuose masyvuose. Java atveju tiesiog sukuriame keletą klasės egzempliorių ir su jais toliau dirbame:
replaceChars solutionl=new replaceChars("Java galinga!"); replaceChars solution1=new replaceChars("Aš išmoksiu Java!"); while (!soulution1.atEnd()) {solution1.replaceNextChar('a','x');} while (!solution2.atEnd()) {solution2.replaceNextChar('o','y');}
Korektiškai užprogramavę Java klasės metodus mes automatiškai gauname, kad einamoji pozicija neišeis iš masyvo ribų. Procedūrinėje kalboje tuo negalėtume būti garantuoti ir net jei einamoji pozicija patektų tarp masyvo elementų indeksų aibės, negalėtume garantuoti, kad einamoji pozicija yra korektiška. Daigiadimensinių masyvų atveju procedūrinės kalbos problemos greitai augtų, o Javoje tai nesukelia ypatingų problemų.
Aišku Java nėra šių idėjų pionierė, tą patį galima padaryti ir naudojant kitas objektinio programavimo kalbas. Paprogramės taip pat abstrakčiai aprašo paprogramėje nurodomą veiksmų seką. Galų gale sveikojo skaičiaus priskyrimas taip pat yra savotiška paprasta paprogramė atliekanti tam tikrą operacijų seką, tačiau objektinio programavimo atveju abstrakcijos laipsnis pakyla į kokybiškai naują lygmenį. Pagrindinis objektinio programavimo privalumas pasireiškia tuo, kad klasėje nurodomi santykiai tarp kintamųjų ir metodų gali būti paveldėti.
Pakartotinis panaudojamumas taikant paveldėjimą.
Kintamųjų ir metodų apgaubą (encapsulation) galime kartoti kiek norima kartų. Tačiau kaip pasielgti, jei susidūrėme su panašiomis užduotimis, kur matyti, kad programų kodai turėtų būti panašūs. Objektinio programavimo atveju tokiu atveju naudojamas paveldimumas inheritance, kuris ir praverčia siekiant efektyviai kurti programas skirtas panašiems uždaviniams spręsti.
Susipažinkime su paveldimumu praktiškai. Anksčiau pateiktame pavyzdyje replaceNextChar pakeisdavo pirmą sutiktą c simbolį nauju simboliu d. Tarkime dabar sugalvojome pakeisti ne tik pirmą sutiktą, bet turėti metodą, kuris keičia visus c simbolius simboliu d. Vienas naujos problemos sprendimų būtų papildyti anksčiau parašytą klasę nauju metodu ir išsaugoti ją tuo pačiu vardu. Tačiau toks sprendimas turi akivaizdžių trūkumų. Pirma - gal būt kitiems metodams pilnai pakanka paprastojo metodo varianto ir naujasis metodas liks daugeliu atvejų nereikalingu. Antra - gal būt kitam šaus į galvą šią klasę papildyti kitais metodais ir išsaugojimas tuo pačiu vardu iššauks sunkiai aptinkamas klaidas. Paveldimumas kaip tik čia ir praverčia, nes mes praplečiame anksčiau parašytą klasę nekeisdami jos pradinio varianto. Todėl tiems, kuriems bus reikalingas supaprastintas variantas naudosis pradiniu klasės variantu, o jei kas norės papildyti savais metodais - rašys savą klasės praplėtimo variantą. Kadangi klasės plėtiniai turi naujus vardus, nekils problemos dėl vardo dubliavimosi. Dabar pateiksime savo praplėtimo variantą:
class betterReplaceNextChar extends ReplaceNextChar{ public int replaceAllChars(char c, char d) { int i=0; while(!atEnd()) { replaceNextChar(c,d); i++;} return i;} }
Taigi dabar turime naują klasę betterReplaceNextChar, kuri paveldi ankstesniosios ReplaceNextChar visus kintamuosius ir metodus ir turi naują metodą. Paveldimumą mes jau naudojome ir anksčiau rašydami Java įskiepius ir dar ne kartą ateityje susidursime su paveldimumo pavyzdžiais.
Programų lankstumas (maintainability)
Mes jau pabrėžėme, kad objektinis programavimas suteikia papildomas galimybes pasinaudoti anskčiau parašytomis programomis (Java atveju - klasėmis). Sutikite, kad kompiuteriai ir jų architektūra labai greitai keičiasi. Vieną kartą padarytas kompiuteris yra nelankstus prisitaikymo prie naujų sąlygų prasme. Programinė įranga negali to sau leisti ir turi būti parašyta taip ir su tokiomis priemonėmis, kad ją nesunkiai būtų galima perkelti prie besikeičiančių kompiuterių.
Taip pat ir gretimoms probleminėms sritims rašomas programas turėtų būti nesunku modifikuoti. Pavyzdžiui algalapiams ir sodros dokumentams rašomos programos galėtų pasinaudoti viena kitos metodais. Kartais iš anksto sunku numatyti kuria kryptimi vyks pokyčiai. Tarkime pradžioje buvo madinga apjungti greta esančius kompiuterius, dabar komopiuterius serveriai apjungia į pasaulinį tinklą. Jūs patys galite tęsti pavyzdžių sąrašą be galo; sunkiai atrasite sričių, kad keleto metų bėgyje galėtume naudoti tą pačią programavimo įrangą. Keičiant programas mes norėtume kaip galima daugiau pasinaudoti senomis, mažiau reikėtų programuoti naujai. Tačiau čia iškyla pavojus, kad su laiku pasimiršo programų detalės ir senos programos gali šia prasme padaryti meškos paslaugą. Modernios programavimo kalbos kreipia didžiulį dėmesį į programinės įrangos lankstumo problemą.
Visos išvardintos objektinio programavimo savybės vienaip ar kitaip buvo naudingos programų lankstumui padidinti. Tiesiogiai tas pasakytina apie paveldimumo savybę. Ši savybė leidžia programas auginti panašiai kaip sniego rutulį: pradedame nuo mažo, suridename didesnį, dar didesnį ir taip toliau. Kintamųjų apsaugos mechanizmas daro kodą labiau suprantamą; skaitydamas jūsų programos kodą kitas programuotojas supras, kad private kintamieji skirti ne jam ir kad koncentruos dėmesį į atvirus kintamuosius. Metodų ir kintamųjų apgauba taip pat skaidrina programos suprantamumą. Iš pirmo žvilgsnio tai atrodo keistai, tačiau, prisiminus, kad apgauba iš esmės supaprastina programos kodo sudėtingumą, nesunkiai sutiksite, kad trumpos programos lengviau skaitomos.
Paminėsime vieną netiesioginių apgaubos mechanizmo privalumų. Kadangi kiti objektai gali bendrauti tik su viešais klasės kintamaisiais ir metodais, privačius kintamuosius ir metodus galimi keisti ir tobulinti laisvai ir dėl to neturės nepatogumų kitos klasės. Kad tai svarbu galima įsitikinti tokiu pavyzdžiu. Panagrinėkime 2000 jau praėjusią problemą. Siūlydami savo paslaugas šiai problemai spręsti programuotojai išeikvojo aibę laiko ir užsidirbo krūvą pinigų. Taip atsitiko todėl, kad senos programos nenaudojo objektinio programavimo ir iš esmės tą pačią problemą reikia spręsti kiekvienai atskirai procedūrine kalba parašytai programai.
Paanalizuokime metų apskaičiavimo klasę. Ji atrodo kiek keistai, tačiau metų išsaugojimui užimamos atminties taupumo prasme yra gana logiška.
public class Year { private byte decadeDigit; private byte yearDigit; public Year(int thisYear) { byte yearsSince1900=(byte)(thisYear-1900); decadeDigit=(byte)(yearsSince1900/10); yearDigit=(byte)(yearsSince1900-(decadeDigit*10));} public int getYear() { return decadeDigit*yearDigit;} //Other methods }
Tarkime šia klase naudojasi aibė kitų klasių. Artėjant 2000 krūva programuotojų sunerims, nes ši klasė duos neteisingą metų reikšmę. Tačiau jokios problemos šiuo atveju nėra. Dalykas tame, kad nekeisdami public tipo kintamuosius mes galime pataisyti klasę ir tuo pačiu nuimti galvos skausmą visiems mūsų klasės vartotojams perrašydami mūsų metų apskaičiavimo klasę taip:
public class Year { private byte centuryDigit; private byte decadeDigit; private byte yearDigit; public Year(int thisYear) { centuryDigit=(byte)(thisYear/100); int lastTwo=(byte)(thisYear-(centuryDigit*100)); decadeDigit=(byte)(lastTwo/10); yearDigit=(byte)(lastTwo-(decadeDigit*10)); } public int getYear() { return decadeDigit*yearDigit*centuryDigit;} //Other methods }
Dabar programa veiks korektiškai iki 12799 metų, o atėjus tam laikui galėsime vėl atlikti panašius pakeitimus.
Tarp kitko Javos API dalis skirta darbui su datomis (Date klasė) buvo parašyta
taip, kad jai negrėsė 2000 metų problema.
Susipažinkime su Java objektinio programavimo konkrečiomis detalėmis.
Klasių hierarchijos sąvoka svarbi kalbant apie paveldimumą. tarkime turime tris klases: Mom, Son ir Daughter. Savaime aišku, kad Son ir Daughter klasės paveldės Mom klasės kintamuosius ir metodus. Kodas atrodys maždaug taip:
class Mom { //declarations, definitions } class Son extends Mom ( //declarations, definitions } class Daughter extends Mom { //declarations, definitions }
Mes ką tik sukūrėme klasių hierarchiją. Panašiai daroma ir realiame pasaulyje, tik čia dažniausiai paveldima iš tėvo ir motinos.
3-2 lentelėje išvardyti terminai, naudojami hierarchijai apibūdinti. Mom yra pagrindinė klasė, į kuria remiasi kitos dvi klasės. Son ir Daughter yra Mom klasės poklasės (subclasses) ir Mom yra Son ir Daughter. superklasė (superclass).
Terminas | Apibrėžimas |
---|---|
Klasių hierarchija | Grupė klasių apjungta paveldimumo saitais |
Superklasė | Klasė, kurią praplečia kokia nors kita klasė. |
Poklasė | Klasė, kuri praplečia kitą klasę |
Pagrindinė (bazinė) klasė | Kokios nors klasių hierarchijos viršūnėje (neturinti sau superklasių) esanti superklasė |
Dabar galėsime naudoti šį žodyną kalbant apie Java klasių hierarchiją. Java kalboje kiekviena klasė turi tik vieną tiesioginę superklasę. Trumpai tai apibūdinama vienapaveldimumu. Kita vertus klasė gali turėti daug superklasių, tačiau tarp jų tik viena bus tiesioginė superklasė.
Tačiau jei kiekviena klasė turi vieną tiesioginę superklasę, kyla klausimas kas yra Mom klasės superklasė. 3-1 paveikslėlis iliustruoja šio klausimo atsakymą. Trumpai tariant visų klasių hierarchijos viršūnėje yra Object klasė ir jei nenurodytas paveldimumas, tai nutylimasis paveldimumas yra extends Object.
3-1 pav. Kaip mūsų klasių hierarchija įsikomponuoja į bendrą Java klasių hierarchiją.
Taigi mūsų hierarchijos pavyzdys ekvivalentus tokiam kodui:
class Mom extends Object { //declarations,definitions }
Kodėl tokia globali hierarchija naudinga? Atsakymas paprastas: kadangi žinome, kad visos klasės turi bendrą superklasę Object, jos metodus ir kintamuosius gali naudoti bet kuri kita klasė.
Object klasė apibrėžia equality metodą skirtą patikrinti ar dviejų klasių turinys vienodas. Taip pat Object klasėje yra realizuotas daugiagijiškumo (multithreading) savybės. Taip pat atpuola daugelio hierarchijų tarpusavio sąsajų problema, nes visos jos yra vienos globalios hierarchijos dalys. Naudinga ir tai, kad esame garantuoti, kad kiekviena klasė turi savo superklasę.
Kiekviena Java klasė turi tris iš anksto apibrėžtus kintamuosius: null, this ir super. Pirmieji du yra Object tipo. null žymi neegzistuojantį objektą, o this nurodo tą patį objekto egzempliorių. super nurodo į tiesioginę superklasę. Pasižiūrėkime pavyzdį.
null kintamasis
2 skyriuje "Java programavimo pagrindai" mes sakėme, kad bet koks klasę žymintis kintamasis turi būti inicijuojamas. Jei jis nėra inicijuotas, jo reikšmė lygi null specialiam kintamajam. null objektas neturi jokių kintamųjų ir metodų, todėl su juo negalime atlikti jokių manipuliacijų. Tačiau jūs ne kartą klysite savo programose ir bandysite panaudoti neinicijuotą objektą. Tuomet gausite standartinį pranešimą: NullPointerException. Pateiktas žemiau pavyzdys parašytas kiek nekorektiškai, nes ReplaceChars metodas naudojamas nepatikrinus ar jis nėra null:
public void someMethod(ReplaceChars A) { A.replaceNextChar('a','b');}
Čia ir būtų tokio panaudojimo pavyzdys, kurio vykdymas sustotų ir būtų gautas pranešimas NullPointerException:
ReplaceChars B; someMethod(B);
Kad programa nesustotų, teks tikrinti ar objektai nėra null. Jeigu klasės tipo kintamasis nėra null - galėsime juo naudoti, priešingu atveju - nesinaudosime, kad negauti NullPointerException klaidos pranešimą. Perrašykime pavyzdį naujai:
public void someMethod(replaceChars A) { if (A==null) { System.out.println("A is null!!!");} else ( A.replaceNextChar('a','b'); }
this kintamasis
Kartais jums reikės perduoti nuorodą į einamąjį objektą kitai programai. Tuo atveju ir naudojamas this. Grįžkime prie mūsų Son, Daughter ir Mom klasių pavyzdžio. Tarkime Son ir Daughter konstruktoriai naudoja Mom kintamąjį. Šiuo atveju ir pravers this kintamasis.
public class Son{ Mom myMommy; public Son(Mom mommy) { myMommy=mommy;} //methods } public class Daughter { Mom myMommy; public Daughter(Mom mommy) { myMommy=mommy;}
Kai Mom konstruoja savo Sons ir Daughters, jei reikės perduoti nuorodą ė save pačią. Praktiškai tai gali atrodys taip:
public class Mom ( Son firstSon; Son secondSon; Daughter firstDaughter; Daughter secondDaughter; public Mom() ( firstSon=new Son(this); secondSon=new Son(this); firstDaughter=new Daughter(this); secondDaughter=new Daughter(this);} //other methods}
Jei Mom konstruosime su
Mom BigMama=new Mom();
tai 3-2 paveikslėlyje gausime tokią klasių hierarchiją:
3-2 paveikslėlis. BigMama klasės šeima.
super kintamasis
Jums gana dažnai reikės pasiekti savo tėvo metodą, kurį savo klasėje perkrovėte nauju metodu. Tarkime jūs norite tėvo konstruktorių papildyti keliais naujais kintamaisiais. Čia ir pasitarnaus super kintamasis.
Grįžkime prie Mom, Son ir Daughter hierarchijos. Tarkime Mom apibrėžia cleanUpRoom metodą, kurio paskirtis atspindėta metodo pavadinime. Toliau tarkime Son nori turėti lygiai tą patį metodą, tik gale papildomai atspausdinti "Cleaned up my room!" pranešimą. Šioje situacijoje pravers super kintamasis.
public class Mom { //declarations, constructors public void cleanUpRoom() { //code for cleaning up room } //other methods) } public class Son { //kintamieji, konstruktoriai public void cleanUpRoom() { super.cleanUpRoom(); System.out.println("Cleaned up my room!!");} //other methods }
Negalvokite, kad super kintamasis pažymi naują egzemliorių. Todėl
super objektą nereikia inicijuoti su new. Super išsprendžia
vardo problemą, nes jūs negalite iš anksto žinoti kaip bus
pavadintas Mom klasės objektas ir todėl esate priversti
kreiptis į jį abstrakčiu super vardu.
Konstruktoriai, kaip ir metodai gali naudoti super kintamąjį. Pvz.:
public class SuperClass { private int onlyInt; public SuperClass(int i) { onlyInt=i;} public int getOnlyInt() { return onlyInt;} }
Mūsų poklasė gali panaudoti parašytą konstruktoriuje kodą panaudodama super kintamąjį:
public class SubClass extends SuperClass { private int anotherInt; public SubClass(int i, int j) { super(i); anotherInt=j;} public int getAnotherInt() { return anotherInt;} }
Naudojant super kintamąjį su konstruktoriais reikia atsiminti porą išimčių. Pirma, šį kintamąjį galite naudoti tik konstruktoriaus kamiene. Antra, jis turi būti panaudotas pačioje konstruktoriaus kamieno pradžioje.
Klasių inicijavimo pavyzdžių mes jau turėjome. New operatorius išleidžia klasės kintamąjį į gyvenimą ir tuomet klasės kintamąjį vadiname objektu arba egzemplioriu. Pasiaiškinkime kai kurias klasės inicijavimo detales. Mūsų pateiktuose pavyzdžiuose konstruktorius neturėdavo parametrų, t.y. tipinis objekto paskelbimas atrodydavo taip:
someClass A=new someClass();
Buvo ir pavyzdžių, kai konstruktorius turėjo kintamųjų. Konstruktorius yra tiesiog specifinis metodas, todėl yra leidžiama jo perkrova. Čia pateikiamas tokios perkrovos pavyzdys:
public class Box { int boxWidth; int boxLength; int boxHeight; public Box(int i) { boxWidth=i; boxLength=i; boxHeight=i;} public Box(int i, int j) { boxWidth=i; boxLength=i; boxHeight=j;} public Box(int i,int j, int k) { boxWidth=i; boxLength=j; boxHeight=k;} //other methods }
Jei konstruojant Box klasės objektą pateikiamas tik vienas parametras, laikoma, kad tai yra kubas; jei du parametrai - laikome, kad pagrindas yra kvadratas, o antras parametras nurodo aukštį ir, galiausiai, kai į konstruktorių bus kreipiamasi pateikiant tris parametrus, jie bus panaudoti aprašant stačiakampio gretasienio pagrindą ir aukštį. Konstruktoriaus perkrova leidžia lanksčiai naudotis Box konstruktoriumi.
Java konstruktoriai kiek skiriasi nuo metodų kintamųjų pradinių reikšmių priskyrimo būdu. Jei Java klasė turi bent vieną tiesiogiai realizuotą konstruktorių, tai visi klasės konstruktoriai nepaveldi superklasės konstruktorių. Į tai reikia atsižvelgti numatant kokios bus pradinės klasės kintamųjų reikšmės.
Kalbėdami apie duomenų apsaugos privalumus pristatėme private modifikatorių. Šis modifikatorius leidžia naudoti kintamąjį ar klasę tik tos klasės viduje. Yra dar du kiti klasės metodų ir kintamųjų modifikatoriai, tai protected ir final. Kadangi su private jau susipažinome, pakalbėkime apie protected.
Protected modifikatorius.
Protected modifikatorius suteikia teisę naudotis kintamuoju arba metodu tik klasėms, kurios yra tame pačiame pakete. Kokiam paketui yra priskiriama klasė yra paskelbiama jos aprašymo pradžioje:
package somePackage;
Jei tiesiogiai nenurodytas klasės pavadinimas, ji patalpinama į nutylimąjį einamojo katalogo Java klasių paketą.
Jei jūs nenurodote tiesiogiai metodo ar kintamojo modifikatoriaus,
tai kompiliatorius leidžia juos naudoti einamojo katalogo klasių
nariams. Tačiau jei vėliau nuspręsite įtraukti klasę į paketą,
nutruks ryšys tarp einamojo katalogo klasių narių ir atsiras programos
derinimo sunkumų. Todėl rekomenduojama vengti nutylimojo modifikatoriaus
ir nuspręsti koks nenutylimasis geriausiai jums tinka.
Private Protected modifikatorius
Private protected modifikatorius buvo pasirodęs vienoje Java 1 versijų. Šie kintamieji ir metodai galėjo būti pasiekiami tik iš poklasės. Tačiau vėliau šio modifikatorius buvo atsisakyta. Šį sprendimą kalbos autoriai motyvuoja tuo, kad atmetus private protected modifikatorių likusieji sudaro nuoseklią seką: private labiausiai apsaugotas, nutylimasis mažiau, protected dar mažiau ir public yra laisviausiai prieinamo tipo. Šį teiginį iliustruoja lentelė
Modifikatorius | Tos pačios klasės nariai | To paties paketo klasės | Poklasės nariai | Visos kitos klasės |
---|---|---|---|---|
Private | Taip | Ne | Ne | Ne |
Taip | Taip | Ne | Ne | |
Protected | Taip | Taip | Taip | Ne |
Public | Taip | Taip | Taip | Taip |
Mes kiek plačiau susipažinome su dviem Java objektinio programavimo sąvokomis: duomenų apsauga ir apgauba. Dabar kiek plačiau pakalbėsime apie paveldėjimą. Paveldėjimas Javoje gali būti reguliuojamas naudojant abstrakčias klases ir metodus. Šie Java elementai naudingi kuriant Java klasių hierarchiją.
Klasių hierarchijos struktūrizavimas
Paveldėjimo privalumą mes grindėme tuo, kad jis leidžia neperrašinėti iš naujo klasės kodo ir visą jį paveldėti poklasėms. Tačiau tai tik viena naudinga paveldimumo savybė. Kitas paveldimumo panaudojimas yra susietas su klasių hierarchijos konstravimu.
Pirmame skyriuje mes nagrinėjome pavyzdį: "Nueik į parduotuvę ir nupirk pieno" ir tai aprašėme objektinio programavimo terminais. Panagrinėkime vieną šios problemos komponentę - "pieno paketus".
Tarkime mums reikia sukurti visą prekybos sistemą. Mes galime parašyti klasę, skirtą pienui. Bet ir pieno yra skirtingų rūšių, tarkime rūgpienis. Jei mes visas pieno rūšis apjungsime į vieną klasę, gausime tik vieną prekyboje naudojamų narių pavyzdį. Todėl sukurkime kiek platesnę klasių hierarchiją:
3-3 bėžinys. Pieno produktų klasės hierarchija.
Čia gera iliustracija kaip galima inicijuoti poklasę ir ją naudoti kaip superklasės kintamąjį. Mūsų pieno produktų atveju bus būtina pasirūpinti metodu, kuris praneš ar produktas tinkamas naudoti. Iliustracijai parašykime abstraktų kodą:
public class dairyProduct { //Kintamieji, konstruktoriai public boolean sourThisWeek() { //atitinkamas kodas } //other methods public void putOnSale() { //kodas pateikiantis produktą vartojimui }
Čia kaip ir kitais atvejais naudingas duomenų tipo pakeitimas (casting). Tarkime turėjome lowfatMilkType kintamąjį ir jį norime pakeisti dairyProduct tipu. Tai galima padaryti taip:
lowfatMilk M=new lowfatMilk(); dairyProduct D=M; if (D.sourThisWeek()) ( System.out.println("Nepirk");}
Kokia iš to nauda? Tarkime mūsų parduotuvės valdytojas nori patikrinti kokie pieno paketai suges šią savaitę. Tokius produktus reikėtų skubiai pateikti prekybai. Analogiškus veiksmus galėsite atlikti su lowfatMilk, Milk, Cheese ir Yogurt objektais panaudodami vieną ir tą patį metodą:
public void dumpSourGoods(dairyGood d) { if (d.sourThisWeek()) { d.putOnSale();} }
Jeigu nebūtume pirma sukūrę struktūrizuotos klasės, mums būtų tekę kiekvieno tipo produktui parašyti atskirą metodą.
Aprašytame pavyzdyje mes sukūrėme klasių hierarchiją, kuri buvo naudinga pritaikant tą patį metodą įvairioms produktų rūšims. Bet mūsų dairyProduct klasė turi metodus, kurių turinys neaprašytas. Kai mes juos rašėme turėjome omenyje, kad jie bus perkrauti poklasėse. Tačiau kitas programuotojas gali nesuprasti jūsų intencijos ir nerealizuoti reikiamų metodų. Kad išvengti tokių programavimo klaidų, Java programuotojams siūlo specialų abstract modifikatorių.
Kai panaudojate abstract modifikatorių, visos poklasės yra priverčiamos realizuoti abstrakčius metodus. Mūsų pavyzdžio dairyProduct klasę parašysime taip:
public class dairyProduct { //kintamieji, konstruktoriai public abstract boolean sourThisWeek(); //kiti metodai public abstract void putOnSale(); }
dairyProduct klasė vis dar gali būti inicijuojama kitose klasėse. Tačiau mes galime paskelbti ir ją abstrakčia, kad uždrausti jos tiesioginį inicijavimą:
public abstract myAbstractClass { //code }
Nors mes ir paskelbėme klasę abstrakčia, jos viduje galime aprašyti metodus ir kintamuosius. Šie metodai bus naudingi poklasėms, nes jos juos paveldės.
Polimorfizmas ir Java interfeisai
Polimorfizmu vadiname Java savybę, kuri suteikia galimybę grupei metodų naudoti tą patį metodą, tačiau kiekvienas metodo panaudojimas gali duoti skirtingus rezultatus. Tokia Java savybė yra grindžiama interfeisais. dairyGoods klasėje mes jau naudojome tokį polimorfizmą. Pavyzdžiui putOnSale ir sourThisWeek metodai bus apibrėžti visose hierarchijos klasėse ir kiekviena konkreti realizacija apibrėš kokiu principu nustatyti senstančius produktus ir kokiomis sąlygomis juos pateikti vartojimui.
Tačiau polimorfizmas yra kiek ribotas. Mes tik esame garantuoti, kad visos hierarchijos klasės turės metodus paskelbtus hierarchijos viršūnėje. Daugeliu atvejų poklasėms bus reikalingi metodai, kurie nereikalingi visai klasių hierarchijai. Pavyzdžiui, kadangi pienas ir jogurtas yra skysti, jiems reikės cleanUpSpill metodo. Tačiau tokio metodo paskelbimas sūriui būtų beprasmis.
Struktūrizuota klasių hierarchija nesugeba išspręsti šios problemos. Net jei mes apibrėžtume storeGood klasę virš visų klasių ir priverstume visas apibrėžti cleanUpSpill metodą, tai būtų neracionalu, nes daugeliui jis nereikalingas. Taigi mums reikia kokiu nors būdu paskelbti, kad tam tikram pogrupiui reikia realizuoti cleanUpSpill metodą, o likusiam - ne. Tokia Java priemonė vadinama interfeisu.
Apibrėžkime interfeisus, kurie pareikalaus skystiems produktams apdoroti jų galimą išsiliejimą.
interface spillable { public void cleanUpSpill(); public boolean hasBeenSpilled(); }
Iš pavyzdžio matyti, kad interfeiso metodai apibrėžiami analogiškai abstraktiems metodams. Jie ir turi būti abstraktūs, nes juos turi apibrėžti poklasės, kuri implementuoja interfeisą. Metodo realizacija interfeiso viduje yra neleidžiama. Pasižiūrėkime interfeiso panaudojimą mūsų klasės Milk aprašyme.
public class Milk extends dairyProduct implements Spillable { //kintamieji, konstruktoriai public boolean hasBeenSpilled { //atitinkamas kodas } public void cleanUpSpill { //atitinkamas kodas } //other methods }
Raktinis žodis čia yra implements. Jis ir paskelbiame, kad poklasės ar klasės viduje yra būtina realizuoti Spillable interfeiso metodus. Turėdami šiuos metodus Milk klasėje, mes galėsime jais naudotis. Interfeisai kaip ir klasės apibrėžia duomenų tipus. Todėl galime naudoti interfeisų kintamuosius; skirtumas tik tas, kad interfeiso tipo kintamojo negalima tiesiogiai inicijuoti. Pasižiūrėkime tokio panaudojimo pavyzdį.
class Milk M=new Milk(); Spillable S=(Spillable)M; if (S.hasBeenSpilled()) {S.cleanUpSpill();}
Tokiu būdu yra pasiekiama, kad klasių struktūroje tam tikri metodai būtų pareikalauti implementuoti ne visose, o tik tose poklasėse, kurioms jie yra prasmingi.
Klasėje leidžiama implementuoti daugiau nei vieną interfeisą. Pavyzdžiui galime apibrėžti gendantiems produktams tinkamą interfeisą. Mūsų Milk klasė turėtų implementuoti abiejų interfeisų metodus.
public class Milk implements Spillable, Perishable { //klasės realizacija }
Dar geriau būtų paskelbti Perishable implementaciją dairyGoods klasėje, nes visi produktai genda. tai atlaisvintų poklasių autorius kiekvieną kartą rašyti implements Perishable.
Šiame skyriuje susipažinome su aibe sąvokų ir koncepcijų. Mes supratome Objektinio Programavimo (OP) privalumus. Susipažinome su paveldėjimu, perkrova ir Java masyvais. 3-3 lentelėje reziumuosime aptartas OP sąvokas ir koncepcijas.
Koncepcija | Aprašymas |
---|---|
Klasė | Programuotojo kuriamas tipas, aprašantis duomenis ir metodus. |
Objektas, egzempliorius, inicijavimas | Klasės tipo kintamasis po jo inicijavimo. |
Duomenų apsauga | Būdas apsaugoti duomenis nuo kitų objektų. |
Apgauba (encapsulation) | Duomenų ir metodų grupavimas į paketą. |
Panaudojimo modifikatoriai | Java raktiniai žodžiai, naudojami paskelbti kokios kitos klasės gali naudoti duomenis ir metodus. |
Inicijavimas | Klasės tipo objekto sukūrimas. Inicijavimo metu sukuriamas klasės egzempliorius. |
Konstruktorius | Objekto inicijavimo metu kviečiamas kodas. |
Klasių hierarchija | Daugiasluoksnė diagrama atspindinti santykį tarp klasių. |
Paveldėjimas | Naujos klasės kūrimas paveldint super klasės metodus ir kintamuosius. |
Superklasė | Klasė, kurią paveldi duota klasė. |
Poklasė | Klasė paveldinti iš kitos klasės. |
Metodų perraša (overriding) | Metodų poklasėje perrašymas nauju turiniu, kuris keičia anksčiau apibrėžtą superklasėje algoritmą. |
Kad iliustruoti OP metodologiją pavyzdžiu, apibrėžkime klasių hierarchiją. Tegul grupės objektai būna skirt grafikos sistemai. Tarkime klientas parašė sukurti piešimo programą. Paveikslo elementus reikėtų judinti negadinant jų bendro vaizdo. pirmasis programos variantas turėtų būti sudarytas iš parastų piešimo elementų, bet ateityje sistema turėtų išsiplėsti į turtingą grafikos priemonę. jei demonstracinę versiją sukursime iki kitos savaitė, kontraktas mums garantuotas; kitu atveju - prasti popieriai.
Kadangi mes nežinome visų reikiamų formų piešti, teks pasitelkti OP programavimo priemones, kad sukurti kiek galima jo tobulinimui ateityje tinkamą kodą. Paveldimumas čia labai pravers. Svarbiausia teisingai apmąstyti klasių hierarchiją.
Prisiminkime interfeisų panaudojimą. Juos naudojome pareikalauti, kad grupė objektų tenkintų standartines savybes. Šią priemonę naudosime aprašant piešimo programos klases. Kiekviena forma turi realizuoti keletą būtinų metodų. Pirma - kiekviena forma turi būti nupiešiama kompiuterio ekrane, sugebėti save ištrinti iš ekrano ir keisti padėtį. Šiam reikalui aprašyti skirsime Shape interfeisą.
interface Shape { public void show(); public void hide(); }
Kad naują formą patalpinti ekrane, užtenka tik šių dviejų metodų. Taip pat apibrėšime abstrakčią klasę skirtą keisti formos vietą ekrane ir saugoti informaciją apie formos judėjimo trajektoriją.
abstract class BaseShape { protected int x,y; public void setPos(int newX, int newY) { x = newX; y = newY; } }
Jau turime bendrą visoms formoms interfeisą. Šį interfeisą turės realizuoti kiekvienos formos klasė. Visus kodus jungs baseShape klasė. Dabar liko tik realizuoti paprastas demonstracines formas.
Mes realizuosime stačiakampį ir apskritimą. Šioms formoms apibrėžti reikės atitinkamų duomenų ir metodų. Laikydamiesi duomenų apsaugos praktikos, paskelbsime juos private.
class Rectangle extends BaseShape implements Shape { private int len, width; Rectangle(int x, int y, int Len, int Width) { setPos(x,y); len = Len; width = Width; } public void show() { System.out.println("Staciamkampis(" + x + "," + y + ")"); System.out.println("Ilgis=" + len + ", storis=" + width); } public void hide() {} } class Circle extends BaseShape implements Shape { private int radius; Circle(int x1, int y1, int Radius) { setPos(x1,y1); radius = Radius; } public void show() { System.out.println("Circle(" + x + " " + y + ")"); System.out.println("Radius=" + radius); } public void hide() {} }
Dabar bus reikalinga formų piešimo programa. Kadangi mums reikės sugebėti keisti objektų poziciją ekrane, kiekvieną nupieštą formą turėsime išsaugoti atskirai. Formų grupė ir sudarys mūsų piešinį.
Čia susiduriame su tam tikra problema. Kokia duomenų struktūra gali dirbti su skirtingų tipų objektais? Paprasčiausias sprendimas būtų panaudoti masyvus, kurie Javoje leidžia saugoti bet kokio tipo duomenis. Mes paskelbsime masyvą objektų, kurie realizuoja formų interfeisą. tai leis mums kviesti visas formas nežiūrint į tai, kad nežinome jų tipų iš anksto. Taigi galime sukurti piešimo programą tinkančią piešti dar nesukurtom formom. Čia ir pasireiškia objektinio programavimo privalumai!
class testShapes { public static void main(String ARGV[]) { Shape shapeList[] = new Shape[2]; int i; shapeList[0] = new Rectangle(0,0,5,5); shapeList[1] = new Circle(7,7,4); for (i=0; i<2; i++) { shapeList[i].show(); } } }
Mes sukūrėme paprastą piešimo programą. Šią programą ateityje bus galima natūraliai papildyti kitomis formomis. Galite mus pasveikinti - kontraktą mes gavome!
Tikiuosi, kad susidarėte bendrą vaizdą apie objektinį programavimą. Kitame skyriuje daugiau dėmesio skirsime kalbos sintaksei. Dalis informacijos kartosis, kita dalis bus visai nauja. Tačiau OP samprata jums pravers įvardinant naują informaciją OP terminais.