11. Implementace OOP v Javě
11.1. Objekt
Program v Javě je staticky strukturován na třídy, jejichž instance (objekty)
za běhu dynamicky programu vznikají a zanikají. Objekt je nejprve vytvořen
(instanciován), následně může být používán a nakonec je zrušen.
11.1.1. Vytvoření objektu
K vytvoření objektu slouží operátor new, který má syntaxi:
new voláníKonstruktoru
Operátor alokuje paměť pro objekt a zavolá konstruktor (viz
11.2.3.), což je speciální metoda, která provádí inicializaci
objektu. Jméno konstruktoru je vždy shodné se jménem třídy objektu.
Priklad 11.1. |
new String("ahoj");
vytvoří objekt třídy String a inicializuje ho konstruktorem
s parametrem "ahoj".
|
|
Operátor new vrací referenci, odkazující na vytvořený
objekt. Tato reference se při vytvoření objektu obvykle zároveň
ukládá (1) do referenční
proměnné (viz 7.2.):
Priklad 11.2. |
String retezec; // deklarace proměnné
retezec = new String("ahoj"); // přiřazení reference
nebo zkráceně:
String retezec = new String("ahoj");
|
|
11.1.2. Používání objektu
Používání objektu spočívá ve volání metod a přímé manipulaci se
členskými proměnnými. Pro přístup k proměnné nebo metodě se používá tečková
notace:
reference.jménoProměnné
reference.voláníMetody
Priklad 11.3. |
String retezec = new String("ahoj"); // vytvoření objektu
retezec = retezec.concat("!"); // volání metody
Metoda concat() vrací nový řetězec, který vznikne
připojením parametru na konec řetězce. Výsledek se přiřadí do proměnné
retezec.
|
|
Je možné použít i vícenásobnou přístupovou konstrukci:
Priklad 11.4. |
int delka = retezec.trim().length();
Metoda trim() vrací referenci na nový řetězec (instanci třídy
String), který vznikl z původního oříznutím prázdných znaků. Metoda
length() vrací délku řetězce (typ int). Proměnné delka se
tedy přiřadí délka řetězce retezec po oříznutí metodou trim().
Předchozí zápis lze přepsat jako:
String orezanyRetezec = retezec.trim();
int delka = orezanyRetezec.length();
|
|
11.1.3. Zrušení objektu
Java neumožňuje přímé rušení (dealokaci) objektu. Objekt je zrušen automaticky
až tehdy, neexistuje-li na něj reference. Jsou například rušeny
všechny objekty lokálních proměnných při ukončení metody. "Ruční"
zneplatnění reference se provede přiřazením hodnoty null příslušné
referenční proměnné.
Priklad 11.5. |
String retezec = new String("ahoj");
retezec = null; // zneplatnění reference -- objekt bude zrušen
|
|
Tento automatický mechanismus rušení objektů je znám pod názvem garbage
collection ("úklid smetí"). Jeho výhoda spočívá v nemožnosti
dealokovat již dealokovaný prostor nebo do něj zapisovat, což bývá v jiných
jazycích zdrojem mnoha nepříjemných chyb. Úklid se provádí buďto synchronně, tzn. až při nedostatku paměti pro
další alokace, nebo asynchronně na pozadí, pokud to daná platforma
umožňuje (Unix, Windows95/NT). V programu lze o spuštění úklidu požádat
voláním metody System.gc(), například v době, kdy program neprovádí
výpočty.
Ještě před vlastním zrušením objektu se zavolá metoda finalize() (2) (je-li definována), kde může programátor uložit obsah proměnných
do souboru apod. Deklarace této metody musí vypadat následovně:
protected void finalize() throws Throwable
Při rušení objektu nedochází ke zřetězení volání metod finalize() jako
je tomu u konstruktorů při vytváření objektu (viz 11.2.3.).
11.2. Třída
Pro každý objekt musí být před jeho vytvořením deklarována třída, ať
už navržená v programu nebo knihovní. Knihovní třídy jsou uloženy
v balících (viz kap. 12.).
11.2.1. Deklarace třídy
Nejjednodušší deklarace třídy má syntaxi:
class JménoTřídy {
// tělo třídy
}
Identifikátor JménoTřídy slouží jako objektový typ, například v deklaraci
referenční proměnné (viz 7.2.). V těle třídy mezi složenými
závorkami mohou být deklarace členských proměnných
(viz 11.2.4.) a/nebo deklarace a definice metod
(viz 11.2.2.) (3) - obvykle v tomto pořadí, ale není to vyžadováno. Všimněte
si, že za uzavírající závorkou deklarace třídy není středník. Speciálním případem je třída, která neobsahuje metody. Ta pak slouží jako
klasická datová struktura (record v Turbo Pascalu, struct v C).
Obecná deklarace třídy vypadá takto:
[ modifikátory ] class jménoTřídy
[extends jménoRodičovskéTřídy ]
[implements seznamRozhraní ] {
// tělo třídy
}
Priklad 11.7. |
Deklarace veřejné třídy Appletik, jejíž rodičovskou je třída
java.applet.Applet, a která implementuje rozhraní Cloneable
a Runnable (z balíku java.lang) vypadá takto:
public class Appletik extends java.applet.Applet
implements Cloneable, Runnable {
public void run() { // ...
}
// ...
}
Rozhraní Runnable obsahuje metodu run, a proto musí
třída Appletik její definici obsahovat. Rozhraní Cloneable je
prázdné a slouží jen k identifikaci třídy (její instance je možné klonovat
- viz 11.5.).
|
|
11.2.2. Metody
Nejjednodušší definice metody má tvar:
NávratovýTyp jménoMetody ( parametry ) {
// tělo metody
}
- NávratovýTyp musí být některý datový typ z kap. 7..
Návrat z metody a případné předání návratové hodnoty způsobí příkaz return (viz 9.13.). Je-li návratovým typem void, metoda nevrací
hodnotu.
- jménoMetody musí být platný identifikátor. Pokud mají v jedné
třídě dvě metody shodná jména, musí mít odlišný počet a/nebo typ parametrů -
tzv. přetěžování metod (overloading). Při volání přetížené
metody překladač na základě argumentů rozhodne, kterou volat. Díky přetěžování
nemusí existovat různě pojmenované metody, jejichž funkce je prakticky shodná:
Priklad 11.8. |
Argumentem metody System.out.println(), která provádí výpis na
standardní výstup, může být proměnná kteréhokoliv základního datového typu
nebo nic.
System.out.println(); // odřádkuje
System.out.println("ahoj"); // vypíše ahoj (1)
System.out.println(12345); // vypíše 12345 (2)
Ve všech případech se jedná o volání jiné přetížené metody - překladač se
rozhoduje na základě argumentů - první případ se liší počtem argumentů, druhé
dva jejich typem: (1) předává řetězec, (2) celé číslo.
|
|
- Formální parametry metody:
- Je-li jméno parametru stejné jako jméno členské proměnné, dochází
k jejímuzastínění (hiding) parametrem. K explicitnímu přístupu
ke členské proměnné pak slouží operátor this (5) (viz též 11.8.), který
vrací referenci objektu na sebe:
Priklad 11.10. |
class Trida {
int x;
Trida (int x) { // zastínění členské proměnné x parametrem
this.x = x; // přiřazení hodnoty parametru členské proměnné x
// ...
}
}
|
|
Obecná deklarace metody vypadá takto:
[ práva ] [static] [abstract] [final] [native]
[synchronized] NávratovýTyp jménoMetody ( parametry )
[throws seznamVýjimek ]
- práva - tato položka určuje přístupnost metody vně třídy
a pravidla pro dědičnost. Metoda může být buď veřejná (public),
chráněná (protected), soukromá (private) nebo
nemusí mít práva specifikována vůbec (default access).
Více viz 11.3. a 11.4..
- static - označuje statickou metodu.
Statické metody je možné narozdíl od nestatických volat i tehdy, neexistuje-li
žádná instance dané třídy. Mohou však přímo manipulovat pouze se statickými
(a svými lokálními) proměnnými.
Volání statické metody má tvar:
[ JménoTřídy. ] jménoMetody ( parametry );
JménoTřídy se nemusí specifikovat uvnitř třídy, která statickou metodu
definuje.V jedné třídě nemůže být definována shodně pojmenovaná statická i nestatická
metoda se stejným počtem a typem parametrů - jinak dojde k chybě při
překladu.
- abstract - metody deklarované jako abstraktní nemají
v dané třídě tělo a musí být definovány až potomky třídy. Třída, která
obsahuje abstraktní metody, nemůže být instanciována a musí být rovněž
deklarována jako abstraktní (viz 11.2.1.).
Priklad 11.11. |
abstract class A {
abstract void metoda(); // deklarace abstraktní metody
}
|
|
- synchronized - viz 16.5.2..
- final - označuje koncovou metodu. Koncová metoda nesmí
být potomky překryta.
- native - nativní metody umožňují sloučit programy psané
v Javě a jiných jazycích. Například většina nízkoúrovňových (6) metod z Java Core API je nativních.
V běžných aplikacích se nativní metody nepoužívají kvůli jejich
nepřenositelosti. V appletech se používat nesmí.
- Klíčové slovo throws uvozuje jedno nebo více jmen tříd výjimek (viz kap. 13.), které může metoda vyvolat. Je-li výjimek více
jsou odděleny čárkami.
Priklad 11.12. |
Následující metoda deklaruje, že může vyvolat výjimky třídy ArithmeticException a SecurityException:
void metoda() throws ArithmeticException, SecurityException {
// ...
}
|
|
Zárověň nelze použít dvojic static abstract a final abstract.
Argumenty metod se vždy předávají hodnotou - hodnotu argumentu
sice měnit lze, ale navenek se to neprojeví:
Priklad 11.13. |
Metoda prohod() má zaměnit hodnoty svých dvou argumentů:
void prohod(int x, int y) {
int tmp = x; x = y; y = tmp;
}
void hop() {
int a = 1, b = 2;
prohod(a,b); // tohle nefunguje! (1)
// nadále a = 1, b = 2
}
Po zavolání metody prohod (1) se hodnoty argumentů nezamění. Pro
správnou funkci je třeba provést tuto úpravu: proměnné x, y se
umístí do jedné instance třídy (2), na kterou se metodě prohod předá
reference (3):
class XY {
public int x, y; // (2)
}
void prohod(XY xy) {
int tmp = xy.x;
xy.x = xy.y;
xy.y = tmp;
}
void hop() {
XY xy = new XY(); // objekt xy namísto a, b
xy.x = 1;
xy.y = 2;
prohod(xy); // (3)
// nyní: xy.x = 2, xy.y = 1
}
|
|
11.2.3. Konstruktory
Konstruktor je speciální metoda, která se volá pouze při vytváření
objektu (viz 11.1.1.) a slouží k jeho inicializaci. Jméno
konstruktoru musí být shodné se jménem třídy, v níž je definován. Návratový
typ konstruktoru se neuvádí.Pro konstruktory platí pravidla uvedená pro metody (viz 11.2.2.). Jejich
deklarace však nesmí obsahovat modifikátory abstract, final, native, static a synchronized a nedochází k dědění konstruktorů.
Priklad 11.14. |
class Bod {
Bod() { // konstrukor třídy Bod
// ...
}
}
|
|
Každá třída má alespoň jeden konstruktor. Pokud ve třídě konstruktor není
uveden, vytvoří překladač automaticky implicitní konstruktor bez
parametrů (default constructor), který nic neprovádí. Konstruktor každé třídy musí na začátku obsahovat volání konstruktoru
rodičovské třídy, aby se zajistila řádná inicializace zděděných proměnných
(tzv. řetězení konstruktorů).
K volání rodičovského konstruktoru slouží klíčové slovo super (viz též
11.8.):
super( parametryKonstruktoru );
Volání rodičovského konstruktoru lze vynechat pouze v případě, že rodičovská
třída má pouze jeden konstruktor bez parametrů nebo konstruktor implicitní -
pak toto volání překladač na začátek konstruktoru sám doplní.
Volání konstruktoru téže třídy se provádí pomocí operátoru this (viz
též str. 11):
Priklad 11.15. |
class Trida {
public Trida() {
this(0); // volání konstruktoru (*)
}
public Trida(int i) { // (*)
// ...
}
}
|
|
Pokud je potřeba zabránit instanciování dané třídy, stačí nadefinovat všechny
její konstruktory jako soukromé.
11.2.4. Členské proměnné
Členské proměnné se deklarují v těle třídy - mimo těla metod (jinak
by se jednalo o lokální proměnné). Podle konvence se deklarace členských
proměnných uvádějí před deklaracemi metod.Pro členské proměnné platí podobná pravidla jako pro lokální proměnné (viz
9.4.). Jejich deklarace může navíc obsahovat následující modifikátory:
[ práva ] [static] [transient] [final] [volatile]
typ jménoProměnné
Priklad 11.17. |
Deklarace veřejné celočíselné statické konstanty MAX_VALUE,
která je inicializována číslem 0x7fffffff (maximální hodnota pro typ
int) vypadá takto:
public static final int MAX_VALUE = 0x7fffffff;
|
|
11.3. Dědičnost
Třída-potomek se od rodičovské třídy odvodí v deklaraci pomocí klíčového slova
extends (viz 11.2.1.). Potomek od přímého rodiče dědí, tj.
přejímá jako by byly znovu definovány, všechny členské proměnné a metody,
které: (8)
- jsou deklarovány jako veřejné (public) nebo chráněné (protected),
- nemají explicitně určena přístupová práva a zároveň se rodičovská třída
nachází ve stejném balíku jako potomek.
Potomek nedědí členské proměnné a metody, které:
- jsou deklarovány jako soukromé (private) (9) ,
- jsou deklarovány v jiném balíku a nejsou veřejné (public),
- mají v potomkovi stejné jméno (a parametry - u metod) jako v rodičovské
třídě. Tyto členské proměnné (metody) jsou předefinovány - tzv. překrytí proměnných a metod (overriding).
Při překrývání metod je možné přístupová práva pouze rozšířit (na public
nebo protected - v případě implicitních práv). Naopak seznam (resp.
typy) výjimek deklarovaných pomocí throws lze pouze zúžit. Na
změny modifikátorů a členské proměnné omezení kladena nejsou. K překrytým metodám a proměnným rodiče se přistupuje pomocí operátoru super, který vrací referenci na rodičovskou třídu:
super. JménoČlenskéProměnné
super. JménoMetody
Priklad 11.18. |
class A {
void a() {
// ...
}
}
class B extends A {
void a() {
super.a(); // volá metodu rodiče: A.a
a(); // volá sama sebe
// ...
}
}
|
|
11.4. Přístupová práva
Členské proměnné a metody mají specifikována přístupová práva (viz
kap. 11.2.2. a 11.2.4.) určující okruh tříd, které k nim
mají přístup. Přístupem se rozumí možnost přímé manipulace se členskými
proměnnými a volání metod.
Třída má přístup pouze k metodám a členským proměnným, které:
- sama deklaruje,
- dědí (viz 11.3.),
- jsou deklarovány v jiných třídách a jsou veřejné (public),
- jsou umístěny ve třídě stejného balíku (viz kap. 12.)
a zároveň mají přístup nespecifikován nebo nastaven jako public či protected.
Třída nemá přístup ke členským proměnným a metodám z jiných balíků, které jsou
soukromé, chráněné nebo mají nespecifikován přístup.
Priklad 11.19. |
V příkladu 11.16. je konstanta MAX_VALUE deklarována jako
veřejná a je tedy přímo přístupná všem ostatním třídám (i mimo svůj balík).
|
|
11.5. Třída Object
Třída Object z balíku java.lang je kořenovou třídou ve
stromu tříd, tj. všechny třídy, ať už knihovní nebo navržené
programátorem, mají společného (nepřímého) rodiče třídu Object.
Object definuje základní metody, které musí mít každý objekt v Javě -
většinu z nich používá runtime systém. Jedná se například o metody:
- protected native clone() - vytvoří identický objekt (ale nevolá
konstruktor) a přiřadí stejné hodnoty všem členským
proměnným. (10) Funguje pouze u tříd, které
implementují rozhraní Cloneable.
- public boolean equals(Object obj) - porovnává objekt s objektem
obj.
- public final Class getClass() - vrací objekt reprezentující
třídu v runtime systému.
- public String toString() - vrací identifikační řetězec objektu,
- wait(), notify(), notifyAll() - viz
16.5.3..
11.6. Rozhraní (interface)
Rozhraní (interface) je syntaktická struktura obsahující
deklarace konstant a metod (bez implementací). Deklarace rozhraní je podobná
deklaraci třídy:
[public] interface jménoRozhraní
[ extends seznamRozhraní ] {
// tělo rozhraní
}
Tělo rozhraní smí obsahovat pouze deklarace konstant a metod. Při
tom platí následující pravidla:
- Všechny metody a konstanty uvedené v deklaraci rozhraní jsou automaticky
veřejné (public) a nesmí mít specifikována přístupová práva
protected ani private.
- Všechny metody jsou navíc automaticky abstraktní.
- I když u proměnné není modifikátor final, jedná se vždy
o konstantu.
- V rozhraní nesmí být použity modifikátory: transient, volatile a synchronized.
Priklad 11.21. |
public interface NoveRozhraní {
int MAX_DELKA_RETEZCE = 20; // automaticky konstanta
void vypis(String retezec); // automaticky abstraktní metoda
}
|
|
Rozhraní se používají k zachycení společných prvků tříd, které spolu nemusí
souviset ve stromu dědičnosti - společnými prvky jsou konstanty a metody
deklarované rozhraním. Třída, která implementuje dané rozhraní (viz
11.2.), musí obsahovat definice všech(!) metod tohoto rozhraní. Rozhraní
pak lze použít jako objektový typ třídy:
Priklad 11.22. |
Program bude obsahovat schránku (clipboard), přes kterou lze
kopírovat, a vkládat objekty (text, obrázky, zvuky). Všechny objekty, které
lze do schránky umístit, musí kopírování samy umožňovat - musí obsahovat
metodu copy(). Z hlediska návrhu není vhodné, aby třídy takto rozdílných objektů měly
společného rodiče - třídu, deklarující metodu copy(), kterou všechny
povinně zdědí. Výhodnější je deklarovat rozhraní (Clip) obsahující
metodu copy(), které budou jednotlivé třídy implementovat - i v tomto
případě bude zaručena podpora kopírování ze strany objektů.
public interface Clip {
byte[] copy();
}
Metoda copy() bude vracet pole bytů reprezentující objekt ve schránce.
Každá třída, jejíž instance lze kopírovat přes schránku, musí implementovat
rozhraní Clip. Její deklarace bude:
class Obrazek implements Clip {
byte[] copy() {
// ...
}
}
Schránka pak může obsahovat libovolný objekt typu Clip:
class Schranka {
byte[] schranka;
// metoda pro vložení (kopírovatelného) objektu
public void vlozit(Clip objekt) { // typ parametru je rozhraní
schranka = objekt.copy(); // vložení objektu do schránky
}
// ...
}
|
|
Rozhraní může být prázdné. Třídu implementující rozhraní lze identifikovat
operátorem instanceof, viz 8.2..
Rozhraní neumožňují vícenásobnou dědičnost, neboť třídy prostřednictvím
rozhraní nedědí definice (kód) metod. Hierarchie rozhraní je nezávislá na
hierarchii tříd.
11.7. Inicializace tříd a rozhraní
K inicializaci třídy nebo rozhraní dochází při prvním aktivním
použití, které nastane, je-li splněna aspoň jedna
z podmínek (11) :
- je vyvolána metoda nebo konstruktor deklarovaný danou třídou.
- je vytvořeno pole s prvky typu dané třídy,
- je proveden přístup k nekonstantní členské proměnné třídy nebo konstantě
rozhraní,
- je proveden přístup ke členské proměnné třídy s modifikátorem final
nebo static, která je inicializována hodnotou vypočítanou za běhu programu.
Inicializace třídy se skládá z inicializace statických členských proměnných a
vykonání statických inicializátorů (viz dále), přičemž před tím musí
být inicializována nejprve její rodičovská třída. Inicializace rozhraní spočívá pouze v inicializaci v něm definovaných
konstant, nedochází automaticky k inicializaci rodičovského rozhraní.
11.7.1. Inicalizátory
Od JDK 1.1 lze k inicializaci třídy (resp. instance) použít statický (resp.
nestatický) inicializátor. Jeho syntaxe je:
deklaraceTřídy {
[static] {
// inicializace
}
}
Jedná se o blok, který obsahuje inicializace členských proměnných. Statický
inicializátor vyvolává pouze třída. Nestatický inicializátor vyvolávají
všechny konstruktory třídy. Nestatický inicializátor smí vyvolávat pouze
výjimky, které deklarují všechny konstruktory třídy. (12) .
Priklad 11.23. |
class A {
static {
String ahoj = "ahoj";
}
// ...
}
|
|
11.8. Vnořené třídy
Od verze JDK 1.1 je možné třídy do sebe vnořovat (13) a Java rozeznává dva druhy tříd:
- Třídy nejvyšší úrovně (top-level classes) - jsou
"normální" třídy a statické třídy (14) umístěné v jiné třídě (na úrovni vnoření nezáleží). Do této skupiny
se také řadí také vnořená rozhraní.
Priklad 11.24. |
Obě následující třídy TopLevel1 a TopLevel2 jsou třídy nejvyšší
úrovně.
class TopLevel1 {
static class TopLevel2 {
// ...
}
interface Cool {
// i rozhraní může být vnořené
}
}
|
|
Třídy nejvyšší úrovně mohou být zpřístupněny (závisí na přístupových právech)
udáním úplného jména třídy. Podobně jako u balíků (viz kap.
12.) se používá tečková notace: TopLevel1.TopLevel2 je úplné
jméno vnořené třídy TopLevel2. Třídy nejvyšší úrovně umožňují vytvářet hierarchickou strukturu na úrovni
tříd, nikoliv pouze na úrovni balíků.
- Vnitřní třídy (inner classes) - jsou nestatické vnořené třídy a lze je umístit i do těla metody nebo do bloku.
Vnitřní třídy definované v bloku nebo metodě nejsou "zvenku" přístupné
a nemohou mít modifikátory public, protected, private
a static. Vnitřní třídy nesmí obsahovat statické proměnné a metody,
statické inicializátory, ani deklarace rozhraní.
Vnitřní třídy jsou obecnějším nástrojem pro případy, kdy jiné jazyky používají
ukazatele na funkce. Zvláštním druhem vnitřní třídy je nepojmenovaná, anonymní
třída (anonymous class). Deklarace anonymní třídy je součástí
rozšířené syntaxe operátoru new:
new Typ ( parametry ) {
// tělo anonymní třídy
}
Typ představuje:
- jméno konstruktoru rodičovské(!) třídy, od které je anonymní třída
odvozena (následují jeho parametry), nebo
- jméno rozhraní - anonymní třída jako jediná může přímo instanciovat
rozhraní (zde se parametry neuvádí). (15)
Priklad 11.25. |
Vytvoření členské proměnné r jako instance rozhraní Runnable (viz
též 16.2.) vypadá následovně:
class NejakaTrida {
Runnable r = new Runnable() {
public void run() {
// ...
}
}
}
|
|
Vnitřní třídy mají přímý přístup k soukromým statickým proměnným a metodám
vnější třídy a nemusí používat jejich úplná jména. Třídy nejvyšší
úrovně mohou manipulovat pouze se statickými proměnnými a statickými metodami
vnějších tříd. Pokud vnitřní třída ve svých metodách používá lokální proměnné nebo parametry,
které sama nedeklaruje, musí tyto být deklarovány s modifikátorem final.
Protože po zavedení vnitřních tříd v JDK 1.1 může mít daná třída více instancí,
je v některých případech použití operátorů new a super nutno
explicitně uvést referenci na instanci vnější třídy: (16)
referenceNaVnějšíInstanci. new
referenceNaVnějšíInstanci. super
Podobně byla rozšířena syntaxe operátoru this, kde je možné specifikovat
vnější třídu:
JménoVnějšíTřídy. this
Priklad 11.26. |
class Vnejsi {
static int i = 10; // (1)
int x;
class Vnitrni {
Vnitrni() {
int i = 0; // (2)
int j = Vnejsi.this.i; // (3)
// ...
}
}
static Vnitrni delej(Vnejsi obj) {
// ...
return obj.new Vnitrni(); // (4)
}
}
Na řádce (3) je proměnné j přiřazen obsah statické proměnné i
(1) - v případě neuvedení třídy by se přiřadila hodnota proměnné i (2). Na řádce (4) je nutné u operátoru new uvést referenci na instanci
obj, neboť metoda delej() je statická a jako taková nemůže
(automaticky) předat konstruktoru referenci na instanci vnější třídy.
|
|
|