Arvutiteaduse instituut
  1. Kursused
  2. 2017/18 kevad
  3. Objektorienteeritud programmeerimine (LTAT.03.003)
EN
Logi sisse

Objektorienteeritud programmeerimine 2017/18 kevad

  • Kodutööd ja praktikumid
  • Loengud
  • Kursuse korraldus
  • IDE juhendid
  • Edasijõudnute rühm

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.
  • Arvutiteaduse instituut
  • Loodus- ja täppisteaduste valdkond
  • Tartu Ülikool
Tehniliste probleemide või küsimuste korral kirjuta:

Kursuse sisu ja korralduslike küsimustega pöörduge kursuse korraldajate poole.
Õppematerjalide varalised autoriõigused kuuluvad Tartu Ülikoolile. Õppematerjalide kasutamine on lubatud autoriõiguse seaduses ettenähtud teose vaba kasutamise eesmärkidel ja tingimustel. Õppematerjalide kasutamisel on kasutaja kohustatud viitama õppematerjalide autorile.
Õppematerjalide kasutamine muudel eesmärkidel on lubatud ainult Tartu Ülikooli eelneval kirjalikul nõusolekul.
Courses’i keskkonna kasutustingimused