Sunteți pe pagina 1din 90

Universitatea TRANSILVANIA din Braşov

Facultatea de Matematică şi Informatică


Catedra de Informatică Aplicată

Iniţiere în
programarea orientată pe
obiecte din perspectivă Java

Dorin Bocu
...O parte din eforturile ştiinţei calculatoarelor sunt dedicate îmbunătăţirii
permanente a paradigmelor de modelare. Prin istoric şi prin realizările de ultimă
oră, ingineria softului nu face decât să confirme această aserţiune. Modelarea
orientată pe obiecte este un exemplu remarcabil de instrument, gândit pentru a
fi utilizat în realizarea de sisteme soft, competitive din punct de vedere al
preţului şi al calităţii. Programarea orientată pe obiecte îngăduie fanilor ei să
verifice, în practică, forţa unui stil de a modela, a cărui capacitate de a mapa
domeniul problemei peste domeniul soluţiei este cu mult superioară altor
paradigme.
Cuvânt înainte al autorului

Au trecut ani buni de când lumea a început să întrebuinţeze, în vorbire şi în alte contexte, sintagma
“programare obiect orientată” sau, ceva mai aproape de spiritul limbii române, “programare orientată pe
obiecte”. Pregătită şi anunţată de numeroasele controverse pe marginea preamultelor slăbiciuni ale vechilor
paradigme de programare, orientarea pe obiecte s-a instalat confortabil de-a lungul întregului proces de
dezvoltare a unui sistem soft, devenind în zilele noastre “religia care guvernează atât etapa de modelare a
unui sistem soft cât şi etapa de implementare”. Au apărut limbaje, precum şi medii de programare şi
dezvoltare a sistemelor soft, a căror arhitectură este esenţial tributară ideii de orientare pe obiecte (Java, C++,
C#, Object Pascal – câteva exemple mai cunoscute de limbaje, Delphi, C-Builder, Visual C++ - câteva exemple
mai cunoscute de medii de programare, Rational Rose, ObjectiF – două dintre mediile de dezvoltare cu bună
răspândire în lumea ingineriei softului).
Este clar, orientarea pe obiecte nu este doar o modă în programare, ci o modalitate, fără rival,
pentru moment, de a dezvolta sisteme soft.
Pentru informaticienii a căror practică se reduce la “apropierea cât mai rapidă de tastatură” pentru a
rezolva o problemă, orientarea pe obiecte este o încercare care tulbură minţile şi întârzie rezolvarea
problemei. Seriile de studenţi care “mi-au trecut prin mână” mi-au întărit convingerea că însuşirea spiritului
orientării pe obiecte este o problemă destul de dificilă, deoarece, aproape tot ceea ce este reclamat de
obşnuinţele omului în materie de învăţare, este dificil de operaţionalizat când este vorba de însuşirea acestui
spirit. Mult mai apăsat decât în alte paradigme, în orientarea pe obiecte, specialistul trebuie să acorde atenţia
cuvenită elaborării soluţiei înainte de a o implementa. Metodele cele mai răspândite de învăţare a limbajelor
de programare se bazează pe formula:

Prin aplicaţii, spre descoperirea subtilităţilor sintactice, semantice şi pragmatice ale unui limbaj
de programare.

Cititorul atent a înţeles faptul că învăţarea unui limbaj de programare este un exerciţiu de voinţă care
presupune parcurgerea a trei etaje:

• Învăţarea stereotipurilor sintactice fundamentale ale limbajului (sintaxa limbajului)


• Descoperirea unui număr cât mai mare de semantici primitive, care pot fi învelite cu sintaxa limbajului
(semantica limbajului)
• Formarea unor deprinderi de utilizare eficientă a limbajului, în funcţie de natura şi complexitatea
problemei, pentru rezolvarea respectivei probleme (pragmatica limbajului).

Ideea călăuzitoare a acestui support de curs este de a invita cititorul să înveţe singur sintaxa
orientării pe obiecte, încercând să îl ajute, îndeosebi în efortul de deconspirare a semanticii şi pragmaticii
orientării pe obiecte.
Trimiterile de natură sintactică vor fi raportate la limbajul Java.
Trebuie să mărturisesc, totodată, faptul că, în viziunea acestei lucrări, fiecare cititor este o instanţă
cognitivă activă, capabilă de efort permanent de abstractizare, singura modalitate de a ajunge la o
înţelegere superioară a esenţei unei paradigme de modelare şi, în particular, de programare.
Atitudinea de spectator, cu instinct de conservare puternic, la derularea ideilor din această lucrare, este
absolut contraproductivă pentru atingerea obiectivului fundamental: învăţarea cât mai multor elemente -
suport, esenţiale pentru modelarea / programarea orientată pe obiecte a soluţiei unei probleme.
Ce se va întâmpla cu cei care nu se vor putea împăca cu ideea de a se dezmorţi voluntar, iată o
problemă în legătură cu care prefer să spun doar atât: nu peste mult timp vor descoperi că au îmbătrânit
înainte de vreme, fără a fi înţeles mare lucru despre plăcerea de a uita de scurgerea timpului, realizând
lucruri care să uimească, deopotrivă, pe alţii şi pe propriul lor creator.
Referindu-mă la studenţii pentru care, în principal, am scris această lucrare, trebuie să spun că, în
viziunea mea, ideea de student se confundă cu imaginea unui individ care are apetenţă pentru studiu.
Dacă, în facultate, se mai studiază şi discipline care, unora li se par, nefolositioare, datoria studentului este să
facă efortul de a se identifica cu ideile fundamentale ale unui număr cât mai mare de discipline, socotite de el
folositoare. Dacă, în facultate, studenţii se mai întâlnesac şi cu profesori ale căror căutări nu sunt încă suficient
de bine sistematizate, acesta nu este un motiv de a renunţa la dorinţa de a cunoaşte.
De la natură, studentul poate fi asimilat cu un obiect care are toate capabilităţile necesare pentru a
parcurge drumul dificil, dar pasionant, al cunoaşterii elementelor fundamentale, în diferite ramuri ale
matematicii şi ştiinţei calculatoarelor.

Aş aminti, tuturor celor care nu au realizat încă, faptul că omul are nevoie de ştiinţă pentru a înţelege o
parte din realitatea necunoscută (rezultă că ştiinţa are funcţie explicativă), pentru a modela comportamentul
unor fenomene şi procese din realitatea obiectivă, în vederea optimizării dinamicii lor (rezultă că ştiinţa are
funcţie modelatoare), pentru a îmbogăţi realitatea obiectivă cu obiecte artificiale (rezultă că ştiinţa are funcţie
demiurgică).

Sper ca cititorul să înţeleagă bine rolul activ pe care trebuie să şi-l asume în deconspirarea, prin studiu
individual şi exerciţii perseverente, a etajelor sintactice esenţiale programării în Java.
1 Cum se explică permanenta nevoie de paradigme
noi în ingineria softului. Ce înţelegem prin
orientarea pe obiecte

1.1 Cum se explică permanenta nevoie de paradigme noi în ingineria


softului?
Binefacere sau blestem, omenirea este, asemenea întregului univers, într-o continuă deplasare spre alte
repere ontologice şi gnoseologice. Avea dreptate Heraclit din Efes când, folosind cuvinte meşteşugit alese,
constata că singura certitudine a universului pare a fi devenirea. Dacă, acum peste 2500 de ani în urmă,
devenirea ocupa o poziţie centrală în modul de gândire al lui Heraclit, în zilele noastre devenirea s-a transformat
în izvor nesecat şi cauză a tuturor transformărilor importante pe care le suportă universul cunoscut omului. S-ar
părea că nimic din ceea ce face omul nu durează. Acest fapt este, după ascuţimea simţurilor noastre, în opoziţie
cu ceea ce Natura sau Marele Creator fac. Omul aspiră la eternitate luptând cu efemerul inerent al
creaţiilor lui. Marele Creator este eternitatea însăşi. Las pe seama filozofilor continuarea efortului de preamărire
sau punere la index a rolului pe care îl joacă devenirea în viaţa omului şi, de ce nu, în univers.
Caracterul obiectiv al devenirii poate explica, în genere şi nevoia de paradigme1 noi în ingineria softului
şi, în particular în programare.
*
Permiţându-mi o scurtă digresiune, ingineria softului este o ramură a ştiinţei calculatoarelor care se
ocupă, la urma urmei, de studierea unei probleme extrem de delicate: “Dată o problemă oarecare, ce trebuie
făcut pentru a o rezolva cu ajutorul calculatorului?”
Confruntaţi de-a lungul timpului, cu diferite tipuri de probleme, specialiştii în ingineria softului au făcut
o descoperire banală:

Efortul depus pentru a rezolva o problemă cu ajutorul calculatorului este direct proporţional cu
complexitatea problemei.

Odată făcută această descoperire iese la iveală altă întrebare: “Cum definim complexitatea unei
probleme?” Atenţie, cititorule, este vorba de complexitatea unei probleme nu de complexitatea soluţiei
algoritmice a unei probleme.
Complexitatea unei probleme este o caracteristică intrinsecă a enunţului asumat al acesteia.
Complexitatea soluţiei algoritmice a unei probleme este o caracteristică intrinsecă a modului în care o
anumită instanţă cognitivă (de pildă un student din anul II) obţine respectiva soluiţie algoritmică.
Este, sper, cât se poate de clar faptul că cele două tipuri de complexitate cuantifică caracteristicile
structurale ale unor colecţii de obiecte, diferite din punct de vedere al sferei de cuprindere şi al conţinutului.
Cei care au parcurs unul sau mai multe cursuri de iniţiere în Algoritmică şi Programare îşi amintesc,
probabil, înţelesul pe care îl are complexitatea unui algoritm. Formalizarea matematică a complexităţii
algoritmilor este un prilej minunat de a ilustra forţa de analiză a matematicii de puterea continuului, aplicată
universului algoritmilor, al căror comportament este eminamente discret.
Este corect să evidenţiem, în acest punct, utilitatea teoretică şi practică (îndeosebi în cazul aplicaţiilor
critice relativ la complexitatea algoritmilor folosiţi) a eforturilor de clasificare a algoritmilor secvenţiali şi
paraleli, în funcţie de complexitatea lor Acest gen de afirmaţii sunt prezentate în toată cuprinderea şi adâncimea
lor în cărţi fundamentale pentru învăţărea programării calculatoarelor2.
Ingineria softului (IS) ia act de această realitate şi, în replică, îşi concentrează atenţia asupra
complexităţii problemelor. O încercare de a evidenţia elementele cu ajutorul cărora putem descrie
complexitatea unei probleme în ingineria softului ne conduce la seria de observaţii de mai jos.

• Rezolvarea unei probleme cu ajutorul calculatorului înseamnă, de cele mai multe ori, simularea /
asistarea de către calculator a unor activităţi, desfăşurate de către sisteme de altă natură. Aşadar,

1
Este cazul să spunem că, prin paradigmă se înţelege, fără a exagera cu explicaţiile, un mod de abordare a problemelor dintr-un anumit
domeniu, evident, cu scopul de a rezolva aceste probleme. Poate că limba română nu avea neapărată nevoie de acest cuvânt, dar insistenţa cu
care este utilizat în alte părţi poate fi considerată o scuză pentru utilizarea lui în această carte.
2
Knuth, Cormen
aproape invariabil, ne aflăm în faţa unor dificultăţi care rezultă din caracterul instabil al relaţiei
dintre model şi sistemul modelat. Neglijarea acestui aspect simplifică efortul de realizare, în cele din
urmă, a unui sistem soft, dar preţul plătit este exprimat, sintetic, astfel: uzură morală rapidă şi
posibilităţi de adaptare reduse. Se poate lupta cu mijloace raţionale împotriva caracterului instabil al
relaţiei dintre model şi sistemul modelat? Răspunsul este: da, se poate lupta, dacă specialistul în IS este
dispus să anticipeze impactul posibilelor schimbări ale sistemului modelat asupra soluţiei
(modelului). Anticipând schimbările sistemului modelat, simplificăm efortul de adaptare a soluţiei la
aceste schimbări.

• Modelul care stă la baza soluţiei unei probleme abstractizează viziunea specialistului sau grupului de
specialişti în IS, asupra sistemului modelat. Semn al posibilităţilor noastre limitate de a cunoaşte, orice
viziune nu poate decât să aproximeze comportamentul sistemului modelat. Chiar şi în aceste condiţii,
de la a aproxima defectuos până la a aproxima inspirat este nu doar un drum lung ci şi plin de
diverse tipuri de încercări. Exemple de astfel de încercări:
• specificarea completă a cerinţelor funcţionale;
• acordarea atenţiei cuvenite cerinţelor non-funcţionale;
• alegerea inspirată a modalităţilor de armonizare a cerinţelor contradictorii;
• alegerea inspirată a paradigmei de modelare, etc.

• Trecerea, cu succes, peste dificultăţile schiţate mai sus este realizată “în condiţii ergonomice” dacă
managementul acestor dificultăţi (plus nenumărate altele) este tratat cu maximum de seriozitate şi
competenţă.

Încercând să forţăm o concluzie, complexitatea unei probleme în IS este influenţată de:

1. Stabilitatea relaţiei model-sistem modelat; lipsa de stabilitate în această relaţie, asumată conştient,
induce creşterea complexităţii problemei de rezolvat.

2. Complexitatea structurii sistemului modelat (creşterea acesteia induce, de asemenea, creşterea


complexităţii problemei de rezolvat).

3. Desfăşurarea în condiţii de eficienţă a procesului de rezolvare a unei probleme, adaugă un plus de


complexitate oricărei probleme. Neasumându-se acest plus de complexitate, putem uşor compromite
procesul de realizare a unui sistem soft (=soluţia executabilă pe un calculator real a unei probleme date).
Un management bun are de rezolvat probleme de procurare şi alocare optimă a resurselor, comunicare
între partenerii de proiect (=specialiştii în IS, beneficiarii, utilizatorii), lansare cu succes pe piaţă (dacă
este cazul), etc. Nu aş sfătui pe nici un specialist în IS să considere normală o afirmaţie de genul: “Dacă
cele spuse la punctul 2 sunt tratate cu maximum de seriozitate şi competenţă, 1 şi 3 nu mai înseamnă
mare lucru”. Îndeosebi în cazul proiectelor mari, s-a dovedit, de nenumărate ori, neadevărul unei astfel
de afirmaţii.

În IS, complexitatea problemelor nu este un accident; mai mult, putem spune că este legitim să ne
aşteptăm la elemente de complexitate chiar şi acolo unde s-ar părea că acestea sunt lipsă. Dacă mai
amintim cititorului observaţia tendenţioasă, potrivit căreia complexitatea de calitate se întemeiază pe
simplitate, acesta înţelege mai bine insistenţa cu care prezint implicaţiile complexităţii unei probleme asupra
calităţii soluţiei acestei probleme. Calitate? Complexitate3? Complexitate de calitate? Multe se mai pot spune.
Specialistul în IS doreşte să ştie câteva lucruri simple:

• Care sunt primejdiile?


• Care sunt exigenţele?
• Cum se poate evita un eşec?, etc.

Astfel de întrebări, ne vom pune, într-o formă sau alta şi în această lucrare de iniţiere în programarea
orientată pe obiecte şi le puteţi găsi reluate în cărţile Iniţiere în ingineria sistemelor soft şi Iniţiere în

3
Orice exerciţiu de modelare urmăreşte, indiferent de acuitatea mijloacelor de investigaţie, un anumit mod de organizare a complexităţii
sistemului modelat.
modelarea obiect orientată a sistemelor soft utilizând UML, scrise de D. Bocu şi apărute la Editura
Albastră.
Nevoia presantă de a căuta, permanent, răspunsuri noi la astfel de întrebări, justifică avalanşa de noutăţi
care caracterizează şi lumea limbajelor de modelare, specifice ingineriei softului.

1.2 Ce se înţelege prin orientarea pe obiecte?


S-au scris destule cărţi pe tema orientării spre obiecte. Şi mai multe sunt, probabil, articolele din
revistele de specialitate. Unele, foarte convingătoare. Altele mai puţin. Nu cred că voi umple, brusc, golul care
mai există, încă, în limba română, scriind această carte. Dar voi încerca să fac înţeles modul de gândire al unui
specialist în IS, care vede realitatea informaţională în termeni specifici orientării spre obiecte.
Orice sistem informaţional4 este mai uşor de înţeles dacă avem elemente suficiente referitoare la
datele vehiculate în sistem, procedeele de prelucrare (vehiculare) a datelor din sistem, interfeţele sistemului
cu mediul în care acesta operează. Fiecare dintre elementele specificate mai sus are reguli proprii de organizare
şi fiinţare. În plus, mai există şi relaţiile strânse dintre aceste elemente. De fapt, întreaga istorie a IS se învârte în
jurul ecuaţiei prezentată în Figura 1.

<Soluţia unei probleme> = <Organizarea datelor>


+ <Organizarea prelucrărilor>
+ <Optimizarea interfeţelor>

Figura 1. Ecuaţia generală a soluţiei unei probleme în IS.


Ecuaţia de mai sus a primit de-a lungul timpului numeroase rezolvări.
*
Înainte de a descrie tipurile fundamentale de rezolvări, voi efectua o scurtă trecere în revistă a
vocabularului IS, esenţial în comunicarea dintre partenerii unui proiect de dezvoltare a unui sistem soft.

Prin urmare, o firmă producătoare de soft produce şi livrează produse şi servicii care se adresează
nevoilor şi cerinţelor clienţilor. Cerinţele clienţilor constitue un exemplu de problemă cu care trebuie să se
confrunte echipa de dezvoltare. Produsele şi serviciile care satisfac aceste cerinţe pot fi considerate ca fiind
soluţii. Pentru a livra soluţii valide (= de calitate, la preţ de cost redus şi în timp util) firmele trebuie să facă
permanent achiziţie, comunicare (partajare) şi utilizare de cunoştinţe specifice referitoare la domeniul
problemei de rezolvat. Pentru a discrimina şi comunica eficient informaţiile necesare pentru rezolvarea unei
probleme, firmele folosesc tehnologii adecvate. O tehnologie este, în general vorbind, un instrument pe care
membrii echipei de dezvoltare trebuie să înveţe s-o folosească la întregul ei potenţial.
Aşadar, problema de rezolvat, la care se adaugă necesitatea de a învăţa modul de utilizare a
tehnologiilor necesare în rezolvarea problemei, ne permit o imagine, încă optimistă, asupra complexităţii
efortului de realizare a unui sistem soft. Amplificarea acestei complexităţi este motivul pentru care întregul
efort de realizare a unui sistem soft este structurat sub forma unui proiect. În cadrul unui proiect se desfăşoară,
după reguli precise, toate activităţile necesare pentru a asigura succesul efortului de realizare a unui sistem soft.
Două dintre dimensiunile esenţiale pentru succesul unui proiect sunt limbajul de modelare folosit şi tipul de
proces utilizat pentru a materializa forţa limbajului de modelare. Despre toate acestea, mai multe aspecte la
cursul care îşi propune iniţierea studenţilor în ingineria sistemelor soft.

Revenind la întrebarea de bază a acestui paragraf, să observăm că, încă de la apariţia primelor
calculatoare, a apărut o nouă dilemă în faţa omenirii:

Cum putem învăţa calculatoarele să ne rezolve în mod optim problemele?

4
Ca şi realitatea informaţională, sistemul informaţional desemnează un ansamblu de resurse care optimizează fluxurile informaţionale dintr-
un sistem gazdă. Pentru o documentare mai atentă invit cititorul să răsfoiască atent o carte bună de ingineria softului.
La început, când calculatoarele erau folosite exclusiv pentru rezolvarea unor probleme cu carater
ştiinţific, soluţia era programarea în cod maşină. Dificultăţile de bază în rezolvare acestor tipuri de probleme
erau două :elaborarea modelelor matematice şi transpunerea lor în cod maşină. Mai ales programarea în cod
maşină, era un gen de exerciţiu la care nu se îngrămădeau decât indivizi care manifestau un interes ieşit din
comun pentru sistemele electronice de calcul din acea generaţie. Apariţia limbajelor de asamblare a generat o
relaxare a “cortinei de fier” instalată de codul maşină între calculatoare şi marea masă a curioşilor. O relaxare
asemănătoare s-a produs şi în ceea ce priveşte tipurile de probleme abordabile cu ajutorul calculatoarelor. Era
momentul în care intrau pe scenă aplicaţiile de gestiune, datorită apariţiei memoriilor externe. Locul datelor
simple începe să fie luat de date structurate şi din ce în ce mai voluminoase. Presiunea exercitată de cererea
crescândă de aplicaţii de gestiune a impus trecerea la limbajele de nivel înalt şi mediu. În paralel, cu deosebire
în faţa proiectelor uriaşe (controlul traficului aerian, gestiunea tranzacţiilor unor bănci, informatizarea tot mai
multor activităţi ale intreprinderilor industriale) apare nevoia disciplinării procesului de derulare a acestor
proiecte din faza de specificare a cerinţelor, trecând prin faza de analiză, continuând cu proiectarea soluţiei
şi terminând (grosier vorbind) cu programarea.
De unde această nevoie de disciplinare? Foarte simplu, complexitatea problemelor (parcă am mai
auzit undeva despre acest lucru) nu mai putea fi stăpânită lucrând fără metodă. Astfel au apărut, de-a
lungul timpului, tot felul de mode în ceea ce priveşte abstractizarea soluţiei şi nu numai.
Managementul unui proiect trebuie să aibă, permanent, în vizor, cel puţin, asigurarea limbajului de
modelare adecvat şi a modelului de dezvoltare optim5. De la moda programelor-mamut (singura posibilă în
vremea programării în cod maşină) s-a ajuns treptat la nevoia modularizării (= descompunerea problemei iniţiale
în subprobleme, eventual aplicarea repetată a acestui procedeu, obţinându-se nişte piese ale soluţiei numite
module, care, asamblate, compuneau, în sfârşit soluţia). Întrebarea care a „furnicat”, ani la rând, creierele
teoreticienilor şi practicienilor, deopotrivă, era:

Care sunt criteriile după care se face modularizarea unei soluţii?

Există o serie de factori externi ai calităţii unui sistem soft care ţin sub presiune tendinţa de a
modulariza de amorul artei, precum: corectitudinea, robusteţea, reutilizabilitatea, portabilitatea, etc. Există
şi o serie de factori interni ai calităţii unui sistem soft, care presează, în egală măsură, precum: structurarea
soluţiei, claritatea algoritmilor, calitatea arhitecturii, etc.
Menţinerea în echilibru a acestor factori de presiune este o sarcină extrem de dificilă. În cele din
urmă, cercetătorii au ajuns la concluzia că, din punct de vedere al celui care lucrează, materializarea acestor
factori la parametri satisfăcători sau marginalizarea lor, depind de decizia luată în ceea ce priveşte relaţia
dintre date şi prelucrări în procesul de modularizare a soluţiei unei probleme. Istoria ne arată că au existat
teoreticieni şi practicieni încredinţati că singura metodă de modularizare validă este modularizarea dirijată de
date (altfel spus, înainte de a te ocupa de organizarea prelucrărilor, rezolvă, în mod optim, problema organizării
datelor; din schema de organizare a acestora se va deriva şi schema de structurare a prelucrărilor). Tot istoria ne
arată că au existat şi teoreticieni şi practicieni încredinţati că singura metodă de modularizare validă este
modularizarea dirijată de prelucrări (altfel spus, ocupă-te, mai întâi, de organizarea prelucrărilor şi, mai apoi,
fă rost de datele necesare şi organizează-le conform cerinţelor prelucrărilor). Curând s-a observat că dihotomia
date-prelucrări nu este benefică sub nici o formă. A existat o soluţie de compromis (modularizarea dirijată
de fluxurile de date), care s-a dovedit în timp nesatisfăcătoare în numeroase situaţii din realitate. Din punct de
vedere al criticilor fundamentale care se pot formula la adresa acestor abordări esenţa este următoarea: Dacă
soluţia unei probleme înseamnă ansamblul date-prelucrări, fiecare componentă având o existenţă relativ
autonomă şi reguli proprii de organizare, atunci avem situaţia din Figura 2.
Esenţial în mesajul transmis de Figura 2 este faptul că datele sunt sficient de izolate de prelucrări
pentru ca o modificare în structura unei componente să dea uşor peste cap structura celeilalte
componente.
Conform paradigmelor caracterizate în Figura 2, era posibilă o caracterizare de tipul celei prezentate
pentru problema de mai jos.

Problema 1: Să se realizeze un sistem soft care simulează deplasarea unui om pe o suprafaţă


plană.

5
Mai multe detalii în această privinţă în cartea Iniţiere în ingineria sistemelor soft, D. Bocu, Editura Albastră, Cluj-Napoca, 2002
DATE

Problemă
(activitate-sistem
real al cărui
comportament
trebuie modelat)

PRELUCRĂRI

Figura 2 Perspectiva clasică asupra relaţiei dintre date şi prelucrări în structura unei
soluţii
Dacă aş fi un partizan al modularizării dirijate de date, mai întâi mi-aş pune următoarele întrebări: care
sunt atributele informaţionale care caracterizează un om (stilizat convenabil, să spunem)?. Cum se caracterizează
un plan? Cum se memorează intern şi extern datele despre aceste obiecte? După ce am “distilat” satisfăcător
lumea datelor, încep să mă ocup şi de lumea prelucrărilor necesare în jurul acestor date. Obţin, inclusiv în
viziunea limbajelor de programare suport, două lumi, care interferă, dar au reguli de organizare şi reprezentare în
memorie distincte. Mai trist, legătura dintre date şi prelucrări este o problemă care se gestionează prin
programare, ca şi când nu ar fi suficiente problemele celelalte. Ce ne facem, însă, dacă apare necesitatea
simulării deplasării unei vieţuitoare în genere, pe o suprafaţă plană? Pentru fiecare vieţuitoare în parte iau
travaliul de la capăt? Posibil, dar total contraindicat din multe puncte de vedere( preţ de cost, extensibilitate,
încadrare în termenele de execuţie, etc.). Este limpede, reformulat ca mai sus, enunţul problemei ne pune în faţa
sarcinii de a modela comportamentul unor obiecte, heterogene ca tip, dar între care există afinităţi, atât de natură
informaţională cât şi comportamentală. Astfel apare, ceea ce în modelarea obiect orientată se numeşte problema
gestiunii similarităţilor unor colecţii de obiecte. Gestiunea corectă a similarităţilor unei colecţii de obiecte,
heterogene din punct de
vedere al tipului definitor, Soluţia orientată pe obiecte a problemei
se realizează desfăşurând în
paralel efort de clasificare
şi, acolo unde este cazul,
ierarhizare cu ajutorul C1
operaţiilor de Problemă
generalizare / specializare. (activitate-sistem
real al cărui
Cum arată lumea privită din comportament
această perspectivă? trebuie modelat)
Simplificând intenţionat, C11 C12
cam ca în Figura 3.

Nu voi da, încă,


nici o definiţie, dar voi face C111
o serie de observaţii pe care
le voi aprofunda în C121 C122
capitolele următoare.

Figura 3. Perspectiva orientată pe obiecte a soluţiei unei probleme

1. Se insinuează faptul că soluţia orientată pe obiecte a unei probleme se obţine în urma unui demers de
organizare a unor obiecte, care au atât proprietăţi informaţionale cât şi comportament (se manifestă,
astfel, într-un cadru absolut natural, un mai vechi principiu utilizat în modelare şi anume principiul
încapsulării datelor şi prelucrărilor. Încapsulare facem şi în Pascal, când scriem unit-uri care furnizează
anumite servicii unor categorii bine definite de utilizatori. Aceste unit-uri au interfaţă şi implementare.
Manifestarea principiului încapsulării, în acest cadru, însemna ascunderea detaliilor de
implementare faţă de utilizatori, prin publicarea unei interfeţe stabile. O interfaţă este stabilă dacă utilizatorul
ei nu sesizează eventualele modificări aduse implementării interfeţei. 6

2. În sfârşit, aplicând riguros principiul încapsulării, putem defini clase de obiecte care au o importanţă
esenţială pentru maparea domeniului problemei peste domeniul soluţiei. Există, însă, numeroase alte
raţiuni pentru care principiul încapsulării se aplică conjugat cu alt principiu important în modelarea
orientată pe obiecte: principiul moştenirii. Aplicarea acestui principiu ne ajută să raţionalizăm
redundanţele care apar, în mod inerent, în procesul de elaborare a unei soluţii în genere. Mai
mult, principiul moştenirii pregăteşte terenul pentru rezolvarea unor probleme interesante care ţin de
polimorfism şi genericitate. Exemplul de referinţă în această privinţă este Java.

Fără a mai insista prea mult, să desprindem concluzia care se impune evident la acest nivel de
prezentare: Modelând orientat pe obiecte, asigurăm maximum de corespondenţă posibilă între obiectele
care populează sistemul modelat şi obiectele care dau, practic, viaţă soluţiei. Lucru absolut remarcabil,
deoarece principiul încapsulării (temelie a modularizării de calitate în orientarea pe obiecte) introduce elemente
de stabilitate deosebită a soluţiei chiar şi în situaţia în care apara modificări în sfera domeniului problemei.

Prăpastia dintre date şi prelucrări este înlocuită de reguli precise de asociere a datelor şi
prelucrărilor, pentru a descrie tipuri de obiecte întâlnite în domeniul problemei şi care sunt importante
pentru economia de resurse a soluţiei.

Din această perspectivă privind lucrurile, este evident faptul că modelarea orientată pe obiecte este
altceva decât modelarea clasică (indiferent de nuanţă). Desluşirea acestui altceva, la nivel de sintaxă,
semantică şi pragmatică (prin raportare la un limbaj de programare) ne va preocupa în continuare. Cei
care se grăbesc să abordeze şi specificul modelării orientate pe obiecte, abstracţie făcând de limbajele de
programare- suport pentru implementare, pot consulta lucrarea Iniţiere în modelarea obiect orientată utilizând
UML7.

6
Manifestarea principiului încapsulării, în acest cadru, însemna ascunderea detaliilor de
implementare faţă de utilizatori, prin publicarea unei interfeţe stabile. O interfaţă este stabilă dacă utilizatorul
ei nu sesizează eventualele modificări aduse implementării interfeţei.
7
D. Bocu, Editura Albastră, Cluj-Napoca
2 Concepte şi principii în programarea orientată pe
obiecte
2.1 Concepte în programarea orientată pe obiecte
Începând cu acest capitol, orientarea pe obiecte va fi privită, nu de la înălţimea preceptelor ingineriei
softului, ci din perspectiva programării Java. Subliniez, încă odată, marea provocare pentru un programator
care încearcă forţa unui limbaj în materie de obiect orientare nu este în sintaxă, semantica asociată
diferitelor tipuri de enunţuri sintactice sau stilul de codificare, ci însuşirea spiritului orientării pe obiecte
aşa cum este el promovat de elementele suport ale limbajului.
De aceea, reamintesc cititorului conştient faptul că va trebui să se întrebuinţeze serios pentru a descifra,
dacă mai este cazul, oferta limbajului C++ în ceea ce priveşte: tipurile fundamentale de date (similarităţi
remarcabile cu Java, dar şi deosebiri, datorate în principal faptului că în C++ pointerii se manifestă cu foarte
multă vigoare în cele mai neaşteptate contexte), reprezentarea structurilor de prelucrare (din nou, similarităţi
remarcabile între C++ şi Java), operaţiile I/O relative la consola sistem, din perspectivă C, precum şi suportul C
pentru lucrul cu fluxuri, dacă se doreşte acest lucru. Nu voi spune decât următoarele: C++ este un superset al
limbajului C; compilatoarele de C++ sunt realizate astfel încât toate enunţurile din C sunt acceptate, dar
ele recunosc şi o varietate mare de enunţuri specifice modului de lucru în orientarea pe obiecte.
După cum rezultă din titlul acestui paragraf, în atenţia mea se vor afla enunţurile tipice programării
orientate pe obiecte în Java.
Înainte de a ajunge la aceste enunţuri, trebuie să facem primii paşi în învăţarea spiritului orientării pe
obiecte. Voi prezenta, în continuare conceptele fundamentale de care “ne lovim” frecvent când programăm
orientat pe obiecte.
Spuneam în Capitolul 1 că, din perspectivă orientată pe obiecte, sistemul pe care îl modelăm va fi
întotdeauna abstractizat de o colecţie de tipuri de obiecte, între care există anumite relaţii. Să ne imaginăm, de
exemplu, că vrem să modelăm lumea poligoanelor astfel încât să putem oferi suport pentru învăţarea
asistată de calculator a proprietăţilor poligoanelor. Există o mare diversitate de poligoane. Chiar şi cele care
sunt de maxim interes din punct de vedere al predării/învăţării în şcoală, sunt suficient de multe pentru a pune
probleme de abordare a prezentării proprietăţilor lor. Şi în acest caz, ca în oricare altul, la început avem în faţa
ochilor realitatea de modelat, care poate fi sau nu structurată după legile ei naturale.
Pentru un individ cu pregătire matematică adecvată este evident că obiectele din Figura 4 sunt
clasificate aprioric. Eventual, putem spune că lipsesc unele tipuri de poligoane, pentru că inventarul făcut de noi
în Figura 4 este
incomplet.

Figura 4. Diferite tipuri de poligoane, aşa cum se pot întâlni, în realitatea modelată, prin
reprezentanţi
Probleme noastră nu este de a stabili nişte frontiere înăuntrul cărora să avem obiecte de acelaşi tip, ci de
a spune care sunt obiectele care nu ne interesează. Nu se întâmplă, întotdeauna, aşa. Există probleme în care
efectiv trebuie să depunem eforturi pentru a clasifica obiectele.

Operaţia de clasificare presupune identificarea unor categorii de obiecte, apelând, simultan la


omiterea unor detalii, socotite nesemnificative, pentru a obţine efectul de similaritate în procesul de
caracterizare a obiectelor.
Dacă în atenţia noastră se află problema clasificării poligoanelor, atunci, dacă în caracterizarea unui
poligon reţinem atribute precum: lista coordonatelor vârfurilor, definiţia(), aria(), perimetrul(), atunci
rezultatul clasificării este o clasă de obiecte pe care o putem numi clasa Poligon. Astfel că putem da definiţia de
mai jos.

Definiţia 1 Se numeşte clasă o colecţie de obiecte care partajează aceeaşi listă de atribute
informaţionale şi comportamentale.

Prin urmare, primul concept important cu care ne întâlnim în programarea orientată pe obiecte este
conceptul de clasă. Rezolvarea orientată pe obiecte a unei probleme se bazează esenţial pe abilitatea
specialistului (în cazul nostru programatorul) de a descoperi care sunt clasele pe baza cărora se poate construi
soluţia. Presupunând că avem şi noi această abilitate şi, în acord cu criteriile proprii de clasificare (reflectate şi în
inventarul din Figura 4), obţinem următoarea colecţie de clase candidate la obţinerea soluţiei problemei noastre.

Clasa patrulaterelor

Clasa triunghiurilor Clasa hexagoanelor

Figura 5. Clasele candidate la obţinerea soluţiei pentru problema modelării poligoanelor


Departe de mine ideea că am dat o soluţie definitivă problemei clasificării poligoanelor. Am prezentat,
însă, o soluţie tributară unei anumite viziuni. În conformitate cu această viziune, singurele poligoane care
prezintă interes pentru noi sunt triunghiurile, patrulaterele şi romburile. Figura 5 ne atrage atenţia, explicit,
asupra diversităţii tipologice a patrulaterelor, fapt care evidenţiază necesitatea recurgerii şi la alt operator decât
clasificarea pentru a gestiona această diversitate. Situaţia este, oarecum asemănătoare şi în cazul triunghiurilor,
dar nu am subliniat explicit acest lucru.
Făcând abstracţie de aceste elemente, deocamdată, să revenim asupra problemei care ne interesează cel
mai mult în acest moment: cum stabilim proprietăţile unei clase?

Regulile de bază în stabilirea proprietăţilor unei clase sunt următoarele:


• Lista atributelor informaţionale ale unei clase este, întotdeauna, rezultatul unui compromis între
necesitatea unui maximum de informaţii despre obiectele clasei respective (informaţii, care, în
fond, caracterizează starea obiectelor din clasa respectivă) şi necesitatea unui minimum de
redundanţe acceptate. Obiceiul unor programatori de a supradimensiona lista atributelor unei
clase pe motiv că “mai bine să fie decât să le ducem lipsa”, nu este un model de urmat, nici atunci
când există memorie “cu carul”.
• Odată specificate, atributele trebuie declarate ca fiind resurse private ale clasei, folosind
sintaxa specifică limbajului pentru ascunderea unei resurse. Dogma orientării pe obiecte în
legătură cu lista atributelor este că acestea sunt accesibile, diferitelor categorii de clienţi, în
setare ca şi în consultare, prin intermediul unor metode speciale de setare(numite şi
modificatori) sau consultare(numite şi selectori), cărora li se mai adaugă metode speciale
implicate în crearea obiectelor unei clase (constructorii), respectiv, eliminarea acestor obiecte
(destructorii). Evident, mai există şi alte tipuri uzuale de metode, precum iteratorii sau indexatorii,
cărora li se acordă o atenţie specială în C#.
• Odată specificată lista atributelor informaţionale se poate trece la specificarea listei operaţiilor
clasei, listă care abstractizează comportamentul clasei. În procesul de specificare a
comportamentului unei clase trebuie avute permanent în vedere cele două dimensiuni ale
comportamentului unei clase: comportamentul reclamat de gestiunea stării obiectelor (crearea
lor, setarea valorilor atributelor, modificarea valorilor atributelor, consultarea valorilor atributelor,
distrugerea obiectelor) precum şi comportamentul reclamat de relaţia clasei în cauză cu alte
clase. Lista operaţiilor unei clase, la nevoie, poate fi organizată, din punct de vedere al modului de
acces la aceste operaţii. Un singur lucru este general valabil în această privinţă: faptul că orice
clasă trebuie să afişeze o listă cu operaţiile publice, care asigură accesul clienţilor la serviciile
oferite de clasă. Lista acestor operaţii se numeşte, în mod normal, interfaţa clasei.
• Atenţie, cititorule! Când specifici resursele unei clase, eşti preocupat să spui ce fac obiectele clasei
respective, omiţând intenţionat cum face clasa ceea ce trebuie să facă. Aşadar, nu strică să facem
o distincţie necesară între definirea unei clase (= specificarea atributelor şi a operaţiilor) şi
implementarea clasei (= scrierea codului asociat operaţiilor clasei). Definirea răspunde unor
comandamente externe (ce ţin de modul de utilizare a obiectelor clasei); implementarea răspunde
unor comandamente care ţin de intimitatea comportamentului obiectelor (mod de reprezentare în
memorie a obiectelor, mod de implementare a operaţiilor în acest context). Să mai adăugăm că o
operaţie implementată se mai numeşte şi metodă.

Folosind notaţia UML pentru reprezentarea vizuală a proprietăţilor unei clase, avem situaţia din Figura
6.

Un concept, inevitabil în programarea orientată pe obiecte este şi conceptul de obiect. L-am folosit,
deja, la modul intuitiv, ca fiind o parte a unei realităţi având o anumită valoare de întrebuinţare în contextul în
care apare. Acum este momentul să dăm următoarea definiţie.

Definiţia 2. Se numeşte obiect o instanţă a unei clase.

De la teoria generală a tipurilor de date, se ştie că instanţa unui tip de dată este o variabilă având o
anumită reprezentare în memorie, deci o identitate, şi o anumită stare din punct de vedere al conţinutului
memoriei asociate. În toate limbajele de programare, care oferă suport orientării pe obiecte, clasa este asimilată
unui tip de dată (este drept un tip special), care devine folositor în momentul în care se manifestă prin
intermediul instanţelor. Instanţele unei clase se vor numi, aşadar, obiecte sau, uneori, variabile obiect.
Dogmatic vorbind, dacă soluţia unei probleme este modelată ca o singură clasă, atunci ne aşteptăm ca
dinamica aplicaţiei corespunzătoare să fie opera comportamentului unei instanţe a clasei. Ce se întâmplă,
însă, dacă soluţia unei probleme este abstractizată de o ierarhie sau de o reţea de clase? În acest caz, dinamica
aplicaţiei este opera colaborării între instaţele unora dintre clasele ierarhiei sau reţelei în cauză. Semantic
vorbind, modul în care colaborează mai multe obiecte pentru a rezolva o anumită problemă, este greu de fixat
într-o formulă definitivă. Din punct de vedere tehnic, însă, rezolvarea este relativ simplă, după cum se poate
deduce şi din Figura 7.

<Nume clasă>

Lista atributelor informaţionale,


specificate prin nume, tip şi eventual
valoare implicită

Lista operaţiilor, specificate prin


signatură

Figura 6 Notaţia UML pentru o clasă


Obiect

(Furnizor)
(Produs)
ListareFurn(CodProd) AdresaListaFurn: XXXXXXXX
CodProd:112233

Clasa definitoare
a obiectului

Produs Furnizor

char codprod[11];
:
ListaFurn *AdresaListaFurn;
:
void afisare();
: ListareFurn(char codp[11])
:

Operaţia afisare() apelează operaţia ListareFurn()

Figura 7.Exemplu de comunicare(colaborare) între două obiecte


Notaţia UML8 pentru o clasă şi înţelesul complet al noţiunii de signatură9 pot fi urmărite, luând-o
înaintea timpului, consultând lucrarea [Iniţiere în modelarea obiect orientată utilizând UML, Dorin Bocu,
Editura Albastră, Cluj-Napoca, 2002]
În Figura 7 se prezintă schema fundamentală pentru colaborarea dintre obiecte. Unul dintre obiecte
(obiectul de tip Produs, în cazul nostru) iniţiază comunicarea cu celălalt obiect (de tip Furnizor, în cazul
nostru). Se mai obişnuieşte să se spună că obiectul de tip Produs i-a trimis un mesaj obiectului de tip Furnizor.
Este de aşteptat ca, într-o formă sau alta, mesajul să fie urmat de un răspuns. Aşadar, dacă OFurnizor este o
variabilă de tip Furnizor şi dacă această variabilă este accesibilă unui obiect de tip Produs, atunci comunicarea
este posibilă, cu condiţia ca obiectil de tip Produs să cunoască interfaţa clasei definitoare a obiectului
OFurnizor şi să aibă acces la această interfaţă. În varianta cea mai simplă, un mesaj are structura:

<Obiect>.<Nume_metodă>([<Lista_de parametri_actuali>]);

ceea ce ne îndreptăţeşte să dăm definiţia de mai jos.

Definiţia 3. Se numeşte mesaj apelul unei metode a unui obiect, apel efectuat de către un client
potenţial al obiectului în cauză.

Cunoaşterea acestui fapt este deosebit de importantă în situaţia în care vrem să explicăm semantica
polimorfismului în programarea orientată pe obiect, ceea ce vom face în partea următoare a acestui capitol.

În sfârşit, să mai menţionez faptul că răspunsul pe care îl dă un obiect când primeşte un mesaj de la alt
obiect depinde de starea în care se află obiectul care primeşte mesajul. Este un aspect asupra căruia
programatorul trebuie să fie atent, deoarece o stare improprie a obiectului destinatar, poate provoca
eşuarea iniţiativei obiectului expeditor de a comunica. Multe excepţii apar, în faza de testare a programelor,
tocmai pentru că nu s-a manifestat suficientă preocupare pentru evitarea utilizării obiectelor atunci când acestea
sunt într-o stare critică (referinţe nerezolvate, resurse necesare insuficiente, etc.).
Fie, în continuare, definiţia conceptului de stare.

Definiţia 4. Se numeşte stare a unui obiect o abstractizare a valorilor atributelor acelui obiect
precum şi a relaţiilor pe care obiectul le are cu alte obiecte.

8
UML-prescurtare de la Unified Modeling Language-Limbaj de modelare unificat, specificat de Rational Software Corporation şi omologat
ca standard de facto de către grupul OMG
9
Signatura cuprinde, în genere: numele opreraţiei, lista de parametri şi, opţional, tipul returnat
Aşadar, recapitulând, conceptele esenţiale cu care operăm în lumea orientării pe obiecte sunt: clasă,
obiect, stare obiect, mesaj.

2.2 Principii în programarea orientată pe obiecte


Am văzut în paragraful 2.1 care sunt conceptele cele mai importante cu care ne întâlnim în programarea
orientată pe obiecte. Se ştie de la alte discipline exacte că, fără principii de utilizare a lor, conceptele sunt bune
doar de pus în raft, ca nişte bibelouri cu care putem aminti posterităţii de o lume dispărută. Conceptele prind
viaţă, cu adevărat, doar în momentul în care sunt acompaniate de un set de principii care fixează regulile
esenţiale de utilizare a conceptelor. Evident, vom vedea care sunt aceste principii. Ce ne mai aşteaptă dincolo de
ele? Ne aşteaptă invitaţia de a proba singuri valabilitatea acestor principii, la început, improvizând cu
inerentă stângăcie, mai apoi descoperind adevărate şabloane de rezolvare a unor probleme tip. Ne stă la
dispoziţie, în cantităţi industriale, în internet, experienţa tuturor celor care şi-au făcut o religie din orientarea pe
obiecte şi au realizat aplicaţii de referinţă în acest spirit. Voi prezenta, în continuare, principiile pe care le
consider absolut necesare pentru a programa în spiritul orientării pe obiecte.

Abstractizarea
Obişnuiesc să insist pe importanţa acestui principiu deoarece mânuirea lui fără inspiraţia necesară (iar
inspiraţia, într-un domeniu, are cunoaşterea acelui domeniu ca înaintemergător) poate pune lesne sub semnul
întrebării calitatea unei soluţii.

Abstractizarea este procesul de ignorare intenţionată a detaliilor nesemnificative şi de reţinere a


proprietăţilor definitorii ale unei entităţi.

Prin urmare, din perspectivă procesuală, abstractizarea este o modalitate de a reflecta asupra
proprietăţilor unei entităţi, cu scopul de a obţine reprezentări care descriu comportamentul entităţii, reprezentări
care pot îndeplini simultan funcţii explicative, funcţii modelatoare şi, de ce nu, funţii demiurgice, la diferite
paliere de profunzime. Utilitatea unei abstracţii se manifestă atunci când apar beneficiari în sfera ei de
manifestare. Ca un exemplu, referindu-ne la limbajele de programare, putem observa că, toate limbajele oferă
suport specific pentru abstractizare. Cu cât suportul pentru abstractizare este mai consistent, cu atât putem spune
că avem de-a face cu un limbaj de programare de nivel mai înalt. In cazul limbajelor de programare,
abstractizarea înseamnă un anumit gen de apropiere de limbajul uman şi, prin aceasta, de gândirea umană.
De asemenea, la nivelul limbajelor de programare vorbim despre trei tipuri fundamentale de abstracţii,
ca rezultate ale procesului de abstractizare: abstracţiile procedurale, abstracţiile la nivelul datelor şi clasele.
Abstracţiile procedurale sunt cel mai mult folosite în programare. Utilizarea lor metodică permite
ignorarea detaliilor legate de desfăşurarea proceselor. Toate funcţiile puse la dispoziţia programatorilor în C prin
intermediului sistemului de fişiere antet sunt exemple de abstracţii procedurale, a căror utilizare este uşor de
învăţat dacă le cunoaştem: numele, eventual lista de parametri şi/sau tipul returnat, plus semantica operaţiilor
realizate de respectivele abstracţii. Nu trebuie să avem, neapărat, informaţii despre implementarea acestor
funcţii. Care este câştigul? Se crează, la un anumit nivel de abstractizare a unui program, posibilitatea ca acesta
să fie exprimat ca o succesiune de operaţii logice şi nu în termeni de instrucţiuni primare ale limbajului. Pentru
lizibilitatea programului şi, în consecinţă, pentru depanare, aşa ceva este de maxim interes.

Abstracţiile la nivelul datelor permit, de asemenea, ignorarea detaliilor legate de reprezentarea unui
tip de date, în beneficiul utilizatorilor tipului de date. Un exemplu remarcabil de astfel de abstracţie la nivelul
datelor este tipul variant, specificat de cei de la firma Borland în cadrul limbajului Object Pascal. Cei care au
realizat aplicaţii Delphi şi “au dat cu nasul” peste tipul variant au probabil amintiri plăcute despre versatilitatea
şi uşurinţa în utilizare a acestui tip de dată.

Clasele ca abstracţii combină într-o nouă abstracţie, extrem de puternică şi polivalentă semantic, cele
două abstracţii mai sus pomenite, care ţin de acea epocă din istoria programării în care deviza era:
“Algoritmi+structuri de date=programe”. Arsenalul pus la dispoziţia programatorului de clase, în calitate de
instrumente de abstractizare va fi în atenţia acestui curs, în continuare.

Încapsularea
Principiul încapsulării insistă pe separarea informaţiilor de manipulare a unei entităţi de aspectele
implementaţionale. Se deduce, cu uşurinţă, faptul că încapsularea este o varietate de abstractizare. Practicată
metodic şi cu suport sintactic adecvat, în programarea orientată pe obiecte, încapsularea este susţinută şi la alte
nivele, de către limbaje de programare diferite. De exemplu, conceptul de unit din Object Pascal, permite
încapsularea resurselor unei aplicaţii Delphi, ceea ce, în fond, înseamnă modularizare, la un nivel de
abstractizare mai înalt decât cel specific încapsulării la nivel de clase. Cuvintele cheie pe care se sprijină
operaţionalizarea principiului încapsulării sunt interfaţa şi implementarea. Motivul pentru care se insistă atât
pe acest principiu este simplu: separând interfaţa de implementare şi admiţând că interfaţa este foarte bine
structurată (rezultă că este stabilă în timp şi acceptată de utilizatori din punct de vedere al utilităţii şi
comodităţii în utilizare) înseamnă că eventuale modificări ale implementării (absolut fireşti în condiţiile
îmbunătăţirii permanente a mediilor de execuţie şi programare) nu vor putea afecta utilizatorii.
Este bine ca cititorul să facă distincţie între încapsulare şi localizarea datelor şi a funcţiilor, în
cadrul aceleeaşi entităţi. Încapsularea are nevoie de localizare pentru a sublinia caracterul de black-box al
entităţilor, dar ea înseamnă mult mai mult decât atât. De asemenea, nu trebuie să fetişizăm încapsularea,
aşteptând de la ea să garanteze siguranţa în procesul de manipulare a obiectelor. Cât de sigur în utilizare este un
sistem soft, hotărăşte programatorul, care combină eficient forţa principiului încapsulării cu procedeele
tehnice de asigurare a protecţiei faţă de ingerinţele cu efecte negative.
Aşadar, din perspectiva încapsulării, o clasă, indiferent de limbajul în care va fi implementată, trebuie să
aibă, obligatoriu, două compartimente, ca în Figura 8.

Student

Date şi operaţii private


(Implementarea)

Operaţii publice

(Interfaţa)

Figura 8Componentele obligatorii ale unei clase, din perspectiva încapsulării


Moştenirea
Multă vreme, moştenirea a fost un vis pentru a cărui îndeplinire, într-o formă destul de primitivă,
programatorii trebuiau să depună eforturi intense. Apariţia limbajelor care oferă suport sintactic pentru
operaţionalizarea principiilor orientării pe obiecte (OO), a confirmat, printre altele şi marea importanţă a
principiului moştenirii pentru specificul unei soluţii OO. Din perspectivă mecanică privind lucrurile, acest
principiu afirmă posibilitatea ca o clasă B să moştenească o parte dintre proprietăţile unei clase A. În acest
fel avem la dispoziţie un mecanism practic pentru gestiunea similarităţilor, naturale, de altfel într-o societate de
obiecte foarte diversificată. În paragraful 2.1 (Figura 4) am prezentat, deja, un exemplu de societate posibilă de
obiecte, în care diversitatea punea probleme de clasificare, lăsând deschisă problema gestiunii similarităţilor, în
cazul mulţimii patrulaterelor. Mergând pe linia utilizării notaţiei UML pentru reprezentarea unei soluţii OO,
atunci trebuie să spunem că dacă avem o clasă B care moşteneşte o parte dintre proprietăţile clasei A, acest lucru
va fi redat grafic în felul în care este arătat în Figura 9.
A Clasa părinte, superclasa,
clasa de bază

SPECIALIZARE
Simbolul care indică

GENERALIZARE
relaţia de generalizare
dintre clasele A şi B

B
Clasa copil, subclasa,
clasa derivată

Figura 9. O parte din semantica relaţiei de moştenire care operează între clase
După cum se vede, relaţia de moştenire nu este doar o relaţie al cărei înţeles se reduce la posibilitatea ca
B să moştenească o parte din proprietăţile lui A; semantica relaţiei de moştenire este mult mai subtilă.
Mai întâi, este vorba despre necesitatea de a reflecta cu îndemânarea necesară la cele două posibilităţi
de a identifica o astfel de relaţie: procedând top-down sau bottom-up, deci specializând, după ce am identificat
o clasă rădăcină, sau generalizând, după ce am terminat operaţia de clasificare a claselor şi am început să
organizăm clasele în familii, după similarităţile care le leagă. Indiferent de abordare, rezultatul trebuie să fie
acelaşi: o ierarhie de clase, în care similarităţile sunt distribuite pe nivele de abstractizare, reducând astfel la
minimum redundanţele şi pregătind terenul pentru o reutilizare elegantă a codului şi pentru o serie de alte
avantaje care însoţesc un lanţ de derivare. Exemplificăm cu ierarhia din Figura 10, a cărei semantică ne este deja
cunoscută.
În Figura 10 regăsim aproape toate elementele specifice unei soluţii obiect orientate, care foloseşte
judicios principiul moştenirii. Astfel, aproape toate soluţiile orientate pe obiect au o clasă rădăcină (care, în
anumite situaţii poate fi o clasă abstractă, adică o clasă care nu poate avea instanţe directe, dar referinţe având
tipul ei pot fi asociate cu instanţe ale descendenţilor), dacă soluţia se reduce la o singiră ierarhie de clase. În
ierarhie putem întâlni şi clase intermediare, precum şi clase terminale sau frunză.

Poligon
Clasă rădăcină,
poate fi şi
abstractă

Clasă
Triunghi Patrulater intermediară

Paralelogram Trapez Patrulater


inscriptibil

Clase frunză
Romb Dreptunghi

Figura 10.Principiul moştenirii în acţiune


Din punctul de vedere al programatorului, pe lângă utilitatea moştenirii în procesul de gestiune a
similarităţilor, mai există un avantaj care poate fi exploatat în faza de implementare a soluţiei, avantaj derivat din
principiul moştenirii sub forma unui alt principiu care afirmă că:

Orice părinte poate fi substituit de oricare dintre descendenţi

Acest principiu este de mare utilitate pentru programatori, el fiind operaţionalizat prin intermediul
operaţie de casting, aplicată obiectelor ale căror clase definitoare sunt în relaţie de moştenire. Evident, este
vorba de un casting implicit în lumea obiectelor (descendentul moştenind tot ceea ce se poate de la strămoşi, va
putea opera în locul strămoşului). Castingul de acest tip se numeşte up-casting. Se practică, explicit şi down-
castingul, dar programatorul trebuie să fie pregătit să răspundă de consecinţe. Într-un down-casting, este posibil
ca obiectul de tip strămoş, convertit la un obiect de tip descendent, să nu asigure coerenţa informaţională de care
are nevoie descendentul, deci pot apare, principial, excepţii. În Java se întâlnesc amândouă tipurile de casting.
În sfârşit, în Figura 10 avem un exemplu de utilizare a moştenirii simple, potrivit căreia un descendent
poate avea un singur strămoş. Unele limbaje de programare(C++, de exemplu) acceptă şi moştenirea multiplă,
ceea ce înseamnă că o clasă poate avea doi sau mai mulţi strămoşi. O astfel de soluţie pentru principiul
moştenirii este plină de riscuri, cu toate avantajele pe care le presupune. Moştenirea multiplă poate genera
ambiguităţi care pot pune la încercare răbdarea programatorului.

B C

Figura 11. Exemplu de moştenire multiplă


În exemplul din Figura 11, clasa D are ca strămoşi clasele B şi C. Obiectele clasei D vor avea moştenite
resursele lui A, atât pe traseul lui B cât şi pe traseul lui C. Ambiguitatea referirii la o resursă a lui A în cadrul
unei instanţe a lui D este evidentă. Când este considerată strict necesară, moştenirea multiplă se utilizează cu
discernământ. Java şi C# nu mai promovează moştenirea multiplă, propunând alte soluţii la această problemă.
Moştenirea nu este doar o problemă de sintaxă, ci este esenţa însăşi a programării orientate pe obiecte.
Pe temeiul moştenirii are rost să vorbim în continuare de principiul polimorfismului, aplicat la lumea obiectelor.

Polimorfismul
După cum se vede şi în exemplul prezentat în Figura 10, ierarhia de clase care modelează, la urma
urmei, comportamentul unei aplicaţii poate avea mai multe clase frunză, deci clase care pote genera instanţe.
Crearea unei instanţe este, indicutabil o problemă în care programatorul trebuie să se implice, el trebuind să
stabilească, în funcţie de dinamica aplicaţiei, ce constructor va coopera la crearea unei instanţe. În ideea că avem
o aplicaţie care se ocupă de simularea învăţării poligoanelor, vi se pare interesant să declarăm câte o variabilă
pentru fiecare tip de poligon din ierarhia prezentată în Figura 10 (mai puţin clasa rădăcină)? Nu este interesant
din două motive:

1. Explozia de variabile obiect nu este un indiciu de performanţă în programare.


2. Odată create obiectele, programatorul trebuie să ştie în orice moment ce fel de obiect lucrează
la un moment dat. Chiar că aşă ceva poate deveni supărător într-o aplicaţie de mare
complexitate.

Nu ar fi mai civilizat să declarăm o variabilă de tip Poligon în care să păstrăm instanţe create cu
ajutorul constructorilor oricăror descendenţi care pot avea urmaşi direcţi? Admiţând că, în clasa Poligon, am
declarat o operaţie calculArie() care este suprascrisă în fiecare descendent, atunci situaţia creată este următoarea:
obiectul creat cu ajutorul constructorului unui descendent al clasei Poligon şi păstrat într-o variabilă de tip
Poligon (principiul substituţiei îngăduie acest lucru), să-i spunem p, va putea să apeleze diferite implementări ale
operaţiei calculArie(), în funcţie de contextul în care se crează p. Aşadar, un mesaj de tipul p.calculArie() ce
răspuns va genera? Răspunsul va fi dependent de context, controlul contextului (adică alegerea metodei specifice
clasei definitoare a obiectului păstrat în p) realizându-se de către sistem prin mecanisme specifice, în timpul
execuţiei programului. Acest mod de legare a unui nume de operaţie de codul metodei specifice tipului definitor
al variabile obiect gazdă se numeşte late binding. Este un mod de legare mai puţin performant decât legarea
statică (la compilare), dar avantajele scuză dezavantajele insignifiante, în condiţiile în care viteza procesoarelor
şi disponibilul de RAM sunt în expansiune permanentă.
S-a înţeles, probabil, faptul că polimorfismul este de neconceput fără moştenire în care specializarea
claselor să se bazeze şi pe suprascrierea unor metode în clase aflate în relaţie de moştenire. Tot ceea ce trebuie să
ştie programatorul de aici încolo este avertismentul că:

Moştenirea şi polimorfismul sunt ideale ca uz şi contraindicate ca abuz.

Terminând excursia în lumea conceptelor şi a principiilor programării orientate pe obiecte, nu ne


rămâne decât să anunţăm că în capitolele următoare vom încerca să vedem aceste abstracţii la lucru în context
Java.
3 Specificarea şi implementarea unei clase din
perspectivă Java
3.1 În loc de introducere
Fără să am pretenţia că am epuizat semantica modelării orientate pe obiect, în capitolele precedente, în
acest capitol voi începe incursiunea în lumea elementelor suport oferite de Java pentru implementarea orientată
pe obiect a modelelor obiect orientate.
Laboratoarele de cercetare şi lumea practicienilor văd, încă, în acest limbaj un moment de referinţă în
zbuciumata evoluţie a limbajelor de programare. Apariţia limbajului C# la orizont, „prin bunăvoinţa” celor de la
Microsoft, se anunţă un concurent redutabil, care, îşi propune să câştige, deopotrivă, atenţia fanilor Java şi C++.
Până ce apele se vor limpezi, mă voi ocupa de ceea ce, tocmai, am anunţat în lista subiectelor care fac obiectul
acestui capitol.

3.2 Atenţie la importanţa efortului de abstractizare!


Voi încerca să arăt cum se specifică şi implementează clasele în Java. În tot acest timp, încercaţi,
împreună cu mine, să nu minimalizaţi nici o clipă importanţa hotărâtoare a abstractizării în elaborarea unor
soluţii stabile şi flexibile10.
În acest scop voi considera un exemplu pretext de problemă prin intermediul căreia voi încerca să pun
în valoare puterea de reprezentare a limbajului Java.

Să se scrie codul Java care pune la dispoziţia unor utilizatori potenţiali capabilităţi de lucru cu
liste simplu înlănţuite de numere întregi, pentru care resursele necesare reprezentării sunt alocate
dinamic. În continuare mă voi referi la această problemă cu acronimul LSI.

De ce numai acest tip de listă? De ce doar numere întregi? Pentru că aşa cere utilizatorul în acest
moment. Nici mai mult, nici mai puţin. Aşa se întâmplă şi în viaţa de toate zilele. Specialiştii în IS trebuie să
livreze beneficiarilor ceea ce aceştia se aşteaptă să primească. Amorul artei sau dispreţul faţă de beneficiar,
sunt taxate necruţător de către ansamblul regulilor jocului din industria de soft.
Nu voi exagera cu descrierea travaliului conceptual în urma căruia am ajuns la nişte concluzii în
legătură cu rezolvarea problemei LSI. Dar, câteva elemente de descriere a atmosferei trebuie, totuşi precizate.
Se ştie că structurile dinamice de date sunt preferate structurilor statice de date, atunci când
utilizarea chibzuită a memoriei şi flexibilitatea relaţiilor dintre obiectele care populează structura sunt
critice pentru calitatea unui sistem soft.
Se mai ştie, totodată, că o structură dinamică de date este complet specificată dacă am clarificat:

• Tipul datelor care populează structura.


• Mecanismul de înlănţuire a datelor din structură.
• Punctele de intrare în structură.
• Operaţiile cu ajutorul cărora întreţinem şi consultăm structura.

Fiecare din cerintele de mai sus, poate face obiectul unor consideraţii cu implicaţii interesante asupra
soluţiei. Mă limitez doar la a observa faptul că o structură de date dinamică este un exemplu reprezentativ
de tip de dată care poate fi modelat orientat pe obiecte, în aşă fel încât serviciile oferite să fie complete şi
uşor de apelat.
Intrând puţin în domeniul problemei, dacă ne gândim la o listă simplu înlănţuită, semantica ei poate fi
vizualizată, parţial, ca în Figura 12.

10
Nu este o contradicţie între termeni. Cele mai bune modele, într-o lume care nu stă pe loc, sunt modelele care pot fi socotite, în acelaşi
timp, închise şi deschise, deci stabile şi flexibile.
Adresa de start a primului nod

Data_ 1 Data_2 Data_n

Informaţe
nod
Legătura spre următorul nod Ultimul nod nu are un succesor

Figura 12. Incercare de vizualizare a unei liste simplu înlănţuite


Prin urmare, obiectele care fac parte din inventarul problemei sunt: Nod (având ca proprietăţi
informaţionale Data şi Adresa de legătură cu următorul element iar ca proprietăţi comportamentale, cel puţin
operaţii legate de crearea unui nod şi consultarea proprietăţilor lui informaţionale) şi Lista (un obiect care, în
principiu este o agregare11 de obiecte de tip Nod).
Un obiect care poartă marca tipului Lista va avea, aşadar, proprietăţi informaţionale şi comportament
specific. Abstractizând cu înverşunare, lista proprietăţilor informaţionale ale unui obiect de tip Lista ar putea să
conţină doar adresa primului nod din listă. Însă, dacă trecem în revistă exigenţele clienţilor faţă de un obiect de
tip Lista, vom descoperi că este indicat să avem un atribut pentru a indica dacă lista conţine sau nu elemente (un
exemplu de redundanţă în procesul de utilizare a memoriei interne care asigură o viteză sporită în procesul de
utilizare a unei liste). Dacă acest atribut este un întreg care indică chiar numărul elementelor din listă, cu atât mai
bine. Aşadar, în notaţie UML, în acest moment am putea avea situaţia din Figura 13.

Lista

-Nod Astart;
Nod
-Nod AUltim;
-Int NrElem;
-Int Data;
-Nod Urmatorul;
+void setareAStart(Nod Element);
+Nod consultaAStart();
+void setareNrElem(int ne);
+Nod(int Numar);
+int consultaNrElem();
+void setareData(int Numar);
+setareAUltim(Nod Element)
+int consultareData();
+Nod consultaAUltim();
+void setareUrmator(Nod AUrm)
+void insereazaDNod(Nod Element);
+Nod consultUrmator()
+void insereazaINod(Nod Element);
+void stergeNodAdr(Nod Element);
+void stergeNodNum(int Nr);
+salveazaInFisier(char numef[]);
+incarcaDinFisier(char numef[]);)

Figura 13. Clasele candidate la rezolvarea problemei LSDI


Ce observaţii putem face? Clasa Nod este o clasă concretă, adică, având constructor, poate avea
instanţe. Să mai observăm faptul că atributele clasei Nod sunt prefixate cu câte un semn -, ceea ce, în UML,
înseamnă că sunt private. Conform dogmei OO, absolut normal. De asemenea, să mai observăm că operaţiile
clasei Nod sunt prefixate cu semnul + şi sunt scrise cu font drept, ceea ce, în UML, înseamnă că sunt publice şi
au deja implementare valabilă. Semantica acestei clase şi a contextului în care operează s-ar părea că nu ne
pretinde specificarea unor operaţii private sau protejate. Oare? Este adevărat că în Java, de exemplu, crearea unui
obiect de tip Nod se va face rezonabil, chiar şi dacă nu am prevedea un constructor, dat fiind faptul că obiectele
se crează şi în acest caz, atributele fiind iniţializate cu valori predefinite (0 pentru numere, null pentru obiecte,
false pentru valori booleene, ’\u0000’ pentru caractere). Dacă un astfel de comportament implicit nu este de
acceptat, atunci este loc pentru a specifica operaţii care fac validarea stării obiectelor. Aceste operaţii ar putea fi
folosite, ca uz intern, de orice altă operaţie care este sensibilă la starea obiectului care o foloseşte la un moment
dat.
Pe de altă parte, observăm faptul că unele dintre operaţiile clasei Lista sunt scrise cu caractere italice. În
UML aceasta înseamnă că aceste operaţii sunt abstracte. Prin urmare, clasa Lista este o clasă abstractă, deci
nu are constructor şi tipul introdus de această clasă nu va putea avea instanţe directe. Pentru a fi utilă, clasa Lista

11
In modelarea obiect orientată se foloseşte relaţia de agregare pentru a indica o asociere între obiecte care, din punct de vedere semantic,
sunt într-o relaţie parte-întreg. Reprezentarea unei relaţii de agregare este la latitudinea programatorului.
trebuie să aibă descendenţi. Care vor fi aceşti descendenţi? Vom considera că aceşti descendenţi sunt doi: o clasă
care modelează o listă simplu înlănţuită de întregi, în care cuvântul de ordine la creare este ordinea fizică
de introducere şi o clasă care modelează o listă simplu înlănţuită de întregi, în care elementele listei sunt
introduse astfel încât, după fiecare introducere acestea să fie în ordine crescătoare. Pe scurt: liste
indiferente la ordinea numerelor întregi şi liste sensibile la ordinea numerelor întregi. Vom obţine ierarhia din
Figura 14.

Lista

ListaOarecare ListaSortata

Figura 14. Ierarhia claselor care modelează două varietăţi de listă simplu înlănţuită.
Se poate observa că cele două varietăţi posedă operaţiile necesare pentru a simula, în caz de nevoie şi
comportamentul unei stive (AStart este Top-ul iar inserareINod() şi stergeNodAdr(AStart) sunt operaţiile
specifice unei stive, adică Push() şi Pop()). Cititorul bănuieşte că inserareINod() este abreviere de la „inserare
înainte de nod” iar inserareDNod() este abtreviere de la „inserare după nod”.
Consideraţii de acest gen şi chiar mai profunde, trebuie să prilejuiască orice încercare de rezolvare
orientată pe obiect a unei probleme.

3.3 Specificarea şi implementarea unei clase în Java


Este cunoscut faptul că, în Java, orice aplicaţie este puternic obiect orientată, cel puţin datorită cadrului
sintactic obligatoriu pentru realizarea unui applet sau a unei aplicaţii Java obişnuite. Dacă spiritul orientării pe
obiecte este bine înţeles, atunci Java este o soluţie interesantă pentru multe probleme a căror rezolvare presupune
realizarea unor aplicaţii pentru care lumea Internet-ului este puternic deschisă. Indiferent de tipul aplicaţiei,
piesele de bază în realizarea acesteia sunt clasele. Vom avea, în cazul unei aplicaţii Java obişnuite, o singură
clasă publică, care conţine funcţia main() şi una sau mai multe clase care modelează, la diferite nivele de
rafinare, comportamentul aplicaţiei. De asemenea, în cazul unui applet Java, vom avea o singură clasă publică
care extinde clasa Applet şi una sau mai multe clase care modelează, la diferite nivele de rafinare,
comportamentul applet-ului.
Privită de la cel mai înalt nivel de abstractizare, definiţia unei clase în Java este:

[<Listă modificatori>] class <Nume clasă> [extends <Clasa de bază>] [implements <Lista interfeţe>]
{
//Listă de resurse sau corp clasă
}

Resursele unei clase sunt de două tipuri: atribute şi/sau operaţii. Problema noastră, după cum am văzut,
este de a organiza aceste resurse, astfel încât să putem beneficia de o serie de avantaje din punct de vedere
al efortului de dezvoltare cât şi din punct de vedere al calităţii softului12.
În paragraful 2.2 am văzut că o încapsulare corectă (în acord şi cu dogma OO) înseamnă să declarăm ca
private atributele şi să definim o interfaţă corespunzătoare clasei. Vom vedea, mai jos, ce înseamnă acest lucru
în cazul problemei noastre.
Atributele unei clase se specifică sintactic astfel:

[<Listă modificatori atribut>] Tip <Lista identificatori variabile>;

Operaţiile unei clase se specifică prin signatură şi implementare ca mai jos:

[<Listă_modificatori_operaţie>]Tip_returnat Identificator_metodă>

12
Despre calitatea softului cititorul poate găsi elementele esenţiale în D. Bocu, Iniţiere în ingineria sistemelor soft, Editura Albastră, 2002
([<Listă parametri>) [throws <Lista excepţii>]
{
<Corp operaţie>
}
Formalizmul folosit pentru prezentarea sintaxei de definire a unei clase se bazează, după cum se poate
deduce, pe următoarele convenţii:

Orice construcţie a utilizatorului este prezentată între simbolurile < ...>.


Orice construcţie opţională este încadrată de simbolurile [...].
Cuvintele rezervate sunt evidenţiate prin îngroşare.

Întrebarea firească care se pune este următoarea: la ce ne folosesc aceste elemente de variaţie în
procesul de definire a unei clase.
Voi încerca să răspund pe rând la toate ramurile acestei intrebări.

Modificatorii aplicabili claselor


După cum se poate observa, cuvântul cheie class poate fi prefixat, opţional, de un modificator. Lista
completă a acestor modificatori este: abstract, final, public.

Modificatorul de clasă „abstract”


Java permite extinderea unei clase existente cu o subclasă. Cu timpul, este posibil să vă constituiţi
propriile biblioteci de clase care consideraţi că vor fi extinse de alţi programatori. Pentru unele clase, poate să fie
inutil să implementaţi o operaţie cât timp nu se cunoaşte cum va fi extinsă clasa. În astfel de cazuri, puteţi utiliza
cuvântul cheie abstract pentru a indica faptul că în descendenţi toate operaţiile clase trebuie să fie supradefinite
obligatoriu.
Clasa Lista din Figura 14 ar putea fi definită, prin urmare astfel, în Java:

abstract class Lista


{
private Nod AStart;
private Nod AUltim;
private int NrElem;
public void setareAStart(Nod Element);
public Nod consultaAStart();
public void setaretNrElem();
public int consultaNrElem();
public Nod consultaAUltim();
public abstract void insereazaDNod(Nod Element);
public abstract void insereazaINod(Nod Element);
public abstract void stergeNodAdr(Nod Element);
public abstract void stergeNodNum(int Nr);
public final void salveazaInFisier(char numef[]);
public final Nod incarcaDinFisier(char numef[]);)
}
Este evident faptul că descendenţii ListaOarecare şi ListaSortata sunt obligaţi să implementeze toate
operaţiile clasei Lista, care sunt abstracte. De asemenea, să observăm că implementarea definitivă a operaţiilor
de salvare/restaurare a listei este realizată în clasa Lista şi va fi folosită fără modificări în descendenţi.

Modificatorul de clasă „final” (tocmai l-am utilizat mai sus)


În general vorbind, Java permite unei clase să extindă o altă clasă. Atunci când definim o clasă, în contextul
în care anticipăm utilizarea ei, s-ar putea să nu dorim extinderea ei de către alte clase. În acest caz, prin
includerea cuvântului cheie final în cadrul definiţiei clasei vom împiedica crearea de subclase ale clasei în cauză.
Aşadar, clasa:

public final class NumeClasa {...}

nu va putea să aibă urmaşi.


Modificatorul de clasă „public”
Atunci când utilizăm cuvântul cheie public în cadrul declaraţiei clasei, ne asigurăm că acea clasă este
vizibilă / accesibilă de oriunde. Dacă dorim să controlăm accesul la o clasă, cuvântul cheie public nu are ce
căuta în declaraţia clasei. Să reamintim, totodată, faptul că Java permite o singură clasă publică într-un fişier cu
cod sursă. Evident, caracterul public al unei clase poate avea alte conotaţii în contextul organizării codului sursă
al unei aplicaţii cu ajutorul pachetelor. Mai precis spus, o clasă publică poate fi utilizată din exteriorul pachetului
în care a fost declarată, pentru a crea instanţe sau pentru a o extinde. În schimb, o clasă care nu a fost declarată
publică este considerată o clasa friend , putând fi accesată doar din interiorul pachetului în care este rezidentă.

Modificatorii aplicabili atributelor


Domeniul de valabilitate al unui atribut defineşte locaţiile din program în care atributul este cunoscut.
În procesul de definire a unei clase putem controla domeniul unui atribut al clasei precedând declaraţia lui cu
unul din cuvintele cheie: public, private, protected, static, final, tranzient, volatile. Să menţionăm faptul că
un atribut care nu este însoţit de nici un modificator este vizibil friendly, adică doar din interiorul clasei şi din
clasele din acelaşi pachet.

Modificatorul de atribut „public”


Un atribut public este vizibil / accesibil oriunde este vizibilă / accesibilă clasa care îl conţine. Aşadar,
pentru a declara ca public un atribut vom proceda ca mai jos:
:
public int vizibilOriundeEsteVizibilaClasa;
:
Dogma spune că o clasă care ajunge la client trebuie să-şi ascundă atributele faţă de acesta, accesul la
ele fiind mijlocit de interfaţă, dacă este cazul. Nu este, însă, exclus ca o serie de clase neterminale (care nu sunt,
deci clase frunză) să declare ca publice o parte a atributelor, protecţia lor fiind controlată, la nivelul
descendenţilor prin intermediul interfeţelor sau al organizării în pachete.

Modificatorul de atribut „private”


Un atribut privat este vizibil numai în interiorul clasei sale. Subclasele şi clienţii externi nu pot accesa
aceste atribute.

Modificatorul de atribut „protected”


Un atribut al clasei, declarat ca protejat, este accesibil în descendenţii clasei sau în cadrul pachetului din
care face parte clasa deţinătoare. Atenţie, un atribut declarat ca protected într-o clasă va putea fi accesat în
scriere şi citire în toţi descendenţii clasei în cauză, rezidenţi chiar şi în afara pachetului gazdă al clase care deţine
atributul protejat. Nu va fi permis accesul direct la atributul protejat pentru clase care nu sunt descendeţi ai clasei
care declară atributul protejat.

Modificatorul de atribut „static”


Orice atribut care nu este declarat ca static este numit atribut de instanţă, ceea ce înseamnă că fiecare
instanţă are propria copie a atributului. Atunci când este în interesul comportamentului clasei ca un atribut să fie
partajat de toate obiectele clasei în cauză, acel atribut va fi declarat ca static.

Modificatorul de atribut „final”


Atunci când în definiţia unei clase menţionăm un atribut final, indicăm compilatorului faptul că acel
atribut are valoare constantă, care nu poate fi modificată de program. Iniţializarea atributului cu o valoare se
poate face la crearea obiectlui gazdă, prin contribuţia constructorului sau în cadrul unei declaraţii de tipul:

:
protected static final int nr=10;
:
Atenţie! Cele două metode de iniţializare sunt mutual exclusive.

Modificatorul de atribut „transient”


Atunci când declarăm un atribut ca fiind de tip transient, indicăm compilatorului Java faptul că atributul
nu este o parte permanentă a obiectului, deci de uz intern şi în cazul serializării, de exemplu, nu va fi salvat pe
memoria externă. Un atribut tranzient se declară astfel:

:
private transient String password;
:

Modificatorul de atribut „volatile”


Atunci când se compilează programul, compilatorul analizează codul şi, adeseori va efectua anumite
manevre cu scopul de a optimiza performanţele codului. Atunci când dorim să scoatem un atribut de sub
incidenţa unei astfel de eventualităţi, o declarăm ca volatilă. Practic, aceasta înseamnă că există situaţii
particulare în care comunicaţia cu alte programe sau rutine necesită neintervenţia compilatorului asupra unui
atribut, esenţial pentru buna desfăşurare a comunicaţiei în cauză.

Modificatorii aplicabili operaţiilor


În această secţiune vom prezenat o serie de modificatori care, de cele mai multe ori, sunt aplicabili
metodelor care implementează operaţiile claselor. Aceşti modificatori sunt: public, private, protected, static,
final, abstract, native, synchronized.

Modificatorul de metodă „public”


Semnificaţia acestui modificator, în cazul în care se aplică unei metode, este asemănătoare cu cea pe care o
are când se aplică unui atribut.

Modificatorul de metodă „private”


Semnificaţia acestui modificator, în cazul în care se aplică unei metode, este asemănătoare cu cea pe care o
are când se aplică unui atribut.

Modificatorul de metodă „protected”


Semnificaţia acestui modificator, în cazul în care se aplică unei metode, este asemănătoare cu cea pe care o
are când se aplică unui atribut.

Modificatorul de metodă „static”


Semnificaţia acestui modificator, în cazul în care se aplică unei metode, este, într-o oarecare măsură,
asemănătoare cu cea pe care o are când se aplică unui atribut. Mai precis, trebuie să spunem că o metodă statică,
pentru a fi utilizată nu reclamă neapărat o instanţă, putând fi utilizată şi printr-un apel de tipul:

NumeleClasei. NumeleMetodeiStatice(parametri);

O metodă statică poate fi utilizată pentru a accesa alţi membri statici ai clasei, dar, în nici un caz, pentru a
accesa variabile nestatice.

Modificatorul de metodă „final”


Am văzut, deja, în cursul 2, că anumite metode ale claselor pot fi supradefinite în clasele descendente.
Dacă, din variate motive, dorim să blocăm posibilitatea supradefinirii unei metode, atunci vom informa
compilatorul de această intenţie declarând metoda ca finală astfel:

public final void MetodaNuPoateFiSupradefinita();

Modificatorul de metodă „abstract”


Dacă o metodă a unei clase este precedată de cuvântul cheie abstract, atunci compilatorul nu va autoriza
crearea de instanţe ale clasei în cauză. Totodată, o clasă care extinde clasa în cauză va trebui să implementeze
metoda abstractă obligatoriu. Declararea se face astfe:

public abstract void implementareUlterioară();

Atenţie! O metodă abstractă nu poate fi privată sau finală.


Semantica cuvântului cheie abstract (= metoda va fi implementată în descendenţi) vine în contradicţie cu
semantica cuvintelor cheie private şi final care spun, în moduri diferite, că metoda nu poate fi modificată.

Modificatorul de metodă „native”


Acest modificator se utilizează pentru a spune compilatorului că o anumită metodă utilizează cod scris într-
un alt limbaj de programare, cum ar fi C/C++, de exemplu. Această posibilitate trebuie folosită cu discernământ
deoarece loveşte puternic în portabilitatea aplicaţiei, aşa cum este ea înţeleasă în Java.

Modificatorul de metodă „synchronized”


Se ştie că Java oferă suport pentru multitasking sub forma „un program mai multe fire de execuţie”. În
funcţie de activitatea programului respectiv, uneori este necesar să garantăm faptul că, două sau mai multe fire
nu pot accesa simultan aceeaşi metodă. Prin urmare, pentru a controla numărul de fire de execuţie care pot
accesa o metodă la un moment dat, utilizăm cuvântul cheie synchronized. Atunci când compilatorul Java
întâlneşte o metodă prefixată de cuvântul cheie synchronized, introduce un cod special care blochează metoda
când un fir începe execuţia instrucţiunilor metodei şi o deblochează când firul îşi încheie execuţia. În mod uzual,
sincronizarea metodelor este reclamată de necesitatea partajării datelor.
Am prezentat, mai sus, semantica cuvintelor cheie ascunse sub sintagma „modificatori”, aplicaţi atributelor
sau operaţiilor.
Sintaxa care stă la baza definirii unei clase mai conţine, opţional şi cuvântul cheie extends pentru a indica o
clasă de bază clasei în curs de definire.
De asemenea, în sintaxa prezentată se mai evocă şi eventualitatea apariţiei cuvântului cheie implements
urmat de numele interfeţelor pe care le implementează clasa, atunci când este cazul. Evident, cuvântul cheie
extends este legat de problematica moştenirii în programarea orientată pe obiecte în Java iar cuvântul cheie
implements este legat de problematica utilizării interfeţelor, pentru a introduce numeroase elemente de
flexibilitate în programarea Java, inclusiv rezolvarea moştenirii multiple, neadmisă direct în Java.
Despre aceste două probleme vom discuta pe îndelete in Capitolul 5.
Înainte de a trece la prezentarea cadrului C++ pentru definirea unei clase putem urmări, mai jos, codul Java
asociat definirii claselor Nod şi Lista, aşa cum apar ele în lumina observaţiilor făcute până acum.

//---------------------------------------------
//Cod Java care demareaza procesul de rezolvare
//a problemei LSI
//---------------------------------------------
//Specificare minimala a clasei Nod
//Implementare clasa Nod
class Nod
{
private int Data;
private Nod Urmatorul;

//constructor
public Nod(int Numar)
{
Data=Numar;
};

//modificator atribut Data


public void setareData(int nr)
{
Data=Numar;
};

//selector atribut Data


public int consultareData()
{
return Data;
};
//Modificator atribut Urmatorul
public void setUrmator(Nod Element)
{
Urmatorul=Element;
};

//Selector atribut Urmatorul


public Nod consultaUrmator()
{
return Urmatorul;
};
};

//Specificare aproape completa a clasei abstracte Lista


abstract class Lista
{
private Nod AStart;
private Nod AUltim;
private int NrElem;

//Modificator atribut AStart


public void setareAStart(Nod Element)
{
AStart=Element;
};

//Selector atribut AStart


public Nod consultaAStart()
{
return AStart;
};

//Modificator atribut NrElem


public void setareNrElem(int ne)
{
NrElem=ne;
};

//Selector atribut NrElem


public int consultaNrElem()
{
return NrElem;
};

//Modificator atribut AUltim


public void setareAUltim(Nod Element)
{
AUltim=Element;
};

//Selector atribut AUltim


public Nod consultaAUltim()
{
return AUltim;
};

//Metoda abstracta
//Va fi implementata in descendenti
//Insereaza un element dupa ultimul nod introdus
public abstract void insereazaDNod(Nod Element);

//Metoda abstracta
//Va fi implementata in descendenti
//Insereaza un element inaintea ultimului nod introdus
public abstract void insereazaINod(Nod Element);

//Metoda abstracta
//Va fi implementata in descendenti
//Sterge un element de adresa specificata
public abstract void stergeNodAdr(Nod Element);

//Metoda abstracta
//Va fi implementata in descendenti
//Insereaza un element de pozitie specificata
public abstract void stergeNodNum(int Nr);

//Metoda publica finala


//Deocamdata neimplementata
//Salveaza lista reperata de AStart
//intr-un fisier de nume specificat
public final void salveazaInFisier(char numef[])
{
System.out.println("Neimplementata...");
};

//Metoda publica finala


//Deocamdata neimplementata
//Restaureaza lista folosind informatiile din fisierul
//de nume specificat
public final Nod incarcaDinFisier(char numef[])
{
System.out.println("Neimplementata...");
return null;
};
}

Codul prezentat mai sus trebuie să fie extins şi rafinat pentru a acoperi toate cerinţele iniţiale ale
problemei şi pentru a face faţă exigenţelor de calitate fireşti (modularizare corectă, fiabilitate, extensibilitate,
etc.).
4 Moştenirea în programarea orientată pe obiecte din
perspectivă Java

4.1 Scurtă introducere


Am văzut, deja, faptul că unul dintre principiile importante în programarea orientată pe obiecte
este principiul moştenirii. Aşa cum se întâmplă, în general, cu principiile, nici principiul moştenirii nu
trebuie fetişizat sau bagatelizat. Bagatelizarea lui înseamnă, ceva de genul: „Hai să facem moştenire ca să ne
aflăm în treabă sau ca să vedem dacă funcţionează”. Fetişizarea, din contră, ar însemna „Sfinte Sisoe, nimic mai
frumos şi mai eficient decât aplicarea principiului moştenirii la tot pasul”.Ambele variante sunt false. Cum am
mai spus şi altă dată, principiul moştenirii este disponibil pentru uz, nu pentru abuz. Ceva mai concret spus:

Moştenirea este o modalitate performantă de reutilizare a codului, dar nu este întotdeauna cel
mai bun instrument pentru îndeplinirea acestui obiectiv.

Dacă este folosită necorespunzător, programele obţinute vor fi destul de fragile.

Moştenirea poate fi utilizată cu succes în cadrul unui pachet, unde implementările subclaselor şi
superclaselor sunt controlate de aceeaşi programatori. De asemenea, poate fi folosită la extinderea claselor
care au fost concepute şi documentate exact în acest scop.

Însă, moştenirea unor clase concrete obişnuite, în afara graniţelor unui pachet, poate fi periculoasă.
Trebuie să subliniez faptul că toate consideraţiile pe care le fac în acest paragraf se referă la moştenirea
înţeleasă ca moştenire a implementării (o clasă extinde o altă clasă, exprimându-ne în spirit Java). Se ştie,
desigur că relaţia de moştenire operează şi în relaţia dintre interfeţe, chestiune care nu cade sub incidenţa
observaţiilor critice, de mai sus, la adresa moştenirii.

Defecţiunea esenţială care poate apare când apelăm la moştenire se referă la faptul că, prin
moştenire, încapsularea are de suferit în mod natural.

Şi aceasta deoarece funcţionarea corectă a unei subclase depinde de detaliile de implementare ale
superclasei. Implementarea superclasei se poate modifica de la o versiune a programului la alta, situaţie în care ,
subclasa este expusă deteriorării chiar şi în cazul în care codul ei a rămas neatins. Prin urmare, subclasa este
obligată să evolueze odată cu superclasa, exceptând, bineînţeles, situaţia în care autorii superclasei au conceput-
o şi documentat-o special pentru a fi extinsă.

O regulă de bun simţ în utilizarea principiului moştenirii ne spune că este bine să folosim
moştenirea numai dacă subclasa este, într-adevăr, un subtip al superclasei.

Altfel spus, o clasă B trebuie să extindă o clasă A numai dacă între cele două clase există o relaţie
de tipul „B este un A”.
Cu referire la mulţimea poligoanelor, părerea unor aşa zişi specialişti, conform căreia putem deriva
clasa dreptunghi din clasa triunghi este complet în afara logicii relaţiei de moştenire. Nici în visul cel mai
frumos un dreptungi nu este un triunghi. În schimb, putem spune că triunghiul este un descendent al poligonului,
pe motiv că orice triunghi este un poligon, etc.

Concluzionând, moştenirea este un aspect important în programare, dar problematic, pentru că


poate fi aplicat denaturându-i esenţa şi pentru că încalcă, în mod natural, regulile încapsulării.
Moştenirea se poate folosi eficient numai când între superclasă şi subclasă există o relaţie reală de genul
„tip-subtip”. Chiar şi în acest caz, codul obţinut poate fi fragil, având probleme când apar modificări şi fiind
predispus la anumite breşe în securitatea resurselor unei clase.
Evitarea unor astfel de probleme este posibilă, în anumite situaţii, prin utilizarea compunerii în locul
moştenirii.
Relaţia de compunere presupune ca în definiţia unei clase A să apară şi instanţe ale altor clase, ale
căror servicii urmează să fie utilizate de către clasa A. Pe de altă parte, este tot atât de adevărat faptul că multe
probleme rezolvate în C++ cu ajutorul pointerilor, nu ar avea rezolvare echivalentă semantic în Java, fără
suportul moştenirii.

4.2 Moştenirea în Java


Nu este cazul să revin asupra sintaxei operaţiei de moştenire în Java.
Java a optat pentru un cuvânt cheie pentru a informa compilatorul de intenţia unei clase de a moşteni
proprietăţile altei clase. Acest cuvânt cheie este extends. Ceea ce trebuie să semnalăm ca important, pentru
spiritul moştenirii în limbajul Java, este faptul că acesta nu oferă suport pentru moştenirea multiplă, în sensul
în care este aceasta înţeleasă în alte limbaje. În Java, o clasă poate avea o singură superclasă. De aici rezultă
posibilitatea teoretică de a construi doar ierarhii de clase, cu suportul oferit de moştenirea cu extends. Dat fiind
faptul că în practică există şi nenumărate situaţii în care semantica moştenirii multiple se impune ca alternativa
cea mai valabilă, Java oferă o portiţă pentru a simula moştenirea multiplă cu ajutorul interfeţelor. Jongleriile care
se pot face cu ajutorul interfeţelor au depăşit de mult intenţiile iniţiale ale specificatorilor limbajului Java. Mă
voi ocupa de acest subiect în paragraful 5.3.

Exemplul 4.1 ilustrează moştenirea, având ca idee călăuzitoare specializarea clasei de bază prin
adăugare de membri noi. De asemenea, Exemplul 5.1 ilustrează şi cele două tipuri de casting posibile în
programarea orientată pe obiecte din Java. În esenţă, este vorba despre faptul că, având contextul din Figura 15,
sunt posibile două tipuri de casting, principial deosebite.

Figura 15. Derivare pretext pentru două tipuri de casting


Primul tip de casting este implicit. Este vorba despre conversia unei variabile obiect de tip B la o
variabilă de tip A. Motivul pentru care acest tip de conversie este implicit este simplu: resursele lui A se
regăsesc printre resursele lui B. Astfel că, atunci când B este coerent din punct de vedere al stării, nu există
nici un motiv ca A să fie altfel, în urma conversiei.
Al doilea tip de casting necesită acordul explicit al programatorului pentru a fi efectuat şi într-un anumit
sens, asumarea răspunderii pentru această conversie. Este vorba de conversia unei variabile obiect de tip A la o
variabilă de tip B. De ce este necesar acordul? Foarte simplu: resursele lui A sunt incluse printre resursele
lui B. Conversia de mai sus este posibil să aducă variabila de tip B într-o stare improprie pentru utilizarea unor
metode, deoarece unele date membre pot rămâne neiniţializate adecvat. Cu toate acestea, down casting-ul este
esenţial în programarea generică, asupra căreia vom reveni într-un curs special.

Exemplul 4.1
//Clasa de bază
//Modelează informaţional obiectul Fruct
//Potrivit abordării din acest cod
//metoda consultaTipFruct() nu poate fi
//redefinită în descendenţi
class Fruct
{
private int tipfruct;
Fruct(int t)
{
tipfruct=t;
System.out.println("Constructor Fruct...");
}
final int consultaTipFruct()
{
return tipfruct;
}
}

//Subclasă a clasei Fruct


//Para este un fruct->este respectat spiritul natural
//al moştenirii
//Se adaugă noi atribute informaţionale
//Se adaugă metode specifice
//Este un exemplu clasic de specializare prin adaugare
class Para extends Fruct
{
private double greutate;
private int forma;
Para(int t,double g,int f)
{
super(t);
System.out.println("Constructor Para...");
greutate=g;
forma=f;
}
double consultaGreutate()
{
return greutate;
}
int consultaForma()
{
return forma;
}
}

//Subclasă a clasei Fruct


//Portocala este un fruct->este respectat spiritul natural
//al moştenirii
//Se adaugă noi atribute informaţionale
//Se adaugă metode specifice
//Este un exemplu clasic de specializare prin adăugare
class Portocala extends Fruct
{
private int tipcoaja;
Portocala(int tf,int tc)
{
super(tf);
System.out.println("Constructor Para...");
tipcoaja=tc;
}

int consultaTipCoaja()
{
return tipcoaja;
}
}

//Clasa care utilizează ierarhia de clase de mai sus


//Tipurile Para şi Portocala au acelaşi supertip
//dar sunt incompatibile la atribuire
//deoarece sunt părţi ale unor lanţuri de derivare
//diferite
public class Mosten1
{
public static void main(String[] s)
{
//Declarare variabilă referinţă
//având tipul clasei rădăcină
Fruct of;

//Declarare şi alocare variabilă referinţă


//de tip Para
Para obpara=new Para(1,1.5,2);

//Declarare şi alocare variabilă referinţă


//de tip Portocala
Portocala obport=new Portocala(10,1);

//Utilizare normală a variabilei de tip Para


System.out.println("Para ..creata ca referinta Para");
System.out.println("Tip fruct:"+ obpara.consultaTipFruct());
System.out.println("Greutate fruct:"+obpara.consultaGreutate());
System.out.println("Forma fruct:"+obpara.consultaForma());

//Utilizare normala a variabilei de tip Portocala


System.out.println("Portocala creata ca referinta Portocala");
System.out.println("Tip fruct:"+obport.consultaTipFruct());
System.out.println("Tip coaja:"+obport.consultaTipCoaja());

//Exemplu de Up casting (implicit)


of=new Para(1,2.5,3);

//Exemplu de Down casting (explicit);


//Foarte util in programarea generica
obpara=(Para)of;

//Utilizare variabile referinţă


//setate prin casting explicit
System.out.println("Para ...creata ca referinta Fruct");
System.out.println("Tip fruct:"+obpara.consultaTipFruct());
System.out.println("Greutate fruct:" +obpara.consultaGreutate());
System.out.println("Forma fruct:"+obpara.consultaForma());
}
};

Exemplul 4.2 ilustrează specializarea prin redefinirea metodelor, ca bază pentru comportamentul
polimorfic al obiectelor în Java. Mecanismul polimorfismului îl voi pune în discuţie în Capitolul 5.

Exemplul45.2
//Clasa de baza
//Modelează informţional obiectul Fruct
//Metoda consultaTipFruct() se va redefini
//in descendentul Para
class Fruct
{
private int tipfruct;
Fruct(int t)
{
tipfruct=t;
System.out.println("Constructor Fruct...");
}
int consultaTipFruct()
{
return tipfruct;
}
}

//Subclasă a clasei Fruct


//Para este un fruct->este respectat spiritul natural
//al moştenirii
//Se adaugă noi atribute informaţionale
//Se redefineşte metoda consultaTipFruct()
//Este un exemplu clasic de specializare prin redefinire
class Para extends Fruct
{
private double greutate;
private int forma;
Para(int t,double g,int f)
{
super(t);
System.out.println("Constructor Para...");
greutate=g;
forma=f;
}

int consultaTipFruct()
{
System.out.println("Consultare tip fruct..redefinire Para");
return super.consultaTipFruct();
}

double consultaGreutate()
{
return greutate;
}
int consultaForma()
{
return forma;
}
}

//Subclasă a clasei Fruct


//Portocala este un fruct->este respectat spiritul natural
//al moştenirii
//Se adaugă noi atribute informaţionale
//Se redefineşte metoda consultaTipFruct()
//Este un exemplu clasic de specializare prin redefinire
class Portocala extends Fruct
{
private double greutate;
private int forma;
Portocala(int t,double g,int f)
{
super(t);
System.out.println("Constructor Portocala...");
greutate=g;
forma=f;
}

int consultaTipFruct()
{
System.out.println("Consultare tip fruct..redefinire Porto");
return super.consultaTipFruct();
}

double consultaGreutate()
{
return greutate;
}
int consultaForma()
{
return forma;
}
}

public class Mosten2


{
public static void main(String[] s)
{
//Declarare variabilă referinţă de tipul clasei rădăcină
Fruct of;

//Alocare referinţă utilizând constructorul unei subclase


//a clasei rădăcină (Para)
of=new Para(1,2.75,3);
System.out.println("Para ...creata ca referinta Fruct");
//Utilizare, de fapt, în spirit polimorfic
//a variabilei definite şi alocate mai sus
System.out.println("Tip fruct:"+of.consultaTipFruct());

//Alocare referinţă utilizând constructorul unei subclase


//a clasei rădăcină (Portocala)
of=new Portocala(2,0.75,4);
System.out.println("Portocala ...creata ca referinta Fruct");
//Utilizare, de fapt, în spirit polimorfic
//a variabilei definite si alocate mai sus
System.out.println("Tip fruct:"+of.consultaTipFruct());
}
};

4.3 Moştenirea multiplă în Java


Deşi o modalitate comodă de a ieşi din impas în anumite situaţii, moştenirea multiplă este criticată pentru
confuziile pe care le poate genera, dacă este utilizată fără un „efort de inventariere”, adecvat focalizat asupra
proprietăţilor claselor candidate la calitatea de superclase pentru o clasă derivată din ele. Duplicarea câmpurilor
şi a metodelor, precum şi violarea crasă a încapsulării sunt principalele motive de îngrijorare când este vorba de
utilizarea moştenirii multiple. Aşadar, ne aflăm în situaţia din Figura 16.
Dacă semantica din domeniul problemei impune o asemenea abordare, din punct de vedere conceptual,
atunci în Java soluţia pentru posibilitatea de a spune despre C că este A sau B sau A şi B o reprezintă utilizarea
interfeţelor. Interfaţa este o varietate de clasă, caracterizată prin faptul că poate declara metode abstracte şi
publice şi, dacă este necesar, variabile care sunt considerate, implicit, de tip public, static şi final, deci
constante. În foarte mare măsură, comportamentul unei interfeţe este asemănător comportamentului unei clase.
Atât de profundă este asemănarea încât, în anumite situaţii interfeţele şi clasele se pot substitui reciproc.
A B

Figura 16.Moştenirea multiplă


Pentru a înţelege modul de lucru cu interfeţele consider esenţiale următoarele precizări:

1. Mai întâi trebuie să învăţăm cum se declară o interfaţă. Aceasta este o problemă de sintaxă, mult mai
simplă decât problema conceptuală, care trebuie clarificată înainte de a ajunge la sintaxă. Din punct de vedere
conceptual, tipul de raţionament pe care îl facem seamănă cu cel pe care îl facem când specificăm o clasă
abstractă care are toate metodele virtuale pure, în C++ sau abstracte în Java. Din punct de vedere sintactic, avem
cadrul:

interface <Nume_interfaţă> [extends <Listă _de_interfeţe>]


{
<Signaturi de metode>
}

Se observă, deja, amănuntul care deosebeşte „mecanica utilizării claselor” de „mecanica utilizării
interfeţelor”, anume, suportul pentru moştenire multiplă în cazul interfeţelor. Pe acest amănunt se bazează
alternativa Java la moştenirea multiplă relativ la clase.

2. În al doilea rând, utilizarea unei interfeţe este posibilă, în mai multe moduri, după ce am specificat şi
implementat o clasă care o utilizează. Ne amintim de sintaxa:

class <Nume_clasă> [extends Nume_superclasă] implements <Listă de interfeţe>


{
<Date membre>
<Funcţii membre>
};

După cum se poate observa, o clasă poate implementa mai multe interfeţe, depăşindu-se, astfel,
restricţia Java în legătură cu moştenirea multiplă în relaţia dintre clase. Să mai subliniez şi faptul că o clasă care
implementează una sau mai multe interfeţe poate avea cel mult un strămoş, eventual nici unul. Disciplina astfel
introdusă este, sper, clară: metodele unei clase (care are, eventual un strămoş) pot fi acoperitoare ca
specificare şi implementare pentru listele de metode ale unor de interfeţe. În acest mod, avem la dispoziţie
un mecanism de a vedea, din unghiuri diferite, ansamblul resurselor unei clase. Este clar că soluţia Java
determină programatorii să mediteze mai atent înainte de a face joncţiunea cu mai multe interfeţe.

3. Utilizarea efectivă a interfeţelor este diversă: ca tipuri definitoare pentru referinţe la obiecte, ca tipuri
implicate în casting, ca tipuri utile în programarea generică, etc. Căteva modalităţi de utilizare a interfeţelor se
pot vedea în Exemplul 4.3 şi în Exemplul 4.4.

Exemplul4.3
//Interfata I1
//Expune functia f1()
interface I1
{
public void f1();
}
//Interfata I2
//Expune functia f2()
interface I2
{
public void f2();
};

//Interfata I12
//Extinde interfetele I1 si I2
//Exemplu de mostenire multipla
//a interfetelor
interface I12 extends I1,I2
{
public void f3();
}

//Clasa A implementeaza interfata I1


class A implements I1
{
public A()
{
System.out.println("AAAA...");
};
public void f1()
{
System.out.println("f1....");
};
};

//Clasa B implementeaza interfata I2


class B implements I2
{
public B()
{
System.out.println("BBBB...");
};
public void f2()
{
System.out.println("f2....");
};
};

//Clasa C implementeaza interftata I12


//Evident,functiile expuse de interfetele
//I1 si I2 sunt implementate si de catre
//clasa C
//Deoarece I12 este derivata din I1 si I2
//ni se ingaduie sa privim instantele de tip
//I12 ca fiind de tip I1 sau I2, dupa cum
//este in interesul nostru
class C implements I12
{
public C()
{
System.out.println("CCCC...");
};
public void f3()
{
System.out.println("f3...");
};
public void f1()
{
System.out.println("f1....");
};
public void f2()
{
System.out.println("f2....");
};
};

public class TestInterf


{
public static void main(String args[])
{
//Crearea unui obiect de tip I1
//utilizand clasa A
I1 ob1=new A();
System.out.println("Ura...");

//Utilizarea obiectului de tip I1


ob1.f1();

//Crearea unui obiect de tip I12


//utilizand clasa C
I12 ob12=new C();

//Utilizarea obiectului de tip I12


ob12.f1();
ob12.f2();
ob12.f3();

//Crearea unui obiect de tip I1


//utilizand clasa C
I1 ob2=new C();

//Utilizare obiect de tip I1


//creat cu suport C
ob2.f1();

//Down casting dirijat de


//interfete
ob2=(I1)ob12;
ob2.f1();
}
}

Exemplul 4.4
//Interfata IConst care declara
//doua constante si o metoda
interface IConst
{
int Numar=100;
String s="Bine ati venit!";
void ftest();
}
//A foloseste interfata IConst
//Este obligata sa implementeze ftest()
class A implements IConst
{
A()
{
System.out.println("Constructor..");
};
public void ftest()
{
System.out.println("ftest() la lucru...");
};
}

public class TInterf1


{
public static void main(String s[])
{
//Creare variabila obiect
//de tip A
//se mostenesc constantele
//interfetei IConst
A va=new A();
System.out.println(va.Numar);
System.out.println(va.s);
va.ftest();
//Creare variabila obiect
//de tip IConst
IConst iva=new A();
System.out.println(iva.Numar);
System.out.println(iva.s);
iva.ftest();
};
}

Înainte de a pune punct subiectului să mai precizez următoarele:

• Un program nu poate crea instanţe dintr-o interfată.


• Toate metodele unei interfeţe sunt implicit publice şi abstracte. Nici un alt tip nu este permis.
• Toate metodele trebuie să fie implementate de clasa care utilizează interfaţa.
5 Polimorfismul în programarea orientată pe obiecte
din perspectivă Java
5.1 Să reamintim, pe scurt, ce este polimorfismul.
După cum stau lucrurile în limbajele de programare orientate pe obiecte, polimorfismul este singurul
principiu a cărui forţă se manifestă în timpul execuţiei programelor. Valoarea principiului moştenirii este esenţial
concentrată în posibilitatea de a reutiliza efortul de dezvoltare a unui sistem soft. Încapsularea este, de asemenea,
un principiu a cărui manifestare nu este evidentă decât de pe poziţia de programator, în esenţă. Ar fi, însă,
nedrept să nu subliniem că atât încapsularea cât şi moştenirea trebuie să fie mânuite cu multă abilitate
pentru a obţine efecte polimorfice de mare subtilitate şi utilitate.
Încercând o definiţie a polimorfismului, independentă de limbajul de programare şi din punctul de
vedere al programatorului care beneficiază de el, numim polimorfism posibilitatea ca un apel de funcţie
(metodă , operaţie) să genereze răspunsuri diferite în funcţie de contextul în care a fost formulat.

5.2 Tipuri de polimorfism la nivelul limbajelor de programare.


Exemplificare recapitulativă în C++.
Nevoia de polimorfism, resimţită acut mai ales în programare, este în mare măsură sinonimă cu nevoia
de confort. În stadiul în care se află, actualmente, realizările specialiştilor în materie de polimorfism la nivelul
limbajelor de programare, putem semnala următoarele tipuri importante de polimorfism:

• Polimorfismul orientat pe suprascrierea funcţiilor în programarea clasică.


• Polimorfismul orientat pe suprascrierea funcţiilor în cadrul definiţiei unei clase.
• Polimorfsimul orientat pe supraîncărcarea operatorilor în programarea orientată pe obiecte.
• Polimorfismul orientat pe redefinirea funcţiilor în programarea orientată pe obiecte, într-un lanţ
de derivare.

Indiferent de tipul lui, polimorfismul de calitate cere investiţie de timp şi creativitate, pe moment, în
beneficiul unor viitoare reutilizări, cu minimum de efort din partea clienţilor.

Polimorfismul orientat pe suprascrierea funcţiilor în programarea clasică


Această formă de polimorfism este, practic, cea mai veche. Ea presupune posibilitatea de a scrie funcţii care
au acelaşi nume, retunează acelaşi tip de dată, dar se pot deosebi prin tipul şi numărul parametrilor. Această
posibilitate este ilustrată în Exemplul 5.1, perfect legal în programarea în limbajele C/C++.

Exemplul 5.1
//Suprascrierea funcţiilor în programarea clasică în C
//Sunt specificate şi implementate două versiuni,
//diferite prin lista de parametri ale funcţiei suma()
#include <iostream.h>
#include <conio.h>

//Prima versiune a funcţiei suma()


//Parametrul s este transmis prin referinţă
void suma(float &s,int o1,int o2)
{
s=o1+o2;
};

//A doua versiune a funcţiei suma()


//Parametrul s este transmis prin referinţă
void suma(float &s,int o1, int o2, int o3)
{
s=o1+o2+o3;
};

void main()
{
float st;
clrscr();

//Utilizarea versiunii 2
suma(st,12,13,14);
cout<<st<<endl;

//Utilizarea versiunii 1
suma(st,12,13);
cout<<st;
getch();
};

Care sunt observaţiile care se impun? Mai întâi, este de remarcat faptul că trebuie să existe un programator
care este suficient de informat cu privire la variaţiile de comportament ale unei funcţii având acelaşi nume şi care
returnează acelaşi tip de dată. Deşi nu excludem posibilitatea de a întâlni o astfel de situaţie şi în alte contexte,
programatorii versaţi ştiu foarte bine cât de mult valorează versionarea unei funcţii în programarea generică,
atunci când soluţia template-urilor prezintă unele inconveniente. În al doilea rând, dacă versionarea este realizată
cu simţ de răspundere, utilizarea diferitelor versiuni în diferite situaţii este extrem de comodă şi benefică, găsind
o soluţie de partajare a codului versiunilor între mai multe programe. În al treilea rând, nu putem trece cu
vederea faptul că la compilare este realizată legarea unui apel de versiune de codul aferent (acest gen de legare se
numeşte early binding).

Polimorfismul orientat pe suprascrierea funcţiilor în cadrul definiţiei unei clase


Această formă de polimorfism satisface unele cerinţe de versionare a comportamentului operaţiilor unei
clase, în spiritul celor spuse relativ la suprascrierea în stil clasic a funcţiilor. După cum se anticipează în
Exemplul 5.2 (de cod C++), acest tip de polimorfism poate fi combinat cu polimorfismul orientat pe
supradefinirea metodelor într-un lanţ de derivare.

Exemplul 5.2
#include <iostream.h>
#include <conio.h>

//Clasa de baza
class Super
{
int numar;
public:
Super(int n)
{
numar=n;
};

//Versiunea 1 a functiei f1()


void f1()
{
cout<<"Super::Functie de test"<<endl;
getch();
};

//Versiunea 2 a functiei f1()


// Suprascrie prima versiune a lui f1() inauntrul
//clasei Super
//In raport cu clasa Baza f1()este virtuala
//Deci urmeaza sa fie supradefinita
virtual void f1(int n)
{
cout<<"Super::Numar: "<<n<<endl;
getch();
};
};

//Clasa derivata
class Baza:public Super
{
public:
Baza(int n):Super(n)
{
};
void f1(int n)
{
cout<<"Baza::Numar: "<<n<<endl;
getch();
};
};

void main()
{
//Pointer la Super
Super *PSuper;

//Alocare dinamică a memoriei pentru pointer-ul Psuper


//în context Super
PSuper=new Super(10);
clrscr();

//Utilizare Psuper; apelare succesiva a doua versiuni ale


//functiei f1()
PSuper->f1();
PSuper->f1(10);
delete PSuper;

//Alocare dinamică a memoriei pentru pointer-ul PSuper


//în context Baza
PSuper=new Baza(12);
PSuper->f1(12);
delete Psuper;
};

Polimorfsimul orientat pe supraîncărcarea operatorilor în programarea orientată pe obiecte


Subiect ocolit de specificatorii limbajului Java, însă generator de satisfacţii deosebite pentru
programatorii în C++. Ideea de bază constă în faptul că este la latitudinea celor care programează orientat pe
obiecte în C++ să redefinească comportamentul unui foarte mare număr de operatori (+, -, *, >>, <<, new,
delete, etc.).
Atenţie! Nu poate fi schimbată nici aritatea nici prioritatea operatorilor predefiniţi, prin
supraîncărcare.
Protocolul de supraîncărcare a unui operator, astfel încât acesta să opereze asupra obiectelor unei clase
este următorul:

1. Definiţia clasei trebuie să conţină o funcţie operator membru sau o funcţie operator prietenă,
având sintaxa specială:
Varianta funcţie membră

<Tip returnat> operator # (<Lista de argumente>);

sau

Varianta funcţie friend

friend <Tip returnat> operator # (<Lista de argumente>);

În această sintaxă, atrag atenţia cuvântul cheie operator (care informează compilatorul că funcţia
supraîncarcă un operator) şi caracterul # care semnifică un substitut pentru operatorul pe care doriţi să-l
supraîncărcaţi, altul decât: “.” , “*” , “::” , “?” .
De remarcat faptul că, alegând varianta funcţie membră, un operator binar va fi specificat ca o funcţie
cu un parametru, care va indica operandul din stânga, operandul din dreapta fiind vizibil prin
intermediul pointerului this. De asemenea, dacă alegem varianta funcţie membră, un operator unar va
fi implementat ca o funcţie fără parametri, pointerul this permiţând referirea operandului. Defecţiunea
în cazul utilizării unei funcţii membru pentru supraîncărcarea unui operator este clară: parametrul din
stânga trebuie să fie un obiect, nu poate fi o constantă. Este evident că, în aceste condiţii funcţiile
prietene sunt de preferat.

2. Funcţiile operator se vor implementa folosind una din sintaxele:

<Tip returnat> <Nume clasă>::operator # (<Lista de argumente>)


{
// Corp funcţie operator specificată ca membră
};

sau

<Tip returnat> operator # (<Lista de argumente>)


{
// Corp funcţie operator specificată ca prietenă
};

Lucrurile pot fi înţelese şi mai bine, urmărind Exemplul 5.3 (cod C++).

Exemplul 5.3
#include<conio.h>
#include<iostream.h>

//Clasa complex contine functia operator + ca membru


//operatorul + este extins la multimea numerelor complexe
//cu ajutorul unei metode membru a clasei complex

//Clasa complex contine functia operator - ca functie friend


//operatorul - este extins la multime numerelor complexe
//cu ajutorul unei metode friend
class complex
{
float x,y;
public:
complex(){};
complex(float a,float b)
{
static int i;
i++;
clrscr();
cout<<"Lucreaza constructorul...Obiectul->:"<<i;
getch();
x=a;
y=b;
};
void disp_nc();

//prototipul operatorului +
complex operator+(complex &op2);

//prototipul operatorului -
friend complex operator-(complex &op1,complex &op2);
};

void complex::disp_nc()
{
cout<<x<<"+i*"<<y;
};

//Implementare operator +
//Aceasta sintaxa transforma apelul <ob1+ob2>
//in +(ob2), ob1 fiind accesibil prin pointerul
//special <this>
complex complex::operator+(complex &op2)
{
complex temp;
temp.x=op2.x+x;
temp.y=op2.y+y;
return temp;
};

complex operator -(complex &op1,complex &op2)


{
complex temp;
temp.x=op1.x-op2.x;
temp.y=op1.y-op2.y;
return temp;
};

void main()
{
complex tamp1,tamp2;
complex *pod1,*pod2;
complex ob1(10,10),ob2(11,11);
clrscr();
gotoxy(20,10);cout<<"Primul numar complex ->:";
ob1.disp_nc();
getch();
gotoxy(20,11);cout<<"Al doilea numar complex->:";
ob2.disp_nc();
getch();
ob1=ob1+ob2;
gotoxy(20,13);cout<<"Suma numerelor complexe->:";
ob1.disp_nc();
getch();

pod1=new complex(200,200);
pod2=new complex(300,300);
tamp1=*pod1;

clrscr();
gotoxy(20,10);cout<<"Al treilea numar complex ->:";
tamp1.disp_nc();

tamp2=*pod2;
gotoxy(20,11);cout<<"Al patrulea numar complex ->:";
tamp2.disp_nc();

gotoxy(20,14);cout<<"Suma numerelor complexe->:";


tamp1=tamp1+tamp2;
tamp1.disp_nc();

tamp1=*pod1;
tamp2=*pod2;
tamp1=tamp1-tamp2;
gotoxy(20,15);cout<<"Diferenta numerelor complexe->:";
tamp1.disp_nc();
getch();
}

Polimorfismul orientat pe redefinirea funcţiilor în programarea orientată pe obiecte, într-un lanţ de


derivare
Este element suport esenţial pentru specializarea claselor într-un lanţ de derivare, specializare care se
realizează prin redefinirea comportamentului unor metode ale strămoşilor. Pentru a se îmbina extinderea
comportamentului cu reutilizarea codului, este de dorit ca redefinirea comportamentului să planifice utilizarea
comportamentului versiunii din strămoş.
Exemplul 5.4 ne arată cum se pune problema redefinirii în C++.

Exemplul 5.4
#include <iostream.h>
#include <conio.h>

//Structura suport pentru pastrarea


//coordonatelor varfurilor poligoanelor
struct Varf
{
int x,y;
Varf *Legs;
};

//Clasa Poligon
//Clasă abstracta->nu are constructor şi destructor
//Furnizează prototipurile metodelor definitie() şi arie()
//ca metode virtuale pure.
//Furnizează implementarea pentru metodele:
// perimetru()
// ElibMem()
// setare_pvarfuri()
// consultare_pvarfuri()
// setare_nrvarfuri()
// consulatre_nrvarfuri()
class Poligon
{
Varf *pvarfuri;
int nrvarfuri;
public:

//Metode virtuale pure


//Vor fi redefinite în descendenţi
virtual void definitie()=0;
virtual float arie()=0;
float perimetru();
void ElibMem();
void setare_pvarfuri(Varf *p);
Varf * consultare_pvarfuri();
void setare_nrvarfuri(int nv)
{
nrvarfuri=nv;
};
int consultare_nrvarfuri()
{
return nrvarfuri;
};

};

float Poligon::perimetru()
{
cout<<"perimetru(): ";
cout<<"Calculul perim. este neimplem... Poligon" <<endl;
getch();
return 0;
};

void Poligon::ElibMem()
{
Varf*pwork;
while (pvarfuri!=NULL)
{
pwork=pvarfuri->Legs;
delete pvarfuri;
pvarfuri=pwork;
};
};

void Poligon::setare_pvarfuri(Varf *p)


{
pvarfuri=p;
};

Varf * Poligon::consultare_pvarfuri()
{
return consultare_pvarfuri();
};

//Clasa Triunghi
//Clasă concretă având ca superclasă clasa Poligon
//Redefineşte comportamentul metodelor:
// definitie(); arie()
//Furnizează constructor şi destructor

class Triunghi:public Poligon


{
public:
Triunghi(Varf *pt,int tnrv)
{
setare_pvarfuri(pt);
setare_nrvarfuri(tnrv);
cout<<"Constructor Tringhi..."<<endl;
};
virtual ~Triunghi()
{
cout<<"Destructor Triunghi..."<<endl;;
ElibMem();
};

//Redefinire metode
void definitie();
float arie();
};

void Triunghi::definitie()
{
cout<<"definitie(): ";
cout<<"Triunghiul este poligonul cu trei laturi"<<endl;
getch();
};

float Triunghi::arie()
{
cout<<"arie(): ";
cout<<"Neimplementata deocamdata...Triunghi"<<endl;
getch();
return 0;
};

class Patrulater:public Poligon


{
public:
Patrulater(Varf *pt,int tnrv)
{
setare_pvarfuri(pt);
setare_nrvarfuri(tnrv);
cout<<"Constructor Patrulater..."<<endl;
};

//Destructor virtual
virtual ~Patrulater();
void definitie();
float arie();
};

Patrulater::~Patrulater()
{
ElibMem();
cout<<"Destructor Patrulater..."<<endl;
};

void Patrulater::definitie()
{
cout<<"definitie(): ";
cout<<"Patrulaterul este poligonul cu patru laturi"<<endl;
getch();
};

float Patrulater::arie()
{
cout<<"arie(): ";
cout<<"Neimplementata deocamdata...Patrulater"<<endl;
getch();
return 0;
};

class Paralelogram:public Patrulater


{
public:
Paralelogram(Varf *pt,int tnrv):Patrulater(pt,tnrv)
{
cout<<"Constructor Paralelogram..."<<endl;
};

//Destructor virtual
virtual ~Paralelogram()
{
ElibMem();
cout<<"Destructor Paralelogram..."<<endl;
};

//Redefinire metode
void definitie();
float arie();
};

void Paralelogram::definitie()
{
cout<<"definitie(): ";
cout<<"Paralelogramul este patrulat. cu laturile paral. doua cate doua"<<endl;
getch();
};

float Paralelogram::arie()
{
cout<<"arie(): ";
cout<<"Neimplementata deocamdata...Paralelogram"<<endl;
getch();
return 0;
};

class Dreptunghi:public Paralelogram


{
public:
Dreptunghi(Varf *pt,int tnrv):Paralelogram(pt,tnrv)
{
cout<<"Constructor dreptunghi..."<<endl;
};
virtual ~Dreptunghi()
{
ElibMem();
cout<<"Destructor Dreptunghi..."<<endl;
};
void definitie();
float arie();
};

void Dreptunghi::definitie()
{
cout<<"definitie(): ";
cout<<"Dreptunghiul este paralelogramul cu un unghi drept"; cout<<endl;
getch();
};

float Dreptunghi::arie()
{
cout<<"arie(): ";
cout<<"Neimplementata deocamdata...Dreptunghi"<<endl;
return 0;
};

void main()
{
Poligon *RefPol;
Patrulater *RefPatr;
clrscr();

RefPol=new Triunghi(NULL,3);
RefPol->arie();
RefPol->definitie();
RefPol->perimetru();
cout<<endl;
delete RefPol;

RefPatr=new Patrulater(NULL,4);
RefPatr->arie();
RefPatr->definitie();
RefPatr->perimetru();
cout<<endl;
delete RefPatr;
RefPatr=new Paralelogram (NULL,4);
RefPatr->arie();
RefPatr->definitie();
RefPatr->perimetru();
delete RefPatr;
};

Pentru o mai bună înţelegere a Exemplului 5.4, sunt necesare o serie de precizări în ceea ce priveşte
genul de polimorfism ilustrat.
Mai întâi, din punct de vedere sintactic, trebuie să observăm faptul că informăm compilatorul de
intenţia de redefinire a unei metode în aval (într-un lanţ de derivare) prin specificarea acesteia în clasa
gazdă ca metodă virtuală sau ca metodă virtuală pură.

Prototipul unei metode virtuale are sintaxa:

virtual <Tip returnat> <Nume metoda>([<Lista de parametri>]);

Prototipul unei metode virtuale pure are sintaxa:

virtual <Tip returnat> <Nume metoda>([<Lista de parametri>])=0;


Clasele care conţin cel puţin o metodă virtuală pură sunt clase abstracte, deci nu pot avea instanţe
directe, neavând nici constructori. În schimb, clasele abstracte pot fi folosite pentru a declara referinţe
către descendenţi, ceea ce exte extrem de folositor dacă dorim polimorfism.
De remarcat că redefinirea se bazează pe o restricţie importantă: în procesul de redefinire se conservă
signatura (numărul de parametri, tipul lor şi tipul returnat).
Odată ce o metodă a fost declarată virtuală sau virtuală pură, compilatorul ştie că această metodă este
posibil să fie redefinită în descendenţi şi, de asemenea, compilatorul ştie că pentru clasa care conţine metode
virtuale şi pentru toate clasele descendente ei, la crearea primului obiect, constructorul va crea şi tabela VMT
(Virtual Methode Table), o structură partajată de toate obiectele unei clase, folosită de sistem pentru a realiza
genul de legare a unui apel de codul contextual, numit late binding. Prin urmare, atunci când se crează un
obiect, al cărui tip definitor este undeva într-un lanţ de derivare, dacă în amonte a existat intenţie de redefinire a
unor metode, sistemul va crea, numai în cazul primului obiect de tipul respectiv, o tabelă care conţine adresele
metodelor virtuale ale clasei. Aceste adrese vor fi utilizate în procesul de late binding.
Să mai observăm faptul că, fără a fi prefixaţi de cuvântul cheie virtual, destructorii sunt apelaţi pe
principiul “Întotdeauna lucrează constructorul tipului definitor al unei variabile obiect sau al unui
pointer la un obiect”, ceea ce înseamnă un gen de legare statică a destructorului. Dacă dorim legare dinamică,
atunci destructorul este declarat ca virtual.
Efectul poate fi urmărit în Exemplul 5.4.

5.3 Polimorfismul în context Java


Java implementează principiul polimorfismului la scara posibilităţilor proprii. În Java nu avem decât
programare orientată pe obiecte, oricare ar fi calitatea acesteia. Astfel că se oferă suport pentru polimorfism
orientat pe suprascrierea funcţiilor şi polimorfism orientat de supradefinire. Java nu oferă sintaxă pentru
supraîncărcarea operatorilor, deci nu este posibil polimorfismul aferent. Merită să remarcăm faptul că
supradefinirea în Java este mai simplă decât în C++, din punct de vedere sintactic vorbind. Pur şi sumplu, dacă
compilatorul sesizeză că în amontele unui lanţ de derivare există o metodă care este supradefinită în aval, atunci
compilatorul generează informaţii necesare pentru realizarea legării la execuţie. Cerinţa conservării signaturii
în procesul de supradefinire este prezentă şi în Java.
Un model de utilizare a polimorfismului se poate observa în Exemplul 5.5.

Exemplul 5.5
//Clasa radacina
class Poligon
{
private String definitie;
public Poligon(String d)
{
definitie=new String(d);
};

public String citesteDefinitie()


{
return definitie;
};

//Metoda va fi supradefinita in descendenti


public void arie()
{
System.out.println("Poligon...neimplementata!");
};
};

class Triunghi extends Poligon


{
public Triunghi(String d)
{
super(d);
};

//Supradefinire
public void arie()
{
System.out.println("Triunghi...neimplementata!");
};
};

class Patrulater extends Poligon


{
public Patrulater(String d)
{
super(d);
};

//Supradefinire
public void arie()
{
System.out.println("Patrulater...neimplementata!");
};
};

public class Polimorf


{
public static void main(String[] s)
{
//Referinta la radacina
Poligon PRef;

//Alocare in context Poligon


PRef=new Poligon("Linie franta inchisa");
System.out.println(PRef.citesteDefinitie());

//Sintaxe la utilizare este aceeasi in cele trei contexte


PRef.arie();
System.out.println("");

//Alocare in context Triunghi


PRef=new Triunghi("Poligonul cu trei laturi");
System.out.println(PRef.citesteDefinitie());
PRef.arie();
System.out.println("");

//Alocare in context Patrulater


PRef=new Patrulater("Poligonul cu patru laturi");
System.out.println(PRef.citesteDefinitie());
PRef.arie();
System.out.println("");
};
};

Nu am motive să reiau discuţia pe marginea mecanismului de legare dinamică a metodelor supradefinite


în Java. Chiar dacă compilatorul foloseşte alt gen de informaţii, la intrare, rezultatul final, pentru programator
este acelaşi.
Nu consider o problemă deosebită comentarea şi exemplificarea suprascrierii în clasele Java.
6 Tratarea structurată a excepţiilor în programarea
orientată pe obiecte
6.1 O problemă, în plus, în programare: tratarea excepţiilor
Programatorii adevăraţi trebuie să ia, obligatoriu, în calcul şi posibilitatea de a crea programe robuste,
care fac faţă atât cerinţelor specificate dar nerafinate suficient, cât şi cerinţelor nespecificate dar formulate de
utilizator, din diverse motive. Programele care au aceste calităţi se numesc robuste.
În programarea clasică, soluţia acestei probleme se putea numi, destul de exact spus, programare
defensivă. Seamănă puţin cu conducerea preventivă din şoferie dacă ne gândim că programând defensiv, în
fond punem răul înainte, deci nu ne bazăm pe cumsecădenia şi buna pregătire a utilizatorului.
Încercarea de a trata situaţiile de excepţie care pot apare la execuţia unui program, folosind metode
clasice (programarea defensivă) duce la creşterea semnificativă a complexităţii codului ceea ce afectează, în mod
direct, lizibilitatea şi, în mod indirect, corectitudinea codului
Pentru a face faţă cerinţelor legate de problema tratării excepţiilor (aşa se numesc în jargon profesional
erorile care apar în timpul execuţiei programelor) anumite limbaje de programare oferă suport adecvat. Includem
aici limbaje precum: Object Pascal, C++, Java, Visual C++.
Nu toate compilatoarele de C++ oferă suport, dar standardul ANSI C++ cere acest lucru în mod explicit.
Compilatoarele din familia Borland, începând cu versiunea 4.0 oferă acest suport.
Esenţialul din punctul de vedere al programatorului C++ este ca el să-şi formeze abilitatea de a scrie, în
jurul aplicaţiilor, cod C++ care îndeplineşte funcţia de handler de excepţii.

6.2 Maniera Java de tratare a excepţiilor


Aşa cum am menţionat deja, încercarea de a trata situaţiile de excepţie care pot apare la execuţia unui
program, folosind metode clasice (programarea defensivă) duce la creşterea semnificativă a complexităţii
codului, ceea ce afectează, în mod direct, lizibilitatea şi, în mod indirect, corectitudinea codului. Din această
cauză cei care au creat Java au gândit un sistem de tratare a excepţiilor (în continuă evoluţie, de la o versiune la
alta a limbajului Java) care să permită programatorului:

• Tratarea situaţiilor de excepţie, pe cât posibil, independent de fluxurile de control normale;


• Tratarea excepţiilor, la un nivel superior celui în care apar;
• Propagarea excepţiilor la nivelele superioare în mod ierarhic;
• Tratarea unitară a excepţiilor de acelaşi tip.

În mare parte, asemănător sistemului C++ de tratare a excepţiilor, sistemul Java are, totuşi, o ofertă mai bine
pusă la punct din acest punct de vedere. Java se bazează pe un număr restrâns de cuvinte cheie (try, catch,
throw, finally, throws) şi pe o ierarhie de clase, specializate în tratrarea unor clase de erori.
Pentru a înţelege mai bine mecanismul tratării excepţiilor în Java, consider că este utilă o scurtă descriere a
modului în care apar şi sunt procesate excepţiile în Java. Astfel, când apare o excepţie în interiorul unei metode a
unei clase Java, se creează un obiect excepţie (obiect ce caracterizează excepţia şi starea programului în
momentul când excepţia apare). Odată creat acest obiect, el este “aruncat” utilizând cuvântul cheie throw.
Sarcina creării şi aruncării obiectului excepţie aparţine programatorului. Din momentul aruncării unui obiect
excepţie, folosind cuvântul cheie throw, maşina virtuală Java (JVM), prin componenta RuntimeSystem, preia
obiectul şi îl transmite secvenţei de cod responsabilă de tratarea excepţiei respective. În acest scop,
RuntimeSystem va căuta un handler al excepţiei (=o secvenţă de cod responsabilă de tratarea excepţiei),
începând de la nivelul (nivelul este o metodă) în care a apărut excepţia şi continuând la nivelele superioare.
Căutarea se face în urmă (backward), utilizând stiva de apel (call stack). Primul handler (un bloc try-catch),
corespunzător obiectului excepţie, se va ocupa de soluţionarea excepţiei. Dacă RuntimeSystem a epuizat stiva
de apel, fără a găsi o metodă care să ofere un handler al obiectului excepţie aruncat, RuntimeSystem va fi
responsabil de tratarea excepţiei respective (va afişa un mesaj de eroare şi va opri firul de execuţie). Mecanismul
descris mai sus poate fi vizualizat ca în Figura 19.
Problema care se află în faţa programatorului este, evident, următoarea: cum poate folosi raţional
mecanismul respectiv? Spun aceasta deoarece, ca orice facilitate a limbajului şi suportul oferit pentru tratarea
excepţiilor poate fi utilizat în mod abuziv. A abuza de tratarea excepţiilor înseamnă a vedea excepţii şi unde nu
este cazul, fapt care provoacă complexificarea artificială a codului şi, foarte important, diminuează
performanţele programului, deoarece, aşă cum rezultă şi din Figura 17, mecanismul tratării excepţiilor consumă
resurse pentru a se desfăşura corespunzător. De aceea, este necesară o disciplinare a gândirii
programatorului, în ceea ce priveşte decizia de a considera excepţie sau nu un anumit context de
prelucrare, în interiorul unei metode şi apoi, decizia de a aborda, într-un anumit mod, problema tratării
excepţiei respective. În esenţă, programatorul trebuie să acumuleze suficienţă experienţă încât să deosebească
o excepţie nerecuperabilă de o excepţie din care se poate reveni.

RuntimeSuystem - JVM Nivel_1


-preia obiectul excepţie
-caută, începând cu nivelul j, în
sus, primul handler
corespunzător (=primul handler Nivel_i Nivel Tratare
care rezolvă o excepţie de tipul Excepţie
celei aruncate) Conţine un handler (un bloc
-transferă obiectul excepţie try-catch)
handler-ului -preia obiect excepţie
-tratează excepţie

Nivel_j Nivel apariţie


excepţie
-crează obiectul excepţie
Exception exc=new
Exception();
-aruncă excepţia
throw(exc);

Figura 17. Mecanismul Java de tratare a excepţiilor


Pentru a crea un obiect excepţie, Java pune la dispoziţia utilizatorului o ierahie de clase, aflată în
pachetul java.lang. Pe lângă clasele de tip excepţie aflate în java.lang, fiecare pachet Java introduce propriile
tipuri de excepţii. Utilizatorul însuşi poate defini clase de tip excepţie, care însă pentru a avea instanţe
compatibile cu sistemul Java, trebuie să fie descendenţi ai clasei Throwable, clasă care ocupă o poziţie
importantă în ierarhia simplificată a claselor de tip excepţie, prezentată în Figura 18.

Throwable

Exception Error

RuntimeException

Figura 18. Ierarhia simplificată a claselor de tip excepţie din pachetul java.lang
După cum se poate observa în Figura 18, clasa Throwable are doi descendenţi: clasa Error şi clasa
Exception. Nici una din cele două clase nu adaugă metode suplimentare, dar au fost introduse pentru a delimita
două tipuri fundamentale de excepţii care pot apare într-o aplicaţie Java (de fapt, acest mod de gândire este
aplicabil în orice limbaj de programare care oferă suport pentru tratarea sistematică a excepţiilor).
Clasa Error corespunde excepţiilor care nu mai pot fi recuperate de către programator. Apariţia unei
excepţii de tip Error impune terminarea programului. Aruncarea unei excepţii de tip Error înseamnă că a apărut
o eroare deosebit de gravă în execuţia programului sau în maşina virtuală Java. În marea majoritate a cazurilor,
aceste excepţii nu trebuie folosite de către programator, nu trebuie prinse prin catch, şi nici aruncate prin throw
de către programator. Aceste tipuri de erori sunt utilizate de JVM, în vederea afişării mesajelor de eroare.
Clasa Exception este, de fapt, clasa utilizată efectiv de către programatori în procesul de tratare a
excepţiilor. Această clasă şi descendenţii ei modelează excepţii care pot fi rezolvate de către program, fără a
determina oprirea programului. Prin urmare, regula este simplă: dacă Java nu conţine o clasă derivată din
Exception care poate fi utilizată într-un anumit context, atunci programatorul va trebui să o
implementeze, el însuşi, ca o clasă derivată din Exception.
Există o mare varietate de clase derivate din Exception care pot fi utilizate. Mai mult, fiecare pachet
Java adaugă noi tipuri de clase derivate din Exception, clase legate de funcţionalitatea pachetului respectiv.
Dacă astfel lucrează cei de la SUN, de ce n-ar lucra la fel şi un programator oarecare?
Din categoria claselor derivate din Exception, se remarcă clasa RuntimeException13 şi clasele derivate
din ea. Din această categorie fac parte excepţii care pot apare în execuţia unui program, în urma unor operaţii
nepermise de genul: operaţii aritmetice interzise (împărţire la zero), acces nepermis la un obiect (referinţă null),
depăşirea index-ului unui tablou sau şir, etc.
Nu ne rămâne decât să prezentăm protocolul de lucru cu excepţii în Java.

Aruncarea excepţiilor
Aruncarea unei excepţii se face cu ajutorul cuvântului cheie throw, conform sintaxei:


throw <obiectExceptie>;

unde <obiectExceptie> este o instanţa a clasei Throwable sau a unei clase, derivată din aceasta. Evident, în
locul variabilei <obiectExceptie> poate fi o expresie care returnează un obiect de tip convenabil. De fapt, în
practică, modul de aruncare a unei excepţii urmează schema:


throw new <clasaExceptie>(“Mesaj”);

Evident, putem avea şi cazuri în care o funcţie poate arunca în mod indirect o excepţie, ceea ce
înseamnă că funcţia nu va conţine o expresie throw, ci va apela o funcţie care poate arunca o excepţie.
O metodă poate arunca mai multe excepţii. Important este să înţelegem că prin aruncarea unei excepţii
se iese din metodă fără a mai executa secventele de cod care urmau. În cazul în care o funcţie aruncă o excepţie,
fie prin throw , fie prin apelul unei funcţii, fără a avea o secvenţă try-catch de prindere atunci această funcţie
trebuie să specifice clar această intenţie în definiţia funcţiei. Pentru acest caz, sintaxa de definire a funcţie este:

public void <numeMetoda> throws <clasExcept1>,<clasExcept2>, …


{

throw <obiectExcep1>;

throw <obiectExcept2>;

};

13
Detalii cu privire la descendenţii clasei RuntimeException se pot găsi în Călin Marin Văduva, Programarea
în Java, Editura Albastră, 2001.
Prinderea excepţiilor
Pentru a beneficia de avantajele mecanismului de tratare a excepţiilor, odată ce am aruncat o excepţie
este nevoie să o prindem. Prinderea unei excepţii se face prin intermediul unui bloc try-catch, a cărui sintaxă
generică este prezentată mai jos.


try
{
//Cod ce poate arunca o excepţie
}
catch(<clasExcept1> <idExcept1>)
{
//handler exceptie de tip <clasExcept1>
}
catch(<clasExcept2> <idExcept2>)
{
//handler exceptie de tip <clasExcept2>
}

[finally
{
//secvenţa de cod executată oricum
}]

După cum se poate observa, structura de prindere a excepţiilor poate fi delimitată în trei blocuri.
Blocul try, numit şi bloc de gardă atrage atenţia că secvenţa de cod inclusă în el poate arunca, în
anumite condiţii, excepţii. În cazul în care acest lucru nu se întâmplă, secvenţa din interiorul blocului de gardă se
execută în întregime, controlul fiind predat primei instrucţiuni de după construcţia try-catch.
În cazul în care se aruncă o excepţie, execuţia secvenţei din blocul de gardă se întrerupe şi se
declanşează procedura de tratare a excepţiei.
Tratarea excepţiei se poate face prin intermeiul blocurilor catch, numite şi handlere de excepţii. În
momentul în care apare o excepţie în regiunea de gardă, se parcurge lista blocurilor catch în ordinea în care apar
în programul sursă. În cazul în care excepţia aruncată corespunde unui bloc catch, se execută codul eferent
blocului şi se termină căutarea în listă, considerându-se că excepţia a fost rezolvată. Sintaxa ne arată că pot exista
mai multe blocuri catch, ceea ce înseamnă că în blocul de gardă pot fi aruncate excepţii de mai multe tipuri.
Situaţiile deosebite care pot apare în utilizarea blocurilor catch sunt următoarele: am putea dori să
tratăm excepţii de tip EC1 şi EC2, unde EC2 este o clasă derivată din EC1. Datorită faptului că blocurile
catch sunt parcurse secvenţial este necesar să avem handler-ul clasei EC2 înaintea handler-ului clasei EC1,
altfel, nu se va ajunge niciodată la secvenţa catch de tratare a excepţiilor de tipul EC2.
De asemenea, putem avea situaţii în care să dorim tratarea unei excepţii pe mai multe nivele. În acest
caz, se poate lua în considerare faptul că, odată prinsă o excepţie într-un bloc catch, o putem re-arunca cu un
apel simplu de tip throw.
În sfârşit, blocul finally, dacă este folosit, cuprinde secvenţa de cod care se va executa, indiferent dacă
apare sau nu o excepţie, situaţie reclamată de nenumărate contexte în care apariţia unei excepţii, ca şi lipsa
acesteia, presupun rezolvarea unor probleme care pot scuti sistemul de introducerea unor elemente perturbatoare
prin nerezolvarea lor.
În Exemplul 6.1 şi în Exemplul 6.2 se pot vedea elementele de bază ale tratării excepţiilor într-o
aplicaţie Java. Utilizarea cuvântului cheie finally nu mi se pare o problemă deosebită.

Exemplul 6.1
//Metoda arunca o exceptie la nivelul superior
//Clasa care modeleaza exceptiile clasei NumarReal
class ENumarReal extends Exception
{
public ENumarReal(String s)
{
super(s);
};
}

//Clasa NumarReal
//o tentativa de modelare a lucrului cu numere reale
class NumarReal
{
private double numar;

public NumarReal(double nr)


{
numar=nr;
};

public double getNumar()


{
return numar;
};

//Metoda div() imparte doua numere reale


//Suspecta de a arunca o exceptie la impartirea la zero
//Declara acest lucru cu ajutorul cuvantului cheie throws
public NumarReal div(NumarReal n) throws ENumarReal
{

if (n.getNumar()==0)
throw new ENumarReal("Exceptie...Impartire la zero...");
else
return new NumarReal(this.getNumar()/n.getNumar());
};
}

public class Except1


{
public static void main(String [] s)
{
//Blocul de garda care capteaza exceptia aruncata
//de metoda div()
try
{
NumarReal onr1=new NumarReal(12);
NumarReal onr2=new NumarReal(6);
System.out.println(onr1.div(onr2).getNumar());
onr1=new NumarReal(11);
onr2=new NumarReal(0);
onr1.div(onr2);
}
catch(ENumarReal e)
{
System.out.println(e.getMessage());
};
};
};

Exemplul 6.2
//Metoda arunca o exceptie dar o si capteaza
//Clasa care modeleaza exceptiile clasei NumarReal
class ENumarReal extends Exception
{
public ENumarReal(String s)
{
super(s);
};
}

//Clasa NumarReal
//o tentativa de modelare a lucrului cu numere reale
class NumarReal
{
private double numar;

public NumarReal(double nr)


{
numar=nr;
};

public double getNumar()


{
return numar;
};

//Metoda div() imparte doua numere reale


//suspecta de a genera o exceptie la impartirea la zero
//Are bloc try-catch pentru captarea si tratarea
//exceptiei
public NumarReal div(NumarReal n)
{
try
{
if (n.getNumar()==0)
throw new ENumarReal("Exceptie...Impartire la zero...");
else
return new NumarReal(this.getNumar()/n.getNumar());
}
catch(ENumarReal e)
{
System.out.println(e.getMessage());
return new NumarReal(0);
};
};
}

public class Except


{
public static void main(String [] s)
{
NumarReal onr1=new NumarReal(12);
NumarReal onr2=new NumarReal(6);
System.out.println(onr1.div(onr2).getNumar());
onr1=new NumarReal(11);
onr2=new NumarReal(0);
onr1.div(onr2);
};
};
7 Programare generică în C++ şi Java
7.1 Ce este programarea generică
Adeseori, programatorul se află în situaţia de a efectua acelaşi tip de prelucrare asupra unor tipuri de
date diferite. Soluţia începătorului este “scrierea de cod complet pentru fiecare tip de dată”. Vrem să sortăm un
fişier după o cheie întreagă? Scriem o funcţie care realizează acest lucru, folosind, de exemplu, metoda bulelor.
Vrem să sortăm un fişier după o cheie alfanumerică? Scriem o funcţie care ştie să sorteze fişierul după o astfel de
cheie, folosind tot metoda bulelor. Nu ne va fi greu să observăm că, în cele două rezolvări date de noi există
un element de invarianţă: codul şablon care efectuează sortarea. Deosebirile se referă la tipurile de date
implicate în procesul de sortare (fişierele pot avea înregistrări de lungime diferită şi, evident, cu structură
diferită iar cheile de sortare pot fi diferite ca tip). Problema în faţa căreia ne aflăm nu este o problemă de
algoritmică ci una de tehnică de programare. Programarea care are în vedere specificarea unor structuri de
prelucrare capabile să opereze asupra unor tipuri variate de date se numeşte programare generică.
Evident, există limbaje de programare în specificarea cărora au fost prevăzute şi elemente suport pentru
rezolvarea acestui tip de problemă. De exemplu, în Object Pascal se poate face programare generică apelând la
tipuri procedurale şi la referinţele de tip pointer. În C++ se pot utiliza, în scopuri generice, suprascrierea
funcţiilor, conceptul de pointer, funcţiile şablon sau clasele şablon şi pointerii la funcţii. În sfârşit, în Java,
utilizând cu abilitate moştenirea şi interfeţele putem simula genericitatea de o manieră destul de acceptabilă.
Aş evidenţia, dintre toate tipurile de elemente suport prezentate mai sus, clasele şablon din C++, socotite
abstracţii foarte puternice, care permit simularea a ceea ce, în ingineria softului, numim metaclase.

7.2 Genericitatea în Java


Java nu dispune de pointeri şi de template-uri în adevăratul sens al cuvântului. S-ar putea crede că
genericitatea este dificilă sau aproape imposibilă în Java. Adevărul este că lucrurile nu stau chiar aşa. În
programarea orientată pe obiecte Java, putem combina forţa referinţelor la clase cu puterea oferită de moştenire
şi interfeţe pentru a obţine un suport interesant pentru programarea generică. Moştenirea ajută la crearea
cadrului organizat în care putem specifica mai multe tipuri de obiecte asupra cărora efectuăm aceleaşi
prelucrări. Conversiile down, permise între “rubedeniile” unei ierarhii de clase sunt esenţiale pentru a
implementa genericitatea. Interfeţele ajută la specificarea cadrului natural de introducere, în Java, a referintelor
la metodele membre ale unor clase. Exemplul 7.1 ilustrează rolul moştenirii în scrierea de cod Java pentru
crearea şi vizualizarea unei liste simplu înlănţuite generice.
Exemplul 7.2 ilustrează simularea pointerului la o metodă generică, în Java, cu ajutorul unei instanţe a
unei clase singleton.
Exemplul 7.3 ilustrează simularea pointerului la o metodă generică, în Java, cu ajutorul interfeţelor.

Exemplul7.1
//Clasa care modeleaza nodul listei
//capabil sa pastreze orice tip de data
class Nod
{
private Object inf;
Nod legs;
public Object read()
{
return inf;
};
public void write(Object x)
{
inf=x;
};
}

//Clasa care modeleaza comportamentul


//unei liste simplu inlantuite
class Lista
{
Nod start;
Nod prec;
public Lista()
{
start=null;
};
public void adaugdupa(Object on)
{
if(start==null)
{
Nod tamp=new Nod();
start=tamp;
start.legs=null;
start.write(on);
prec=start;
}
else
{
Nod tamp=new Nod();
tamp.write(on);
prec.legs=tamp;
tamp.legs=null;
prec=tamp;
}
}

//Metoda nu respecta cerintele


//care i-ar da dreptul sa figureze
//in API-ul clasei.
//Am specificat-o din motive didactice.
//Se poate observa un prilej potrivit pentru utilizarea
//enuntului instance of
//In intentie, aceasta metoda este un iterator
public void PentruToate()
{
Nod w=start;
int tip=0;
Integer i;
String s;
do
{
if(w.read() instanceof Integer) tip=1;
if(w.read() instanceof String) tip=2;
switch(tip)
{
case 1:
{
i=(Integer)(w.read());
System.out.println(i);break;
}
case 2:
{
s=(String)(w.read());
System.out.println(s);break;
}
};
w=w.legs;
}
while(w!=null);

}
}

public class CreLisGen


{
public static void main(String[] arg)
{

Nod obiect;
Lista lis=new Lista();
for(int i=0;i<8;i++)
{
obiect=new Nod();
obiect.write(new Integer(i).toString());
lis.adaugdupa(obiect.read());
obiect=new Nod();
obiect.write(new Integer(i));
lis.adaugdupa(obiect.read());
}
lis.PentruToate();
}
}

Exemplul 7.2
//Clasa pretextInt introduce o strategie concreta de comparare
//relativ la numere intregi
//Exemplu de clasa SINGLETON
class pretextInt
{

//Constructor privat
//pentru a asigura caracterul de singleton
private pretextInt()
{
};

public static final pretextInt INSTANCE=new pretextInt();

public int comp(Object a,Object b)


{
Integer ia,ib;
ia=(Integer)a;
ib=(Integer)b;
if(ia.intValue()<ib.intValue())return -1;
if(ia.intValue()==ib.intValue())return 0;
else return 1;
};
};

//Clasa pretextInt introduce o strategie concreta de comparare


//relativ la numere reale in virgula mobila
//Exemplu de clasa SINGLETON
class pretextFlo
{
//Constructor privat
//pentru a asigura caracterul de singleton
private pretextFlo()
{
};

public static final pretextFlo INSTANCE=new pretextFlo();

public int comp(Object a,Object b)


{
Float fa,fb;
fa=(Float)a;
fb=(Float)b;
if(fa.floatValue()<fb.floatValue())return -1;
if(fa.floatValue()==fb.floatValue())return 0;
else return 1;
};
};

public class Simpfunc


{
public static void main(String[] s)
{
Integer nri1=new Integer(100);
Integer nri2=new Integer(200);
Float nrf1=new Float(1.75);
Float nrf2=new Float(1.0);
System.out.println(pretextInt.INSTANCE.comp(nri1,nri2));
System.out.println(pretextFlo.INSTANCE.comp(nrf1,nrf2));
};
};

Exemplul 7.3
//Interfata prin intermediul careia se va simula
//ideea de pointer la metoda comp
interface SimPointMet
{
int comp(Object a,Object b);
};

//Clasa gazda a primei versiuni a metodei comp


//Va utiliza interfata SimPointMet
class pretextInt implements SimPointMet
{

public pretextInt()
{
};

public int comp(Object a,Object b)


{
Integer ia,ib;
ia=(Integer)a;
ib=(Integer)b;
if(ia.intValue()<ib.intValue())return -1;
if(ia.intValue()==ib.intValue())return 0;
else return 1;
};
};

//Clasa gazda a celei de-a doua versiuni a metodei comp


//Va utiliza interfata SimPointMet
class pretextFlo implements SimPointMet
{

public pretextFlo()
{
};

public int comp(Object a,Object b)


{
Float fa,fb;
fa=(Float)a;
fb=(Float)b;
if(fa.floatValue()<fb.floatValue())return -1;
if(fa.floatValue()==fb.floatValue())return 0;
else return 1;
};
};

public class PointMet


{
public static void main(String[] s)
{
//Interfata SimPointMet lucreaza in context pretextInt
Integer ni1,ni2;
Float nf1,nf2;
SimPointMet INSTANCE1=new pretextInt();

//Interfata SimPointMet lucreaza in context pretextFlo


SimPointMet INSTANCE2=new pretextFlo();
ni1=new Integer(100);
ni2=new Integer(200);
nf1=new Float(200);
nf2=new Float(100);
System.out.println(INSTANCE1.comp(ni1,ni2));
System.out.println(INSTANCE2.comp(nf1,nf2));
};
};
8 Fluxuri obiect orientate şi serializare în Java
8.1 Scurtă introducere
Deşi mai tânăr decât C++, Java a acumulat deja o experienţă apreciabilă în ceea ce priveşte rezolvarea
problemei persistenţei datelor. El propune mai multe ierarhii de clase, care pun în valoare conceptul, deja clasic,
de flux şi propune şi elemente suport pentru serializarea colecţiilor de obiecte. La fel ca în C++, stream-urile
Java oferă posibiliatea tratării unitare a interfeţelor de comunicare între entităţile unui sistem informatic, fie ele
entităţi soft sau hard.

Un stream este un canal de comunicaţie generalizat, definit în mod


unic prin “capetele” sale: sursa şi destinaţia.

De cele mai multe ori, unul din capete este chiar programul în care se declară stream-ul. Şi în Java,
există două tipuri fundamentale de stream-uri: input stream-urile, utilizate pentru citirea datelor din diferite
surse şi output stream-urile, utilizate pentru scrierea datelor în diferite destinaţii. Mai putem observa şi alte
asemănări între perspectiva Java şi perspectiva C++, în ceea ce priveşte persistenţa: există fluxuri standard şi
alte fluxuri decât cele standard (relativ la fişiere, relativ la şiruri de caractere, relativ la buffe-re de octeţi),
există filtre de diferite tipuri.
Programatorul care vrea să înveţe să lucreze eficient cu fluxurile în Java, se izbeşte de o situaţie
oarecum asemănătoare celei din C++, dacă nu cumva mai rea:

Instrumentele puse la dispoziţie de Sun sunt extrem de diversificate şi se promovează chiar


filozofii diferite de lucru cu fluxurile, datorită faptului că prima ierarhie de clase care fundamenta lucrul
cu fluxuri era orientată pe 8 biţi ( două ierarhii având drept clase rădăcină clasele InputStream şi
OutputStream ) iar din raţiuni de implementare a conceptului de Internationalization s-a dezvoltat o soluţie
alternativă care este orientată pe 16 biţi (două ierarhii având drept clase rădăcină clasele Reader şi Writer)14.

Astfel că, programatorul se confruntă cu două ierarhii de clase, între care există destule asemănări
pentru a nu dispera cu totul dar şi destule deosebiri pentru a nu putea renunţa la nici una dintre ele deocamdată.
Cert este că soluţia Java pentru lucrul cu fluxuri este puternic orientată pe obiecte, ca soluţie tehnică. În sfârşit,
să mai precizăm faptul că oferta C++ pentru salvarea-restaurarea obiectelor îşi găseşte în Java un răspuns mai
îndrăzneţ, sub forma serializării.
Despre toate acestea în cele ce urmează.

8.2 Stream-uri standard în Java


Java pune la dispoziţia utilizatorului, în ideea comunicării cu consola, trei stream-uri standard:

• Standard Input
• Standard Output
• StandardError.

Stream-ul Standard Input este utilizat pentru preluarea datelor, în timp


ce celelalte două sunt utilizate pentru afişarea datelor şi a mesajelor de eroare. Implicit, Standard Input preia
datele de la tastatură iar celelalte două afişează datele la monitor. Unul dintre avantajele utilizării stream-urilor,
în comunicarea cu utilizatorul, îl reprezintă şi posibilitatea de a redirecta stream-urile standard spre alte
periferice.
În Java, toate stream-urile standard sunt accesate prin clasa System: pentru Standard Input avem
System.in, pentru Standard Output avem System.out, pentru Standard Error avem System.err.
System.in este un membru static al clasei System şi este de tipul InputStream, o clasă abstractă din
pachetul java.io. O parte dintre funcţiile clasei InputStream şi aspecte relativ la redirectare în cele ce urmează.

14
Pentru mai multe detalii relativ la structura acestor ierarhii se poate consulta Călin Marin Văduva, Programarea în Java, Editura Albastră,
Cluj-Napoca, 2001
Funcţii de citire şi de control al poziţiei la citire:

public abstract int read() throws IOException


public int read(byte b[]) throws IOException
public int read(byte b[], int off, int len) throws IOException
public long skip(long n) throws IOException

Funcţii de repetare citire, funcţii de gestiune buffer:

public synchronized void mark (int readlimit)


public synchronized void reset() throws IOException
public boolean markSuported()

Funcţii de informare:

public int available() throws IOException

Funcţia de închidere stream:

public void close() throws IOException.

De fapt, aceste metode ale clasei InputStream prefigurează elementele fundamentale ale strategiei Java
de lucru cu fluxurile.
Funcţia read(), fără nici un parametru, citeşte octetul curent din stream şi îl returnează sub forma unui
întreg între 0 şi 255. Dacă s-a ajuns la capătul stream-ului, se returnează valoarea -1. Funcţiile read, având
ca parametru un tablou de octeţi, citesc de la poziţia curentă din stream un număr de octeţi egal cu len sau cu
lungimea tabloului b şi îl încarcă în tabloul b, la poziţia off dacă aceasta este specificată. Ele returnează numărul
de octeţi citiţi în buffer-ul b sau -1 dacă s-a ajuns la capătul stream-ului.
Funcţia skip este utilizată pentru a muta poziţia citirii peste un anumit număr de octeţi. Toate aceste
metode blochează firul de execuţie în care ne aflăm, până când toate datele care se cer sunt disponibile, s-a ajuns
la sfârşitul stream-ului sau s-a aruncat o excepţie.

Redirectarea stream-urilor standard se poate realiza cu ajutorul următoarelor trei funcţii, disponibile în
clasa System:

public static void setIn(InputStream in)


public static void setOut(PrintStream out)
public static void setErr(PrintStream err)

În Exemplul 8.1 sunt arătate elementele de protocol fundamentale pentru lucrul cu stream-uri în Java,
cu referire la stream-urile standard.
Este vorba despre următoarele elemente invariabile:

• Asocierea fluxului cu un fişier, echipament standard sau altă structură de date.


• Efectuarea de operaţii de tipul citire sau scriere de date.
• Poziţionarea în flux, când acest lucru este posibil
• Închiderea fluxului

Aşa cum se va vedea şi în exemplele care vor urma şi cum, de altfel, era previzibil din signatura
metodelor pe care le-am anunţat ca făcând parte din structura clasei InputStream, tratarea excepţiilor în cazul
operaţiilor I/O este imperativă.

În Exemplul 8.2 se arată cadrul Java pentru redirectarea stream-urilor standard.

Exemplul 8.1
//Utilizare stream-uri standard
//Acestea sunt asociate implicit cu echpamentele periferice
//Tastatura – Sistem.in
//Ecranul monitorului – Sistem.out / Sistem.err
import java.io.*;
import java.util.*;
public class IO1
{
public static void main(String[] s)
{
boolean exit=false;
System.out.println("Incerc IO\n "+ " Informatii despre sistem");
while(!exit)
{
System.out.println("Optiuni....");
System.out.println("\t (D) Data");
System.out.println("\t (P) Proprieteti sistem");
System.out.println("\t (T) Terminare");
try
{
char readChar=(char)System.in.read();
int avlb=System.in.available();
System.in.skip(avlb);
switch(readChar)
{
case 'D':
case 'd': System.out.println("Data:"+
new Date().toString());
break;
case 'P':
case 'p': Properties prop=System.getProperties();
prop.list(System.out);
break;
case 'T':
case 't': System.out.println("La revedere...");
exit=true;
break;
}
}
catch(IOException e)
{
System.err.println(e.getMessage());
}
}
}
}

Exemplul 8.2
// Exemplifică redirectarea stream-urilor standard
import java.io.*;
public class Redirect
{
// Arunca exceptii IOException la consola
public static void main(String[] args) throws IOException
{
//Flux de intrare cu buffer asociat cu fisierul
//text care contine programul
BufferedInputStream in = new BufferedInputStream(
new FileInputStream("Redirect.java"));
//Filtru asociat cu fluxul definit mai sus
PrintStream out =new PrintStream( new BufferedOutputStream(
new FileOutputStream("test.out")));

//Redirectare fluxuri standard


System.setIn(in);
System.setOut(out);
System.setErr(out);

//Filtrarea stream-ului standard cu ajutorul clasei


//BufferedReadre pentru a permite utilizarea metodei readLine()
//versiune ne-deprecated.

//Deschidere flux
BufferedReader br = new BufferedReader(new InputStreamReader (System.in));
String s;

//Citire flux pana la terminare


while((s = br.readLine()) != null)
System.out.println(s);

//Inchidere flux
out.close();
}
}

8.3 Clasa File în lucrul cu stream-uri


Clasa File, din biblioteca I/O Java, furnizează o abstractizare independentă de platformă pentru obţinerea
informaţiilor despre fişiere, ca de exemplu: numele de cale, dimensiunea fişierului, data modificării, etc. Pentru a
obţine astfel de informaţii despre fişier trebuie ca, mai întâi, să creaţi un obiect File utilizând unul din
constructorii de mai jos:

File (String cale);


File (String cale, String nume);
File (File dir, String nume);

Parametrul cale din prima versiune de constructor conţine calea către fişier, în timp ce acelaşi parametru, din
cea de-a doua versiune, conţine calea directorului. Paramerul nume specifică numele fişierului. Parametrul dir,
din ce-a de-a treia versiune permite utilizarea unui alt obiect File, ca director.
Utilitatea clasei File poate fi desprinsă, ca un început, şi din Exemplul 8.3 şi Exemplul 8.4.

Exemplul8.3
//Listarea tuturor fişierelor din directorul curent
import java.io.*;
public class TestFile
{
public static void main(String[] sir)
{
File dc=new File(".");
String listaf[]=dc.list();
for(int i=0;i<listaf.length;i++)
{
if(i % 23==0)
{
try
{
System.in.read();
System.in.read();
}
catch(IOException e)
{}
};
System.out.println(listaf[i]);
}
}
}

Exemplul 8.4
//Listarea tuturor fisierelor din directorul curent
//avand o extensie data
import java.io.*;
class JavaFileFilter implements FilenameFilter
{
public boolean accept(File dir, String nume)
{
return nume.endsWith(".java");
}
}
public class FiltruF
{
public static void main(String[] sir)
{
File dc=new File(".");
String listaf[]=dc.list(new JavaFileFilter());
for(int i=0;i<listaf.length;i++)
{
if(i % 23==0)
{
try
{
System.in.read();
System.in.read();
}
catch(IOException e)
{}
};
System.out.println(listaf[i]);
}
}
}

8.4 Citirea datelor dintr-un stream


Aşa cum, probabil că s-a înţeles, există două grupuri mari de stream-uri, în funcţie de obiectivul lor:
scrierea sau citirea datelor.
Pentru citirea datelor dintr-un flux avem clasele derivate din clasele abstracte InputStream sau
Reader. Amândouă aceste clase sunt clase abstracte care furnizează metode care permit operaţii asemănătoare
celor pe care le-am prezentat deja în discuţia referitoare la stream-urile standard.
Referindu-ne la InputStream, fiind o clasă abstractă nu poate fi utilizată în instanţierea unui obiect
stream. Pentru crearea obiectelor de tip stream, pornind de la clasa InputStream, s-au derivat mai multe clase.
Aceste clase le-am putea împărţi, la rândul lor, în două grupuri importante:

• clase stream conectate la diferite tipuri de surse;


• clase stream care se conectează la cele de mai sus, adăugând noi operaţii şi funcţionând ca “filtre”
aplicate operaţiilor de citire.

Clasele din prima categorie sunt derivate direct din clasa InputStream.
Pentru a putea utiliza efectiv interfaţa anunţată de clasa InputStream a fost nevoie de construirea unor clase
derivate din aceasta, clase care să poată fi conectate la diferite tipuri de surse reale. Dintre aceste clase remarcăm
ca fiind cel mai mult folosite:

• ByteArrayInputStream
Este o clasă care permite conectarea unui stream la un tablou de octeţi. Operaţiile de citire din stream vor
permite citirea datelor din tabloul de octeţi, gestiunea operaţiilor fiind asumată de către instanţa stream.

• StringBufferInputStream
Permite conectarea unui stream la un şir de caractere. Această clasă este considerată deprecated,
recomandându-se utilizarea clasei StringReader.

• FileInputStream
Este una dintre cele mai utilizate clase de tip stream şi ne oferă posibilitatea conectării cu un fişier pentru a
citi datele înregistrate în acesta. După cum se poate vedea, la analiza atentă a definiţiei clasei
FileInputStream, aceasta conţine mai multe versiuni de constructori, care permit asocierea stream-ului cu
un fişier în diferite moduri: numele specificat ca o variabilă sau constantă String, numele specificat ca o
variabilă File, numele specificat ca o variabilă FileDescriptor.

O categorie importantă de clase derivate din InputStream o formează clasele de tip “filtru”, derivate
din clasa FilterInputStream, la rândul ei, derivată din clasa InputStream.
Dintre clasele din această categorie se cuvine să remarcăm câteva utilizate intens:

• DataInputStream
Este una dintre cele mai utilizate clase dintre cele de tip filtru. Această clasă conţine mai multe funcţii, care
permit citirea unor tipuri fundamentale de date (int, float, double, char, etc) într-un mod independent de
maşină. De regulă, această clasă este utilizată împreună cu clasa DataOutputStream, clasă care are operaţii
de scriere în stream, orientate pe tipurile fundamentale. Împreună, aceste două clase, oferă o soluţie elegantă
la problema gestiunii fişierelor a căror înregistrare are structura definită de utilizator.

Este momentul să remarcăm că, în principiu, în Java, la fel ca în C++, putem avea fluxuri de octeţi,
fluxuri de caractere şi fluxuri de date cu structură cunoscută.

Revenind la clasa DataInputStream, prezentăm, în continuare, câteva dintre metodele mai mult folosite.

Metoda Rolul
boolean readBoolean() Citeşte o dată booleană
byte readByte Citeşte un octet
Int readUnsignedByte() Citeşte un octet unsigned
short readShort() Citeşte un short (16 biţi)
char readChar() Citeşte un caracter Unicode
int readInt() Citeşte un întreg pe 32 biţi
long readLong() Citeşte un long pe 64 biţi
float readFloat() Citeşte un număr real în virgulă mobilă simplă
precizie
double readDouble() Citeşte un număr real în virgulă mobilă dublă
precizie
String readLine() Citeşte o linie
String readUTF() Citeşte un şir de caractere în format UTF
(Unicode Text Format)

Tabel 1. Metode ale clasei DataInputStream


Dintre clasele de tip filtru merită să mai remarcăm şi clase precum: BufferedInputStream,
LineNumberInputStream, ZipInputStream, etc.

8.5 Scrierea datelor într-un stream


Pentru scrierea datelor într-un stream avem clasele derivate din clasele abstracte OutputStream sau
Writer. Amândouă aceste clase sunt clase abstracte, care furnizează metode care permit operaţii de scriere a
datelor în stream-uri, complementare celor de citire, ca funcţionalitate.

Referindu-ne la clasa OutputStream, fiind o clasă abstractă nu poate fi utilizată în instanţierea unui
obiect stream. Totuşi, ea este o ocazie de a specifica o interfaţă general valabilă în operaţiile de scriere în fluxuri,
având următoarea definiţie:

public abstract class OutputStream


{
public abstract void write(int b) throws IOException
public void write(byte b[] ) throws IOException
public void write(byre[], int off, int len) throws IOException
public void flush()throws IOException
public void close()throws IOException
}

Pentru crearea obiectelor de tip stream, pornind de la clasa OutputStream, s-au derivat mai multe
clase. Aceste clase le-am putea împărţi, la rândul lor, în două grupuri importante:

• clase stream conectate la o destinaţie;


• clase stream care se conectează la cele de mai sus, adăugând noi operaţii şi funcţionând ca “filtre”
aplicate operaţiilor de scriere.

Clasele din prima categorie sunt derivate direct din clasa OutputStream.
Pentru a putea utiliza efectiv interfaţa anunţată de clasa OutputStream a fost nevoie de construirea unor
clase derivate din aceasta, clase care să poată fi conectate la diferite tipuri de destinaţii reale. Dintre aceste clase
remarcăm ca fiind cel mai mult folosite:

• ByteArrayOutputStream
Este o clasă care permite conectarea unui stream la un tablou de octeţi. Operaţiile de scriere în stream vor
permite adăugare de date în tabloul de octeţi, gestiunea operaţiilor fiind asumată de către instanţa stream.

• FileOutputStream
Este clasa pereche a clasei FileInputStream, dintre cele mai utilizate clase de tip stream şi ne oferă
posibilitatea conectării cu un fişier pentru a scrie date în acesta. După cum se poate vedea, la analiza atentă a
definiţiei clasei FileOutputStream, aceasta conţine mai multe versiuni de constructori, care permit
asocierea stream-ului cu un fişier în diferite moduri: numele specificat ca o variabilă sau constantă String,
numele specificat ca o variabilă File, numele specificat ca o variabilă FileDescriptor.

O categorie importantă de clase derivate din OutputStream o formează clasele de tip “filtru”, derivate
din clasa FilterOutputStream, la rândul ei, derivată din clasa OutputStream.
Dintre clasele din această categorie se cuvine să remarcăm câteva utilizate intens:

• DataOutputStream
Este una dintre cele mai utilizate clase dintre cele de tip filtru. Această clasă conţine mai multe funcţii care
permit citirea unor tipuri fundamentale de date (int, float, double, char, etc) într-un mod independent de
maşină. De regulă, această clasă este utilizată împreună cu clasa DataInputStream, clasă care are operaţii
de citire în stream, orientate pe tipurile fundamentale. Împreună, aceste două clase oferă o soluţie elegantă la
problema gestiunii fişierelor, a căror înregistrare are structura definită de utilizator.
Clasa DataOutputStream, are o serie de metode folosite, după caz, la realizarea operaţiilor de scriere
în stream-uri.

Metoda Rolul
void writeBoolean(boolean v) Scrie o dată booleană
void writeByte(int v) Scrie un octet
void writeBytes(String s) Scrie un şir de caractere ca o secvenţă de
octeţi
void writeShort(int v) Scrie un short (16 biţi)
void writeChar(int v) Scrie un caracter Unicode
void writeInt(int v) Scrie un întreg pe 32 biţi
void writeLong(long v) Scrie un long pe 64 biţi
void writeFloat(float v) Scrie un număr real în virgulă mobilă
simplă precizie
void writeDouble(double v) Scrie un număr real în virgulă mobilă
dublă precizie
void writeChars(String s) Scrie un şir de caractere ca o secvenţă de
16 biţi
void writeUTF(String S) Scrie un şir de caractere în format UTF
(Unicode Text Format)

Tabel 2. Metode ale clasei DataOutputStream


Dintre clasele de tip filtru merită să mai remarcăm şi clase precum: BufferedOutputStream,
PrintStream, ZipOutputStream, etc.

Relativ la lucrul cu fişiere, un rol important îl joacă clasa RandomAccessFile, care nu este subclasă
nici a clasei InputStream, nici a clasei OutputStream. Însă, cu ajutorul instanţelor ei, puteţi efectua în acelaşi
timp atât operaţii de scriere cât şi de citire. În plus, după cum arată şi numele, un obiect RandomAccessFile
furnizează acces aleator la datele dintr-un fişier, ceea ce instanţele descendenţilor claselor InputStream sau
OutputStream nu pot. Pentru compatibilitate, la utilizare, cu clasele DataInputStream şi DataOutputStream,
clasa RandomAccessFile implementează interfeţele DataOutput şi DataInput, interfeţe pe care le
implementează şi clasele DataInputStream şi DataOutputStream.

O discuţie asemănătoare se poate purta relativ la ierarhiile de clase ale căror rădăcini sunt clasele
Reader şi Writer, iearhii care implementează alternativa I/O Java pe 16 biţi. Funcţionalitatea lor, însă, nu elimină
cu totul utilitatea ierarhiilor pe care le-am prezentat mai sus, pe scurt. Înţelegerea exactă a modului de lucru cu
oricare dintre ierarhiile menţionate mai sus poate fi realizată consultând documentaţia aferentă kit-urilor jdk1.o
sau jdk1.1.

“Jungla” protocoalelor de lucru cu stream-uri în Java este, după cum se vede, mult mai diversificată
decât oferta C++. Programatorul din lumea reală trebuie să se acomodeze cu elementele fundamentale relativ la
stream-urile Java, rămânând ca în situaţii excepţionale să înveţe utilizarea unor procedee excepţionale de
manevrare a stream-urilor. Exemplele care urmează încearcă să evidenţieze elemente de protocol socotite
uzuale în lucrul cu stream-uri în Java.

Exemplul 8.5
//Situatii tipice de utilizarea fluxurilor in Java
import java.io.*;

public class IOStreamDemo


{
// Metoda ridica exceptii la consola
public static void main(String[] args) throws IOException
{
//1a. Citirea orientata pe linii intr-un fisier text
BufferedReader in = new BufferedReader(
new FileReader("IOStreamDemo.java"));
String scit;
String sImRAM = new String();

//s2 pastreaza continutul fisierului IOStreamDemo.java


//ca imagine RAM
while((scit = in.readLine())!= null)
sImRAM += scit + "\n";
in.close();

// 1b. Citire de la tastatura:


BufferedReader stdin =new BufferedReader(
new InputStreamReader(System.in));
System.out.print("Enter a line:");
System.out.println(stdin.readLine());
System.in.read();

// 2. Citire din memorie


//Se va folosi sImRAM, creat la 1a
StringReader in2 = new StringReader(sImRAM);
int c;

//Afisare imagine memorie a continutului


//fisierului IOStreamDemo.java
while((c = in2.read()) != -1)
System.out.print((char)c);
System.in.read();
System.in.read();

// 3. Preluare date formatate in memorie


//Din nou se apeleaza la imaginea memorie a
//fisierului IOStreamDemo.java

try
{
DataInputStream in3 =new DataInputStream( new
ByteArrayInputStream(sImRAM.getBytes()));
while(true)
System.out.print((char)in3.readByte());
}
catch(EOFException e)
{
System.err.println("End of stream");
}
System.in.read();
System.in.read();
// 4. Creare fisier format output
try
{
BufferedReader in4 =new BufferedReader(
new StringReader(sImRAM));
PrintWriter out1 =new PrintWriter(
new BufferedWriter(
new FileWriter("IODemo.out")));
int lineCount = 1;
while((scit = in4.readLine()) != null )
out1.println(lineCount++ + ":" + scit);
out1.close();
}
catch(EOFException e)
{
System.err.println("End of stream");
}

String sir;
BufferedReader inper = new BufferedReader(
new FileReader("IODemo.out"));
System.out.println("################################");
while((sir = inper.readLine())!= null)
System.out.println(sir);

inper.close();
System.in.read();
System.in.read();

// 5. Salvare si consultare date cu tip


try
{
DataOutputStream out2 =new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("Data.txt")));
out2.writeDouble(3.14159);
out2.writeBytes("Acesta este numarul PI\n");
out2.writeDouble(1.41413);
out2.writeUTF("Radacina patrata a lui 2");
out2.close();
DataInputStream in5 =new DataInputStream(
new BufferedInputStream(
new FileInputStream("Data.txt")));
BufferedReader in5br =new BufferedReader(
new InputStreamReader(in5));
// Trebuie sa folositi DataInputStream pentru date:
System.out.println(in5.readDouble());
// Numai metoda readUTF() va recupera
// sirul Java-UTF corect:
// Cu readLine() se citesc corect date
// scrise cu writeBytes.
System.out.println(in5br.readLine());
System.out.println(in5.readDouble());
System.out.println(in5.readUTF());
}
catch(EOFException e)
{
System.err.println("End of stream");
}

// 6.Citire/scriere fisier in acces aleator


RandomAccessFile rf =new RandomAccessFile("rtest.dat", "rw");
for(int i = 0; i < 10; i++)
rf.writeDouble(i*1.414);
rf.close();

rf =new RandomAccessFile("rtest.dat", "rw");


rf.seek(5*8);
rf.writeDouble(47.0001);
rf.close();
rf =new RandomAccessFile("rtest.dat", "r");
for(int i = 0; i < 10; i++)
System.out.println("Value " +i+":"+rf.readDouble());
rf.close();
}
}

Ca observaţii finale la cele discutate până acum relativ la problema persistenţei datelor în Java, aş
menţiona:
• Puternica orientare pe obiecte a soluţiilor Java la problema persistenţei.
• Flexibilitatea cu care putem utiliza diferitele varietăţi de fluxuri (filtrare, redirectare)
• Obligativitatea tratării excepţiilor I/O în codul Java, ceea ce sporeşte coeficientul de robusteţe al codului
Java afectat operaţiilor I/O.

8.6 Serializarea obiectelor


Java 1.1 introduce un nou concept, numit serializarea obiectelor. Utilizând serializarea, un obiect
poate fi transformat într-o secvenţă de octeţi ( care poate fi transmisă în reţea sau care poate fi stocată într-o
specie de memorie), secvenţă care poate fi folosită pentru refacerea completă a obiectului iniţial.
Avantajul serializării este evident, prin faptul că simplifică procedura de transmitere a unui obiect între
două entităţi ale unui sistem informaţional automatizat.

Prin utilizarea serializării, programatorul a scăpat de grija


transformării obiectului într-o succesiune de octeţi, de ordonarea lor,
de problema diferenţei de reprezentare pe diferite platforme, toate
acestea făcându-se automat.

În rezumat, putem spune că serializarea introduce un alt nivel de transmitere a datelor, la care unitatea
fundamentală de transfer este obiectul. În practică, serializarea obiectelor este impusă de situaţii precum:

• RMI (Remote Method Invocation)


Comunicare de obiecte între aplicaţii aflate pe calculatoare diferite, într-o reţea.
• Lightweight persistence
Posibilitatea stocării unui obiect, ca ansamblu unitar, în vederea utilizării lui în cadrul unei execuţii
ulterioare.
• Tehnologia Java Beans
Tehnologie Java de lucru cu componente.

Procedeul Java de serializare/deserializare a unui obiect este simplu, dacă acesta îndeplineşte anumite
condiţii.

Procedeul Java de serializare/deserializare


Serializarea unui obiect este o opereaţie relativ simplă, care implică lucrul cu clasa
ObjectOutputStream, care înfăşoară un stream de tipul OutputStream, conectat la o destinaţie. Conexiunea cu
un stream OutputStream se face prin intermediul constructorului:

public ObjectOutputStream(OutputStream out) throws IOException;

Clasa ObjectOutputStream este derivată din clasa OutputStream şi implementează interfaţa


ObjectOutput (derivată din DataOutput). Interfaţa ObjectOutput declară metodele specifice serializării unui
obiect. Dintre aceste metode, cea mai importantă este metoda writeObject având signatura:

public final void writeObject(Object obj) throws IOException

Prin intermediul clasei ObjectOutputStream, se pot scrie atât date primitive (cu ajutorul metodelor
declarate de interfaţa DataOutput) cât şi obiecte, folosind metoda writeObject.
Deserializarea (adică reconstituirea unui obiect dintr-un stream) este, de asemenea, simplă şi implică
utilizarea clasei ObjectInputStream. Această clasă înfăşoară un stream de tipul InputStream, stream transmis
ca şi parametru în constructorul clasei:

public ObjectInputStream(InputStream in)


throws IOException, StreamCorruptedException

Clasa ObjectInputStream este derivată din clasa InputStream şi implementează interfaţa


ObjectInput (derivată din interfaţa DataInput). Interfaţa ObjectInput declară metodele specifice deserializării
unui obiect. Dintre acestea, cea mai importantă este metoda readObject, definită astfel:

public final Object readObject()


throws OptionalDataException, ClassNotFoundException, IOException

Excepţia OptionalDataException apare în cazul în care, în locul unui obiect în stream se află un tip de
dată primitiv.
Excepţia ClassNotFoundException apare atunci când clasa obiectului din stream nu poate fi găsită, în
contextul actual de execuţie.
O înţelegere mai bună a serializării / deserializării poate fi obţinută urmărind exemplele de mai jos.

Exemplul 8.6
// Exemplificarea serializării obiectelor
import java.io.*;

class Data implements Serializable


{
private int i;
Data(int x)
{
i = x;
}
public String toString()
{
return Integer.toString(i);
}
}

public class Serializ implements Serializable


{
//Generare numar aleator intreg
private static int r()
{
return (int)(Math.random() * 10);
}

private Data[] d = {new Data(r()), new Data(r()), new Data(r())};


private Serializ next;
private char c;
// Valoarea lui i indică numarul de elemente din lista
Serializ(int i, char x)
{
System.out.println(" Serializ constructor: " + i);
c = x;
if(--i > 0)
next = new Serializ(i, (char)(x + 1));
}
Serializ()
{
System.out.println("Constructor implicit");
}
public String toString()
{
String s = ":" + c + "(";
for(int i = 0; i < d.length; i++)
s += d[i].toString();
s += ")";
if(next != null)
s += next.toString();
return s;
}
// Ridica exceptii la consola
public static void main(String[] args)
throws ClassNotFoundException, IOException
{
Serializ w = new Serializ(6, 'a');
System.out.println("w = " + w);
ObjectOutputStream out =new ObjectOutputStream(
new FileOutputStream("serializ.out"));
out.writeObject("Serialize storage");
out.writeObject(w);
out.close(); // Also flushes output
ObjectInputStream in =new ObjectInputStream(
new FileInputStream("serializ.out"));
String s = (String)in.readObject();
Serializ w2 = (Serializ)in.readObject();
System.out.println(s + ", w2 = " + w2);

ByteArrayOutputStream bout =new ByteArrayOutputStream();


ObjectOutputStream out2 =new ObjectOutputStream(bout);
out2.writeObject("Serializ storage");
out2.writeObject(w);
out2.flush();

ObjectInputStream in2 =new ObjectInputStream(


new ByteArrayInputStream(bout.toByteArray()));
s = (String)in2.readObject();
Serializ w3 = (Serializ)in2.readObject();
System.out.println(s + ", w3 = " + w3);
}
}

Exemplul 8.7
import java.io.*;
import java.util.*;

class House implements Serializable {}

class Animal implements Serializable


{
String name;
House preferredHouse;
Animal(String nm, House h)
{
name = nm;
preferredHouse = h;
}
public String toString()
{
return name + "[" + super.toString() + "], " + preferredHouse +
"\n";
}
}

public class Serializ1


{
public static void main(String[] args)
throws IOException, ClassNotFoundException
{
House house = new House();
ArrayList animale = new ArrayList();
animale.add(new Animal("Grivei -cainele", house));
animale.add(new Animal("Coco papagalul", house));
animale.add(new Animal("Vasile -motanul", house));
System.out.println("animale: " + animale);

ByteArrayOutputStream buf1 =new ByteArrayOutputStream();


ObjectOutputStream o1 =new ObjectOutputStream(buf1);
o1.writeObject(animale);

//Inca odata
o1.writeObject(animale);

// Scriem si intr-un stream diferit


ByteArrayOutputStream buf2 =new ByteArrayOutputStream();
ObjectOutputStream o2 =new ObjectOutputStream(buf2);
o2.writeObject(animale);

// Acum le citim
ObjectInputStream in1 =new ObjectInputStream(
new ByteArrayInputStream(buf1.toByteArray()));
ObjectInputStream in2 =new ObjectInputStream(
new ByteArrayInputStream(buf2.toByteArray()));
ArrayList animale1 =(ArrayList)in1.readObject();
ArrayList animale2 =(ArrayList)in1.readObject();
ArrayList animale3 =(ArrayList)in2.readObject();
System.out.println("animale1: " + animale1);
System.out.println("animale2: " + animale2);
System.out.println("animale3: " + animale3);
}
}

Exemplele 8.6 şi 8.7 arată, printre altele, şi condiţiile pe care trebuie să le îndeplinească un obiect pe care
vrem să îl serializăm. În speţă, este vorba de faptul că pentru ca obiectul să poată fi serializat, clasa lui
definitoare trebuie să respecte una din următoarele condiţii:

• Să implementeze interfaţa Serializable


În această situaţie transformările obiect-stream şi stream-obiect se pot face automat.

• Să implementeze interfaţa Externalizable şi să suprascrie metodele acesteia writeExternal şi


readExternal.
În acest caz, programatorul este responsabil (prin specificarea metodelor anterior amintite) de transformarea
obiectului în secvenţe de octeţi şi invers.
Evident, spaţiul de care dispunem nu ne permite să tratăm exhaustiv problemele pe care le pune asigurarea
persistenţei datelor în aplicaţiile Java. Scopul acestei cărţi a fost de a realiza o deschidere asupra universului
problematic dezvoltat în Java în jurul ideii de persistenţă. Stă în puterea fiecărui cititor în parte să se aplece cu
temeinicie asupra aspectelor de detaliu sau , de ce nu, filosofice, neelucidate încă.
9 Programare concurentă cu suport obiect orientat.
Perspectiva Java
9.1 Noţiuni de programare concurentă
Programarea concurentă presupune existenţa mai multor sarcini care trebuie să fie executate în paralel,
ceea ce implică, într-o formă sau alta, partajarea resurselor comune ale sistemului pe care se derulează execuţia
sarcinilor în cauză.
Făcând abstracţie de arhitectura hard15 care o susţine, programarea concurentă poate fi materializată prin
suport pentru programarea multitasking, suport pentru programarea multifir şi suport pentru
programarea distribuită.
Atunci când o aplicaţie este implementată pe un sistem multiprocesor, se spune că avem o aplicaţie
multiprocesată. În situaţia în care aplicaţia este implementată pe o reţea de calculatoare, spunem că aplicaţia este
distribuită.
În acest curs nu ne vom interesa de programarea distribuită şi nici de programarea multitasking. Pentru a fi
posibilă programarea multitasking, mai întâi sistemul de operare trebuie să fie capabil de execuţia simultană a
mai multor programe. În cazul în care aşa ceva este posibil, la nivelul limbajelor de programare ne putem pune
probleme de sincronizare a accesului la resursele comune. Din acest punct de vedere, relaţia dintre Java şi
Windows, ca sistem de operare, nu este extrem de cordială. În schimb Object Pascal, limbajul de programare în
mediul de programare vizuală Delphi, gândit pentru a realiza aplicaţii având ca ţintă platforma Windows, posedă
înveliş sintactic specific pentru rezolvarea problemelor de partajare a resurselor critice, în spiritul WIN32API16.
Pe de altă parte, programarea distribuită este posibilă, în Java, apelând la tehnologii care susţin corespunzător
acest stil de programare(RMI, CORBA, etc.).
În această carte ne vom interesa de posibilităţile pe care le oferă Java pentru a face programare multifir.
Majoritatea compilatoarelor de C++, legate de maşina MSDOS, nu oferă suport nativ sau înveliş sintactic
corespunzător pentru programarea multifir, ci doar rudimente sintactice pentru simularea greoaie a multitasking-
ului.
În sfârşit, să mai observăm că programarea multifir la care ne referim va fi asociată cu posibilităţile unei
maşini monoprocesor, ceea ce înseamnă, iarăşi, că sistemul de operare este “arbitrul” care stabileşte regulile de
bază care trebuie urmate pentru ca un fir de execuţie să poată accesa reursele partajabile ale maşinii (îndeosebi
timpul UC). Arbitrajul exercitat de sistemul de operare (în relaţia cu timpul UC) se reduce, practic, la acordarea
unor cuante de timp UC tuturor firelor de execuţie active la un moment dat, eventual, în funcţie de priorităţile
asociate acestora la creare. Toate celelalte probleme care decurg din execuţia simultană a mai multor fire de
execuţie sunt de competenţa programatorilor. Aceste probleme se regăsesc, generic, în sintagma “comunicare şi
sincronizare”, pentru care limbajele oferă mijloace a căror întrebuinţare este la latitudinea programatorilor.
Să mai observăm că noţiunea de fir de execuţie se referă la o unitate de prelucrare, asociată cu noţiunea de
proces, în sensul că fiecare fir de execuţie este găzduit în spaţiul de adrese al unui proces. În fine, trebuie spus că,
referindu-ne la Java, funcţia main() a unui program Java este, ea însăşi, un fir de execuţie, care se numeşte firul
principal de execuţie. O situaţie asemănătoare apare şi în Object Pascal unde programul principal, aflat în fişierul
cu extensia .dpr este asimilat cu noţiune de fir de execuţie principal.
Lucrul cu fire de execuţie este o necesitate, în foarte multe situaţii de programare. Dacă, de exemplu, ne
gândim la o aplicaţie care simulează calculul tabelar, nu este greu de priceput necesitatea mai multor fire de
execuţie: unul care se ocupă de interactivitatea cu utilizatorul, unul care gestionează implicaţiile modificării
conţinutului celulei curente asupra conţinutului altor celule, etc. Vom încerca, în continuare, să fixăm, cât mai
clar posibil, bazele utilizării firelor de execuţie în Java.

9.2 Fire de execuţie (thread-uri) în Java


Pentru ca programatorul Java să poată realiza aplicaţii multifir, Java oferă în pachetul java.lang, deci chiar
în java core, două clase şi o interfaţă:
• clasa Thread
• clasa ThreadGroup
• interfaţa Runnable

15
Sistem monoprocesor, sistem multiprocesor, sistem vectorial, sistem distribuit, etc.
16
Interfaţa de Programare a Aplicaţiilor sub sistemul de operare Windows.
Clasa Thread şi interfaţa Runnable oferă suport pentru lucrul cu fire de execuţie, ca entităţi separate ale
aplicaţiei iar clasa ThreadGroup permite crearea unor grupuri de fire de execuţie, în vederea tratării acestora
într-un mod unitar.
Notaţie UML pentru
interfaţă
Runnable
Relaţie de realizare

Thread
ThreadGroup

Figura 19. Resurse Java predefinite pentru programarea multifir. Relaţiile dintre ele.
După cum se poate observa, în Figura 23, clasa Thread implementează interfaţa Runnable iar clasa
ThreadGroup se compune din mai multe obiecte Thread. Simbolurile folosite pentru a indica relaţia de
compunere dintre ThreadGroup şi Thread, precum şi relaţia de realizare dintre Thread şi Runnable sunt de
provenienţă UML. Deoarece discuţia referitoare la grupuri de fire se bazează pe înţelegerea lucrului cu fire
independente, în continuare ne vom ocupa de problema utilizării firelor de execuţie independente. Pentru a crea
un fir de execuţie în Java avem două posibilităţi:

• Definirea unei clase derivate din clasa Thread


• Definirea unei clase care implementează interfaţa Runnable

Crearea unui fir de execuţie derivând clasa Thread


Alegând această variantă, avem de efectuat un număr redus de operaţii:
• Definirea unei clase derivate din clasa Thread.
Derivarea se face, după cum se ştie, cu o sintaxă de tipul:

class FirulMeu extends Thread


{
//Date membre
//Funcţii membre
}

• Suprascrierea funcţiei public void run(), moştenită de la clasa Thread, în clasa derivată.
Această metodă trebuie să implementeze comportamentul firului de execuţie. Aşa cum metoda main()
este metoda apelată de Java Runtime System în momentul în care se execută o aplicaţie Java, metoda
run() este metoda apelată când se execută un fir. De fapt, trebuie să subliniem că atunci când se
porneşte maşina virtuală Java (JVM), odată cu ea se porneşte un fir de execuţie care apelează metoda
main(). JVM îşi va înceta execuţia în momentul in care nu mai există fire în execuţie sau a fost
apelată metoda exit() a clasei System.

• Instanţierea unui obiect fir, folosind operatorul new:

FirulMeu firulmeu=new FirulMeu();

• Pornirea firului instanţiat, prin apelul metodei start(), moştenită de la clasa Thread.

firulmeu.start();
Acestea sunt operaţiile strict necesare pentru a începe lucrul cu fire de execuţie în Java, utilizând clasa
Thread.

Exemplul 9.1

//Clasa care modeleaza firul


class TFirPers extends Thread
{
static int id=0;
int[] vect=new int[10];

//Constructorul clasei
public TFirPers()
{
};

//Metoda run(), care modeleaza comportamentul firului


public void run()
{
id++;
System.out.println("Lucreaza TFirPers.... "+id);
for(int j=0;j<10;j++)
vect[j]=j;
};
};

//Clasa care modeleaza aplicatia


public class Fire
{
public static void main(String sir[])
{
int[] vecmain=new int[20];

//Declarare referinte
TFirPers fir1,fir2;

//Alocare referinte-fir de executie


fir1=new TFirPers();
fir2=new TFirPers();

//Lansarea in executie a firelor


fir1.start();
fir2.start();

try
{

//Intarzierea firului principal pentru


//a lasa ragaz firelor derivate din Thread
//sa lucreze
Thread.currentThread().sleep(2000);
}
catch(InterruptedException e)
{}

//Valorificarea rezultatelor furnizate de


//cele doua fire de executie
for(int k=0;k<10;k++)
vecmain[k]=fir1.vect[k];
for(int l=0;l<10;l++)
vecmain[l+10]=fir2.vect[l]+10;
for(int i=0;i<20;i++)
System.out.println(vecmain[i]);
};
};

Codul Java, prezentat în Exemplul 9.1, face apel la un mic subterfugiu (adormirea firului principal de
executie timp de 2 secunde pentru a lăsa timp firelor de executie, paralele firului principal, să-şi îndeplinească
atribuţiile. Dacă nu se acordă acest răgaz, se va observa că firul principal va accesa datele corespunzătoare firelor
secundare înainte ca acestea să fie conforme aşteptărilor noastre. Altfel spus, paralelismul specific lucrului cu
mai multe fire de execuţie este efectiv şi, prin Exemplul 9.1, se atrage deja atenţia asupra modificării atitudinii
programatorului faţă de problema organizării structurilor de prelucrare.

Crearea unui fir de execuţie utilizând interfaţa Runnable


O altă modalitate de a crea fire de excuţie este utilizarea interfeţei Runnable. Această modalitate devine
interesantă în momentul în care se doreşte ca o clasă de tip Thread, pe care o implementăm, să moştenească
capabilităţi disponibile în alte clase. Operaţiile specifice creării unui fir de execuţie utilizând interfaţa Runnable
sunt următoarele:

• Definirea unei clase care implementează interfaţa Runnable.


Aceasta se face utilizând sintaxa adecvată şi implementând cel puţin metodele interfeţei Runnable (de
fapt, doar metoda public void run()).

class FirRunnable extends Exemplu implementes Runnable


{
//Definitie
}

• Clasa care implementează interfaţa Runnable trebuie să suprascrie funcţia public void run().

public void run()


{
//Cod aferent
}

• Se instanţiază un obiect al clasei de mai sus, cu o sintaxă de tipul:

FirRunnable obiectRunnable=new FirRunnable();

• Se crează un obiect de tip Thread, utilizând un constructor care are ca şi parametru un obiect de
tip Runnable. În acest mod se asociază un fir cu o metodă run().

Thread firulMeu=new Thread(obiectRunnable);

• În sfârşit, se porneşte firul, la fel ca în metoda derivării din Thread a firului.

Paşii precizaţi mai sus se pot vedea şi în Exemplul 9.2

Exemplul 9.2
//Clasa care modeleaza aplicatia
public class FirRunn
{
public static void main(String s[])
{
System.out.println("Creare obiect Runnable...");
classRunnable obiectRunn=new classRunnable();
System.out.println("Creare fir...");
Thread fir=new Thread(obiectRunn);
System.out.println("Start fir...");
fir.start();
System.out.println("Din nou in main()...");
}
}

//Clasa auxiliara
class Display
{
public void display(String mesaj)
{
System.out.println(mesaj);
}
}

//Clasa care implementeaza interfata Runnable si mosteneste


//clasa Display
class classRunnable extends Display implements Runnable
{
public void run()
{
int nrpasi=3;
display("Run are "+nrpasi+" pasi de facut...");
for(int i=0;i<3;i++)
display("Pasul: "+i);
display("Run si-a terminat munca...");
}
}

Controlul unui fir de execuţie


Problema controlului unui fir de execuţie este legată de cunoaşterea stărilor posibile ale firelor de execuţie.
În speţă, pe timpul execuţiei unui program Java multifir, o instanţă Thread poate să se afle în una din următoarele
patru stări: new, runnable, blocked şi dead. Atunci când creăm un fir de execuţie, acesta intră în starea new. În
această stare firul de execuţie aşteaptă apelarea metodei start() a firului. Nici un cod nu rulează încă.
În starea runnable un fir execută codul prezent în metoda sa run(). Pentru ca firul să treacă din starea new
în starea runnable trebuie executată metoda start(). Nu se recomandă apelarea directă a metodei run() deoarece
face acest lucru, în locul dumneavostră, metoda start(). Când un fir de execuţie este inactiv despre el se spune că
este în starea blocked sau not runnable. Un fir poate deveni inactiv dacă apelăm metode precum sleep(),
suspend() sau wait(), ori dacă trebuie să aştepte după anumite metode I/O până la finalizarea execuţiei acestora.
După cum se va vedea, fiecare dintre aceste metode are un mecanism propriu de refacere a stării runnable
pentru fir. În Exemplul 11.1 am folosit deja metoda sleep() pentru a rezolva o problemă banală de sincronizare
între firul principal de execuţie şi firele secundare.

Suspendarea şi reluarea execuţiei unui fir


Am văzut deja cum putem suspenda execuţia unui fir pentru o perioadă de timp prin utilizarea metodei
sleep() a clasei Thread. Putem suspenda execuţia unui fir şi până la apariţia unor condiţii obiective de reluare a
execuţiei. În acest sens putem utiliza metoda suspend() a clasei Thread, care pune un fir în starea not runnable,
până la apelarea metodei resume(). Ca un exemplu, utilizarea metodei suspend() permite oprirea unei secvenţe
de animaţie la apăsarea butonului mouse-ului şi reluarea animaţiei la ridicarea degetului de pe butonul mouse-
ului. De asemenea, un gen special de suspendare/reluare a execuţiei unui fir se realizează şi cu ajutorul perechii
de metode wait()/notify() asupra căreia vom reveni mai jos.
9.3 Sincronizarea firelor
Dacă sunt executate asincron, mai multe fire care partajează anumite date s-ar putea să fie obligate să-şi
sincronizeze activităţile pentru a obţine rezultate corecte. Pe lângă posibilităţile pe care le oferă metode precum
sleep() sau suspend()/resume(), în Java a fost introdus modificatorul synchronized, tocmai pentru a introduce
un cadru adecvat atomizării activităţilor, în condiţii de concurenţă la resurse.
Ideea de bază a modificatorului synchronized este cât se poate de simplă: primul fir care intră în posesia
unui obiect marcat de modificatorul synchronized rămâne proprietar al obiectului până când îşi termină
execuţia. În acest mod se crează un cadru simplu pentru evitarea coliziunilor în timpul accesului concurent la
resurse.
Ceea ce este simplu nu este întotdeauna şi eficient. Uneori, preţul sincronizării s-ar putea să fie mai
mare decât poate suporta clientul aplicaţiei (timpii de execuţie pot fi diminuaţi drastic).

Sincronizare bazată pe modificatorul synchronized


Modifcatorul synchronized poate fi utilizat pentru a realiza sincronizarea firelor. Orice fir are propia sa
memorie de lucru, unde îşi ţine copii proprii ale variabilelor pe care le utilizează. Când este executat un fir,
acesta operează numai asupra acestor copii. Memoria principală (main memory), asociată firului principal,
conţine copia master a fiecărei variabile. Există reguli care condiţionează modul în care se poate efectua schimb
de conţinut între cele două tipuri de copii ale variabilelor. Important pentru sincronizare este, însă, faptul că
memoria main conţine şi zăvoare, care pot fi asociate obiectelor sau metodelor declarate synchronized. Firele
pot intra în competiţie pentru achiziţionarea zăvoarelor. Acţiunile de zăvorâre şi dezăvorâre (dacă un astfel de
cuvânt există!) sunt atomice, asemenea acţiunilor de citire sau scriere. Aceste zăvoare pot fi utilizate pentru
sinconizarea activităţilor unui program multifir.
Declararea ca synchronized a unui obiect sau a unei metode determină asocierea acestora cu un zăvor.
Important este că un singur fir, la un moment dat, poate să închidă zăvorul, altfel spus, un singur fir poate
deţine obiectul asociat cu zăvorul.
Dacă un fir vrea să acceseze un obiect sau o metodă sincronizată, dar găseşte zăvorul închis, el trebuie să
aştepte într-o coadă, până când zăvorul va fi deschis de către proprietarul lui circumstanţial.
Astfel că, în aplicaţiile Java multifir, putem întâlni: sincronizare cu metode, sinconizare pe blocuri,
sincronizare cu obiecte, pentru a introduce la anumite nivele, disciplina de utilizare mutual exclusivă a acestor
trei categorii de concepte. Elemente de sintaxă şi aspecte referitoare la modul de utilizare a acestor tehnici se pot
urmări în Exemplul 9.3, Exemplul 9.4 şi Exemplul 9.5.

Modelele teoretice utilizate în limbajele care oferă suport pentru programarea multifir oferă şi alte soluţii la
problema sincronizării. Java oferă suport pentr majoritatea acestor modele, remarcându-se, faţă de alte limbaje,
prin aducerea problemei concurenţei în interiorul limbajului, spre deosebire de alte soluţii, care se bazează
pe enunţuri sistem pentru implementarea prelucrărilor multifir. Pentru informarea cititorului, două mari
direcţii de rezolvare a problemelor de sincronizare sunt: monitoarele (introduse de C.A.R. Hoare) şi semafoarele
(introduse de Dijkstra). Fiecare dintre aceste soluţii pune în discuţie concepte precum secţiunea critică, prin care
se înţelege o porţiune de cod la care accesul concurent trebuie monitorizat, pentru a evita disfuncţiile în
utilizarea anumitor resurse.
Atrag atenţia cititorului şi asupra ofertei limbajului Java în ceea ce priveşte posibilitatea de a defini
grupuri de fire, a căror manevrare unitară poate constitui un avantaj, în anumite situaţii.
De asemenea, în Java există şi posibilitatea de a defini nişte fire speciale, numite daemon-i, fire a căror
destinaţie este asigurarea de servicii pentru celelalte fire de execuţie. Exemplul clasic de daemon, în Java este
firul care asigură funcţia de garbage collector.

Exemplul 9.3
//Ilustreaza sincronizarea bazata pe obiecte
//Obiectul monitor este balanta.
class unFir extends Thread
{

//Obiectul monitor
static Integer balanta = new Integer(1000);
static int cheltuieli=0;
public void run()
{
int vol;
for(int i=0;i<10;i++)
{
try
{
sleep(100);
}
catch(InterruptedException e){}
int bon=((int)(Math.random()*500));

//Accesul la blocul de cod de mai jos este


//monitorizat cu ajutorul obiectului balanta
synchronized(balanta)
{
if(bon<=balanta.intValue())
{
System.out.println("Verif:"+bon);
balanta=new Integer(balanta.intValue()-bon);
cheltuieli+=bon;
System.out.print("Balanta: "+balanta.intValue());
System.out.println("Cheltuieli: "+cheltuieli);
}
else
{
System.out.println("Respins: "+bon);
}
}
}
}
}

public class Lacat


{
public static void main(String s[])
{
new unFir().start();
new unFir().start();
}
}

Exemplul 9.4

//Ilustreaza sincronizarea cu obiecte sinchronized


//apeland la wait() si notify()
class Fir1 extends Thread
{
Object ob;
Fir1(Object obi)
{
ob=obi;
}
public void run()
{
while(true)
{
System.out.println("Firul "+getName());
try
{
synchronized(ob)
{
ob.wait();
}
}
catch(InterruptedException e)
{}
}
}
}

class Fir2 extends Thread


{
Object ob;
Object obman=new Object();

Fir2(Object obi)
{
ob=obi;
}
public void run()
{
while(true)
{
System.out.println("Firul "+getName());
try
{
synchronized(ob)
{
ob.notify();
}
}
catch(Exception e)
{
System.out.println("Exceptie: "+e);
}
try
{
synchronized(obman)
{
obman.wait(2000);
}
}
catch(InterruptedException e)
{}
}
}
}

public class WaitNoti


{
public static void main(String[]s)
{
//Obiect pretext pentru sincronizare
Object obiect=new Object();

//Obiect pe care se face asteptarea


Object obman=new Object();
Fir1 fir1=new Fir1(obiect);
Fir2 fir2=new Fir2(obiect);
fir1.start();
fir2.start();
try
{
synchronized(obman)
{
obman.wait(35000);
}
}
catch(InterruptedException e)
{}
}
}

Exemplul 9.5
//Ilustreaza sincronizarea cu metode synchronized
class Distribuitor
{
int marfa=0;

//Metoda atomizata cu ajutorul modificatorului


//synchronized
public synchronized int consuma()
{
int temp;
while(marfa==0)
{
try
{
wait();
}
catch(InterruptedException e)
{}
}
temp=marfa;
marfa=0;
System.out.println("Consumat :"+temp);
notify();
return temp;
}

//Metoda atomizata cu ajutorul modificatorului


//synchronized
public synchronized void produce(int vol)
{
while(marfa!=0)
{
try
{
wait();
}
catch(InterruptedException e)
{}
}
marfa=vol;
notify();
System.out.println("Produs :"+marfa);
}
}

class unFir extends Thread


{
boolean producator=false;
Distribuitor distr;
public unFir(Distribuitor d,String t)
{
distr=d;
if(t.equals("Producator"))
producator=true;
}
public void run()
{
for(int i=0;i<20;i++)
{
try
{
sleep((int)(Math.random()*1000));
}
catch(InterruptedException e)
{}
if(producator)
distr.produce((int)(Math.random()*6)+1);
else distr.consuma();
}
}
}

public class ProdCons


{

public static void main(String s[])


{
Distribuitor dis=new Distribuitor();
new unFir(dis,"Consumator").start();
new unFir(dis,"Producator").start();
}
}

Exemplul 9.5 ne arată cum putem combina sincronizarea bazată pe metode synchronized cu posibilităţile
oferite de sincronizarea bazată pe aşteptare. Esenţiale, în sincronizarea bazată pe aşteptare, sunt metodele wait()
şi notify().
Bibliografie esenţială
[1] Eckel, B., Thinking in Java, 2nd edition, Revision 12, format electronic.
[2] Jamsa & Klander, C şi C++ (Manualul fundamental de programare în C
şi C++), Editura Teora.
[3] Joshua Bloch, Java. Ghid practic pentru programatori avansaţi, Editura
Teora, 2002.
[4] Lemay, L., Cadenhead, R., Java 2 fără profesor în 21 de zile, Editura
Teora, 2000.
[5] Mark C. Chan, ş.a., Java. 1001 secrete pentru programatori, Editura
Teora
[6] Negrescu,L., Limbajele C şi C++ pentru începători, Limbajul C++
(volumul II), Editura Albastră, Cluj-Napoca
CUPRINS

CUVÂNT ÎNAINTE AL AUTORULUI.............................................................................................. 3

1 CUM SE EXPLICĂ PERMANENTA NEVOIE DE PARADIGME NOI ÎN INGINERIA


SOFTULUI. CE ÎNŢELEGEM PRIN ORIENTAREA PE OBIECTE ........................................... 6

1.1 CUM SE EXPLICĂ PERMANENTA NEVOIE DE PARADIGME NOI ÎN INGINERIA SOFTULUI?........ 6


1.2 CE SE ÎNŢELEGE PRIN ORIENTAREA PE OBIECTE? ..................................................................... 8

2 CONCEPTE ŞI PRINCIPII ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE................ 12

2.1 CONCEPTE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE .......................................................... 12


2.2 PRINCIPII ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE ............................................................ 16

3 SPECIFICAREA ŞI IMPLEMENTAREA UNEI CLASE DIN PERSPECTIVĂ JAVA ...... 21

3.1 ÎN LOC DE INTRODUCERE ........................................................................................................... 21


3.2 ATENŢIE LA IMPORTANŢA EFORTULUI DE ABSTRACTIZARE!.................................................. 21

4 MOŞTENIREA ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE DIN PERSPECTIVĂ


JAVA .................................................................................................................................................... 30

4.1 SCURTĂ INTRODUCERE .............................................................................................................. 30


4.2 MOŞTENIREA ÎN JAVA ............................................................................................................... 31
4.3 MOŞTENIREA MULTIPLĂ ÎN JAVA .............................................................................................. 35

5 POLIMORFISMUL ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE DIN


PERSPECTIVĂ JAVA ....................................................................................................................... 40

5.1 SĂ REAMINTIM, PE SCURT, CE ESTE POLIMORFISMUL. ............................................................ 40


5.2 TIPURI DE POLIMORFISM LA NIVELUL LIMBAJELOR DE PROGRAMARE. EXEMPLIFICARE ÎN
C++. 40
5.3 POLIMORFISMUL ÎN CONTEXT JAVA ......................................................................................... 50

6 TRATAREA STRUCTURATĂ A EXCEPŢIILOR ÎN PROGRAMAREA ORIENTATĂ PE


OBIECTE............................................................................................................................................. 52

6.1 O PROBLEMĂ, ÎN PLUS, ÎN PROGRAMARE: TRATAREA EXCEPŢIILOR...................................... 52


6.2 MANIERA JAVA DE TRATARE A EXCEPŢIILOR .......................................................................... 52

7 PROGRAMARE GENERICĂ ÎN C++ ŞI JAVA ...................................................................... 58

7.1 CE ESTE PROGRAMAREA GENERICĂ.......................................................................................... 58


7.2 GENERICITATEA ÎN JAVA ........................................................................................................... 58

8 FLUXURI OBIECT ORIENTATE ŞI SERIALIZARE ÎN JAVA........................................... 63

8.1 SCURTĂ INTRODUCERE .............................................................................................................. 63


8.2 STREAM-URI STANDARD ÎN JAVA............................................................................................... 63
8.3 CLASA FILE ÎN LUCRUL CU STREAM-URI .................................................................................. 66
8.4 CITIREA DATELOR DINTR-UN STREAM ...................................................................................... 67
8.5 SCRIEREA DATELOR ÎNTR-UN STREAM...................................................................................... 69
8.6 SERIALIZAREA OBIECTELOR ..................................................................................................... 73

9 PROGRAMARE CONCURENTĂ CU SUPORT OBIECT ORIENTAT. PERSPECTIVA


JAVA .................................................................................................................................................... 78

9.1 NOŢIUNI DE PROGRAMARE CONCURENTĂ ................................................................................ 78


9.2 FIRE DE EXECUŢIE (THREAD-URI) ÎN JAVA ............................................................................... 78
9.3 SINCRONIZAREA FIRELOR .......................................................................................................... 83

BIBLIOGRAFIE ESENŢIALĂ ......................................................................................................... 88

S-ar putea să vă placă și