3. praktikum (Objektid. Klassid)


Teemad

Objektid ja klassid. Konstruktorid. Isendiloome. Muutujate ja meetodite nähtavus.

Pärast selle praktikumi läbimist oskab üliõpilane


Veidi järjenditest

Enne, kui lähme praktikumi põhiosa juurde, meenutame pisut järjenditega seonduvat (pikemalt oli eelmise praktikumi materjalis).

Paljude ülesannete puhul on mõistlik mingit hulka ühte tüüpi muutujad koos käsitleda. Näiteks kuue ujukomaarvu puhul võime kasutada järjendit, mille tekitame massiiviloomega:

double[] arvud = new double[6];

Nüüd on olemas muutujad arvud[0], arvud[1], arvud[2], arvud[3], arvud[4] ja arvud[5]. Vaikimisi on neil väärtuseks 0.0. Uusi väärtusi saab omistada nagu ikka muutujatele väärtusi omistame, nt. arvud[3] = 4.6;

Järjendi elemendiga saame teha tehteid (nt. 34*arvud[3]), võime neid ekraanile väljastada: System.out.println(arvud[3]);

Kui aga tahaksime kogu järjendit väljastada, siis rea System.out.println(arvud); toimel tuleb ekraanile midagi sellist: [D@15b7986
Muutuja arvud väärtuseks ei ole massiiv ise vaid viit (reference) massiivile. (Natuke pikemalt käsitleme seda temaatikat praktikumi lõpuosas.)

Kui tahame kõik järjendi elemendid ekraanile saada, siis on sobiv vahend for-tsükkel. Näiteks programmilõik
for (int i = 0; i < arvud.length; i++) {
    System.out.println(arvud[i]);
}

või ka

for (double elem : arvud){
    System.out.println(elem);
}

 

Enne, kui läheme tänase praktikumi põhiosa juurde, meenutame veel eelmises praktikumis käsitletud käsurea argumente. Tegemist on sõnejärjendiga, mille elemendid saavad väärtused programmi käivitamisel ja mille nimi määratakse peameetodi formaalse parameetri nimena. Kuigi ka Eclipse'i käivitamisel saab argumentide väärtusi määrata, on see ilmekam käsurealt käivitades pärast kompileerimist (nagu 1. praktikumis käsitletud):
    java Klassinimi Tartu Riia

Aga nüüd klasside ja isendite juurde

Javas programmeerimine seisneb klasside koostamises. Klasse koostasime juba ka eelmistes praktikumides. Kuna need sisaldasid peameetodit (main), siis võib neid nimetada peaklassiks ja neid sai käivitada. Samuti kasutasime klassi Math-meetodeid. Nüüd asume koostama klasse, mis on kui uued andmetüübid, mida saab kasutada üsna analoogiliselt lihttüüpidega (int, char, ... ). Väga olulisel kohal on koostatud klasside isendite (objektide) loomine. Klassikirjelduses on oma koht andmetel (mis näitavad olekut) ja meetoditel (mis kirjeldavad käitumist). Andmeid kujutatakse muutujate abil. Kuna muutujad saavad väärtused konkreetsete isendite loomisel, siis nimetatakse neid isendimuutujateks (ka isendiväljadeks).

Olgu meil klass Isik, milles on kaks välja:

class Isik {

  String nimi;      // isendiväli isiku nime jaoks
  double pikkus;    // isendiväli isiku pikkuse jaoks
}

Olemegi loonud uue andmetüübi, mis sisaldab kohta sõnele (nime jaoks) ja ujukomaarvule (pikkuse jaoks).

Isendi loomine võiks toimuda peaklassi (nt. TestIsik) peameetodis (main). (Nimi TestIsik ei tähenda siin, et tegemist oleks mingit eriliiki isikuga vaid klassiga, mis on mõeldud klassi Isik isendite testimiseks.)  Klassi isendeid luuakse käsuga new:

class TestIsik {

    public static void main(String[] args) {
        Isik a = new Isik();   
    }

}

Nüüd on loodud klassi Isik isend (objekt) a. Kuigi isend on loodud, on muutujad nimi ja pikkus väärtustamata (tegelikult on neil vaikeväärtused, vastavalt null ja 0.0). Lisades peaklassi rea System.out.println(a.nimi); saaksime isendivälja väärtuse ekraanile.
Väärtustamiseks on mitu võimalust. Võib näiteks muuta klassi Isik kirjeldust

class Isik {

  String nimi = "Toomas Indrek Elvis";      // isendiväli isiku nime jaoks
  double pikkus = 1.92;    // isendiväli isiku pikkuse jaoks
}

Sellisel juhul on aga see mure, et  kõik loodud selle klassi isenditel on vastavatel väljadel täpselt samad väärtused. Teine võimalus oleks muuta väärtusi peaklassis. Näiteks

class TestIsik {

    public static void main(String[] args) {
        Isik a = new Isik();
        a.pikkus = 2.03;
        
System.out.println(a.pikkus); 
    }
}

Konstruktor

Eeltoodud väärtustamine pole paraku objektorienteeritud programmeerimise ideoloogiaga kooskõlas. Hoopis parem on isendiväljad väärtustada isendi loomisel. Selleks täiendame klassi konstruktoriga (erilise protseduuriga, mis rakendub isendiloome käigus).

class Isik {

  String nimi;      // isendiväli isiku nime jaoks
  double pikkus = 1.7;  // isendiväli isiku pikkuse jaoks

  // konstruktor
  Isik(String isikuNimi, double isikuPikkus) {
      nimi = isikuNimi;
      pikkus = isikuPikkus;
  }
}


Nüüd saab isendeid luua  järgmiselt:

Isik a = new Isik("Juhan Juurikas", 1.99);
Isik b = new Isik("Madli Mallikas", 1.55);

Muutujaid a ja b käsitletakse siin kui tavalisi muutujaid, kuid nende tüübiks on Isik. Neid nimetatakse viittüüpi muutujateks, sest nende väärtuseks on viit klassi isendile. (Täpsemalt räägime sellest praktikumi lõpuosas.)

Ülesanne 1

Olgu meil klassi Isik kasutamiseks järgmine peaklass

class TestIsik {
    public static void main (String[] argv) {
         Isik a = new Isik("Juhan Juurikas", 1.99);
         System.out.println("Isik a on " + a);
    }
}

Sisestage see programm ja käivitage. Milline on tulemus?

Tulemus ei ole hästi loetav, sest objekt a ei kuulu Java standardobjektide hulka.

Meetod toString

Selleks, et Java suudaks programmeerija kirjeldatud objekte arusaadaval viisil ekraanile tuua, võib (kuid saab ka teisiti) klassi Isik kirjeldusse lisada meetodi toString(), mis määrab, kuidas antud objekt tekstina välja näeb. Näiteks klassi Isik puhul võib see meetod olla järgmine:

public String toString() {
    return "(" + nimi + "; " + pikkus + ")";
}

Ülesanne 2

Täiendage klassi Isik meetodiga toString(). Testklassis looge mõni isik ja väljastage ekraanile nende andmed, näiteks järgmise rea abil: System.out.println(a.toString());

Tegelikult on toString mõneti ebatavaline meetod, mis rakendub automaatselt isendi sõneksteisendusel (nt. väljastamisel). Seega võime kasutada ka
System.out.println(a);

Piiritlejad private, protected ja public 

Nagu ülalpool mainisime saab isendivälja väärtusi muuta testklassist

a.pikkus = 1.95; // isendi a väljale pikkus omistatakse 1.95

Isendiväljade otsekasutuse (nt. a.pikkus = 1.95) saab keelata, kui nende kirjeldamisel kirjutada muutuja nime ette piiritleja private. Näiteks

private String nimi;
private double pikkus;

Muutujate ja meetodite nähtavust ning kasutust saabki reguleerida piiritlejatega public, private ning protected. Neist public määrab piiranguteta, private klassisisese ning protected klassi- ja selle alamklasside-sisese kasutusõiguse. Kui juurdepääsu määravat piiritlejat pole, siis vaikimisi on juurdepääs olemas sama paketi piires.

Get- ja set-meetodid

Tavaliselt väärtustatakse isendimuutujaid klassi konstruktorite ja meetodite abil. Privaatse isendimuutuja väärtuse tuvastamiseks on mõeldud nn. piilumeetod, mille ainsaks ülesandeks on tagastada vastava muutuja väärtus. Näiteks

String getNimi() {

    return nimi;
}

double getPikkus() {

    return pikkus;
}

Analoogiliselt saab luua ka meetodid väärtuste muutmiseks

void setNimi(String nimi) {
    this.nimi = nimi;
}


void setPikkus(double pikkus) {
    this.pikkus = pikkus;
}

Kuna neid meetodeid on sageli vaja, siis on Eclipse'is nende kirjutamine mugavamaks tehtud, Source-menüüst saab valida Generate Getters and Setters.  

Võtmesõna this kasutatakse isendimuutujatele viitamisel, kui konstruktori või meetodi formaalsete parameetrite nimed langevad kokku isendimuutujate nimedega.

Meetodeid saab kasutada näiteks testklassis koos muutujanimega nt. a.setNimi("Evelin");

Veel konstruktoritest

Eespool koostasime konstruktori, mis isendiloomel rakendamisel vajas kahte argumenti. Sama saab kirjutada ka võtmesõna this abil.

Isik(String nimi, double pikkus) {

   // isendimuutujad nimi ja pikkus saavad väärtusteks
   // konstruktori parameetrite väärtused
   this.nimi = nimi;
   this.pikkus = pikkus;
}

Ka konstrukorite kirjutamine on Eclipse'is mugavamaks tehtud, Source-menüüst saab valida Generate Constructor Using Fields. (On ka genereerimine ülemklassi abil, aga seda me käsitleme pärilusega koos.)

Konstruktorit võib käsitleda kui erilist meetodit kolme tunnusega:
  1. konstruktori nimi langeb kokku klassi nimega;
  2. konstruktori nime ette ei kirjutata tagastustüüpi;
  3. konstruktori poole pöördumine toimub käsuga new antud klassi isendi loomisel ja konstruktor tagastab viida loodud isendile.
Alati leidub üks eriline konstruktor - vaikekonstruktor, mille abil saab isendeid luua ka siis, kui klassis pole kirjeldatud ühtegi konstruktorit. Praktikumi algupoolel just seda kasutasimegi : Isik a = new Isik();  

Samamoodi  (Isik a = new Isik())kasutatakse ka parameetriteta konstruktorit

Isik() {
  nimi = "Nimetu";
  pikkus = 0.0;
}

Konstruktoreid võib klassis olla mitu, sel juhul on tegemist konstruktorite üledefineerimisega. Ühe ja sama klassi konstruktorid peavad üksteisest erinema signatuuri (formaalsete parameetrite arv ja nende tüübid) poolest. Samuti võib klassis olla sama nimega meetodeid, sellisel juhul peavad need erinema formaalsete parameetrite arvu ja/või nende tüüpide poolest.

Kahe konstruktoriga  klass Isik kirjelduse algusosa on järgmine:

class Isik {

  private String nimi;
  private double pikkus;

  Isik(String isikuNimi, double isikuPikkus) {
      nimi = isikuNimi;
      pikkus = isikuPikkus;
  }

  Isik() {
      nimi = "Nimetu";
      pikkus = 0.0;
  }

 
// ERINEVAD MEETODID


}

See, milline konstruktor täidetakse, määratakse argumentide arvu ja tüüpide järgi:

Isik d = new Isik();
Isik e = new Isik("Ülli Õpilane", 2.05);


Võtmesõna this abil on võimalik ühe konstruktori sees pöörduda teise sama klassi konstruktori poole. Klassi Isik viimase toodud versiooniga samaväärne klass:

class Isik {

  private String nimi;
  private double pikkus;

  Isik(String isikuNimi, double isikuPikkus) {
    nimi = isikuNimi;
    pikkus = isikuPikkus;
  }

  Isik() {
    this("Nimetu", 0.0);
  }


//ERINEVAD MEETODID 

}

Meetodid

Eelnev osa oli põhiliselt andmetest - kuidas saab isendiväljadele väärtusi anda, neid vaadata jms. Edasi vaatleme, kuidas saab objekti "õpetada käituma". Tegelikult juba eelmises lõigus olid käsitlemisel get- ja set-meetodid, samuti on meetod ka toString. Meetodite üldine ideoloogia on sama, mis oli eelmistes praktikumides, kus küll veel polnud isendimeetodeid. Muutujat või meetodit, mis otseselt ei seostu antud klassi isendiga, nimetatakse vastavalt klassimuutujaks või -meetodiks. (Meenutame nt. klassi Math.) Eraldamaks neid meetodeid ja muutujaid otseselt isenditega seotutest, lisatakse nende kirjelduste ette piiritleja static, samuti ei ole nende kasutamiseks vajalik klassi isendite olemasolu.

Mäletatavasti peavad meetodil olema tagastustüüp, nimi ning parameetrite tüübid ja nimed (kui parameetreid üldse on). Püüame koostada meetodi, mis leiab isiku pikkuse järgi klassikalise tehnika suusakepi pikkuse sentimeetrites. Esimese hooga võiks arvata, et pikkus tuleks argumendina ette anda. Saaks tõesti ka nii, aga kuna pikkus on isendiväljal olemas, siis on mõistlik seda ära kasutada.

int suusaKepiPikkus(){
    return (int) Math.round(0.85*pikkus*100);
}

Isendimeetodit saab väljaspool seda klassi kasutada vaid isendiga seotult, nt.

System.out.println(e.suusaKepiPikkus()); 

Eelmine rida töötab eeldusel, et isend e on olemas, näiteks loodud järgmise reaga:
Isik e = new Isik("Ülli Õpilane", 2.05);

Sama klassi sees saab meetodit kasutada ilma isendit näitamata. 


Ülesanne 3

Täiendage nüüd klassi Isik nii, et seal oleks vähemasti kolm isendivälja (lisage näiteks mass), erinevaid konstruktoreid, isendiväljadele vastavad get- ja set-meetodid, meetod toStringning lisaks veel mõned meetodid (nt. kehamassiindeksi või suusa pikkuse arvutamiseks). Vähemasti üks meetod peaks vajama ka argumente. (Argumentideks peaksid olema lisaandmed, mitte isendiväljad.) Katsetage loodud meetodeid testklassis.

Järjendid

Eelpool oli juttu nt. arvude järjenditest. Järjendis on üht tüüpi elemendid. See tüüp võib olla aga ka viittüüp.
Näitena koostame klassi Raamat:

class Raamat {

  private String autor;
  private String pealkiri;

  Raamat(String autor, String pealkiri) {
    this.autor = autor;
    this.pealkiri = pealkiri;
  }
}


Koostage ka vastav testklass, milles võime luua selle klassi isendeid, nt.  

Raamat kevade = new Raamat("Oskar Luts", "Kevade");

Klassi Raamat isenditest järjendi loomine toimub järgnevalt:

Raamat[] riiul = new Raamat[100];

Muutuja riiul sisaldab viitasid raamatutele, aga hetkel pole seal veel ühtegi (sisulist) viita. Võite proovida nt. väljastada

System.out.println(riiul[8]);


Paneme Kevade "riiulisse":
riiul[8]=kevade;

Proovige nüüd väljastada.

Lisage nüüd klassi Raamat veel toString meetod.

Proovime tekitada raamatuid "hulgi":

String autor = "Eduard Vilde";

for (int i = 0; i < riiul.length; i++)
   riiul[i] = new Raamat(autor, "Kogutud teosed " + String.valueOf(i + 1));

System.out.println("10. raamat riiulil on " + riiul[9] + ".");


Ülesanne 4

Muutke klassi Raamat nii, et autor oleks hoopis Isik-tüüpi.
private String autor; --> private Isik autor;

Viittüüp vs algtüüp

Praktikumi viimases lõigus räägime mõnede näidete põhjal viittüübi (reference type) ja algtüübi (primitive type) erinevustest. Piirdume küllalt lihtsustatud käsitlusega, millest aga võiks piisata käesoleva kursuse jaoks.

Iga muutuja jaoks eraldatakse mälus koht, kus vastavat väärtust hoida. Kui me deklareerime muutuja, siis ütleme sellega, mis tüüpi väärtusega tegemist on. Kui muutuja on algtüüpi (byte, shortint, long, float, double, char, boolean), siis see väärtus on algtüüpi. Viittüüpi muutuja korral on aga väärtuseks viit vastavale objektile. Kogu aeg me sellele erinevusele mõtlema ei pea, küll aga teatud juhtudel on see väga oluline. Näiteks kui algtüüpi muutuja korral omistame muutujale väärtuseks teise muutuja väärtuse, siis saabki uus muutuja selle väärtuse (mis on siis näiteks arv). Viittüüpi muutuja korral on aga väärtuseks viit ja seetõttu tekib natuke teistsugune seos. Niisiis programmilõigu

int arv1 = 1632;
int arv2 = arv1;
arv2 = 1802;

puhul saab arv2 esialgu väärtuseks muutuja arv1 väärtuse 1632 ja seejärel 1802. Muutuja arv1 enda väärtus ei muutu. Kui nüüd väljastame muutujate väärtused, siis saame

System.out.println("arv1 on: " + arv1);

System.out.println("arv2 on: " + arv2);

Enne, kui proovite, püüdke tulemusi ennustada.

Viittüübi tutvustamiseks loome kõigepealt ühe väga lakoonilise klassi, kus on vaid üks int-tüüpi isendiväli.
class Arv {
    int arv;
}

Teeme nüüd samalaadsete väärtustamistega programmilõigu.

Arv viitarv1 = new Arv();
viitarv1.arv = 1632;
Arv viitarv2 = new Arv();
viitarv2 = viitarv1;
viitarv2.arv = 1802;
System.out.println("viitarv1.arv on: " + viitarv1.arv);
System.out.println("viitarv2.arv on: " + viitarv2.arv);

Enne, kui proovite, püüdke ka neid tulemusi ennustada.

(Näide on inspireeritud raamatust http://java.sun.com/docs/books/jls/, kus on palju huvitavat informatsiooni.)

Ka massiivid on käsitletavad viittüüpi objektidena. Näiteks

int[] arvud1 = {1632};
int[] arvud2 = arvud1;
arvud2[0] = 1802;
System.out.println("arvud1[0] on: " + arvud1[0]);
System.out.println("arvud2[0] on: " + arvud2[0]);

Enne, kui proovite, püüdke ka neid tulemusi ennustada.

Kui on vaja massiive kopeerida, siis saaks seda teha

Ülesanne 5

Katsetage eeltoodud näiteid ja kontrollige oma ennustuste paikapidavust.


1. rühmatöö

Esitamine toimub 4. ja 5. praktikumi ajal.

Juhend on toodud eraldi.