Capitolul VI
Obiecte Java

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. ^

6.2.2 Protecție

Î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 ^
6.3.1.2.1 Metode statice

Î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ă. ^

6.3.1.2.2 Metode abstracte

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ă. ^

6.3.1.2.3 Metode finale

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. ^

6.3.1.2.4 Metode native

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. ^

[cuprins]
(C) IntegraSoft 1996-1998