Java:Objektid

Allikas: Kursused
Redaktsioon seisuga 12. märts 2015, kell 01:01 kasutajalt Ago (arutelu | kaastöö) (→‎Pärimine)
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.