Praktikum 6
Pärilus. Meetodite ülekatmine. Klass Object.
Teemad
Pärilus. Meetodite ülekatmine. Klass Object
. Polümorfism.
Pärast selle praktikumi läbimist üliõpilane
- oskab luua alamklasse;
- oskab kasutada ülemklassi meetodeid;
- oskab katta üle ülemklassi meetodeid;
- oskab kasutada võtmesõna
super
; - tunneb klasside hierarhiat ja teab klassi
Object
kohta selles; - on tuttav polümorfismi ja dünaamilise seostamisega.
Meeldetuletus: automaatkontroll
Ka seekord tuleb koduülesannetele automaatkontroll, seepärast tehke palun kõik klassid vaikepaketti ning Eclipse'i kasutamise korral kontrollige üle projekti kodeering. Täpsemalt lugege 4. praktikumi materjali algusest.
Pärilus
Javas (ja ka mitmetes teistes keeltes) asuvad klassid teatud ülemklasside ja alamklasside hierarhias. Tahtes olemasolevale klassile lisada võimalusi ja omadusi (meetodid, väljad), kuid seda klassi ennast mitte muuta, võime luua uue klassi, milles kirjeldame lisatavad meetodid ja väljad. Niiviisi loodud klassi nimetame alamklassiks (subclass, ka child class, extended class, derived class) ja klassi, millest lähtusime, ülemklassiks (superclass, ka parent class, base class). Sellist tegevust võib üldjuhul korduvalt jätkata. Ülemklassi omadused kanduvad üle tema alamklassidele. Vastavat mehhanismi nimetatakse päriluseks (inheritance). Pärilus on üks objektorienteeritud programmeerimise alustalasid, võimaldades loodud klasside taaskasutamist ja tekitades klasside hierarhilise struktuuri. Hierarhia juureks on klass java.lang.Object
, mis on kõigi teiste klasside otseseks või kaudseks ülemklassiks.
Eelmistes praktikumides loodud klasside puhul me ei märkinud kuidagi eraldi, et tegemist on klassi Object
alamklassidega, seda arvestati vaikimisi. Kui aga loome mõne teise klassi alamklassi, siis tuleb seda eraldi märkida. Näiteks märkimaks, et klass SorteeritudTabel
on klassi Tabel
alamklass, tuleb klassi nime järele lisada võtmesõna extends
ja ülemklassi nimi:
class Tabel { ... } class SorteeritudTabel extends Tabel { ... }
Kui klass peab korraga pärima ja liideseid realiseerima, siis tuleb märkida enne ülemklass ja seejärel realiseeritavad liidesed: class SorteeritudTabel extends Tabel implements Kuvatav { .. }
Päriluse kasutamiseks loo uus klass nagu tavaliselt, seejärel lisa klassi nime järele märksõna extends ja ülemklassi nimi. Kui ülemklass on märkimata, siis kasutatakse vaikimisi java.lang.Object
. Eclipse kasutades saab ülemklassi määrata ka klassi loomise dialoogi Superclass lahtris.
Alamklassis on tegelikult kättesaadavad kõik need ülemklassi muutujad ja meetodid, mille piiritlejaks ei ole private
. Kolmandas praktikumis lõime klassi Isik
, milles olid isendiväljad nime ja pikkuse jaoks ning hulk meetodeid. Püüame nüüd sellele klassile luua alamklassi Tudeng
, milles on (lisaks päritud isendiväljadele) sõnetüüpi isendiväli ülikooli märkimiseks.
public class Tudeng extends Isik { private String ülikool; }
Lisame ka konstruktorid, mida klassi Tudeng
isendi loomisel saab kasutada. Kui kasutame Eclipse'it, siis saame Source-menüüst kasutada nii Generate Constructors using Fields ...
kui ka Generate Constructors from Superclass ...
. IntelliJ puhul kasuta Code-menüüst Generate
valikut.
Kui genereerida võimaluse Generate Constructors using Fields ...
abiga, siis saame näiteks:
public Tudeng(String ülikool) { super(); this.ülikool = ülikool; }
(Täpne kuju sõltub, milline valik ülemklassi konstruktori suhtes tehakse. Võimaluste järjekord, millest esimene on vaikimisi nähtav, sõltub kontruktorite järjekorrast ülemklassis.)
Kui genereerida võimaluse Generate Constructors from Superclass ...
abiga, siis saame näiteks:
public Tudeng() { super(); // ei pea välja kirjutama, kui parameetreid ei kasuta }
või
public Tudeng(String nimi, double pikkus) { super(nimi, pikkus); }
Võtmesõnaga super
saame pöörduda ülemklassi konstruktori poole. Tingimuseks on, et see käsk (kui ta on vajalik), asuks konstruktori esimesel real (kommentaariridu arvestamata). (Ka võtmesõnaga this
sama klassi teise konstruktori väljakutsumine peab olema esimesel real.) Kui selliseid väljakutseid konstruktoris kirjas pole, siis toimub siiski pöördumine ülemklassi ilma argumentideta konstruktori poole.
Konstruktor, millele saaks ette anda nii nime, pikkuse, kui ülikooli, näeb välja selline.
public Tudeng(String nimi, double pikkus, String ülikool) { super(nimi, pikkus); this.ülikool = ülikool; }
Nüüd saame näiteks klassis TestIsik
klassi Tudeng
isendeid luua.
Tudeng t1 = new Tudeng("Eveli Saue", 1.63, "Tartu Ülikool"); Tudeng t2 = new Tudeng("Karel Tammjärv", 1.93, "Tartu Ülikool"); Tudeng t3 = new Tudeng("Tallinna Tehnikaülikool");
Ülesanne 1
Looge klassi Isik
alamklass Tudeng
vastavalt ülaltoodud programmilõikudele. Looge klassis TestIsik
mitmeid klassi Tudeng
isendeid ja katsetage nendega isendimeetodit suusakepiPikkus
.
System.out.println(t1.suusakepiPikkus());
Lisage get
- ja set
-meetodid, millega saab vaadata ja muuta tudengi ülikooli.
Meetodid. Ülekatmine
Nagu nägime, saab alamklassi isend kasutada ülemklassi meetodit. Koostame aga nüüd uue meetodi, mis on vaid klassi Tudeng
isenditele.
char hinne (int punkte) { if (punkte > 90) { return 'A'; } else if (punkte > 80) { return 'B'; } else if (punkte > 70) { return 'C'; } else if (punkte > 60) { return 'D'; } else if (punkte > 50) { return 'E'; } else { return 'F'; } }
Katsetame seda klassis TestIsik
.
Tudeng t1 = new Tudeng("Eveli Saue", 1.63, "Tartu Ülikool"); Isik a = new Isik("Juhan Juurikas", 1.99); System.out.println(t1.hinne(97)); System.out.println(a.hinne(87));
Näeme, et mittetudengist isiku puhul ei õnnestu hinnet määrata.
Vahel on vaja ülemklassist päritud meetodi puhul, et see töötaks alamklassi puhul teistmoodi kui ülemklassis. Sellist sama signatuuriga meetodi uuestikirjeldamist alamklassis nimetatakse meetodi ülekatmiseks (overriding). Kui midagi muuta pole vaja, ei ole mõtet alamklassi seda meetodit uuesti kirjutadagi. Aga meie muudame meetodit nii, et see kirjutaks (lisaks suusakepi pikkuse tagastamisele) ekraanile "Olen tudeng!".
int suusakepiPikkus() { System.out.println("Olen tudeng!"); return (int) Math.round(0.85 * pikkus * 100); }
Kui klassis Isik
on isendivälja pikkus
ees piiritleja private
, siis alamklassi meetod seda nii kasutada ei saa. Kui panna piiritlejaks protected
(või public
), siis saab.
Kui tahta ikkagi alamklassis kasutada seda meetodi varianti, mis on ülemklassis, tuleb kasutada võtmesõna super
.
int suusakepiPikkus() { System.out.println("Olen tudeng!"); return super.suusakepiPikkus(); }
Seda võib teha ka teistes alamklassi meetodites, kui on vaja:
int suusakeppidePikkusedKokku() { return 2 * super.suusakepiPikkus(); }
Ülesanne 2
Katke klassis Tudeng
üle need meetodid, mille 3. praktikumis klassi Isik
tegite. Proovige nende kasutamist.
Klass Object
Nagu öeldud, on kõigi teiste klasside (kas otseseks või kaudseks) ülemklassiks java.lang.Object
(vt. API
). Päriluse tõttu on igas klassis olemas tema meetodid, nt. toString
ja equals
. Tahtes ülemklassis kirjeldatud meetodit panna alamklassis käituma teisiti, peame meetodi üle katma. Tegelikult katsime klassi Object
meetodi toString
üle juba klassis Isik
, aga siis ei nimetanud me seda tegevust nii.
Meetod equals
võrdleb, kas kaks objekti on võrdsed: objekt1.equals(objekt2)
. Vaikimisi toimib equals
järgmiselt.
public boolean equals(Object obj) { return this == obj; // loodetavasti mäletad, et == võrdleb kahe objekti identiteete }
Aga näiteks klassi String
puhul on see nii üle kaetud, et võrreldakse hoopis sõnede sisu.
Ülesanne 3
Katke klassis Tudeng
üle meetod toString
nii, et seal kajastuks tudengiks olemine.
Polümorfism
Tuletage meelde eelmise praktikumi liideste polümorfismi. Kui klass realiseeris liidese, siis sai selle klassi isendeid kasutada igal pool, kus oodati liidese tüüpi objekti. Päriluse puhul toimib sarnane loogika: iga alamklassi isendit saab kasutada seal, kus on oodatud tema ülemklassi tüüpi objekti. Samas iga ülemklassi isend ei ole tema alamklassi tüüpi. Näiteks iga Tudeng on ühilduv Isik ja Object tüüpidega. Samas iga Isik ei ole Tudeng ja iga Object ei ole Isik (ega Tudeng). Klassil saab ju olla mitu alamklassi, nagu liidesel saab olla mitu realisatsiooni.
Mõned näited:
static void kasutaIsikut(Isik i) { ... } static void kasutaObjekti(Object o) { ... } Tudeng tudeng = new Tudeng(); Isik isik = tudeng; // toimib, sest Tudeng on Isiku alamklass Object obj = tudeng; // toimib, sest Isik on Object alamklass kasutaIsikut(tudeng); // OK kasutaObjekti(new Isik()); // OK, siia sobib kõik peale primitiivide Tudeng kavalpea = isik; // kompilaator kurdab: iga Isik ei ole Tudeng
Dünaamiline seostamine
Loome klassi Magistrant
, mis oleks klassi Tudeng
alamklass. Teeme hästi triviaalse klassi.
public class Magistrant extends Tudeng { }
Klassi isendi saame luua vaikekonstruktoriga.
Magistrant m1 = new Magistrant();
Kui nüüd kasutada meetodit m1.toString()
, siis näeme, et rakendus klassis Tudeng
kirjeldatud toString
meetod. Kuna klassis Magistrant
pole meetodit toString
üle kaetud, siis asutakse seda otsima tema vahetust ülemklassist. Kuna klassis Tudeng
on see meetod ilmutatult olemas, siis kasutatakse seda. Kui poleks olnud, siis oleks otsitud klassist Isik
, mis on klassi Tudeng
vahetuks ülemklassiks jne. Tegemist on dünaamilise seostamisega (dynamic binding).
Tähtis on mõista, et dünaamiline seostamine toimib sõltumata muutuja tüübist - tähtis on vaid see, mis klassi isendiga tegu on. Iga objekt mäletab täpselt, mis klassi isend ta on (see fikseeritakse isendi loomisel) ja teab sellest tulenevalt, kust ülekaetud meetodeid otsida.
Magistrant m = new Magistrant(); m.toString(); // käivitub meetod toString klassist Tudeng Object o = new Magistrant(); o.toString(); // hoolimata muutuja tüübist on tegu magistrandi isendiga. käivitub meetod toString klassist Tudeng
Ülesanne 4 (kontroll)
Koostage enda valikul komplekt klasse (vähemalt 5 klassi), mis oleksid ülemklasside-alamklasside hierarhilises struktuuris, milles on vähemasti 3 taset. Mõned võimalikud teemaringid oleksid nt. geomeetrilised kujundid, loomad, sõidukid, spordialad, toiduained. Aga julgesti võite käsitleda ka mingeid muid struktuure.
Mõelge välja ja lahendage seoses loodud klassidega ülesandeid, mis nõuaksid
- meetodite ülekatmist,
- dünaamilist seostamist,
- polümorfismi,
- ja muu ülaltoodu rakendamist.
Lisage programmi kommentaarid sinna, kus meetodite ülekatmist, dünaamilist seostamist, polümorfismi vms on kasutatud (iga asja kohta kommentaar vähemalt ühe koha peale).
Joonistage paberile (või arvutifaili) koostatud klasside hierarhia puustruktuur, mis sisaldab ka klassi Object
.
Ülesanne 5 (kontroll)
Looge klass RaadioKuulaja
. Lisage meetod kuula
, mis võtab parameetriks sõne kujul raadiosaate ja jätab selle meelde. Klassis peab olema ka meetod meenuta
, mis tagastab kõik kuuldud saated sõnede listina.
Looge klass Raadiosaatja
, mis suudab raadiosaateid esitada mitmele kuulajale. Äsja loodud Raadiosaatja
ei tunne ühtegi kuulajat. Kuulajate lisamiseks peab olema meetod lisaKuulaja
, mis jätab argumendiks antud RaadioKuulaja
meelde. Meetod edasta
peab võtma argumendiks sõnena antud raadiosaate ja edastama selle kõigile lisatud kuulajatele (st. pöörduma nende kuula
meetodi poole).
Looge Raadiosaatja
-le alamklass LotoNumbriTeataja
. Lisage parameetriteta meetod loosiJaEdasta
, mis genereerib loto võidunumbrid (10 suvalist arvu vahemikus 1..100) ja teavitab nendest saatja külge lisatud raadiokuulajaid. Saatke kõik numbrid ühe raadiosaatena (ühendage võidunumbrid tühikutega).
Looge Raadiosaatja
alamklass ValiRaadiosaatja
, mille edasta
meetod edastab saated suurte tähtedega.
Looge Raadiosaatjale
alamklass UudisteRaadio
, millel on meetod signatuuriga void uuendaAktuaalsedUudised(List<String> uudised)
aktuaalsete uudiste määramiseks. Uudiste edastamiseks peab olema klassis parameetriteta meetod edastaUudised
(edastab järjest kõik aktuaalsed uudised eraldi raadiosaadetena).
Looge RaadioKuulaja
alamklass HajameelneKuulaja
, mis jätab kuuldud saateid meelde üle ühe (esimene saade jääb meelde, teine ei jää, kolmas jälle jääb jne.)
Tekitage testklass, mis tekitab LotoNumbriTeataja
, ValiRaadiosaatja
ja UudisteRaadio
isendid ja lisab neile mõned tavalised ja mõned hajameelsed kuulajad. Uuendage uudiste raadio aktuaalsete uudiste list väljamõeldud uudistega. Katsetage võidunumbrite, uudiste ja valju raadiosaate edastamist. Ärge unustage kontrollida, kuidas erinevad kuulajad kuuldut meenutavad.
Abstraktsed klassid
Alamklasside-ülemklasside hierarhias on alamklassid järjest spetsiifilisemad, ülemklassid aga vastavalt üldisemad. Mõnikord on mõistlik korraldada, et ülemklass on sedavõrd üldine, et näiteks kõigi meetodite sisu ei täpsustatagi - mõned meetodid koosnevad vaid n-ö laiendatud signatuuridest (meetodi nimi koos formaalsete parameetritega ja nende tüüpidega, piiritlejad ja tagastustüüp) ning sisu jäetakse alamklasside täpsustada. Selliseid klasse nimetatakse abstraktseteks. Abstraktne klass on mitmes mõttes sarnane liidestega - võibki mõelda, et liides on abstraktse klassi erijuht, kus kõik klassi meetodid on abstraktsed.
Abstraktsel meetodil on järgmine üldkuju: piiritlejad abstract tagastustüüp meetodiNimi(parameetrite loetelu);
Kui klass sisaldab vähemalt ühte abstraktset meetodit, tuleb see varustada võtmesõnaga abstract. Abstraktsest klassist (nagu ka liidesest) ei saa luua isendeid. Abstraktse klassi iga alamklass on ise abstraktne või realiseerib ülemklasside kõik abstraktsed meetodid. Abstraktseks ei saa kuulutada konstruktorit ega staatilist meetodit.
Kui abstraktne klass realiseerib liideseid, siis võib osad (või kõik) liidese meetodid realiseerimata jätta. Sellisel juhul käituvad realiseerimata liidese meetodid nagu abstraktsed meetodid - abstraktse klassi alamklass peab need realiseerima või olema ka ise abstraktne klass.
Polümorfismi reeglite järgi saab kasutada abstraktset klassi muutujate tüübina. See annab võimaluse näiteks kirjeldada seda tüüpi elementidega massiivi, elemente ise aga luua alamklasside konstruktorite abil. Näide abstraktsest klassist:
class Isik { private String eesnimi; private String perenimi; Isik(String eesnimi, String perenimi) { this.eesnimi = eesnimi; this.perenimi = perenimi; } } abstract class IsikuFailistLugeja { protected abstract Isik ridaIsikuks(String rida); public List<Isik> loeIsikud(File sisend) throws Exception { // Files.readAllLines saab kasutada, et kiirelt etteantud failist kõik read listi lugeda List<String> read = Files.readAllLines(sisend.toPath(), StandardCharsets.UTF_8); List<Isik> isikud = new ArrayList<>(); for (String rida : read) { isikud.add(ridaIsikuks(rida)); } return isikud; } } class EesnimiPerenimiLugeja extends IsikuFailistLugeja { protected Isik ridaIsikuks(String rida) { String[] nimeOsad = rida.split(" "); // Malle Mallikas return new Isik(nimeOsad[0], nimeOsad[1]); } } class PerenimiEesnimiLugeja extends IsikuFailistLugeja { protected Isik ridaIsikuks(String rida) { String[] nimeOsad = rida.split(", "); // Mallikas, Malle return new Isik(nimeOsad[1], nimeOsad[0]); } } class DemoAbstract { public static void main(String[] args) throws Exception { EesnimiPerenimiLugeja epLugeja = new EesnimiPerenimiLugeja(); List<Isik> nimed = epLugeja.loeIsikud(new File("nimed.txt")); IsikuFailistLugeja teineLugeja = new PerenimiEesnimiLugeja(); List<Isik> ametlikudNimed = teineLugeja.loeIsikud(new File("ametlikud-nimed.txt")); // IsikuFailistLugeja lugeja = new IsikuFailistLugeja(); // viga! } }
Näeme, et kuigi klassist IsikuFailistLugeja
ei ole võimalik luua isendit tema enda konstruktoriga, on aga võimalik pöörduda tema meetodite (ja ka konstruktori) poole alamklassi isendi abil.
Näide abstraktsest klassist on java.net.URLConnection
. Javas on lihtne erinevatele aadressidele ühendusi luua: new URL("http://ut.ee").openConnection()
ja new URL("https://google.com").openConnection()
loovad vastavatele aadressidele ühendused. Java dokumentatsioonist näeme, et openConnection tagastab objekti tüübiga java.net.URLConnection
. Tegemist on abstraktse klassiga, mille kaudu saab etteantud aadressile andmeid saata või saabuvaid andmeid vastu võtta. Ainus abstraktne meetod selles klassis on connect
, mis määrab selle, kuidas ühendus avada. See meetod realiseeritakse erinevates alamklassides: HttpURLConnection
, HttpsURLConnection
, FtpURLConnection
jne.
Ülesanne 6 (kontroll)
Koostada abstraktne klass Telefon
, mille meetod getNumber
tagastab telefoni numbri (sõnena) ja getHelin
helina (samuti sõnena). Lisaks peab klassis olema abstraktne meetod tähtisInfo
tagastustüübiga String
. Klassis on ka konstruktor numbri ja helina määramiseks ja meetod signatuuriga String viimasedNumbrid(int n)
, mis tagastab telefoninumbri n viimast numbrit.
Koostada klassi Telefon
(mitteabstraktne) alamklass Lauatelefon
, mille konstruktor võtab lisaks numbrile ja helinale ka asukoha (String
). Meetod tähtisInfo
tuleb üle katta nii, et see tagastaks telefoni asukoha.
Koostada klassi Telefon
(mitteabstraktne) alamklass Mobiiltelefon
, mille konstruktor võtab lisaks numbrile ja helinale ka omaniku nime ning info pildistamisvõimaluse (boolean
) kohta. Meetod tähtisInfo
peab tagastama omaniku nime.
Täiendada klassi Telefon
nii, et see realiseeriks liidese Comparable<Telefon>
. Võrdlemisel võtta aluseks telefoninumbri kolmest viimasest numbrist koosnev arv.
Katke Lauatelefoni
ja Mobiiltelefoni
toString
meetod üle nii, et tagastatav sõne annaks edasi kogu telefoni käiva info.
Koostage ka peaklass, kus luuakse telefonidest massiiv ja sorteeritakse see meetodit java.util.Arrays.sort(Object[] o)
kasutades.
@Override
Kui olete Eclipse'il või IntelliJ-l palunud liideses kirjeldatud meetodi mõnda klassi automaatselt genereerida, siis võis meetodi päise kohale ilmuda tekst @Override
. Tegemist on annotatsiooniga
, mis toonitab programmi lugejale ja Java kompilaatorile, et see meetod katab üle ülemklassi meetodi või realiseerib mõne liidese meetodi.
Kuigi Java seda ei nõua, on @Override
annotatsiooni kasutamine siiski väga soovitatav. Esiteks hõlbustab see koodi lugemist, kuna lugeja saab meetodi otstarbest kergemini aimu, ilma ülemklassi või liidese definitsiooni uurimata.
Teiseks, see annotatsioon aitab vigu vältida. Oletame, et ülemklassis on meetod teeMidagi
, mille tahetakse alamklassis üle katta, aga kogemata kirjutati alamklassis meetodi nimeks teeMigadi
. Java arvab, et tahetigi defineerida täiesti uus meetod ja viga võib jupiks ajaks peitu jääda. Kui aga on harjumus annoteerida ülekaetud meetodid alati @Override
'iga, saab kompilaator kohe hoiatada, et ülemklassis pole sellist meetodit.
Java juhib tähelepanu küll @Override
annotatsiooni vigasele kasutamisele, aga ei märgi ära juhtumeid, kus seda pole kasutatud aga võiks. Õnneks saate seadistada oma IDE märku andma, kui kuskil oleks hea mõte @Override
kasutada. Soovitame vajaliku seadistuse kohe ära teha, kasutades seda juhendit.
Veel
- Palun õppige korralikult 1. kontrolltööks, mis põhineb kuue esimese praktikumi materjalidel.
- Ei piisa sellest, et loete läbi ja nagu saaksite aru ning loodate, et küll kontrolltöö ajal jõuate vaadata!
- Palun lahendage ülesanded-näited läbi. Proovige programme mõnevõrra muuta.
- Lahendage ise läbi praktikumide näidisülesanded, mis veel tegemata on.
- Lahendage ise läbi kontrolltöö näidisülesanded, mis on
6. praktikumi harjutusülesanded
. - Kontrolltöö on suurepärane võimalus oma oskusi ja teadmisi näidata! Koguge neid siis enne kontrolltööd.