Documente Academic
Documente Profesional
Documente Cultură
Iniţiere În Programarea Orientată Pe Obiecte Din Perspectivă Java
Iniţiere În Programarea Orientată Pe Obiecte Din Perspectivă Java
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:
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
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ţă.
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.
Î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:
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.
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:
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:
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.
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.
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.
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
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.
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ă>
(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])
:
<Obiect>.<Nume_metodă>([<Lista_de parametri_actuali>]);
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.
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.
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
Operaţii publice
(Interfaţa)
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ă
Clase frunză
Romb Dreptunghi
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
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:
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ă:
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:
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
Informaţe
nod
Legătura spre următorul nod Ultimul nod nu are un succesor
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[]);)
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.
[<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_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:
Î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.
:
protected static final int nr=10;
:
Atenţie! Cele două metode de iniţializare sunt mutual exclusive.
:
private transient String password;
:
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.
//---------------------------------------------
//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;
};
//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);
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
Moştenirea este o modalitate performantă de reutilizare a codului, dar nu este întotdeauna cel
mai bun instrument pentru îndeplinirea acestui obiectiv.
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.
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.
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;
}
}
int consultaTipCoaja()
{
return tipcoaja;
}
}
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;
}
}
int consultaTipFruct()
{
System.out.println("Consultare tip fruct..redefinire Para");
return super.consultaTipFruct();
}
double consultaGreutate()
{
return greutate;
}
int consultaForma()
{
return forma;
}
}
int consultaTipFruct()
{
System.out.println("Consultare tip fruct..redefinire Porto");
return super.consultaTipFruct();
}
double consultaGreutate()
{
return greutate;
}
int consultaForma()
{
return forma;
}
}
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:
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:
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();
}
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...");
};
}
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.
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>
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).
Exemplul 5.2
#include <iostream.h>
#include <conio.h>
//Clasa de baza
class Super
{
int numar;
public:
Super(int n)
{
numar=n;
};
//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;
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ă
sau
Î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.
sau
Lucrurile pot fi înţelese şi mai bine, urmărind Exemplul 5.3 (cod C++).
Exemplul 5.3
#include<conio.h>
#include<iostream.h>
//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;
};
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();
tamp1=*pod1;
tamp2=*pod2;
tamp1=tamp1-tamp2;
gotoxy(20,15);cout<<"Diferenta numerelor complexe->:";
tamp1.disp_nc();
getch();
}
Exemplul 5.4
#include <iostream.h>
#include <conio.h>
//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:
};
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;
};
};
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
//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;
};
//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;
};
//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;
};
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ă.
Exemplul 5.5
//Clasa radacina
class Poligon
{
private String definitie;
public Poligon(String d)
{
definitie=new String(d);
};
//Supradefinire
public void arie()
{
System.out.println("Triunghi...neimplementata!");
};
};
//Supradefinire
public void arie()
{
System.out.println("Patrulater...neimplementata!");
};
};
Î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.
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:
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;
if (n.getNumar()==0)
throw new ENumarReal("Exceptie...Impartire la zero...");
else
return new NumarReal(this.getNumar()/n.getNumar());
};
}
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;
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;
};
}
}
}
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()
{
};
Exemplul 7.3
//Interfata prin intermediul careia se va simula
//ideea de pointer la metoda comp
interface SimPointMet
{
int comp(Object a,Object b);
};
public pretextInt()
{
};
public pretextFlo()
{
};
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:
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ă.
• Standard Input
• Standard Output
• StandardError.
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:
Funcţii de informare:
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:
Î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:
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ă.
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")));
//Deschidere flux
BufferedReader br = new BufferedReader(new InputStreamReader (System.in));
String s;
//Inchidere flux
out.close();
}
}
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]);
}
}
}
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)
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:
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:
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)
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.*;
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();
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.
Î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:
Procedeul Java de serializare/deserializare a unui obiect este simplu, dacă acesta îndeplineşte anumite
condiţii.
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:
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.*;
Exemplul 8.7
import java.io.*;
import java.util.*;
//Inca odata
o1.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:
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:
• 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.
• 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
//Constructorul clasei
public TFirPers()
{
};
//Declarare referinte
TFirPers fir1,fir2;
try
{
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.
• Clasa care implementează interfaţa Runnable trebuie să suprascrie funcţia public void run().
• 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().
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);
}
}
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));
Exemplul 9.4
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)
{}
}
}
}
Exemplul 9.5
//Ilustreaza sincronizarea cu metode synchronized
class Distribuitor
{
int marfa=0;
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