6.1 Declarația unei noi clase de obiecte
Pasul 1: Stabilirea conceptului reprezentat de clasa de
obiecte
Pasul 2: Stabilirea numelui clasei de obiecte
Pasul 3: Stabilirea superclasei
Pasul 4: Stabilirea interfețelor pe care le respectă clasa
Pasul 5: Stabilirea modificatorilor clasei
Pasul 6: Scrierea corpului declarației
Stop: Forma generală a unei declarații de clasă
6.2 Variabilele unei clase
6.2.1 Modificatori
6.2.2 Protecție
6.2.3 Accesarea unei variabile
6.2.4 Vizibilitate
6.2.5 Variabile predefinite: this și super
6.3 Metodele unei clase
6.3.1 Declararea metodelor
6.3.1.1 Numele și parametrii metodelor
6.3.1.2 Modificatori de metode
6.3.1.2.1 Metode statice
6.3.1.2.2 Metode abstracte
6.3.1.2.3 Metode finale
6.3.1.2.4 Metode native
6.3.1.2.5 Metode sincronizate
6.3.1.3 Protejarea metodelor
6.3.2 Clauze throws
6.3.3 Apelul metodelor
6.3.4 Valoarea de retur a unei metode
6.3.5 Vizibilitate
6.4 Inițializatori statici
6.5 Constructori și finalizatori
6.5.1 constructori
6.5.2 Finalizatori
6.5.3 Crearea instanțelor
6.6 Derivarea claselor
6.7 Interfețe
În primul rând să observăm că, atunci când scriem programe în Java nu facem
altceva decât să definim noi și noi clase de obiecte. Dintre acestea, unele vor
reprezenta însăși aplicația noastră în timp ce altele vor fi necesare pentru
rezolvarea problemei la care lucrăm. Ulterior, atunci când dorim să lansăm aplicația
în execuție nu va trebui decât să instanțiem un obiect reprezentând aplicația în
sine și să apelăm o metodă de pornire definită de către aplicație, metodă care de
obicei are un nume și un set de parametri bine fixate. Totuși, numele acestei metode
depinde de contextul în care este lansată aplicația noastră.
Această abordare a construcției unei aplicații ne spune printre altele că vom putea
lansa oricâte instanțe ale aplicației noastre dorim, pentru că fiecare dintre acestea
va reprezenta un obiect în memorie având propriile lui valori pentru variabile.
Execuția instanțelor diferite ale aplicației va urma desigur căi diferite în funcție
de interacțiunea fiecăreia cu un utilizator, eventual același, și în funcție de unii
parametri pe care îi putem defini în momentul creării fiecărei instanțe.
6.1 Declarația unei noi clase de obiecte
Pasul 1: Stabilirea conceptului reprezentat de clasa de obiecte
Să vedem ce trebuie să definim atunci când dorim să creăm o nouă clasă de
obiecte. În primul rând trebuie să stabilim care este conceptul care este reprezentat
de către noua clasă de obiecte și să definim informațiile memorate în obiect și
modul de utilizare a acestuia. Acest pas este cel mai important din tot procesul de
definire al unei noi clase de obiecte. Este necesar să încercați să respectați două
reguli oarecum antagonice. Una dintre ele spune că nu trebuiesc create mai multe clase de
obiecte decât este nevoie, pentru a nu face dificilă înțelegerea modului de lucru al
aplicației la care lucrați. Cea de-a doua regulă spune că nu este bine să mixați
într-un singur obiect funcționalități care nu au nimic în comun, creând astfel clase
care corespund la două concepte diferite.
Medierea celor două reguli nu este întotdeauna foarte ușoară. Oricum, vă va fi mai
ușor dacă păstrați în minte faptul că fiecare clasă pe care o definiți trebuie să
corespundă unui concept real bine definit, necesar la rezolvarea problemei la care
lucrați. Și mai păstrați în minte și faptul că este inutil să lucrați cu concepte
foarte generale atunci când aplicația dumneavoastră nu are nevoie decât de o
particularizare a acestora. Riscați să pierdeți controlul dezvoltării acestor clase de
obiecte prea generale și să îngreunați dezvoltarea aplicației.
Pasul 2: Stabilirea numelui clasei de obiecte
După ce ați stabilit exact ce doriți de la noua clasă de obiecte, sunteți în
măsură să găsiți un nume pentru noua clasă, nume care trebuie să urmeze regulile de
construcție ale identificatorilor limbajului Java definite în capitolul anterior.
Stabilirea unui nume potrivit pentru o nouă clasă nu este întotdeauna un lucru
foarte ușor. Problema este că acest nume nu trebuie să fie exagerat de lung dar trebuie
să exprime suficient de bine destinația clasei. Regulile de denumire ale claselor sunt
rezultatul experienței fiecăruia sau al unor convenții de numire stabilite anterior. De
obicei, numele de clase este bine să înceapă cu o literă majusculă. Dacă numele
clasei conține în interior mai multe cuvinte, aceste cuvinte trebuie de asemenea
începute cu literă majusculă. Restul caracterelor vor fi litere minuscule.
De exemplu, dacă dorim să definim o clasă de obiecte care implementează conceptul
de motor Otto vom folosi un nume ca MotorOtto pentru noua clasă ce trebuie
creată. La fel, vom defini clasa MotorDiesel sau MotorCuReacție. Dacă
însă avem nevoie să definim o clasă separată pentru un motor Otto cu cilindri în V
și carburator, denumirea clasei ca MotorOttoCuCilindriÎnVȘiCarburator nu este
poate cea mai bună soluție. Poate că în acest caz este preferabilă o prescurtare de
forma MotorOttoVC. Desigur, acestea sunt doar câteva remarci la adresa acestei
probleme și este în continuare necesar ca în timp să vă creați propria convenție de
denumire a claselor pe care le creați.
Pasul 3: Stabilirea superclasei
În cazul în care ați definit deja o parte din funcționalitatea de care aveți
nevoie într-o altă superclasă, puteți să derivați noua clasă de obiecte din clasa
deja existentă. Dacă nu există o astfel de clasă, noua clasă va fi automat derivată
din clasa de obiecte predefinită numită Object. În Java, clasa Object este superclasă
direct sau indirect pentru orice altă clasă de obiecte definită de utilizator.
Alegerea superclasei din care derivați noua clasă de obiecte este foarte importantă
pentru că vă ajută să refolosiți codul deja existent. Totuși, nu alegeți cu
ușurință superclasa unui obiect pentru că astfel puteți încărca obiectele cu o
funcționalitate inutilă, existentă în superclasă. Dacă nu există o clasă care să
vă ofere doar funcționalitatea de care aveți nevoie, este preferabil să derivați noua
clasă direct din clasa Object și să apelați indirect funcționalitatea
pe care o doriți.
Pasul 4: Stabilirea interfețelor pe care le respectă clasa
Stabilirea acestor interfețe are dublu scop. În primul rând ele instruiesc
compilatorul să verifice dacă noua clasă respectă cu adevărat toate interfețele pe
care le-a declarat, cu alte cuvinte definește toate metodele declarate în aceste
interfețe. A doua finalitate este aceea de a permite compilatorului să folosească
instanțele noii clase oriunde aplicația declară că este nevoie de un obiect care
implementează interfețele declarate.
O clasă poate să implementeze mai multe interfețe sau niciuna.
Pasul 5: Stabilirea modificatorilor clasei
În unele cazuri trebuie să oferim compilatorului informații suplimentare relative la
modul în care vom folosi clasa nou creată pentru ca acesta să poată executa
verificări suplimentare asupra descrierii clasei. În acest scop, putem defini o clasă
ca fiind abstractă, finală sau publică folosindu-ne de o serie de cuvinte rezervate
numite modificatori. Modificatorii pentru tipurile de clase de mai sus sunt respectiv: abstract,
final și public.
În cazul în care declarăm o clasă de obiecte ca fiind abstractă, compilatorul va
interzice instanțierea acestei clase. Dacă o clasă este declarată finală,
compilatorul va avea grijă să nu putem deriva noi subclase din această clasă. În
cazul în care declarăm în același timp o clasă de obiecte ca fiind abstractă și
finală, eroarea va fi semnalată încă din timpul compilării pentru că cei doi
modificatori se exclud.
Pentru ca o clasă să poată fi folosită și în exteriorul contextului în care a
fost declarată ea trebuie să fie declarată publică. Mai mult despre acest aspect în
paragraful referitor la structura programelor. Până atunci, să spunem că orice clasă
de obiecte care va fi instanțiată ca o aplicație trebuie declarată publică.
Pasul 6: Scrierea corpului declarației
În sfârșit, după ce toți ceilalți pași au fost efectuați, putem trece la
scrierea corpului declarației de clasă. În principal, aici vom descrie variabilele
clasei împreună cu metodele care lucrează cu acestea. Tot aici putem preciza și
gradele de protejare pentru fiecare dintre elementele declarației. Uneori numim
variabilele și metodele unei clase la un loc ca fiind câmpurile clasei. Subcapitolele
următoare vor descrie în amănunt corpul unei declarații.
Stop: Forma generală a unei declarații de clasă
Sintaxa exactă de declarare a unei clase arată în felul următor:
{ abstract | final | public }* class
NumeClasă
[ extends NumeSuperclasă ]
[ implements NumeInterfață [ , NumeInterfață ]* ]
{ [ CâmpClasă ]* }
6.2 Variabilele unei clase
În interiorul claselor se pot declara variabile. Aceste variabile sunt specifice
clasei respective. Fiecare dintre ele trebuie să aibă un tip, un nume și poate avea
inițializatori. În afară de aceste elemente, pe care le-am prezentat deja în
secțiunea în care am prezentat variabilele, variabilele definite în interiorul unei
clase pot avea definiți o serie de modificatori care alterează comportarea variabilei
în interiorul clasei, și o specificație de protecție care definește cine are dreptul
să acceseze variabila respectivă.
6.2.1 Modificatori
Modificatorii sunt cuvinte rezervate Java care precizează sensul unei
declarații. Iată lista acestora:
static
final
transient
volatile
Dintre aceștia, transient nu este utilizat în versiunea curentă a
limbajului Java. Pe viitor va fi folosit pentru a specifica variabile care nu conțin
informații care trebuie să rămână persistente la terminarea programului.
Modificatorul volatile specifică faptul că variabila respectivă
poate fi modificată asincron cu rularea aplicației. În aceste cazuri, compilatorul
trebuie să-și ia măsuri suplimentare în cazul generării și optimizării codului care
se adresează acestei variabile.
Modificatorul final este folosit pentru a specifica o variabilă a
cărei valoare nu poate fi modificată. Variabila respectivă trebuie să primească o
valoare de inițializare chiar în momentul declarației. Altfel, ea nu va mai putea fi
inițializată în viitor. Orice încercare ulterioară de a seta valori la această
variabilă va fi semnalată ca eroare de compilare.
Modificatorul static este folosit pentru a specifica faptul că
variabila are o singură valoare comună tuturor instanțelor clasei în care este
declarată. Modificarea valorii acestei variabile din interiorul unui obiect face ca
modificarea să fie vizibilă din celelalte obiecte. Variabilele statice sunt
inițializate la încărcarea codului specific unei clase și există chiar și dacă nu
există nici o instanță a clasei respective. Din această cauză, ele pot fi folosite de
metodele statice.
În Java există patru grade de protecție pentru o variabilă aparținând unei clase:
- privată
- protejată
- publică
- prietenoasă
O variabilă publică este accesibilă oriunde este accesibil numele clasei.
Cuvântul rezervat este public.
O variabilă protejată este accesibilă în orice clasă din pachetul
căreia îi aparține clasa în care este declarată. În același timp, variabila este
accesibilă în toate subclasele clasei date, chiar dacă ele aparțin altor pachete.
Cuvântul rezervat este protected.
O variabilă privată este accesibilă doar în interiorul clasei în care a
fost declarată. Cuvântul rezervat este private.
O variabilă care nu are nici o declarație relativă la gradul de protecție este
automat o variabilă prietenoasă. O variabilă prietenoasă este accesibilă în
pachetul din care face parte clasa în interiorul căreia a fost declarată, la fel ca și
o variabilă protejată. Dar, spre deosebire de variabilele protejate, o variabilă
prietenoasă nu este accesibilă în subclasele clasei date dacă aceste sunt declarate ca
aparținând unui alt pachet. Nu există un cuvânt rezervat pentru specificarea
explicită a variabilelor prietenoase.
O variabilă nu poate avea declarate mai multe grade de protecție în același timp. O
astfel de declarație este semnalată ca eroare de compilare.
6.2.3 Accesarea unei variabile
Accesarea unei variabile declarate în interiorul unei clasei se face folosindu-ne de o
expresie de forma:
ReferințăInstanță.NumeVariabilă
Referința către o instanță trebuie să fie referință către clasa care conține
variabila. Referința poate fi valoarea unei expresii mai complicate, ca de exemplu un
element dintr-un tablou de referințe.
În cazul în care avem o variabilă statică, aceasta poate fi accesată și fără
să deținem o referință către o instanță a clasei. Sintaxa este, în acest caz:
NumeClasă.NumeVariabilă
6.2.4 Vizibilitate
O variabilă poate fi ascunsă de declarația unei alte variabile cu același nume. De
exemplu, dacă într-o clasă avem declarată o variabilă cu numele unu și
într-o subclasă a acesteia avem declarată o variabilă cu același nume, atunci
variabila din superclasă este ascunsă de cea din clasă. Totuși, variabila din
superclasă există încă și poate fi accesată în mod explicit. Expresia de referire
este, în acest caz:
NumeSuperClasă.NumeVariabilă
sau
super.NumeVariabilă
în cazul în care superclasa este imediată.
La fel, o variabilă a unei clase poate fi ascunsă de o declarație de variabilă
dintr-un bloc de instrucțiuni. Orice referință la ea va trebui făcută în mod
explicit. Expresia de referire este, în acest caz:
this.NumeVariabilă
6.2.5 Variabile predefinite: this și super
În interiorul fiecărei metode non-statice dintr-o clasă există predefinite două
variabile cu semnificație specială. Cele două variabile sunt de tip referință și au
aceeași valoare și anume o referință către obiectul curent. Diferența dintre ele
este tipul.
Prima dintre acestea este variabila this care are tipul referință
către clasa în interiorul căreia apare metoda. A doua este variabila super
al cărei tip este o referință către superclasa imediată a clasei în care apare
metoda. În interiorul obiectelor din clasa Object nu se poate folosi
referința super pentru că nu există nici o superclasă a clasei de
obiecte Object.
În cazul în care super este folosită la apelul unui constructor sau
al unei metode, ea acționează ca un cast către superclasa imediată.
6.3 Metodele unei clase
Fiecare clasă își poate defini propriile sale metode pe lângă metodele pe care le
moștenește de la superclasa sa. Aceste metode definesc operațiile care pot fi executate
cu obiectul respectiv. În cazul în care una dintre metodele moștenite nu are o
implementare corespunzătoare în superclasă, clasa își poate redefini metoda după cum
dorește.
În plus, o clasă își poate defini metode de construcție a obiectelor și metode de
eliberare a acestora. Metodele de construcție sunt apelate ori de câte ori este alocat
un nou obiect din clasa respectivă. Putem declara mai multe metode de construcție, ele
diferind prin parametrii din care trebuie construit obiectul.
Metodele de eliberare a obiectului sunt cele care eliberează resursele ocupate de
obiect în momentul în care acesta este distrus de către mecanismul automat de colectare
de gunoaie. Fiecare clasă are o singură metodă de eliberare, numită și finalizator.
Apelarea acestei metode se face de către sistem și nu există nici o cale de control al
momentului în care se produce acest apel.
6.3.1 Declararea metodelor
Pentru a declara o metodă, este necesar să declarăm numele acesteia, tipul de
valoare pe care o întoarce, parametrii metodei precum și un bloc în care să descriem
instrucțiunile care trebuiesc executate atunci când metoda este apelată. În plus,
orice metodă are un număr de modificatori care descriu proprietățile metodei și modul
de lucru al acesteia.
Declararea precum și implementarea metodelor se face întotdeauna în interiorul
declarației de clasă. Nu există nici o cale prin care să putem scrie o parte dintre
metodele unei clase într-un fișier separat care să facă referință apoi la
declarația clasei.
În formă generală, declarația unei metode arată în felul următor:
[Modificator]* TipRezultat Declarație [ClauzeThrows]* CorpulMetodei
Modificatorii precum și clauzele throws pot să lipsească.
6.3.1.1 Numele și parametrii metodelor
Recunoașterea unei anumite metode se face după numele și tipul parametrilor săi.
Pot exista metode cu același nume dar având parametri diferiți. Acest fenomen poartă
numele de supraîncărcarea numelui unei metode.
Numele metodei este un identificator Java. Avem toată libertatea în a alege numele pe
care îl dorim pentru metodele noastre, dar în general este preferabil să alegem nume
care sugerează utilizarea metodei.
Numele unei metode începe de obicei cu literă mică. Dacă acesta este format din mai
multe cuvinte, litera de început a fiecărui cuvânt va fi majusculă. În acest mod
numele unei metode este foarte ușor de citit și de depistat în sursă.
Parametrii metodei sunt în realitate niște variabile care sunt inițializate în
momentul apelului cu valori care controlează modul ulterior de execuție. Aceste
variabile există pe toată perioada execuției metodei. Se pot scrie metode care să nu
aibă nici un parametru.
Fiind o variabilă, fiecare parametru are un tip și un nume. Numele trebuie să fie un
identificator Java. Deși avem libertatea să alegem orice nume dorim, din nou este
preferabil să alegem nume care să sugereze scopul la care va fi utilizat parametrul
respectiv.
Tipul unui parametru este oricare dintre tipurile valide în Java. Acestea poate fi fie
un tip primitiv, fie un tip referință către obiect, interfață sau tablou.
În momentul apelului unei metode, compilatorul încearcă să găsească o metodă în
interiorul clasei care să aibă același nume cu cel apelat și același număr de
parametri ca și apelul. Mai mult, tipurile parametrilor de apel trebuie să corespundă
cu tipurile parametrilor declarați ai metodei găsite sau să poată fi convertiți la
aceștia.
Dacă o astfel de metodă este găsită în declarația clasei sau în superclasele
acesteia, parametrii de apel sunt convertiți către tipurile declarate și se generează
apelul către metoda respectivă.
Este o eroare de compilare să declarăm două metode cu același nume, același număr
de parametri și același tip pentru parametrii corespunzători. Într-o asemenea
situație, compilatorul n-ar mai ști care metodă trebuie apelată la un moment dat.
De asemenea, este o eroare de compilare să existe două metode care se potrivesc la
același apel. Acest lucru se întâmplă când nici una dintre metodele existente nu se
potrivește exact și când există două metode cu același nume și același număr de
parametri și, în plus, parametrii de apel se pot converti către parametrii declarați
ai ambelor metode.
Rezolvarea unei astfel de probleme se face prin conversia explicită (cast) de către
programator a valorilor de apel spre tipurile exacte ale parametrilor metodei pe care
dorim să o apelăm în realitate.
În fine, forma generală de declarație a numelui și parametrilor unei metode este:
NumeMetodă( [TipParametru NumeParametru]
[,TipParametru NumeParametru]* )
6.3.1.2 Modificatori de metode
Modificatorii sunt cuvinte cheie ale limbajului Java care specifică proprietăți
suplimentare pentru o metodă. Iată lista completă a acestora în cazul metodelor:
- static - pentru metodele statice
- abstract - pentru metodele abstracte
- final - pentru metodele finale
- native - pentru metodele native
- synchronized - pentru metodele sincronizate
În mod normal, o metodă a unei clase se poate apela numai printr-o instanță a
clasei respective sau printr-o instanță a unei subclase. Acest lucru se datorează
faptului că metoda face apel la o serie de variabile ale clasei care sunt memorate în
interiorul instanței și care au valori diferite în instanțe diferite. Astfel de metode
se numesc metode ale instanțelor clasei.
După cum știm deja, există și un alt tip de variabile, și anume variabilele de
clasă sau variabilele statice care sunt comune tuturor instanțelor clasei respective și
există pe toată perioada de timp în care clasa este încărcată în memorie. Aceste
variabile pot fi accesate fără să avem nevoie de o instanță a clasei respective.
În mod similar există și metode statice. Aceste metode nu au nevoie de o
instanță a clasei sau a unei subclase pentru a putea fi apelate pentru că ele nu au
voie să folosească variabile care sunt memorate în interiorul instanțelor. În schimb,
aceste metode pot să folosească variabilele statice declarate în interiorul clasei.
Orice metodă statică este implicit și finală.
Metodele abstracte sunt metode care nu au corp de implementare. Ele sunt
declarate numai pentru a forța subclasele care vor să aibă instanțe să implementeze
metodele respective.
Metodele abstracte trebuie declarate numai în interiorul claselor care au fost
declarate abstracte. Altfel compilatorul va semnala o eroare de compilare. Orice subclasă
a claselor abstracte care nu este declarată abstractă trebuie să ofere o implementare a
acestor metode, altfel va fi generată o eroare de compilare.
Prin acest mecanism ne asigurăm că toate instanțele care pot fi convertite către
clasa care conține definiția unei metode abstracte au implementată metoda respectivă
dar, în același timp, nu este nevoie să implementăm în nici un fel metoda chiar în
clasa care o declară pentru că nu știm pe moment cum va fi implementată.
O metodă statică nu poate fi declarată și abstractă pentru că o metodă statică
este implicit finală și nu poate fi rescrisă.
O metodă finală este o metodă care nu poate fi rescrisă în subclasele
clasei în care a fost declarată. O metodă este rescrisă într-o subclasă dacă
aceasta implementează o metodă cu același nume și același număr și tip de parametri
ca și metoda din superclasă.
Declararea metodelor finale este utilă în primul rând compilatorului care poate
genera metodele respective direct în codul rezultat fiind sigur că metoda nu va avea
nici o altă implementare în subclase.
Metodele native sunt metode care sunt implementate pe o cale specifică unei
anumite platforme. De obicei aceste metode sunt implementate în C sau în limbaj de
asamblare. Metoda propriu-zisă nu poate avea corp de implementare pentru că
implementarea nu este făcută în Java.
În rest, metodele native sunt exact ca orice altă metodă Java. Ele pot fi
moștenite, pot fi statice sau nu, pot fi finale sau nu, pot să rescrie o metodă din
superclasă și pot fi la rândul lor rescrise în subclase.
6.3.1.2.5 Metode sincronizate
O metodă sincronizată este o metodă care conține cod critic pentru un anumit
obiect sau clasă și nu poate fi rulată în paralel cu nici o altă metodă critică sau
cu o instrucțiune synchronized referitoare la același obiect sau clasă.
Înainte de execuția metodei, obiectul sau clasa respectivă sunt blocate. La
terminarea metodei, acestea sunt deblocate.
Dacă metoda este statică atunci este blocată o întreagă clasă, clasa din care
face parte metoda. Altfel, este blocată doar instanța în contextul căreia este
apelată metoda.
6.3.1.3 Protejarea metodelor
Accesul la metodele unei clase este protejat în același fel ca și accesul la
variabilele clasei. În Java există patru grade de protecție pentru o metodă
aparținând unei clase:
- privată
- protejată
- publică
- prietenoasă
O metodă declarată publică este accesibilă oriunde este accesibil numele clasei.
Cuvântul rezervat este public.
O metodă declarată protejată este accesibilă în orice clasă din pachetul căreia
îi aparține clasa în care este declarată. În același timp, metoda este accesibilă
în toate subclasele clasei date, chiar dacă ele aparțin altor pachete. Cuvântul
rezervat este protected.
O metodă declarată privată este accesibilă doar în interiorul clasei în care a
fost declarată. Cuvântul rezervat este private.
O metodă care nu are nici o declarație relativă la gradul de protecție este automat
o metodă prietenoasă. O metodă prietenoasă este accesibilă în pachetul din care face
parte clasa în interiorul căreia a fost declarată la fel ca și o metodă protejată.
Dar, spre deosebire de metodele protejate, o metodă prietenoasă nu este accesibilă în
subclasele clasei date dacă aceste sunt declarate ca aparținând unui alt pachet. Nu
există un cuvânt rezervat pentru specificarea explicită a metodelor prietenoase.
O metodă nu poate avea declarate mai multe grade de protecție în același timp. O
astfel de declarație este semnalată ca eroare de compilare.
6.3.2 Clauze throws
Dacă o metodă poate arunca o excepție, adică să apeleze instrucțiunea throw,
ea trebuie să declare tipul acestor excepții într-o clauză throws.
Sintaxa acesteia este:
throws NumeTip [, NumeTip]*
Numele de tipuri specificate în clauza throws trebuie să fie
accesibile și să fie asignabile la tipul de clasă Throwable. Dacă o
metodă conține o clauză throws, este o eroare de compilare ca metoda
să arunce un obiect care nu este asignabil la compilare la tipurile de clase Error,
RunTimeException sau la tipurile de clase specificate în clauza throws.
Dacă o metodă nu are o clauză throws, este o eroare de compilare ca
aceasta să poată arunca o excepție normală din interiorul corpului ei.
6.3.3 Apelul metodelor
Pentru a apela o metodă a unei clase este necesar să dispunem de o cale de acces la
metoda respectivă. În plus, trebuie să dispunem de drepturile necesare apelului
metodei.
Sintaxa efectivă de acces este următoarea:
CaleDeAcces.Metodă( Parametri )
În cazul în care metoda este statică, pentru a specifica o cale de acces este
suficient să furnizăm numele clasei în care a fost declarată metoda. Accesul la numele
clasei se poate obține fie importând clasa sau întreg pachetul din care face parte
clasa fie specificând în clar numele clasei și drumul de acces către aceasta.
De exemplu, pentru a accesa metoda random definită static în clasa Math
aparținând pachetului java.lang putem scrie:
double aleator = Math.random();
sau, alternativ:
double aleator = java.lang.Math.random();
În cazul claselor definite în pachetul java.lang nu este necesar nici
un import pentru că acestea sunt implicit importate de către compilator.
Cea de-a doua cale de acces este existența unei instanțe a clasei respective. Prin
această instanță putem accesa metodele care nu sunt declarate statice, numite uneori
și metode ale instanțelor clasei. Aceste metode au nevoie de o instanță a clasei
pentru a putea lucra, pentru că folosesc variabile non-statice ale clasei sau apelează
alte metode non-statice. Metodele primesc acest obiect ca pe un parametru ascuns.
De exemplu, având o instanță a clasei Object sau a unei subclase a
acesteia, putem obține o reprezentare sub formă de șir de caractere prin:
Object obiect = new Object();
String sir = obiect.toString();
În cazul în care apelăm o metodă a clasei din care face parte și metoda apelantă
putem să renunțăm la calea de acces în cazul metodelor statice, scriind doar numele
metodei și parametrii. Pentru metodele specifice instanțelor, putem renunța la calea de
acces, dar în acest caz metoda accesează aceeași instanță ca și metoda apelantă.
În cazul în care metoda apelantă este statică, specificarea unei instanțe este
obligatorie în cazul metodelor de instanță.
Parametrii de apel servesc împreună cu numele la identificarea metodei pe care dorim
să o apelăm. Înainte de a fi transmiși, aceștia sunt convertiți către tipurile
declarate de parametri ai metodei, după cum este descris mai sus.
Specificarea parametrilor de apel se face separându-i prin virgulă. După ultimul
parametru nu se mai pune virgulă. Dacă metoda nu are nici un parametru, parantezele
rotunde sunt în continuare necesare. Exemple de apel de metode cu parametri:
String numar = String.valueOf( 12 );
// 12 -> String
double valoare = Math.abs( 12.54 );
// valoare absolută
String prima = numar.substring( 0, 1 );
// prima litera ^
6.3.4 Valoarea de retur a unei metode
O metodă trebuie să-și declare tipul valorii pe care o întoarce. În cazul în care
metoda dorește să specifice explicit că nu întoarce nici o valoare, ea trebuie să
declare ca tip de retur tipul void ca în exemplul:
void a() {
}
În caz general, o metodă întoarce o valoare primitivă sau un tip referință. Putem
declara acest tip ca în:
Thread cautaFirulCurent() {
}
long abs( int valoare ) {
}
Pentru a returna o valoare ca rezultat al execuției unei metode, trebuie să folosim
instrucțiunea return, așa cum s-a arătat în secțiunea dedicată
instrucțiunilor. Instrucțiunea return trebuie să conțină o expresie a
cărei valoare să poată fi convertită la tipul declarat al valorii de retur a metodei.
De exemplu:
long abs( int valoare ) {
return Math.abs( valoare );
}
Metoda statică abs din clasa Math care primește un
parametru întreg returnează tot un întreg. În exemplul nostru, instrucțiunea return
este corectă pentru că există o cale de conversie de la întreg la întreg lung,
conversie care este apelată automat de compilator înainte de ieșirea din metodă.
În schimb, în exemplul următor:
int abs( long valoare ) {
return Math.abs( valoare );
}
compilatorul va genera o eroare de compilare pentru că metoda statică abs
din clasa Math care primește ca parametru un întreg lung întoarce tot
un întreg lung, iar un întreg lung nu poate fi convertit sigur la un întreg normal
pentru că există riscul deteriorării valorii, la fel ca la atribuire.
Rezolvarea trebuie să conțină un cast explicit:
int abs( long valoare ) {
return ( int )Math.abs( valoare );
}
În cazul în care o metodă este declarată void, putem să ne
întoarcem din ea folosind instrucțiunea return fără nici o expresie.
De exemplu:
void metoda() {
if(
)
return;
}
Specificarea unei expresii în acest caz duce la o eroare de compilare. La fel și în
cazul în care folosim instrucțiunea return fără nici o expresie în
interiorul unei metode care nu este declarată void.
6.3.5 Vizibilitate
O metodă este vizibilă dacă este declarată în clasa prin care este apelată sau
într-una din superclasele acesteia. De exemplu, dacă avem următoarea declarație:
class A {
void a() {
}
}
class B extends A {
void b() {
a();
c();
}
void c() { .. }
}
Apelul metodei a în interiorul metodei b din clasa B este
permis pentru că metoda a este declarată în interiorul clasei A care
este superclasă pentru clasa B. Apelul metodei c în aceeași metodă b
este permis pentru că metoda c este declarată în aceeași clasă ca și metoda a.
Uneori, o subclasă rescrie o metodă dintr-o superclasă a sa. În acest caz, apelul
metodei respective în interiorul subclasei duce automat la apelul metodei din subclasă.
Dacă totuși dorim să apelăm metoda așa cum a fost ea definită în superclasă, putem
prefixa apelul cu numele superclasei. De exemplu:
class A {
void a() {
}
}
class B extends A {
void a() { .. }
void c() {
a();// metoda a din clasa B
A.a();// metoda a din clasa A
}
}
Desigur, pentru a "vedea" o metodă și a o putea apela, este nevoie să avem
drepturile necesare.
6.4 Inițializatori statici
La încărcarea unei clase sunt automat inițializate toate variabilele statice
declarate în interiorul clasei. În plus, sunt apelați toți inițializatorii statici ai
clasei.
Un inițializator static are următoarea sintaxă:
static BlocDeInstrucțiuni
Blocul de instrucțiuni este executat automat la încărcarea clasei. De exemplu, putem
defini un inițializator static în felul următor:
class A {
static double a;
static int b;
static {
a = Math.random();
// număr dublu între 0.0 și 1.0
b = ( int )( a * 500 );
// număr întreg între 0 și 500
}
}
Declarațiile de variabile statice și inițializatorii statici sunt executate în
ordinea în care apar în clasă. De exemplu, dacă avem următoarea declarație de
clasă:
class A {
static int i = 11;
static {
i += 100;
i %= 55;
}
static int j = i + 1;
}
valoarea finală a lui i va fi 1 ( ( 11 + 100 ) % 55 ) iar valoarea lui
j va fi 2.
6.5 Constructori și finalizatori
6.5.1 constructori
La crearea unei noi instanțe a unei clase sistemul alocă automat memoria necesară
instanței și o inițializează cu valorile inițiale specificate sau implicite. Dacă
dorim să facem inițializări suplimentare în interiorul acestei memorii sau în altă
parte putem descrie metode speciale numite constructori ai clasei.
Putem avea mai mulți constructori pentru aceeași clasă, aceștia diferind doar prin
parametrii pe care îi primesc. Numele tuturor constructorilor este același și este
identic cu numele clasei.
Declarația unui constructor este asemănătoare cu declarația unei metode oarecare,
cu diferența că nu putem specifica o valoare de retur și nu putem specifica nici un fel
de modificatori. Dacă dorim să returnăm dintr-un constructor, trebuie să folosim
instrucțiunea return fără nici o expresie. Putem însă să specificăm
gradul de protecție al unui constructor ca fiind public, privat, protejat sau prietenos.
Constructorii pot avea clauze throws.
Dacă o clasă nu are constructori, compilatorul va crea automat un constructor
implicit care nu ia nici un parametru și care inițializează toate variabilele
clasei și apelează constructorul superclasei fără argumente prin super().
Dacă superclasa nu are un constructor care ia zero argumente, se va genera o eroare de
compilare.
Dacă o clasă are cel puțin un constructor, constructorul implicit nu mai este creat
de către compilator.
Când construim corpul unui constructor avem posibilitatea de a apela, pe prima linie a
blocului de instrucțiuni care reprezintă corpul constructorului, un constructor
explicit. Constructorul explicit poate avea două forme:
this( [Parametri] );
super( [Parametri] );
Cu această sintaxă apelăm unul dintre constructorii superclasei sau unul dintre
ceilalți constructori din aceeași clasă. Aceste linii nu pot apărea decât pe prima
poziție în corpul constructorului. Dacă nu apar acolo, compilatorul consideră implicit
că prima instrucțiune din corpul constructorului este:
super();
Și în acest caz se va genera o eroare de compilare dacă nu există un constructor
în superclasă care să lucreze fără nici un parametru.
După apelul explicit al unui constructor din superclasă cu sintaxa super(
) este executată în mod implicit inițializarea tuturor variabilelor de instanță
(non-statice) care au inițializatori expliciți. După apelul unui constructor din
aceeași clasă cu sintaxa this(
) nu există nici o altă acțiune
implicită, deci nu vor fi inițializate nici un fel de variabile. Aceasta datorită
faptului că inițializarea s-a produs deja în constructorul apelat.
Iată și un exemplu:
class A extends B {
String valoare;
A( String val ) {
// aici există apel implicit
// al lui super(), adică B()
valoare = val;
}
A( int val ) {
this( String.valueOf( val ) );// alt constructor
}
} ^
6.5.2 Finalizatori
În Java nu este nevoie să apelăm în mod explicit distrugerea unei instanțe atunci
când nu mai este nevoie de ea. Sistemul oferă un mecanism de colectare a gunoaielor care
recunoaște situația în care o instanță de obiect sau un tablou nu mai sunt referite
de nimeni și le distruge în mod automat.
Acest mecanism de colectare a gunoaielor rulează pe un fir de execuție separat, de
prioritate mică. Nu avem nici o posibilitate să aflăm exact care este momentul în care
va fi distrusă o instanță. Totuși, putem specifica o funcție care să fie apelată
automat în momentul în care colectorul de gunoaie încearcă să distrugă obiectul.
Această funcție are nume, număr de parametri și tip de valoare de retur fixe:
void finalize()
După apelul metodei de finalizare (numită și finalizator), instanța
nu este încă distrusă până la o nouă verificare din partea colectorului de gunoaie.
Această comportare este necesară pentru că instanța poate fi revitalizată prin
crearea unei referințe către ea în interiorul finalizatorului.
Totuși, finalizatorul nu este apelat decât o singură dată. Dacă obiectul
revitalizat redevine candidat la colectorul de gunoaie, acesta este distrus fără a i se
mai apela finalizatorul. Cu alte cuvinte, un obiect nu poate fi revitalizat decât o
singură dată.
Dacă în timpul finalizării apare o excepție, ea este ignorată și finalizatorul nu
va mai fi apelat din nou.
6.5.3 Crearea instanțelor
O instanță este creată folosind o expresie de alocare care folosește cuvântul
rezervat new. Iată care sunt pașii care sunt executați la apelul
acestei expresii:
- Se creează o nouă instanță de tipul specificat. Toate variabilele instanței sunt
inițializate pe valorile lor implicite.
- Se apelează constructorul corespunzător în funcție de parametrii care sunt
transmiși în expresia de alocare. Dacă instanța este creată prin apelul metodei newInstance,
se apelează constructorul care nu ia nici un argument.
- După creare, expresia de alocare returnează o referință către instanța nou
creată.
Exemple de creare:
A o1 = new A();
B o2 = new B();
class C extends B {
String valoare;
C( String val ) {
// aici există apel implicit
// al lui super(), adică B()
valoare = val;
}
C( int val ) {
this( String.valueOf( val ) );
}
}
C o3 = new C( "Vasile" );
C o4 = new C( 13 );
O altă cale de creare a unui obiect este apelul metodei newInstance
declarate în clasa Class. Iată pașii de creare în acest caz:
- Se creează o nouă instanță de același tip cu tipul clasei pentru care a fost
apelată metoda newInstance. Toate variabilele instanței sunt inițializate pe
valorile lor implicite.
- Este apelat constructorul obiectului care nu ia nici un argument.
- După creare referința către obiectul nou creat este returnată ca
valoare a metodei newInstance. Tipul acestei referințe va fi Object în
timpul compilării și tipul clasei reale în timpul execuției.
6.6 Derivarea claselor
O clasă poate fi derivată dintr-alta prin folosirea în declarația clasei derivate a
clauzei extends. Clasa din care se derivă noua clasă se numește
superclasă imediată a clasei derivate. Toate clasele care sunt superclase ale
superclasei imediate ale unei clase sunt superclase și pentru clasa dată. Clasa
nou derivată se numește subclasă a clasei din care este derivată.
Sintaxa generală este:
class SubClasă extends SuperClasă
O clasă poate fi derivată dintr-o singură altă clasă, cu alte cuvinte o clasă
poate avea o singură superclasă imediată.
Clasa derivată moștenește toate variabilele și metodele superclasei sale. Totuși,
ea nu poate accesa decât acele variabile și metode care nu sunt declarate private.
Putem rescrie o metodă a superclasei declarând o metodă în noua clasă având
același nume și aceiași parametri. La fel, putem declara o variabilă care are același
nume cu o variabilă din superclasă. În acest caz, noul nume ascunde vechea variabilă,
substituindu-i-se. Putem în continuare să ne referim la variabila ascunsă din
superclasă specificând numele superclasei sau folosindu-ne de variabila super.
Exemplu:
class A {
int a = 1;
void unu() {
System.out.println( a );
}
}
class B extends A {
double a = Math.PI;
void unu() {
System.out.println( a );
}
void doi() {
System.out.println( A.a );
}
void trei() {
unu();
super.unu();
}
}
Dacă apelăm metoda unu din clasa A, aceasta va afișa la consolă
numărul 1. Acest apel se va face cu instrucțiunea:
( new A() ).unu();
Dacă apelăm metoda unu din clasa B, aceasta va afișa la consolă
numărul PI. Apelul îl putem face de exemplu cu instrucțiunea:
B obiect = new B();
obiect.unu();
Observați că în metoda unu din clasa B, variabila referită este
variabila a din clasa B. Variabila a din clasa A este
ascunsă. Putem însă să o referim prin sintaxa A.a ca în metoda doi
din clasa B.
În interiorul clasei B, apelul metodei unu fără nici o altă
specificație duce automat la apelul metodei unu definite în interiorul clasei B.
Metoda unu din clasa B rescrie metoda unu din clasa A.
Vechea metodă este accesibilă pentru a o referi în mod explicit ca în metoda trei
din clasa B. Apelul acestei metode va afișa mai întâi numărul PI și
apoi numărul 1.
Dacă avem declarată o variabilă de tip referință către o instanță a clasei A,
această variabilă poate să conțină în timpul execuției și o referință către o
instanță a clasei B. Invers, afirmația nu este valabilă.
În clipa în care apelăm metoda unu pentru o variabilă referință către
clasa A, sistemul va apela metoda unu a clasei A sau B
în funcție de adevăratul tip al referinței din timpul execuției. Cu alte cuvinte,
următoarea secvență de instrucțiuni:
A tablou[] = new A[2];
tablou[0] = new A();
tablou[1] = new B();
for( int i = 0; i < 2; i++ ) {
tablou[i].unu();
}
va afișa două numere diferite, mai întâi 1 și apoi PI. Aceasta
din cauză că cel de-al doilea element din tablou este, în timpul execuției, de tip
referință la o instanță a clasei B chiar dacă la compilare este de tipul
referință la o instanță a clasei A.
Acest mecanism se numește legare târzie, și înseamnă că metoda care va fi
efectiv apelată este stabilită doar în timpul execuției și nu la compilare.
Dacă nu declarăm nici o superclasă în definiția unei clase, atunci se consideră
automat că noua clasă derivă direct din clasa Object, moștenind toate metodele și
variabilele acesteia.
6.7 Interfețe
O interfață este în esență o declarație de tip ce constă dintr-un set de
metode și constante pentru care nu s-a specificat nici o implementare. Programele Java
folosesc interfețele pentru a suplini lipsa moștenirii multiple, adică a claselor de
obiecte care derivă din două sau mai multe alte clase.
Sintaxa de declarație a unei interfețe este următoarea:
Modificatori interface NumeInterf [ extends
[Interfață][, Interfață]*]
Corp
Modificatorii unei interfețe pot fi doar cuvintele rezervate public
și abstract. O interfață care este publică poate fi accesată și de
către alte pachete decât cel care o definește. În plus, fiecare interfață este în
mod implicit abstractă. Modificatorul abstract este permis dar nu
obligatoriu.
Numele interfețelor trebuie să fie identificatori Java. Convențiile de numire a
interfețelor le urmează în general pe cele de numire a claselor de obiecte.
Interfețele, la fel ca și clasele de obiecte, pot avea subinterfețe.
Subinterfețele moștenesc toate constantele și declarațiile de metode ale interfeței
din care derivă și pot defini în plus noi elemente. Pentru a defini o subinterfață,
folosim o clauză extends. Aceste clauze specifică superinterfața unei
interfețe. O interfață poate avea mai multe superinterfețe care se declară separate
prin virgulă după cuvântul rezervat extends. Circularitatea definiției
subinterfețelor nu este permisă.
În cazul interfețelor nu există o rădăcină comună a arborelui de derivare așa
cum există pentru arborele de clase, clasa Object.
În corpul unei declarații de interfață pot să apară declarații de variabile și
declarații de metode. Variabilele sunt implicit statice și finale. Din cauza faptului
că variabilele sunt finale, este obligatoriu să fie specificată o valoare inițială
pentru aceste variabile. În plus, această valoare inițială trebuie să fie constantă
(să nu depindă de alte variabile).
Dacă interfața este declarată publică, toate variabilele din corpul său sunt
implicit declarate publice.
În ceea ce privește metodele declarate în interiorul corpului unei interfețe,
acestea sunt implicit declarate abstracte. În plus, dacă interfața este declarată
publică, metodele din interior sunt implicit declarate publice.
Iată un exemplu de declarații de interfețe:
public interface ObiectSpatial {
final int CUB = 0;
final int SFERA = 1;
double greutate();
double volum();
double raza();
int tip();
}
public interface ObiectSpatioTemporal extends ObiectSpatial {
void centrulDeGreutate( long moment,
double coordonate[] );
long momentInitial();
long momentFinal();
}
Cele două interfețe definesc comportamentul unui obiect spațial respectiv al unui
obiect spațio-temporal. Un obiect spațial are o greutate, un volum și o rază a sferei
minime în care se poate înscrie. În plus, putem defini tipul unui obiect folosindu-ne
de o serie de valori constante predefinite precum ar fi SFERA sau CUB.
Un obiect spațio-temporal este un obiect spațial care are în plus o poziție pe axa
timpului. Pentru un astfel de obiect, în afară de proprietățile deja descrise pentru
obiectele spațiale, trebuie să avem în plus un moment inițial, de apariție, pe axa
timpului și un moment final. Obiectul nostru nu există în afara acestui interval de
timp. În plus, pentru un astfel de obiect putem afla poziția centrului său de greutate
în fiecare moment aflat în intervalul de existență.
Pentru a putea lucra cu obiecte spațiale și spațio-temporale este nevoie să definim
diverse clase care să implementeze aceste interfețe. Acest lucru se face specificând
clauza implements în declarația de clasă. O clasă poate implementa mai
multe interfețe. Dacă o clasă declară că implementează o anumită interfață, ea
este obligatoriu să implementeze toate metodele declarate în interfața respectivă.
De exemplu, putem spune că o minge este un obiect spațial de tip sferă. În plus,
mingea are o poziție în funcție de timp și un interval de existență. Cu alte
cuvinte, mingea este chiar un obiect spațio-temporal. Desigur, în afară de
proprietățile spațio-temporale mingea mai are și alte proprietăți precum culoarea,
proprietarul sau prețul de cumpărare.
Iată cum ar putea arăta definiția clasei de obiecte de tip minge:
import java.awt.Color;
class Minge extends Jucarie implements ObiectSpatioTemporal {
int culoare = Color.red;
double pret = 10000.0;
double raza = 0.25;
long nastere;
long moarte;
// metodele din ObiectSpatial
double greutate() {
return raza * 0.5;
}
double raza() {
return raza;
}
double volum() {
return ( 4.0 / 3.0 ) * Math.PI * raza * raza * raza;
}
int tip() {
return SFERA;
}
// metodele din interfața ObiectSpatioTemporal
boolean centrulDeGreutate( long moment,
double coordonate[] ) {
if( moment < nastere || moment > moarte ) {
return false;
}
coordonate[0] = x;
coordonate[1] = y;
coordonate[2] = z;
return true;
}
long momentInitial() {
return nastere;
}
long momentFinal() {
return moarte;
}
int ceCuloare() {
return culoare;
}
double cePret() {
return pret;
}
}
Observați că noua clasă Minge implementează toate metodele definite în
interfața ObiectSpatioTemporal și, pentru că aceasta extinde interfața ObiectSpatial,
și metodele definite în cea din urmă. În plus, clasa își definește propriile metode
și variabile.
Să presupunem în continuare că avem și o altă clasă, Rezervor, care este
tot un obiect spațio-temporal, dar de formă cubică. Declarația acestei clase ar arăta
ca:
class Rezervor extends Constructii implements ObiectSpatioTemporal {
}
Desigur, toate metodele din interfețele de mai sus trebuiesc implementate, plus alte
metode specifice.
Să mai observăm că cele două obiecte derivă din clase diferite: Mingea din
Jucării iar Rezervorul din Construcții. Dacă am putea deriva o clasă din două alte
clase, am putea deriva Minge din Jucarie și ObiectSpatioTemporal
iar Rezervor din Constructie și ObiectSpațioTemporal. Într-o
astfel de situație, nu ar mai fi necesar ca ObiectSpațioTemporal să fie o
interfață, ci ar fi suficient ca acesta să fie o altă clasă.
Din păcate, în Java, o clasă nu poate deriva decât dintr-o singură altă clasă,
așa că este obligatoriu în astfel de situații să folosim interfețele. Dacă ObiectSpațioTemporal
ar fi putut fi o clasă, am fi avut avantajul că puteam implementa acolo metodele cu
funcționare identică din cele două clase discutate, acestea fiind automat moștenite
fără a mai fi nevoie de definirea lor de două ori în fiecare clasă în parte.
Putem crea în continuare metode care să lucreze cu obiecte spațio-temporale, de
exemplu o metodă care să afle distanța unui corp spațio-temporal față de un punct
dat la momentul său inițial. O astfel de metodă se poate scrie o singură dată, și
poate lucra cu toate clasele care implementează interfața noastră. De exemplu:
double distanta( double punct[], ObiectSpatioTemporal obiect ) {
double coordonate[] = new double[3];
obiect.centrulDeGreutate( obiect.momentInitial(),
coordonate );
double x = coordonate[0] - punct[0];
double y = coordonate[1] - punct[1];
double z = coordonate[2] - punct[2];
return Math.sqrt( x * x + y * y + z * z );
}
Putem apela metoda atât cu un obiect din clasa Minge cât și cu un obiect
din clasa Rezervor. Compilatorul nu se va plânge pentru că el știe că ambele
clase implementează interfața ObiectSpațioTemporal, așa că metodele apelate
în interiorul calculului distanței (momentInitial și centruDeGreutate)
sunt cu siguranță implementate în ambele clase. Deci, putem scrie:
Minge minge;
Rezervor rezervor;
double punct[] = { 10.0, 45.0, 23.0 };
distanța( punct, minge );
distanța( punct, rezervor );
Desigur, în mod normal ar fi trebuit să proiectăm și un constructor sau mai mulți
care să inițializeze obiectele noastre cu valori rezonabile. Acești constructori ar fi
stat cu siguranță în definiția claselor și nu în definiția interfețelor. Nu avem
aici nici o cale de a forța definirea unui anumit constructor cu ajutorul interfeței. |