Multaj programistoj, kiuj konatiĝas kun nova lingvo, tuj demandas, kiel verki en tiu lingvo "salut-programon", tio estas programo, kiu nur eligas unu frazon da teksto. Do, jen salut-programo en Ĝavo:
Saluto.java |
package aiskursoj; public class Saluto { public static void main(String[] a) { System.out.println("Saluton, amikoj de \u011cavo!"); } } |
Rezulto: Saluton, amikoj de Ĝavo! |
Kelkaj observoj:
Ni ĵus diris, ke klasoj estas centra afero en Ĝavo, kaj ke klasojn oni difinas en dosieroj samnomaj.
Klasoj konsistas el du specoj de membroj: Datenoj kaj metodoj. Iom krude oni povus diri, ke datenoj estas io kaj metodoj faras ion. Ekzemple iu klaso Konto povus havi la datenan membron sumo, kiu priskribas la staton de la konto, kaj metodan membron enpagu, kiu altigas la sumon en la konto.
Ambaŭ povas posedi certajn epitetojn, kiuj difinas ilian objekt-rilaton kaj ilian videblecon:
Klasoj estas samtempe daten-tipoj. Ekzistas jenaj gravaj operacioj pri klasoj kaj iliaj ekzempleroj:
Kiam objekto, do ekzemplero de iu klaso, aperas en Ĝavo-programo, fakte temas ne pri la objekto mem, sed pri referenco al ĝi. Referenco estas simila al adreso; ĝi estas transdonebla ekzemple al metodo sen bezono kopii la objekton.
Kvankam referencoj en komputiloj estas realigitaj simile al montriloj, necesas emfazi, ke ili ne estas montriloj. Ĝavo ne havas montrilojn; ili estas konsiderataj tro danĝeraj kaj erarigaj.
Kiu estas la diferenco inter referencoj kaj montriloj? Montriloj (kiuj ekzistas ekzemple en la lingvo C++) estas manipuleblaj. Oni povas kalkuli per ili, ekzemple trapaŝi tabelon per montrilo pli kaj pli altigata. La danĝero estas en tio, ke tion oni (pro eraro) povas fari, ankaŭ se la montrilo tute ne montras al tabelo. Tial Ĝavo malpermesas tiajn aferojn kaj ne uzas montrilojn.
Ekzistas speciala nul-referenco null, kiu referencas al neniu objekto. Oni povas kompari ĝin al aliaj referencoj; ĝi estas "Identa" nur al si mem.
Enkapsuligo signifas, ke oni protektas la datenajn membrojn de objektoj kontraŭ arbitra manipulado, ĉar tia arbitra manipulado povas produkti ne-integrajn objektojn kun internaj kontraŭdiroj. Ni prenu (intence) malbonan realigon de klaso por signovicoj. Ĝi baziĝas sur la ideo, ke vico aŭ estas malplena aŭ konsistas el sia unua elemento (tie ĉi: signo) kaj el resto, kiu same estas vico. Por simpligi ni ne permesas malplenajn vicojn; anstataŭe ni uzas la nul-referencon null.
Nia ekzemplo enhavas liniojn de la formo /** … */; ili estas komentoj por homaj legantoj. Speciala programo, javadoc, povas el ili kompili dokumenton, kiu priskribas la klason; ni poste parolos pri ĝi. Ankaŭ tekstoj post du oblikvaj stangetoj (//) estas komentoj.
public class MalbonaSignovico { /** jen la unua signo de la vico */ public char unua; /** jen referenco al la resto de la vico */ public MalbonaSignovico resto; /** jen la longo de la vico */ public int longo; /** konstruilo por unu-signa vico */ public MalbonaSignovico(char signo) { unua = signo; longo = 1; resto = null; // ne estas resto } /** alpendigo de alia vico */ public void alpendigu(MalbonaSignovico vico2) { longo = longo + vico2.longo; if (resto == null) { // ni ne havas reston, do vico2 ighu nia resto: resto = vico2; } else { // ni havas reston, do ni alpendigas vico2 al ghi: resto.alpendigu(vico2); } } } |
Kompreneble neniu volas uzi tian malbonan realigon, ĉar Ĝavo ja ofertas multe pli bonajn kaj komfortajn teknikojn. Sed nia klaso havas la precipan malavantaĝon, ke ĝiaj datenaj membroj estas libere manipuleblaj. Se ni havas (en alia klaso) ekzempleron de la MalbonaSignovico, ni povas arbitre ŝanĝi ĝian longon kaj krei ne-integran objekton:
MalbonaSignovico miaVico = new MalbonaSignovico('c'); // "miaVico" nun havas longon 1, ghi konsistas nur el 'c' miaVico.longo = 12; // "miaVico" plu havas nur unu signon, sed kredas havi longon 12 |
En Ĝavo la solvo estas sufiĉe simpla: Datenaj membroj ĉiam havu la videblon private, tiel ke la klaso havu plenan regadon de ili. En la donita ekzemplo la membro longo do estu privata kaj inspektebla (legebla) nur pere de publika metodo. Tiajn inspektajn metodojn oni ofte nomas per la angla verbo get (= ricevi, rivecu):
/** la longo de la vico */ private int longo; /** @return la longon de la signovico */ public int getLongo() { return longo; } |
Metodojn, kiuj rekte ŝanĝas datenan membron, analoge uzas en sia nomo la anglan verbon set (metu). Tie ĉi estus malsaĝ provizi metodon setLongo, ĉar ankaŭ per ĝi eblus krei ne-integran objekton.
La lasta ekzemplo montris al ni specialan elementon de la program-komenta tekniko Javadoc: Certaj ŝlosilaj vortoj, enkondukitaj per heliko (@), priskribas la uzadon de klasoj kaj metodoj (@return priskribas la rezulton de metodo). Ili faciligas la legadon de la klasa kodo.
Heredado inter objektoj signifas, ke klaso povas transpreni ("heredi") ecojn de alia klaso. Tio ofte evitas la neceson skribi la saman program-kodon plurfoje, kio kreus la riskon de eraroj kaj malfaciligus la flegadon de la kodo. Se klaso B heredas de klaso A, oni diras, ke B estas subklaso de A, kaj inverse, ke A estas superklaso de B. Oni diras ankaŭ, ke B estas derivita de A.
Kiel ekzemplon ni prenu asocion, kiu havas du specojn de membroj: naturaj (personoj) kaj juraj (aliaj asocioj, firmaoj, aŭtoritatoj ktp.). Tiuj du specoj havas komunajn kaj nekomunajn ecojn: Ekzemple ambaŭ havas adreson, sed nur personoj havas naskiĝdaton kaj nur asocioj havas prezidanton (aŭ direktoron ktp.)
Se ni verkas por tiuj du membro-specoj du klasojn, PersonaMembro kaj AsociaMembro, estas utile derivi tiujn du klasojn de alia klaso, Membro:
public class Membro { /** la adreso de la membro */ private Adreso adreso; /** metodo por presi adres-shildon */ public void presuAdreson() { … } } |
public class PersonaMembro extends Membro { /** la naskighdato de la membro */ private Date naskighdato; } |
public class AsociaMembro extends Membro { /** la jura reprezentanto de la asocio */ private String reprezentanto; } |
Heredado do ebligas en subklaso voki metodon de superklaso. Se iu metodo de PersonaMembro aŭ de AsociaMembro deziras presi adres-ŝildon, Ĝi povas voki la tiucelan metodon presuAdreson() de Membro, kies korpon ni prezentis per tri punktoj. Jena bildo montras la dependo-rilaton ("estas") inter la klasoj (ĉiu PersonaMembro "estas" Membro, ĉiu AsociaMembro "estas" Membro) kaj la eblon voki metodojn:
La avantaĝo estas ne nur la ŝparo de kelkaj programlinioj. Ekzistas okazoj, en kiuj la diferenco inter la du membro-specoj ne gravas, ekzemple kiam oni volas dissendi cirkuleron al ĉiuj membroj kaj bezonas adres-ŝildojn por kovertoj. Tie eblas simple trairi liston de ĉiuj Membroj kaj al ĉiu sendi la mesaĝon (= voki la metodon) presuAdreson(). La subklasoj tute ne aperas en tiu parto de la programo. Sen la superklaso Membro oni devus krei du apartajn listojn kaj trairi ilin.
Ni vidis, ke heredo permesas al subklasoj uzi metodojn de superklaso. Sed ĉu eblas, ke superklaso uzu metodojn de subklasoj? Kiel ekzemplon ni iom modifu la presadon de adres-ŝildoj: ni deziras persone adresi la reprezentantojn de la membroj. Personaj membroj reprezentas sin mem, nur asociaj membroj havas personon kiel reprezentanton. La metodoj por presi la deziratajn adresojn do estas malsamaj por la du klasoj:
public class PersonaMembro extends Membro { /** la naskighdato de la membro */ private Date naskighdato; /** presu la adreson de reprezentanto (la membro mem) */ public void presuPersonanAdreson() { … } } |
public class AsociaMembro extends Membro { /** la jura reprezentanto de la asocio */ private String reprezentanto; /** presu la adreson de "la reprezentanto" */ public void presuPersonanAdreson() { … } } |
Nun ĉiu ekzemplero de PersonaMembro kaj de AsociaMembro povas voki sian propran metodon por presi la adreson de la reprezentanto. Sed kiel ni povus tion fari en miksita listo de ekzempleroj de la du klasoj, laŭ la menciita ekzemplo? En tia listo ni scias nur, ke ni havas ekzemplerojn de la klaso Membro, kaj tiu ĉi klaso ne havas metodon presuPersonanAdreson. Kaj se ĝi havus, la metodo estus la sama por la du subklasoj.
La solvo estas koncepto nomataj per la esprimoj dinamika bindado, virtualaj metodoj kaj abstraktaj metodoj. En Ĝavo ilia uzo aspektas jene:
public abstract class Membro { /** la adreso de la membro */ private Adreso adreso; /** metodo por presi adres-shildon */ public void presuAdreson() { … } /** metodo por presi adres-shildon de reprezentanto */ public abstract void presuPersonanAdreson(); } |
Rimarku la ŝlosil-vorton abstract ĉe la klaso kaj ĉe la nova metodo, kaj ke tiu metodo ne havas korpon. La vorto abstract ĉe la klaso signifas, ke realaj ekzempleroj de la klaso ne ekzistas, nur de ĝiaj subklasoj; pro tio eblas, ke la klaso havu abstraktajn metodojn, kiuj reale ekzistas nur en subklasoj. La vorto abstract ĉe la metodo signifas, ke la metodo ne reale ekzistas en tiu klaso, sed ke ĝi devas ekzisti en ĉiuj ne-abstraktaj subklasoj.
Ĉar la klaso Membro nun scias pri la la metodo presuPersonanAdreson(), ĝi povas voki ĝin sen scii, ĉu iu Membro reale estas PersonaMembro aŭ AsociaMembro". Kies metodon voki estas decideble nur je la rul-tempo de programo. Tial oni nomas la elekton (bindadon) de la metodo malfrua bindado aŭ virtuala bindado.
Virtuala bindado kostas iomete da tempo dum la rulado de programo. Tial kelkaj objektemaj lingvoj posedas du specojn de metodoj: virtualajn kaj ne-virtualajn (tradiciajn). En Ĝavo ĉiuj metodoj estas virtualaj.