Java:Objektid

Allikas: Kursused
Mine navigeerimisribale Mine otsikasti

Objekt-orienteeritud programmeerimine (OOP)

Objekt-orienteeritud programmeerimine (ingl object oriented programming, OOP) on programmeerimise viis, kus programmi vaadatakse kui klasside/objektide kogumit. Java on suures osas objekt-orienteeritud programmeerimiskeel (primitiivsed andmetüübid (int, double jne) ei ole objektid).

OOP on tehnika, mis võimaldab programmeerimist mugavamaks teha. Eriti kasulik suurte projektide tükeldamisel. OOP ei tee lahendusi kiiremaks.

OOP peamised tehnikad/eesmärgid:

  • informatsiooni kapseldamine (encapsulation). Teised programmeerijad ei saa kasutada osasid minu funktsioone ega muuta osasid minu muutujaid. Informatsioon peidetakse nende eest ära.
  • Modulaarsus (modularity). Koodi jagamine mooduliteks. Seotud kapseldamisega, kus kapseldatud programm viiakse vastavusse pärismaailmaga.
  • Polümorfism (polymorphism). Sama nimega meetod võib erinevate andmetüüpide puhul käituda erinevalt.
  • Pärimine (inheritance). Pärinevussuhted klasside vahel, alamklassid pärivad kõik ülemklassi omadused ja meetodid, lisaks võib alamklass lisada funktsionaalsust.
  • Koodi taaskasutamine. Kirjutada valmismooduleid, mida hiljem saab taaskasutada.

OOP põhikontseptsioon

Klassid on kui terviklikud tarkvara komponendid, mida saab kergesti (taas)kasutada. Keskne mõiste OOP juures on objekt, mis on justkui komponent, millel on andmed (olek) ja funktsionaalsus (käitumine).

Objekti oleku kirjeldamiseks kasutatakse ka järgmisi termineid: atribuut (attribute), omadus (property), (isendi)väli ((instance) field), (isendi)muutuja ((instance) variable).

Objekti andmeid muudetakse objekti meetodite abil. Meetodi aktiveerimiseks saadetakse objektile teade (message).

Näiteks elektronposti nimekirja objekt, millel olek (andmed) on kõik selle nimekirja nimed ja aadressid. Kui saata sellele objektile teade (kutsuda välja funktsiooni), mis lisab nime, muudetakse olekut vastavalt (lisatakse uus nimi). Kui saata sellele objektile teade (kutsuda välja funktsioon), mis prindib objekti välja, prindib objekt välja kõik nimed ja aadressid.

OOP ideoloogia

OOP tehnika tähendab mõnda konkreetset probleemi lahendavate objektide disainimist. Programmi objektid võivad esindada päriselu objekte antud probleemi puhul. Mida paremini (täpsemini, selgemini) päriselu objektid on programmi objektidena esitatud, seda lihtsam ja selgem on nendega opereerida. Näiteks kui programm peab lahendama ülesannet, mis on seotud sõiduki ja selle juhiga, siis on mõistlik luua programmi poolel objektid sõiduki ja juhi (inimese) kohta.

OOP võimaldab (suurema) programmi puhul:

  • koodi paremini struktureerida
  • hoida koodi arusaadavust lihtsana
  • teha hiljem täiendusi koodi lihtsamini

jne.

Klassid ja objekt

Klass defineerib ära objekti abstraktsed omadused. Klass on nagu šabloon, mis kirjeldab millegi olemust. Näiteks klass Koer koosneks kõikide koerte omadustest nagu tõug ja värv ning oskustest nagu haukumine ja istumine. Objekt on konkreetne koer ehk isend. Klass Koer defineerib ära kõikide koerte omadused, objekt Lotte aga konkreetse isendi/koera, millele võib vastata (ei pea tingimata) reaalne koer. OOP puhul võib öelda, et klass on objektide kirjeldus, klass kirjeldab ära, milliseid andmeid/oskusi võivad sellesse klassi kuuluvad isendid (ehk objektid) omada.

Programmis on üldjuhul üks klass Koer, aga mitu erinevat objekti. Kui programm näiteks kirjeldab koerte varjupaiga andmeid, siis iga koera kohta varjupaigas on programmis üks objekt. Klass on aga neil kõikidel ühine.

OOP puhul öeldakse objekti kohta tihti isend (instance) või olem.

Java puhul:

  • staatiline muutuja (public static int dogCount;) on klassi muutuja. Kuna klass ise kirjeldab ära kõik koerad, siis ka see muutuja käib kõikide koerte kohta. Selles võib näiteks hoida, mitu koera on varjupaigas. Kui kuskil seda numbrit muudetakse, muutub see üle kogu programmi.
  • mitte-staatiline muutuja (String name;), öeldakse ka objekti muutuja, isendimuutuja jne, kirjeldab ära ühe konkreetse objekti (koera) andmed, antud juhul koera nime. See võib olla kõikidel koertel erinev. Kui muudetakse ühe objekti nime, siis see ei muuda teiste koerte nimesid.

Klassi kirjeldus kirjeldab ära oskused, mida antud klassi isendid (objektid) teha oskavad. Neid oskusi nimetatakse meetoditeks (sisuliselt on need funktsioonid, aga objektide puhul räägitakse meetoditest). Objekti meetod võimaldab konkreetsel isendil mingit tegevust läbi viia. Näiteks koera puhul võib meetod olla haugu, mis siis paneb haukuma konkreetselt selle koera, kelle juures see meetod välja kutsuti (kellele saadeti vastav teade "haugu").

Java puhul:

  • staatiline meetod e funktsioon (public static int getDogLimit()), nimetatakse ka klassi funktsiooniks või klassi meetodiks, on funktsioon, mis ei sõltu konkreetsest isendist. Näiteks see informatsioon, mis koera varjupaika mahub, ei sõltu ühestki konkreetsest koerast. See on kogu programmi kohta ühine funktsioon.
  • mitte-staatiline meetod (public void bark()), öeldakse ka isendimeetod, objektimeetod, on seotud aga konkreetse objektiga. Seda meetodit või funktsiooni rakendatakse ainult ühele konkreetsele isendile (loomulikult saab ka kõikidele rakendada, kui see iga objekti juures eraldi välja kutsuda).

Vaatame natuke teistsugust koodinäidet: <source lang="java">

/**

* Describes a student which
* has some test results.
*
*/

public class Student { /** * The name of the student. */ public String name; /** * Unique ID over all the students. */ public int ID; /** * The results of three tests. */ public double test1, test2, test3;

/** * Calculates the average test result. * @return Average test result */ public double getAverage() { return (test1 + test2 + test3) / 3; }

/** * Static variable which holds the next unique id. * The value of this variable is usually student count. * This is the same for all the students. */ private static int nextUniqueID = 0;

/** * Gets a unique ID for the student. * The ID number itself is increased. * @return Unique id for the student. */ public static int getUniqueID() { nextUniqueID++; return nextUniqueID; } }

</source>

Eelnevas õpilase näites on nii staatilisi kui mitte-staatilisi muutujaid/meetodeid. Staatiline getUniqueID() ei sõltu otseselt ühestki tudengist. Ta sõltub vaid sellest, mis numbrid on juba määratud (unikaalne number eeldab, et ühelgi teisel tudengil sellist numbrit ei ole). Kõige lihtsam viis unikaalset numbrit saada on iga uue tudengi puhul anda ühe võrra suurem number kui eelmisele anti. Kui meil ülikoolis on 100 õpilast, siis me ei pea ühegi poole neist pöörduma, et saada uus unikaalne id. Seepärast ongi see meetod staatiline, kuna ta on kogu programmi (näiteks kogu ülikooli peale) ühine.

Meetod getAverage() on aga seotud konkreetse tudengiga. Kui ülikoolist küsida "anna keskmine hinne", siis tähendaks see midagi muud. Kuigi jah, ülikooli näite puhul võib väita, et tegelikult ülikool teab ka kõikide tudengite keskmisi hindeid. Jah, teoreetiliselt võiks see meetod olla staatiline, aga siis oleks selle sisu hoopis teine. Antud näite puhul on aga eeldatud, et hinnete info on seotud konkreetse tudengiga. Kui nüüd ühelt konkreetselt tudengilt küsida, mis ta keskmine hinne on, siis ta saab sellele vastata (võtab lihtsalt aritmeetilise keskmise oma hinnetest).

Objektide loomine

Klassi nimi defineerib ära uue andmetüübi. Kõik senised objektid, mis te seni kasutanud olete (String, ArrayList jne) on tegelikult samamoodi kuskil ära kirjeldatud nagu meie Student objekt eelnevalt näidatud klassis.

Seega, klassinimi on kasutatav andmetüübina. Näiteks võime luua muutuja, mille andmetüüp on Student: <source lang="java"> Student std; </source>

Javas muutuja deklareerimine (nagu eelnevalt näidatud) ei loo veel objekti. Üldisemalt:

  • Javas ükski muutuja ei salvesta endas objekti.
  • Muutuja hoiab vaid viidet objektile.

Kui kujutada ette, et andmed paiknevad suvaliselt mälus laiali. Näiteks ühe tudengi andmed hakkavad positsioonilt 100, teise tudengi andmed hakkavad positsioonilt 1190 jne. Muutuja std ei hoia mitte tudengi andmeid endas, vaid hoopis viidet mällu. Seega, lihtsustatult võib öelda näiteks, et std = 100 (tegelikult viide objektile ei ole 1:1 mäluaadress, Javal on endal vahel mingi vastavustabel mäluaadresside jaoks).

Seega, Javas muutuja, mille andmetüüp on objekt (ükskõik kas teie enda loodud või mõni Java sisseehitatud objekt), salvestab vaid viite mällu. Viite (reference) või pointeri (pointer) täpset väärtust programmeerija teadma ei pea. Objekti loomisel teeb Java vajalikud protseduurid (eraldab objektile mälu ja määrab vastava aadressi muutujasse).

Objekti loomine toimub võtmesõnaga new: <source lang="java"> std = new Student(); </source>

Ülaltoodud kood loob uue objekti tüübiga Student, salvestab selle mällu ja paneb vastava (alguse) mäluaadressi std muutujasse. Loodud muutuja std on kasutatav nüüd objektina. Selleks, et objekti muutujaid või meetodeid välja kutsuda, saab kasutada loodud muutujat, näiteks std.name .

Nullviit

On võimalik, et muutuja nagu std, millel on andmetüübiks klass, ei viitagi objektile. Sellisel juhul hoiab std null-viita või tühja viita (null reference). Javas kirjutatakse null-viit null. Muutujale saab null-viida omistada selliselt: <source lang="java"> std = null; </source> ning null-viida testimine käib selliselt: <source lang="java"> if (std == null) { </source>

Kui muutuja väärtus on null, siis isendi muutujate või meetodite poole pöördumine ei ole lubatud. Näiteks kui std = null;, siis std.name pöördumine pole lubatud. Kui seda tehakse, annab programm null pointer exception (NullPointerException) vea.

Instants

Konkreetset isendit või objekti nimetatakse instantsiks (instance). Järgnevalt näide, kuidas luuakse mõned instantsid eelnevalt näidatud Student andmetüübiga.

<source lang="java"> // Declare four variables of type Student Student std, std1, std2, std3; // Create a new object belonging to the class Student, // store a ref to that object in the var std. std = new Student(); std1 = new Student(); // Create a second Student object std2 = std1; // Copy the reference value in std1 into the variable std2. std3 = null; // Store a null reference in the variable std3. std.name = "John Smith"; // Set values of some // instance vars, getUniqueIdisa static method, // therefore is accessed from class Student // (not object instance like std) std.ID = Student.getUniqueID(); std1.name = "Mary Jones"; std1.ID = Student.getUniqueID(); // (Other instance variables have default initial values of zero.) </source>

Järgnevalt pildil on näha, kuidas antud näite puhul näeb välja olukord mälus:

Java-juhend-Objects-in-heap.png

Kui mõni muutuja viitab objektile, siis see on pildil tähistatud noolega. Nagu näha, std, std1 ja std2 muutujad viitavad Student tüüpi objektidele, kusjuures std1 ja std2 viitavad samale objektile! Kui muutujad viitavad samale objektile ja ühe kaudu muudetakse objektis mingit väärtust (näiteks nime), siis muutub see väärtust kõikide objektide jaoks. Ehk siis muutuja ise on lihtsalt viide mällu (n-ö aadress). Kui mitu muutujat viitavad samma kohta mälus, siis andmete muutmisel mälus näevad kõik muutujad seda muudatust. Kui teie kodune aadress (viit) on mitmel teie sõbral (muutuja) ning teie kodus (objekt, millele viidatakse) muutub seinavärv (näiteks üks sõpradest muudab selle ära sõbra_viide_teie_kodule.seinavärv = punane), siis ükskõik, millise (teise) sõbra kaudu küsida teie kodu seinavärvi (prindi teise_sõbra_viide_teie_kodule.seinavärv), on see muutunud (punaseks).

Tasub tähele panna, et String on ka objekt. Seega tudengi nimi objektis on tegelikult omakorda viide kuskile järgmisesse kohta mälus.

Kui ühe mitteprimitiivse (ehk siis andmetüüp on mingi objekt) muutuja väärtuseks määratakse teine mitteprimitiivne muutuja (eelnevas koodis std2 = std1), siis määratakse std2 väärtuseks täpselt sama viide ("mäluaadress") kui std1 muutujal oli. Tulemuseks hakkavad kaks muutujat viitama täpselt samale mälu piirkonnale. std1.name ja std2.name viitavad täpselt samale nimele. Kui ühe muutuja kaudu nime muuta, siis teise muutuja kaudu nime vaadates on ka see muutunud. std1.name ja std2.name on kaks erinevat viisi, kuidas samale mälu piirkonnale saab viidata.

Konstruktor

Üks erilist tüüpi meetod on konstruktor (constructor). See on meetod, mis käivitatakse juhul, kui uus instants luuakse. Vaikimisi on igal klassil see olemas. Isegi juhul, kui seda eraldi kirja pole pandud. Kui koodis pole oluline, et objekti loomisel midagi erilist tuleks teha, pole seda vaja ka eraldi defineerida. Olukorrad, kus konstruktorit oleks vaja eraldi kirjeldada:

  • objekti loomisel oleks vaja kaasa anda mingid parameetrid
  • objekti loomine on teatud välistele objektidele/klassidele keelatud

Näide, kus objekti loomisel antakse kaasa parameetrid. Igal tudengil on nimi. Iga kord objekti loomisel tuleb eraldi teha student.setName("..."). Selle asemel, et eraldi real nimi lisada, teeme seda kohe objekti luues.

Student.java: <source lang="java"> public class Student { private String name;

public Student(String name) { this.name = name; } } </source>

Nüüd saab uut objekti luua selliselt: <source lang="java"> Student s = new Student("Reinuvader"); </source>

Kusjuures new Student(); (ilma argumendita) annab vea. Kui tahaks lubada mõlemad variandid, nii kohustusliku nimega kui ilma nimeta: <source lang="java"> public static class Student { private String name;

public Student() { }

public Student(String name) { this.name = name; } } </source>

Eelneva näite puhul kasutatakse overloading tehnikat. See tähendab, et meil on mitu funktsiooni sama nimega (antud juhul Student), mille argumentide arv ja/või andmetüübid on erinevad. Kui koodis kutsutakse välja new Student();, käivitub ilma argumentidega konstruktor. Kui koodis kutsutakse välja new Student("Koolipoiss");, käivitub konstruktor, mis aktsepteerib sõne argumendina.

Konstruktori definitsiooni puhul pange tähele, et tagastatavat andmetüüpi ei märgita. Meetodi nimi on täpselt sama mis klassil.

Objektide võrdlemine

Kahe objektile viitava muutuja võrdlemisel kujul if (std1 == std2) kontrollitakse, kas mõlemad muutujad viitavad samasse kohta mälus. Ei võrrelda seda, mis mälus kirjas on. Ehk siis objektide sisu ei võrrelda.

String on Javas objekt. Selle võrdlemine käib üldiste objekti reeglite alusel. Kui me teeme kontrolli if (str1 == str2), siis tulemus on true vaid juhul, kui muutujat viiatavad mälus samasse kohta (ehk siis sõned on mälus samas kohas). Java võimaldab sõne luua lihtsustatud kujul: <source lang="java"> String str1 = "tere"; String str2 = "tere"; </source>

Nagu eelnevalt oleme vaadanud, siis objektide loomisel tuleb kasutada võtmesõna new. Ülal väljatoodud näites seda tehtud ei ole. Kuna sõna andmetüüp on väga levinud, võimaldab Java kasutada n-ö lihtsustatud sõne loomist, kus new võtmesõna pole vaja märkida. Sellise kirjapildi puhul ei looda igakord uut sõne (objekti). Kui sama sisuga sõne on juba mälus olemas (näiteks aadressil 123), siis uue sõne loomisel (str2) puhul pannakse see viitama samasse kohta mälus (123). Kui nüüd teha võrdlus if (str1 == str2), saab suure tõenäosusega tulemuseks true.

Sõne on võimalik luua ka selliselt: String str3 = new String("tere");. Sellisel juhul sunnitakse uue sõne jaoks mälus eraldi piirkonda eraldama. Kui sellist sõne võrrelda == võrdlusega, on tulemus false ehk siis algselt loodud "tere" ei paikne samas kohas mälus kui hiljem loodud str3 viitab, kuigi sisu (ehk siis sõne ise) on täpselt sama.

Koodinäide: <source lang="java"> public class StringExample { public static void main(String[] args) { String str1 = "tere"; String str2 = "tere"; // str1 and str2 point to the same memory location System.out.println(str1 == str2); // and the contents are equal System.out.println(str1.equals(str2));

// enforce new object creation String str3 = new String("tere"); // now str3 is stored in a separate location in memory System.out.println(str1 == str3); // but the contents are still equal System.out.println(str1.equals(str3)); } } </source>

Pärimine

Klass kirjeldab objekte, mis on sama struktuuri ja käitumisega (seda me oleme juba vaadelnud). Täiendavalt on võimalik luua klasse, mis kirjeldavad ära objektide osalise, kuid mitte täieliku, vastavuse struktuuri ja käitumise osas. Sellist osalist sarnasust saab luua kasutades pärimist. Pärimine tähendab, et üks klass võib pärida osa (või terve) struktuuri ja käitumist teiselt klassilt. Mõnikord öeldakse "pärimise" asemel ka "laiendamine".

Klassi, mis pärib struktuuri ja käitumist, kutsutakse alamklassiks (subclass). Klassi, millelt päritakse, kutsutakse ülesmklassiks (superclass). Uue klassi kirjutamisel saab deklareerida, et loodav klass on mõne olemasoleva klassi alamklass. Näiteks kui on vaja kirjeldada ära klass B ning see klass peaks pärima mingi osa struktuurist ja käitumisest klassilt A, kirjutatakse klassi päis selliselt: <source lang="java"> class B extends A {

   // additions to, and modifications of,
   // stuff inherited from class A

} </source>

Ühte ülemklassi võivad laiendada mitu alamklassi. Kui näiteks klassid B, C ja D kõik laiendavad klassi A, siis saab öelda, et B, C ja D on osaliselt kattuva struktuuri ja käitumisega. Kattuv osa on see, mis pärineb klassist A. Lisaks võib pärimine toimuda mitmekordselt. Näiteks klass E, mis laiendab klassi D, D omakorda laiendab klassi A. Sellisel juhul öeldakse, et klass E on klassi A alamklass (kuigi mitte otseselt).

Java-juhend-Subclass-superclass.png

Pildil vasakul on näidatud ära "lihtne" olukord, kus üks klass laiendab teist klassi. Paremal pool on visualiseeritud mahukam hierarhia selle kohta, mis eelmises lõigus sai kirjeldatud.

Pärimise näide: sõidukid

Koostame klassid sõidukite kirjeldamiseks. Meil on üks üldine klass, mis kirjeldan ära sõidukid. Seejärel on konkreetset tüüpi sõidukid nagu auto, veok ja mootorratas, mis igaüks omakorda läheb eraldi klassi. Kõik need kolm klassi laiendavad üldist sõiduki klassi. Joonisel näeks see välja sedasi:

Java-juhend-Vehicle-hierarchy.png

Vehicle (üldine sõiduk) võiks sisaldada (objekti) muutujaid nagu registreerimisnumber ja omanik ning (objekti) meetodeid nagu transferOwnership() (omanikuvahetus). Kõik need muutujad ja meetodid on kolme alamklassi peale ühised. Täiendused võiksid olla järgmised:

  • Car (auto) võib lisada muutuja numberOfDoors
  • Truck (veok) võib lisada muutuja numberOfAxels
  • Motorcycle (mootorratas) võib lisada muutuja hasSidecar

Koodis võiks see välja näha sedasi:

<source lang="java"> class Vehicle {

  int registrationNumber;
  Person owner;  // (Assuming that a Person class has been defined!)
  void transferOwnership(Person newOwner) {
      . . .
  }
  . . .

}

class Car extends Vehicle {

  int numberOfDoors;
  . . .

}

class Truck extends Vehicle {

  int numberOfAxles;
  . . .

}

class Motorcycle extends Vehicle {

  boolean hasSidecar;
  . . .

} </source>

Koodinäites on välja toodud 4 erinevat klassi. Üldiselt see tähendab, et kogu kood on laiali jagatud nelja faili: iga klass eraldi failis.

Uue instantsi mõnes sõidukist võiks teha nii: <source lang="java"> Car myCar = new Car(); // from the Car itself System.out.println(myCar.numberOfDoors); // and from superclass Vehicle System.out.println(myCar.registrationNumber); System.out.println(myCar.owner); myCar.transferOwnership(); </source>

Nagu koodis näha, võime Car tüüpi objekti puhul kasutada muutujaid, mis on kirjeldatud Car klassis, kui ka neid muutujaid, mis pärinevad klassist Vehicle.

Tüübid ja alamtüübid

Vaatleme eelnevalt toodid sõidukite näidet. Päriselus on auto, veok ja mootorratas kõik erinevat tüüpi sõidukid. Ehk siis nende kõikide kohta võib öelda sõiduk. Samamoodi on programmis kirjeldatud - kõik pärinevad klassist Vehicle.

Muutuja, mis hoiab viidet objektile, mis on kirjeldatud klassis A, võib hoida viidet ka ükskõik millisele objektile, mis on kirjeldatud mõnes A alamklassis. Sõidukite näite puhul täiendab see seda, et objekt, mille tüüp on Car, võib olla viidatud muutujas, mille andmetüüp on Vehicle:

<source lang="java"> Vehicle myVehicle = myCar; // or Vehicle otherVehicle = new Car(); </source>

Objekt ise "teab", mis tüüpi ta tegelikult on. Informatsioon kirjeldava klassi kohta salvestatakse koos objektiga mällu. Koodis on võimalik testida, kas objekt, kuhu muutuja viitab, on teatud tüüpi (või alamtüüpi):

<source lang="java"> if (myVehicle instanceof Car) </source> See kontrollib, kas objekt, millele viitab myVehicle, on tegelikult mälus hoopis Car tüüpi.

Vastupidiselt üleval esinenud näitele, kus ülemklassile viitava muutuja "väärtuseks" saime panna alamklassiga kirjeldatud objekti (tegelikult siis ülemklassile viitav muutuja hakkab viitama alamklassile mälus), siis vastupidi teha ei saa: <source lang="java"> Vehicle myVehicle = ....;

Car myCar = myVehicle; // illegal! </source>

Ehk siis alamklassile viitava muutujale me ei saa omistada viidet ülemklassi objektile. Näiteks võib myVehicle viidata mälus hoopis mõnda teist alamtüüpi sõidukile (näiteks mootorratas).

Tüübiteisendus

Tüübiteisendust oleme kasutanud primitiivsete andmetüüpide juures: int a = (int)Math.round(...); või double b = (double) 5 / 3;. Sarnaselt saab tüübiteisendust kasutada objektide puhul.

Näiteks, kui on teada, et myVehile on tegelikult Car tüüpi, siis võib teha nii: <source lang="java"> Car c = (Car) myVehicle; </source>

c viitab samasse kohta kuhu myVehicle, aga üks eeldab, et seal mälus on Vehicle, teine aga seda, et objekt vastab Car kirjeldusele. Näiteks:

<source lang="java"> System.out.println(((Car) myVehicle).numberOfDoors); </source>

Sõiduki näide jätkub

<source lang="java"> Vehicle myVehicle = null; System.out.println("Vehicle Data:"); System.out.println("Registration number: " + myVehicle.registrationNumber);

if (myVehicle instanceof Car) { System.out.println("Type of vehicle: Car"); Car c = (Car) myVehicle; System.out.println("Number of doors: " + c.numberOfDoors); } else if (myVehicle instanceof Truck) { System.out.println("Type of vehicle: Truck"); Truck t = (Truck) myVehicle; System.out.println("Number of axels: " + t.numberOfAxels); } else if (myVehicle instanceof Motorcycle) { System.out.println("Type of vehicle: Motorcycle"); Motorcycle m = (Motorcycle) myVehicle; System.out.println("Has a sidecar: " + m.hasSidecar); } </source>


Polümorfism

Polümorfism tähendab seda, et klass võib kirjeldada endale unikaalset käitumist ja samal ajal jagada ülemklassi funktsionaalsust. Öeldakse, et klass laiendab mõne üldisema klassi funktsionaalsust ehk siis lisab näiteks ühe meetodi või täiendab olemasolevat.

Vaatame kujundite näidet. Oletame, et meil on programm, mis joonistab kujundeid ekraanile. Programm oskab tegeleda ristkülikute (Rectangle), ovaalide (Oval) ja ümarate nurkadega ristkülikutega (RoundRect).

Java-juhend-Various-shapes.png

Kujundite esitamiseks programmis võib kasutada klasse Rectangle, Oval ja RoundRect. Neil kolmel klassil oleks ühine superclass Shape, mis koondaks kokku ühisosa. Shape võiks näiteks sisaldada kujundi värvi, positsiooni, suurust jne koos meetoditega, mis neid väärtusti muudab. Näiteks värvi muutmise funktsionaalsus võiks muuta objekti (kujundi) värvi muutuja väärtust ning joonistada kujundi uuesti (nüüd juba uue värviga) välja.

<source lang="java"> class Shape {

Color color; // (must be imported from package java.awt)

void setColor(Color newColor) { // Method to change the color of the shape. color = newColor; // change value of instance variable redraw(); // redraw shape, which will appear in new color }

void redraw() { // method for drawing the shape

// <--- what commands should go here?

}

// more instance variables and methods

} // end of class Shape </source>

redraw() meetodi implementeerimisega tekib probleem, kuna iga kujund joonistab ennast erinevalt. Seega, üldises kujundi klassis Shape me ei saagi kirjutada funktsionaalsust, mis kõiki kujundeid oskab joonistada. Seevastu setColor() meetodiga on lihtsam - see on kõikidel kujunditel ühine (sõltumata sellest, kas tegemist on ristküliku või ovaaliga). Kujundu joonistamise kohta teab aga kõige paremini see kujund ise. Ehk siis ristküliku joonistamisega peaks tegelema ristküliku klass jne. Seega, kõik alamklassid peavad omama meetodit redraw(), kus toimub konkreetse kujundi väljajoonistamine.

Näiteks: <source lang="java"> class Rectangle extends Shape {

 void redraw() {
   // commands for drawing a rectangle
 }

} </source>

Kui programmis on muutuja myShape, mille andmetüüp on Shape, siis see muutuja võib viidata ükskõik millise kujundi peale. Ehk siis tegelik väärtus mälus vastava viida all võib olla kas Rectangle, Oval või RoundRect. Kui koodis on väljakutse:

<source lang="java"> myShape.redraw(); </source>

siis kutsutakse see meetod välja sellel objektil, mis mälus myShape viida all paikneb. Kooditekstist ei ole see tihti välja loetav. Kui programm läheb käima, alles siis saab teada, mis tüüpi objektile myShape viitab.

Näide. Programmis on nimekiri kujunditest, kuhu kuuluvad erinevat tüüpi kujundid (erinevad klassid kujundite kirjeldamiseks).

<source lang="java"> for (Shape s : shapes) {

   s.redraw();

} </source>

Näide tutvustab olukorda, kus shapes on näiteks ArrayList. Iga tsükliga me võtame kogumist järgmise elemendi. Saadud elemendile rakendame redraw() meetodit. Koodi järgi võiks tunduda, et me alati kutsume välja Shape klassis kirjeldatud meetodit (kuna muutuja s on Shape tüüpi). Tegelikult aga käivitub õige meetod vastava kujundi klassis.

Polümorfism tähistab seda, et täpne meetodi käivitamine sõltub sellest, mis objektile muutuja viitab. Viida andmetüüp ei pruugi ühtida mälus oleva andmetüübiga.

Vaatleme meetodi väljakutsumist kui sõnu saatmist objektile. Sõnumile vastatakse sellega, et pannakse vastav meetod käima. Kui kutsuda välja myShape.redraw();, siis saadetakse sõnum objektile, millele viitab muutuja myShape. Objekt (mälus) ise teab, mis tüüpi ta on, ja seega teab, kuidas sellele sõnumile vastata. Seega võib öelda, et myShape.redraw(); saadab alati sõnumi. See, kuidas sellele sõnumile vastatakse, sõltub sellest, kes selle sõnumi kätte saab. Seega nagu eelnevalt öeldud, polümorfism tähendab seda, et erinevad objektid võivad samale sõnumile reageerida erinevalt.

Uue kujundi lisamine

Kui me oma eelmise kujundite näite puhul tahaksime lisada uue kolmnurga kujundi, siis piisab, kui luua uus Shape alamklass nimega Triangle.

<source lang="java"> public class Triangle extends Shape {

 public void redraw() {
   // how to draw triangle
 }

} </source>

Ülejäänud kood, mis näiteks joonistas välja kõiki kujundeid kogumist shapes töötab ilma midagi muutmata.

Meetodite ülekate

Meetodi ülekate (overloading) võiks juba varasemast natuke tuttav mõiste olla. Java võimaldab kasutada ühes klassis kahte sama nimega meetodit. Java jaoks toimub meetodite võrdlus meetodi nime, võtmesõnade ja argumentide järgi. Kui kõik need nimetatud suurused on samad, siis on tegemist kahe sama meetodiga. Näiteid funktsioonide kirjelduse osas:

<source lang="java"> public class OverloadingExample {

public void print() {}

public void print(String s) {}

public void print(int s) {}

public static void main(String[] args) {

print(); print("tere"); print(13); }

} </source>

Meetodite alistamine

Meetodi alistamine või ülekirjutamine (overriding) tähistab seda, kui alamklass kirjeldab mõne ülemklassi meetodi täpselt sama signatuuriga. Sellisel juhul käivitatakse meetodi väljakutsumisel alamklassi realisatsioon sellest funktsioonist.

Kujundite näites meetod redraw() iga konkreetse kujundu juures alistas üldise ülemklassi meetodi redraw().

Soovitatav on kasutada annotatsiooni @Override:

Shape.java: <source lang="java"> public class Shape {

 public void redraw() {
   // some general stuff
 }

} </source>

Rectangle.java: <source lang="java"> public class Rectangle extends Shape {

 @Override
 public void redraw() {
   // rectangle specific stuff
 }

} </source>

Main.java: <source lang="java">

public class Main {

 public static void main(String[] args) {
   Shape s = new Rectangle();
   s.redraw(); // redraw() from Rectangle is called
 }

} </source>

this, super

Eelnevalt vaadeldud näites myShape.redraw(); sõnum saadeti objektile myShape.

Vaatame Shape klassi meetodid setColor:

<source lang="java">

 void setColor(Color newColor) {
   color = newColor; // change the value of instance variable
   redraw(); // redraw the shape with new color
 }

</source>

Koodis saadetakse sõnum (kutsutakse välja meetod) "redraw()", aga millisele objektile see saadetakse?

setColor on sõnum, mis saadeti objektile. redraw() on sõnum, mis saadetakse samal objektile, mis võttis vastu setColor sõnumi. Kui objekt on ristkülik Rectangle, siis käivitatakse redraw() meetod Rectangle klassist jne. redraw() ei pruugi käivitada Shape klassis olevat meetodit. Käivitatav meetod võib olla mõnes teises Shape alamklassis.

Javas on võimalik viidata objektile, mis võttis sõnumi vastu (mille meetodit kutsuti). Vaatleme järgnevalt kahte erilist muutujat: this ja super.

this

Muutuja this viitab sellele objektile, mis võttis saadetud sõnumi vastu (ehk siis objekt, mille meetodit välja kutsuti).

Üks võimalik kasutusjuht on see, kui kogu käesoleva objekt on vaja edasi saata järgmisele funktsioonile. Näiteks programmis on kujundid ning neid kujundeid saab aktiivseks teha. Aktiivseks tegemise funktsioon on staatiline ning asub klassis Shapes. Väljakutse võiks välja näha selline:

<source lang="java"> public class Rectangle extends Shape {

 public void click() {
   Shapes.activate(this);
 }

} </source>

Selliselt antakse kogu objekt kaasa Shapes.activate funktsioonile, mis siis sellega saab midagi edasi teha.

Teine kasutusjuht on see, kui teil on meetodis loodud muutuja, mille nimi ühtib objekti muutujaga. Tavaliselt setterid (set-meetodid) kasutavad seda lahendust:

<source lang="java"> void setColor(Color color) {

 this.color = color; // change the value of instance variable
 redraw(); // redraw the shape usingnew color

} </source>

setColor meetodis on kaks color-nimelist muutujat: funktsiooni argument ja objektimuutuja. Sellises olukorras kohalik meetodi muutuja peidab või varjab objektimuutuja. Meetodi sees color kasutades viidatakse sellele muutujale, mis loodi meetodi sees. Selleks, et viidata meetodi sees objektimuutujale, tuleb kasutada kirjapilti this.color nagu ülevalpool näites viidatud.

super

super viitab sõnumi vastuvõtnud objekti ülemklassile (ehk superclass-ile). super.x viitab objektimuutujale, mis paikneb väljakutsuva klassi ülemklassis. Seda saab ära kasutada juhul, kui alamklassis on kasutusel sama nimega muutuja kui ülemklassis. Sellises olukorras on tegelikult objektil kaks muutujat, mis on lihtsalt sama nimega: üks väljakutsuva klassi juures, teine selle ülemklassi juures. Alamklassis loodud sama nimega muutuja ei asenda ülemklassi samanimelist muutujat, alamklassi muutuja peidab või varjab ülemklassi samanimelise muutuja. Selleks, et viidata varjatud ülemklassi muutujale, saab kasutada super muutujat: super.var.

Teine peamine kasutusala on meetodi alistamisel ülekirjutatava meetodi funktsionaalsus laiendamine. Oletame, et meil on klass Rectangle, mis oskab joonistada ristkülikut pideva joonega. Nüüd on meil vaja lisada klass, mis oskaks joonistada ristkülikut punktiirjoonega. Eeldame, et Rectangle klassis olev meetod redraw() ei muuda joonestiili vaid kasutab vaikimisi joont (mis on pidev joon). Selleks võime teha järgmise realisatsiooni:

<source lang="java"> public class DashedRectangle extends Rectangle {

 public void redraw() {
   // change the line style to dashed
   super.redraw();
 }

} </source>

Teine näide oleks olukord, kus muidu kõikide objektide värvi võib vabalt muuta, kuid kolmnurga puhul on lubatud vaid sinine ja punane (ülejäänud värvide puhul värvimuudatust ignoreeritakse).

<source lang="java"> import java.awt.Color;

public class Triangle extends Shape { @Override void setColor(Color newColor) { if (Color.RED.equals(newColor) || Color.BLUE.equals(newColor)) { // allow only red and blue super.setColor(newColor); } } } </source>