Image
22.6.2016 0 Comments

Java pod lupou I. /10.časť

Späť na úvod >> Späť na programovanie >> Späť na seriál

Abstraktné triedy, rozhrania, balíky

Rozprávanie o triedach by nebolo úplné bez zmienky o abstraktných triedach a abstraktných metódach. S abstraktnými triedami úzko súvisí zvláštny údajový typ – rozhrania.

Abstraktné triedy

Do deklarácie ľubovoľnej triedy môžeme doplniť modifikátor abstract. O takej triede potom hovoríme, že je abstraktná, a chceme tým vyjadriť, že je nejakým spôsobom „nedokončená“ alebo sa aspoň za nedokončenú má považovať. Z abstraktných tried nemožno vytvárať inštancie, možno však od nich odvodzovať nové triedy, preto abstraktnú triedu veľmi pravdepodobne nájdeme na niektorom medzistupni či priamo na vrchole objektovej hierarchie. Takisto môžeme deklarovať premennú, ktorej typom bude abstraktná trieda, a uložiť do nej odkaz na reálnu inštanciu niektorej z odvodených tried.

Ako príklad by sme mohli uviesť hierarchiu grafických objektov v kresliacom programe. Na vrchole objektového stromu sa zrejme bude nachádzať nejaká univerzálna trieda, nazvime ju napríklad GrObject, implementujúca minimálnu množinu spoločných vlastností každého grafického objektu. V programe však sotva nastane situácia, keď by sme vytvorili reálnu inštanciu tejto triedy – pravdepodobne vytvoríme pole typu GrObject[], ktorého jednotlivé prvky budú ukazovať na inštancie potomkov triedy GrObject. Univerzálna trieda je teda výborným kandidátom na „zabstraktnenie“.

Abstraktné metódy

Abstraktná trieda môže byť s výnimkou modifikátora abstract v zásade totožná s neabstraktnou triedou, v takom prípade jediným rozdielom je nemožnosť vytvoriť objekt tejto triedy. (Predpokladá sa, že od abstraktnej triedy budeme odvodzovať potomkov; ak chceme len zabrániť vytváraniu inštancií, je správnejšie deklarovať konštruktor triedy ako privátny – členy triedy by však potom mali byť výhradne statické.) Pokiaľ nepovieme inak, je ľubovoľný potomok takejto triedy neabstraktný.

Abstraktná trieda však môže obsahovať aj jednu alebo viac abstraktných metód. To sú metódy deklarované s modifikátorom abstract. Deklarácia abstraktných metód informuje o názve, počte a typoch argumentov a o type návratovej hodnoty, neobsahuje však telo metódy, t. j. jej implementáciu (deklaráciu treba ukončiť bodkočiarkou). Každá trieda odvodená od takejto abstraktnej triedy potom musí implementovať všetky zdedené abstraktné metódy, inak sa sama stáva abstraktnou. Odvodená trieda môže zdedenú abstraktnú metódu okrem implementácie (t. j. predefinovania plnohodnotnou, neabstraktnou metódou) predefinovať inou, vlastnou abstraktnou metódou.

Ako abstraktné sa obyčajne deklarujú metódy, ktoré je nereálne alebo nemá zmysel naprogramovať pre všeobecný typ objektu. Príkladom v našej kolekcii grafických objektov môže byť metóda show(), ktorá prikazuje objektu, aby sa zobrazil. Zatiaľ čo o každej konkrétnej triede – grafickom objekte, ako napr. bod, čiara, kružnica – dokážeme povedať, ako sa má zobraziť, univerzálna supertrieda GrObject bude mať metódu show() deklarovanú ako abstraktnú – sotva by sme vedeli určiť, ako sa má objekt  GrObject zobraziť.

Abstraktnou nemôže byť hociktorá metóda. Prekladač ohlási chybu pri pokuse o abstraktnú deklaráciu metódy s modifikátorom private, static alebo final. Privátnu metódu nie sme schopní implementovať v odvodenej triede, pretože sa nededí, finálna metóda má predefinovanie zakázané a pri statických metódach abstraktnosť akosi stráca zmysel. Ani trieda ako celok nemôže byť deklarovaná ako abstraktná a finálna zároveň.

Abstraktné metódy nemajú telo. Je preto chybou snažiť sa o ich vyvolanie z inštancie odvodenej triedy prostredníctvom kľúčového slova super.

Trieda, ktorá obsahuje abstraktné metódy, či už vlastné alebo zdedené, musí byť deklarovaná s modifikátorom abstract, inak dôjde pri preklade k chybe.

Príklad abstraktnej triedy

Ako príklad si uvedieme veľmi zjednodušenú deklaráciu spomínanej abstraktnej triedy GrObject, od ktorej odvodíme už v predošlých častiach použitú triedu Point, ktorá bude reprezentovať bod, a ešte jednu triedu Circle, ktorá bude implementovať kružnicu. S ohľadom na obmedzený rozsah časopisu sú deklarácie čiastočne oklieštené – kompletnú verziu nájdete na webovej stránke PC REVUE:

public abstract class GrObject
{
  public abstract void show();
}
 
public class Point extends GrObject
{
  private int x, y;
 
  public void show()
  { System.out.println("Point at ["
     + x + ", " + y + "]"); }
}
 
public class Circle extends GrObject
{
  private int x, y, r;
 
  public void show()
  { System.out.println("Circle at ["
     + x + ", " + y + "]"); }
}

(V triedach chýbajú predovšetkým konštruktory – ale tie si šikovný čitateľ dokáže doplniť už aj sám.)

Rozhrania

S abstraktnými metódami úzko súvisí koncept rozhraní. Rozhranie (interface) v jazyku Java predstavuje referenčný typ. Je blízkym príbuzným triedy, nie je však možné vytvárať jeho inštancie a jeho členmi môžu byť len statické finálne údajové členy a abstraktné metódy. Ktorákoľvek trieda môže o sebe povedať, že dané rozhranie implementuje – tým sa zaväzuje, že implementuje všetky abstraktné metódy uvedené v rozhraní. Tento záväzok nemusí nevyhnutne splniť, ale potom sa stáva abstraktnou a musí byť tak aj deklarovaná. Rozhranie sa teda v podstate správa ako (špeciálna) abstraktná trieda.

Súčasťou rozhrania môžu byť údajové členy, ktoré sa automaticky považujú za verejné, finálne a statické (je dovolené príslušné modifikátory vynechať). Takéto konštantné členy môže potom implementujúca trieda používať ako svoje vlastné (t. j. ako keby ich zdedila).

Deklarácia rozhrania je značne podobná deklarácii triedy:

modifikátory interface MenoRozhrania
  extends Rozhranie, Rozhranie
{
  telo
}

Modifikátormi rozhrania môžu byť kľúčové slová public, protected, private, abstractstatic. Prvé z nich hovorí o tom, že deklarované rozhranie je verejné a prístupné komukoľvek, odkiaľkoľvek. Modifikátory protected, privatestatic sa používajú iba pri vnorených rozhraniach, ktoré zatiaľ preskočíme. Posledný modifikátor abstract je redundantný, pretože každé rozhranie je implicitne abstraktné. Považuje sa za zastaraný a neodporúča sa ho používať.

Rozhrania možno podobne ako triedy zoraďovať do hierarchie dedičnosti. Rozdiel oproti triedam je v tom, že rozhranie môže byť potomkom viacerých superrozhraní. Nadradené rozhrania sa uvádzajú v deklarácii rozhrania spolu so známou klauzulou extends. Zoznam rozhraní oddeľujeme čiarkou. Ak trieda implementuje rozhranie, ktoré je potomkom iných rozhraní, musí táto trieda implementovať okrem abstraktných metód samotného rozhrania všetky zdedené abstraktné metódy jeho predkov.

Telo rozhrania obsahuje deklaráciu jednotlivých abstraktných metód a/alebo údajových členov. Všetky členy i metódy sú implicitne verejné. Inak pre ne platia v zásade rovnaké pravidlá ako pri triedach – nemožno deklarovať dva členy s rovnakým názvom, člen potomka skrýva rovnako pomenovaný člen predka, abstraktná metóda potomka predefinuje abstraktnú metódu predka s rovnakou signatúrou.

Údajové členy musia v deklarácii obsahovať inicializátory (konštanty, ktorých hodnotu nepoznáme, by nám, pochopiteľne, boli na nič). Metódy môžeme deklarovať s modifikátormi public a abstract, ale je to zbytočné a neodporúča sa ich používať. Metódy rozhraní nesmú byť statické, natívne ani synchronizované.

V súvislosti s viacnásobnou dedičnosťou môže dôjsť k situácii, že rozhranie zdedí dva členy s rovnakým názvom. Nie je to chyba; k tej dôjde až pri pokuse o prístup k tomuto členu použitím nekvalifikovaného mena. Na viacnásobné členy preto treba vždy odkazovať pomocou úplného mena v tvare Rozhranie.člen. Ďalej sa môže stať, že rozhranie zdedí jeden a ten istý člen viackrát (cez rôzne cesty v strome dedičnosti). V takom prípade sa tento člen bude v rozhraní nachádzať iba raz (podobne, ako keby sme v C++ použili kľúčové slovo virtual).

Implementácia rozhraní

Trieda, ktorá sa zaväzuje implementovať niektoré rozhranie, musí túto skutočnosť explicitne oznámiť vo svojej deklarácii pomocou klauzuly implements, nasledovanej zoznamom implementovaných rozhraní. Táto klauzula sa uvádza za klauzulu extends, určujúcou predka triedy. Úplná schéma deklarácie triedy potom vyzerá takto:

modifikátory class MenoTriedy
  extends MenoPredka
  implements Rozhranie, Rozhranie
{
    telo
}

Ako vidieť, trieda môže implementovať aj viac ako jedno rozhranie, vďaka čomu je možné do určitej miery zabezpečiť viacnásobné dedenie, ktoré na rozdiel od C++ v tradičnej forme v Jave nenájdeme. Autori Javy zastávajú názor, že nutnosť viacnásobnej dedičnosti je známkou nedokonalého návrhu objektovej hierarchie, ale našťastie doplnením rozhraní do návrhu jazyka umožnili programátorom zariadiť sa podľa svojich potrieb.

Rozhrania, ktoré trieda implementuje, sú jej superrozhraniami (alebo by bolo lepšie nadradenými rozhraniami? …ach, tá slovenčina). Trieda môže mať aj nepriame superrozhrania – to sú jednak predkovia priamych superrozhraní triedy, jednak superrozhrania niektorého z predkov triedy (a aj rôzne iné kombinácie predkov a superrozhraní). Jednoduché, nie?

Trieda môže pri implementácii rozhraní jedinou deklaráciou metódy zabiť dve a viac múch jednou ranou – to v prípade, keď jej superrozhrania obsahujú metódy s rovnakou signatúrou. Tieto metódy sa však nesmú líšiť typom návratovej hodnoty, inak nebude existovať nijaký spôsob, ako implementovať obe metódy a súčasne dodržať pravidlo, že v jednej triede sa nesmú nachádzať dve metódy s rovnakou signatúrou.

Praktická ukážka

Aby sme neomieľali len suchú teóriu, ukážeme si deklaráciu a implementáciu rozhrania na príklade. Do našej kolekcie grafických objektov doplníme rozhranie Resizable, ktoré bude od objektov požadovať implementáciu metódy na zmenu ich veľkosti, a rozhranie Colorable, v ktorom sa budú nachádzať metódy na zmenu farby objektu. (Mimochodom, je zvykom rozhrania pomenovávať tak, aby sa končili príponou -able.)

public interface Resizable
{
  void resize(int factor);
}
 
public interface Colorable
{
  int RED = 1, GREEN = 2, BLUE = 4;
  void setColor(int color);
  int getColor();
}

Pre verejné rozhrania platí rovnaké pravidlo ako pre verejné triedy – ich zdrojový text sa musí nachádzať v súbore s rovnakým názvom a príponou .java. Naše dve rozhrania teda treba uložiť do súborov Resizable.javaColorable.java.

Rozhranie Resizable obsahuje jedinú metódu resize(), ktorá slúži na zmenu veľkosti objektu podľa hodnoty argumentu factor. Druhé rozhranie, Colorable, zahŕňa dve metódy na nastavenie farby (setColor()) a zistenie jej hodnoty (getColor()) a tri konštanty RED, GREENBLUE.

Vytvoríme ďalej novú triedu ColorPoint, ktorá bude potomkom triedy Point a navyše bude implementovať rozhranie Colorable:

public class ColorPoint extends Point
    implements Colorable
{
  private int color;
 
  public void setColor(int color)
  { this.color = color; }
 
  public int getColor()
  { return color; }
}

(V triede opätovne chýba konštruktor, kompletný zdrojový text nájdete na webe.)

Trieda ColorPoint obsahuje implementáciu metód setColor()getColor(), takže skutočne implementuje rozhranie Colorable.

Ako príklad implementácie rozhrania Resizable upravíme deklaráciu triedy Circle:

public class Circle extends GrObject
    implements Resizable
{
  private int x, y, r;
 
  public void resize(int factor)
  { r *= factor; }
 
  public void show()
  { System.out.println("Circle at ["
     + x + ", " + y + "]"); }
}

Zmenu veľkosti kružnice dosiahneme jednoduchým vynásobením jej polomeru parametrom factor.

Ako sme spomínali, rozhrania v Jave sú samostatnými referenčnými typmi. Je teda možné deklarovať premennú typu niektorého rozhrania. Takáto premenná potom môže (a vlastne musí) odkazovať na inštanciu triedy, ktorá dané rozhranie implementuje. Nasledujúce dva príklady sú teda korektné:

Colorable c = new ColorPoint(1, 2);
c.setColor(RED);
 
Resizable r = new Circle(0, 0, 10);
r.resize(3);
r.show();

Balíky

Ako sme niekoľkokrát naznačili v predchádzajúcich častiach, triedy v Jave sú zoskupované do tzv. balíkov (packages). Balík je množina tried, ktoré zvyčajne spolu nejakým spôsobom súvisia. Okrem toho je vďaka balíkom možné deklarovať dve a viac tried či rozhraní s rovnakým názvom – pokiaľ sa budú nachádzať v rôznych balíkoch.

Každý balík má svoj názov, ktorý by mal pozostávať výhradne z malých písmen. Členmi balíka môžu byť triedy, rozhrania a podriadené balíky, usporiadanie je teda hierarchické. Možno, samozrejme, vytvoriť balík, ktorý neobsahuje žiadne triedy/rozhrania, ale čisto len podbalíky. Úplné (tzv. plne kvalifikované) meno balíka Q, ktorý patrí do balíka P, vytvoríme klasickou bodkovou metódou ako P.Q. Plne kvalifikované meno ľubovoľnej triedy, ktorá je prvkom niektorého balíka, dostaneme bodkovým spojením úplného mena tohto balíka a mena triedy.

Zoberme si ako príklad triedy štandardného aplikačného programového rozhrania Java API. Všetky nepriamo patria do hlavného balíka s názvom java (alebo v novších verziách Javy aj do balíka javax). Balík java neobsahuje žiadne triedy, zato však v ňom nájdeme mnoho podbalíkov, ako napríklad awt, lang, net, util a pod. Každý z týchto podbalíkov je určený pre iný okruh úloh: java.awt napríklad poskytuje triedy na realizáciu grafického používateľského rozhrania, triedy balíka java.net sú určené na prácu so sieťou atď. V balíku java.util sa nachádza okrem množstva ďalších tried aj trieda Vector. Jej úplné meno je podľa prv uvedených pravidiel java.util.Vector.

Vráťme sa ešte na chvíľu k deklarácii údajových členov a metód tried. V prípade, že v deklarácii neuvedieme nijaký modifikátor prístupu, bude daný člen (či metóda) prístupný v rámci tejto triedy, všetkých jej potomkov, a aj v rámci všetkých tried toho istého balíka, nebude však prístupný mimo balíka. Typy prístupu sú v Jave teda štyri: private, protected, implicitný (takisto balíkový) prístup a public.

Implementácia balíkov

Každý javovský virtuálny stroj musí vedieť, ako nájsť binárny obraz každej triedy na základe jej plne kvalifikovaného mena. Jednou z najjednoduchších a súčasne najpoužívanejších metód je prevedenie hierarchickej štruktúry balíkov a tried do hierarchickej štruktúry adresárov a súborov. Balíku java tak bude zodpovedať adresár java, ktorý musí obsahovať podadresáre ako applet, awt, io, lang, net, util a pod. Obsah každého podadresára je ekvivalentný obsahu každého balíka. V podadresári java/util by sme teda mali nájsť binárny obraz triedy Vector, súbor Vector.class. Zdrojový kód každej triedy sa zvykne umiestňovať do rovnakého adresára ako jej binárny tvar, takže v spomínanom podadresári nájdeme pravdepodobne aj súbor Vector.java. Teraz je už zrejmé, ako JVM nájde ľubovoľnú triedu – jednoducho zoberie jej plne kvalifikované meno, bodky v ňom nahradí oddeľovačmi adresárov pre danú platformu (t. j. obyčajne / alebo \, prípadne ešte :) a k výsledku pripojí príponu .class.

Je očividné, že virtuálny stroj musí vedieť, kde je koreň celej hierarchie. To sa väčšinou dozvie z premennej prostredia CLASSPATH. Jej obsahom býva postupnosť ciest k adresárom, ktoré potom slúžia ako štartovacie body na hľadanie príslušnej triedy. Je logické, že implementácie umožňujú mať týchto koreňov niekoľko – potom netreba miešať štandardné javovské triedy s triedami, ktoré tvoria vlastný program.

Spomínaný systém ukladania tried v súborovom systéme má jednu drobnú nevýhodu: môže zaberať pomerne dosť veľa miesta na disku, pretože ide o množstvo malých súborov a na niektorých systémoch v takom prípade dochádza k plytvaniu miestom vzhľadom na internú fragmentáciu (to vtedy, keď je veľkosť alokačnej jednotky zbytočne veľká). Riešenie je našťastie triviálne: celý strom binárnych obrazov tried sa skomprimuje do jedného súboru.

Ďalším spôsobom organizácie tried je ich ukladanie do databázy. Tento spôsob používajú niektoré vývojové prostredia.

Deklarácia package

Vysvetlíme si najprv pojem kompilačná jednotka. V prípade ukladania tried do súborového systému je kompilačnou jednotkou súbor so zdrojovým textom jednej alebo viacerých tried. Každá trieda (a pochopiteľne aj rozhranie) môže byť deklarovaná s modifikátorom public alebo bez neho. Pokiaľ je trieda deklarovaná ako verejná (teda s kľúčovým slovom public), musí sa jej zdrojový kód nachádzať v súbore s rovnakým názvom, ako je meno tejto triedy, a s príponou .java. Z toho vyplýva, že v každej kompilačnej jednotke sa smie nachádzať deklarácia najviac jednej verejnej triedy.

Verejná trieda je viditeľná a prístupná všetkým triedam z toho istého balíka, ako aj každej triede z ľubovoľného iného balíka. Naproti tomu trieda deklarovaná bez modifikátora public je prístupná len v rámci svojho balíka, mimo balíka ju používať nemožno. Taká trieda sa správa ako privátna pre svoj balík. Jej deklaráciu možno uložiť do súboru s ľubovoľným názvom.

V každej kompilačnej jednotke sa môže nachádzať informácia o tom, do ktorého balíka náležia triedy v nej deklarované. Túto informáciu zabezpečuje kľúčové slovo package, nasledované plne kvalifikovaným menom balíka. Ak sa deklarácia package v súbore nachádza, musí byť uvedená ako prvá, ináč prekladač ohlási chybu.

Deklaráciu package môžeme aj vynechať. Robí sa to obyčajne v prípade jednoduchých či pomocných programov alebo v raných štádiách vývoja nejakého projektu. Triedy z takejto kompilačnej jednotky sa stávajú súčasťou tzv. nepomenovaného balíka. Takto boli, mimochodom, definované všetky naše doterajšie triedy.

Deklarácia import

Pri použití ľubovoľnej triedy v programe je v zásade potrebné uviesť jej úplné meno, čo je v prípade zložitejšej balíkovej hierarchie značne frustrujúce. Java preto poskytuje príkaz import, pomocou ktorého môžeme požadované triedy sprístupniť pod svojimi krátkymi menami. Tento príkaz, ak ho použijeme, sa musí nachádzať v kompilačnej jednotke medzi príkazom package a začiatkom deklarácie tried.

Príkaz import má dva spôsoby použitia. V prvom uvedieme za kľúčové slovo import plne kvalifikované meno triedy, ako napríklad:

import java.util.Vector;

Odteraz sa môžeme na triedu java.util.Vector v programe odvolávať použitím výrazne kratšieho Vector. Pochopiteľne, nie je možné takto „importovať“ dve triedy s rovnakým krátkym menom Vector ani nemôžeme v danej kompilačnej jednotke deklarovať inú triedu Vector. Tento spôsob takisto neumožňuje importovať podbalíky – nemôžeme napísať import java.util; a použiť potom zápis util.Vector.

Druhý spôsob je vhodnejší v prípade, že chceme sprístupniť viacero tried z jedného balíka. Ak použijeme napríklad takýto zápis:

import java.util.*;

môžeme na všetky triedy balíka java.util odkazovať ich krátkymi menami, ako Vector, Set, HashMap a pod.

V našich doterajších programoch sme používali pre výstup textu na obrazovku konštrukciu System.out.println(...). Šikovný čitateľ by už mal byť schopný tento zápis analyzovať a zistiť, že ide o volanie metódy println() nad objektom out, ktorý je statickým členom triedy System. A tu je malá nezrovnalosť. O niekoľko riadkov vyššie sme predsa tvrdili, že triedy treba sprístupňovať ich úplným menom a tým je pre triedu System meno java.lang.System. Pravidlo, samozrejme, platí – okrem jednej malej výnimky. Triedy balíka java.lang sú natoľko používané, že sa importujú automaticky – každá kompilačná jednotka „obsahuje“ fiktívny príkaz import java.lang.*;.

Jednoznačné mená balíkov

V praxi pri realizácii projektu rôznymi tímami, prípadne pri využívaní cudzích knižníc tried môže nastať nepríjemná situácia, keď je potrebné do jedného programu integrovať dva balíky s rovnakými názvami. To, samozrejme, možné nie je, preto existuje odporúčanie, ako prideľovať balíkom jednoznačné, unikátne mená.

Postup je jednoduchý. V rámci nášho projektu si vytvoríme hierarchiu a pomenovanie balíkov, ako sa nám hodí. Potom vezmeme svoju doménovú internetovú adresu (musíme, pochopiteľne, nejakú mať), usporiadame v nej subdomény v opačnom poradí a výsledok pripojíme na začiatok názvu každého nášho balíka. Ak by teda napríklad časopis PC REVUE (ktorého doménová adresa je pcrevue.sk) vyvíjal nejaké javovské triedy, ich plne kvalifikované mená by sa začínali reťazcom sk.pcrevue. Povedzme, že by to boli triedy na prácu so zvukom a s grafikou, usporiadané do dvoch balíkov soundgraphics. Úplné mená týchto balíkov by teda zneli sk.pcrevue.sound a sk.pcrevue.graphics.

Nabudúce

Aby sme konečne prešli od nevyhnutnej, ale nie veľmi záživnej teórie k praxi, v budúcej časti sa pozrieme na zúbky niektorému zo štandardných balíkov Java API. Dovidenia o mesiac.

 

 

 


Nechajte si posielať prehľad najdôležitejších správ emailom

Mohlo by Vás zaujímať

Ako na to

Tipy a triky: Ako na snímku obrazovky na akomkoľvek počítači s Windows?

02.12.2016 00:13

Ak snímky obrazovky robíte často apotrebujete napríklad funkcie na posun stránok alebo snímanie zobrazenia pri vyššom rozlíšení displeja, zrejme používate nejakú špecializovanú aplikáciu. Väčšina použ ...

Ako na to 1

Tipy a triky: Ako aplikácii prednastaviť spúšťanie s administrátorskými právami?

30.11.2016 00:10

Väčšina aspoň trochu skúsenejších používateľov vie, že aj keď máte na operačnom systéme Windows vytvorený administrátorský účet, aplikácie pre bezpečnosť nefungujú vždy splnými administrátorskými práv ...

Ako na to 2

Tipy a triky: Ako vypnúť uzamykaciu obrazovku vo Windows 10?

29.11.2016 00:10

Rozčuľuje vás, že pred každým prihlásením doúčtu vášho počítača musíte prejsť uzamykacou obrazovkou? Windows 10 na tejto obrazovke ukazuje čas,dátum anejakú zaujímavú fotografiu zrôznych kútov sveta. ...

Žiadne komentáre

Vyhľadávanie

Kyocera - prve-zariadenia-formatu-a4-s-vykonom-a3

Najnovšie videá