Sunteți pe pagina 1din 88

Alexandru-Ioan Stan (Anul univ.

2018-2019) Programare Orientată Obiect 1


Cuprins:

INFORMAȚII GENERALE 3
MODULUL 1 - MEDIUL DE LUCRU IN JAVA 7
1.1. ELEMENTE GENERALE DE COMPILARE ȘI EXECUȚIE A PROGRAMELOR 7
1.2. LIMBAJUL DE PROGRAMARE JAVA 9
1.2.1. COMPILAREA ÎN JAVA 9
1.2.2. MEDIUL DE LUCRU JAVA 10

1.2.3. STILUL DE SCRIERE A CODULUI ÎN JAVA 11


1.3. TESTE DE EVALUARE 12
MODULUL 2 - INTRODUCERE ÎN PROGRAMAREA OBIECTULĂ 16
2.1. INTRODUCERE ÎN PROGRAMAREA OBIECTULĂ 16
2.1.1. PRARADICMA PROGRAMĂRII OBIECTUALE 16
2.1.2. FORMALIZAREA CONCEPTULUI DE OBIECT ÎN JAVA 18

2.1.3. ASCUNDEREA IMPLEMENTĂRII 18


2.1.4. GESTIUNEA MEMORIEI ÎN JAVA 19
2.1.6. OPERATORI 20
2.1.7. CREAREA ȘI DISTRUGEREA OBIECTELOR. SUPRAÎNCĂRCARE 21
2.1.7.2. CUVÂNTUL CHEIE THIS 22
2.1.8. PROGRAMAREA CU OBIECTE DIN MAI MULTE CLASE 25
2.1.9. CUVÂNTUL CHEIE FINAL 32
2.1.10. UPCAST 33
2.1.11. LEGAREA METODELOR ȘI DATELOR MEMBRU ÎN JAVA 34
2.1.12. CLASE ABSTRACTE ȘI INTERFEȚE 35
2.1.13. DOWNCAST 37
2.2. MODELE DE DESIGN (DESIGN PATTERNS) 38
2.2.1. MODELUL DE DESIGN STRATEGY 38
2.2.2 MODELUL DE DESIGN ADAPTER 39
2.2.3 MODELUL DE DESIGN FACTORY METHOD 40
2.2.4. MODELUL DE DESIGN ITERATOR 41
2.3. CLASE INTERIOARE 41
2.3.1. CONCEPTUL DE CLASĂ INTERIOARĂ 41
2.3.2. CLOSURES & CALLBACKS 44
2.3.3. CONTROL FRAMEWORKS 47

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 2


2.3.4. AVANTAJELE FOLOSIRII CLASELOR INTERIOARE 47
2.4. TESTE DE EVALUARE 48
MODULUL 3 - ALTE ELEMENTE DE PROGRAMARE ÎN LIMBAJUL JAVA 52
3.1. EXCEPȚII 52
3.1.1. CONCEPTUL DE EXCEPȚIE 52
3.1.2. GESTIUNEA ERORILOR 52
3.1.3. IERARHIA EXCEPȚIILOR IN JAVA 54
3.1.4. ÎNLĂNȚUIREA EXCEPȚIILOR 55
3.1.5. STRATEGII DE GESTIUNE A EXCEPȚIILOR 56
3.2. COLECȚII 56
3.2.1 COLLECTIONS FRAMEWORK 57
3.2.2 PARCURGEREA COLECȚIILOR 61
3.3. FLUXURI DE INTRARE-IEȘIRE (I/O) 62
3.3.1. TIPURI DE FLUXURI DE INTRARE-IEȘIRE 64
3.3.2. FLUXURI BINARE DE INTRARE-IEȘIRE 64
3.3.3. FOLOSIREA DESIGN PATTERN-UL DECORATOR ÎN IERARHIA CLASELOR DE INTRARE-IEȘIRE 64
3.3.4. FLUXURI DE COMPRESIE A DATELOR 66
3.3.5 CLASELE READER ȘI WRITER 66
3.3.6. CLASA RANDOMACCESSFILE 67
3.3.7. SERIALIZAREA OBIECTELOR 68
3.4. RTTI 69
3.4.1. CLASA CLASS 69
3.4.2. MECANISMUL DE REFLECȚIE 71
3.4.3. MODELUL DE DESIGN PROXY 72
3.5. CONCURENȚA ÎN JAVA 73
3.5.1. NOȚIUNI DE BAZĂ 73
3.5.2. THREADING ÎN JAVA 74
3.5.3. PRINDEREA EXCEPȚIILOR RIDICATE ÎN TASKURI 75
3.5.4. PARTAJAREA RESURSELOR INTRE THREADURI 77
3.5.5. ATOMICITATE ȘI VOLATILITATE 78
3.5.6. STĂRI ALE UNUI THREAD 78
3.5.7. ÎNTRERUPEREA THREAD-URILOR 78
3.5.8. COOPERAREA ÎNTRE THREAD-URI 79
3.6. TESTE DE EVALUARE 80

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 3


Informații generale

Date de identificare a cursului

Date de contact ale titularului de curs Date de identificare a cursului și date de


contact ale tutorilor
Lect. univ. dr. Alexandru-Ioan STAN Denumire: Programare orientată obiect
Birou 437 Cod: ELR0259
Telefon: 0264-418652 Anul: 2
Email: alexandru.stan@econ.ubbcluj.ro Semestrul: 2
Website: http://econ.ubbcluj.ro/~alexandru.stan Tip: obligatoriu
Consultații: conform orarului afișat la Număr de credite: 6
biroul 437 Pagina web a cursului: http://cursuri.elearning.ubbcluj.ro/
Tutori: Lect. univ. dr. Alexandru-Ioan STAN
Email tutori: alexandru.stan@econ.ubbcluj.ro

Condiţionări şi cunoştinţe prerechizite


Se recomandă cunoștințe de programare structurată în C. Aceste cuno știn țe sunt ob ținute la disciplina
“Algoritmi și Structuri de date”, anul II, semestrul 1. Adi țional, studen ții pot opta pentru cursul de Introducere în
Programare, anul I semestrul 2.

Descrierea cursului
Cursul de Programare orientată obiect î și propune să familiarizeze studen ții cu no țiunile principale ale teoriei
generale a Programării Orientate Obiect. Studen ții vor deprinde principalele no țiuni necesare pentru învă țarea
unui limbaj de programare orientat obiect. No țiunile teoretice vor fi exemplificate prin utilizarea limbajului Java.
Mediul de programare utilizat va fi NetBeans 8.2.

Organizarea temelor în cadrul cursului


Temele cursului sunt organizare conform logicii de învă țare a conceptelor de programare obiectuala. Cursul
de bazează pe logica bulgărelui de zăpadă. Astfel, la început se introduce no țiuni facile legate de aceasta
disciplina. Pe parcurs, aceste noțiuni se folosesc la descrierea și învă țarea altor no țiuni mai complicate.
Studenții sunt rugați sa consulte bibliografia aferenta fiecărei teme, atât din suportul de curs obligatoriu
(manualul) cat și din celelalte căr ți indicate. Pentru fiecare tema, suportul de curs dezvolta conceptele
teoretice, prezinta exemple și propune probleme de rezolvat. Pentru o bună aprofundare, studen ții trebuie să
parcurgă la calculator aceste exemple și sa realizeze problemele date ca și tema. Site-ul disciplinei con ține
adițional slide-urile pentru fiecare tema. Slide-urile con țin informa ții sumare, fiind un bun ghid de reamintire a
conceptelor dezvoltate la fiecare capitol. Slide-urile nu sunt suficiente ca și mijloc de învă țare al acestei
discipline.

Disciplina este structurată în trei module de învăţare:


1. Mediul de lucru Java;
2. Introducere în programarea obiectulă;
3. Alte elemente de programare în limbajul Java.
Primul modul abordează procesul de compilare și execu ție a unei aplica ții scrise în limbajul de programare
Java precum și mediul de lucru asociat limbajului.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 4


Modulul de introducere în programarea obiectulă prezintă descrierea, realizarea și implementarea conceptelor
de programare obiectuală în Java.
Al treilea modul vizează familarizarea cu elemente conceptuale fundamentale utilizate în programarea Java:
exceptii, colectii generice, fluxuri de intrare-ie șire, RTTI. Modulul se încheie cu prezentarea conceptelor de
programare concurentă.

Formatul şi tipul activităţilor implicate de curs


La întâlnirile cu studenții, profesorul va prezenta con ținutul teoretic al disciplinei. Studen ții sunt ruga ți sa
participe la aceste întâlniri, pentru ca au posibilitatea sa în țeleagă mai bine conceptele disciplinei. Con ținutul
este disponibil și în manual, studen ții având posibilitatea sa-l însu șească individual. Studiul individual
presupune exercițiu la calculator în limbajul de programare Java, cu exemplele din manual sau cu alte
exemple. Pentru o mai buna înțelegere, studen ții pot să rezolve problemele propuse sau sa realizeze alte
programe software, la liberal or alegere. Pentru fiecare tema, studen ții trebuie sa trimită problemele propuse
rezolvate pentru a fi punctate de către tutori.

Materiale bibliografice obligatorii


1. Bruce Eckel, Thinking in Java, ed. 4-a, Prentice Hall, 2006
2. Alfred AHO, Jeffrey ULLMAN, Principles of compilers design, Addison-Wesley, 1977
3. Ioan SALOMIE, Tehnici de programare obiectuală, Ed. Albastră, 1996

În suportul de curs, la finele fiecărui modul, sunt precizate atât referin țele bibliografice. Sursele bibliografice au
fost astfel stabilite astfel încât să ofere posibilitatea adâncirii nivelului de în țelegere a fiecărui aspect. Acest
suport de curs se bazeaza ca structura si continut pe cursul omonim predat studentiilor de la forma de
invatamant cu frecventa de domnul profesor univ.dr. habil. Gheorghe Cosmin Silaghi. Marea majoritate a
exemplelor de cod si a diagramelor UML asociate sunt preluate din carte „Thinking în Java” a lui Bruce Eckel.

Materiale şi instrumente necesare pentru curs

În cadrul cursului se va apela la o serie de echipamente şi materiale precum: laptop, videoproiector, conexiune
Internet, suport de curs. Laborator va fi echipat cu NetBeans 10.0.

Calendarul cursului
Pe parcursul semestrului sunt programate 4 întâlniri faţă în faţă cu toţi studenţii. În vederea eficientizării
acestor întâlniri faţă în faţă, pentru fiecare din acestea, se recomandă parcurgerea de către student a
suportului de curs pus la dispoziţie încă de la începutul semestrului, iar ulterior întâlnirii, este indicată
rezolvarea sarcinilor şi exerciţiilor aferente fiecărui modul parcurs. De asemenea, anterior întâlnirilor
programate, studenţilor li se recomandă să parcurgă capitolele corespunzătoare temelor abordate la fiecare
întâlnire din cel puţin una din sursele bibliografice indicate. În acest mod, se va facilita orientarea cursului
asupra aspectelor de fineţe din conţinutul disciplinei şi se va permite concentrarea pe modalităţile de aplicare
la nivel practic a informaţiilor deja parcurse.
Fiecare temă presupune parcurgerea unui material didactic indicat și implementarea în Java a unui set de
probleme.

Politica de evaluare şi notare

Nota la aceasta disciplina este compusa din:


- 40% examen teoretic,
- 40% examen practic

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 5


- 20% evaluare laborator
Examenul din sesiune are o pondere de 80% din nota finala și este compus din examen scris și practic.
Examenul scris se compune dintr-un test grila cu 30 de întrebări, 5 întrebări deschise și o problema. Examenul
practice presupune rezolvarea unei probleme la calculator. Problema care se rezolva la calculator are mai
multe subpuncte, în ordine crescândă de dificultate. Nota examenului din sesiune este medie aritmetica a
notelor de la proba scrisa și de la proba practica.

Elemente de deontologie academică


Se vor avea în vedere o serie de aspecte precum: lucrările elaborate de către studenţi pe parcursul activităţilor
vor avea în mod obligatoriu caracter de originalitate, orice tentativă de fraudă va fi sancţionată conform
regulamentelor în vigoare; rezultatele finale se vor comunica în maxim 72 de ore de la finalizarea probelor
scrise sau practice din cadrul sesiunii de examene; studen ții vor putea contestata rezultatele în termenul
transmis de cadrul didactic, care nu va fi mai scurt de 24 ore de la comunicarea rezultatelor, contesta țiile fiind
soluţionate în maximum 24 de ore.

Studenţi cu dizabilităţi

Atât prin intermediul programului de întâlniri fa ță în fa ță, cât şi prin întâlnirile programate în mediul virtual,
titularul de curs se va implica şi va sprijini studenţii, astfel încât aceştia să reuşească să asimileze în mod
optim conceptele şi abilităţile aferente disciplinei. Studen ții care au o dizabilitate motorie sau de altă natură vor
putea contacta tutorii pentru adaptarea cerin țelor la specificul dizabilită ții.

Strategii de studiu recomandate


Se recomanda studenților să parcurgă cursul gradual pe parcursul semestrului. Astfel, în timpul parcurgerii
fiecărui capitol, studenții sunt ruga ți să ruleze exemplele practice din material și să pună accent pe în țelegerea
conceptelor și a modului în care programele sunt realizate și executate. Studen ții sunt încuraja ți să comunice
cu tutorii în cazul în care există nelămuriri legate de materialul cursului. Exerci țiile rezolvate, temele și
întrebările recapitulative indicate la fiecare capitol sunt minimale pentru în țelegerea și aprofundarea
materialului. Recomandăm studenților rezolvarea tuturor problemelor indicate în curs.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 6


Programare orientata obiect

Modulul 1

Mediul de lucru Java

I. Cuprins
1. Elemente generale de compilare și
execuție a programelor
2. Limbajul Java
II. Obiective
3. Teste de evaluare
În acest modul vom prezenta
succint procesul de compilare și
execuție a unei aplicații scrise în
limbajul de programare Java
precum și mediul de lucru asociat
III.limbajului.
Cuvinte cheie
Compilare, interpretare,
preprocesare,, executie, asamblare,
limbaj de programare de nivel inalt,
mediu de lucru Java.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 7


Modulul 1 - Mediul de lucru in Java
1.1. Elemente generale de compilare și execuție a programelor
Un limbaj de programare este o nota ție conven țională destinată formulării algoritmilor și creării de programe
ce îi aplică. În mod similar cu o limbă naturală, un limbaj de programare constă dintr-un alfabet, vocabular,
reguli gramaticale și elemente semantice. El este axat pe formularea instruc țiunile fundamentale care trebuie
executate de procesor.
Inițial, calculatorul nu recunoaște decât limbajul nativ al procesorului dezvoltat în etapa
de proiectare a dispozitivului. Acest limbaj de nivel inferior se numește limbaj de asamblare.
Programarea în limbaj de asamblare este directă și rapidă, dar dificilă, necesitând cuno știn țe
aprofundate despre arhitectura procesorului utilizat. în plus, programele scrise în limbaj de
asamblare sunt lipsite de portabilitate, acestea ne putând rula pe ma șini înzestrate cu
procesoare diferite.
In evoluția limbajelor, mai întâi au apărut limbajele de asamblare iar apoi limbajele de programare de nivel
înalt sau de nivel superior. Acestea din urmă nu necesită cunoa șterea arhitecturii unită ții de calcul ce va
executa programul, ci utilizează nota ții asemănătoare limbajului natural oferind productivitate ridicată muncii
de programare prin nivelul ridicat de abstractizare. Un alt avantaj al limbajelor de nivel înalt este portabilitatea
codului, inexistentă în asamblor.

Figura 1.1 Limbaje inferioare și superioare

In funcție de nivelul de abstractizare al limbajului (apropiere de limbajul natural și de


domeniul conceptual al utilizatorului), limbajele sunt clasificate în cinci mari grupe:
● Limbaje de generația întâi – primele calculatoare puteau fi programate doar în
cod mașină (limbaj mașină) specific procesorului incorporat, format dintr-o succesiune de bi ți 0 și
1;
● Limbaje de generația a doua – limbajele de asamblare ce introduc elemente de
programarea simbolica (coduri mnemonice și adresare simbolica);
● Limbaje de generația a treia – limbaje procedurale ce pot fi folosite, teoretic, în rezolvarea oricărei
probleme ce poate fi descrisă algoritmic; de aceea, ele mai sunt numite limbaje generale sau
universale (Pascal, Fortran, C/C++, C#, Java);
● Limbaje de generația a patra – limbaje neprocedurale ce definesc problema de rezolvat și nu
detaliile algoritmice prin care se realizează rezolvarea (SQL);
● Limbaje de generația a cincea – limbaje utilizate în domenii specifice precum logica fuzzy,
inteligența artificială, sau rețelele neuronale (Prolog, Lisp).

Un limbaj de programare este implementat de un translator automat: compilator sau interpretor:

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 8


● Un compilator va traduce codul sursa într-un cod care să permită executarea directa a de către
procesor acestuia, cum ar fi codul ma șină/limbaj de asamblare. Programele compilate sunt astfel
foarte rapide și optimizate. Ele însă nu sunt portabile, codul ma șină produs ne putând fi rulat de
cat pe tipul de sistem pe care au fost compilate. Limbajele de programare de nivel înalt (C/C++)
produc după compilare executabile de dimensiuni mari;
● Un interpretor este un program care execută direct linie cu linie instruc țiunile scrise într-un limbaj
de programare (numit, în general, program de scripting), transformându-le în instruc țiuni ma șină.

Compilarea într-un limbaj de genera ția a treia ca limbajul C este în general formată din trei faze independente,
care sunt realizate de trei programe ce se execută secven țial:
● Preprocesarea – se parcurg toate codurile sursa specificate linie după linie, căutând comenzi
(directive) spre execuție. Preprocesorul prime ște în intrare un text iar rezultatul lui este și el un
text. De exemplu, atunci când preprocesorul C întâlne ște directiva #include el o substituie cu
textul fișierului header, iar codul sursa introdus poate include la rândul lui alte fi șiere header;
● Compilarea propriu-zisă – fișierele preprocesate con ținând declara țiile obiectelor importate sunt
compilate separat. Rezultatul acestor compilări va fi un set de module obiect (fi șiere cu
extensia .o). Se obține câte unul pentru fiecare modul C ini țial. Tot codul C al modulului e compilat
în cod mașină însă referințele la simboluri externe din alte module sunt satisfăcute fiindcă aceste
module sunt compilate separat;
● Editarea de legături – Legarea modulelor (implicând completarea referin țelor obiectelor globale,
relocarea codului) este făcută de această ultimă fază a compilării. Aceste module obiect pot fi
obținute prin compilarea unor programe scrise în limbaje diferite, de exemplu, în asamblor pentru
a optimiza anumite operații. Rezultatul ob ținut este un program executabil dependent de platforma
pe care s-a realizat compilarea.
Preprocesarea și compilarea propriu-zisă pot fi văzute ca două etape distincte ale unei singure faze ce
transforma codul sursa în cod obiect.

Limbajul Java dispune de un model de compilare in 2 faze:


● Codul sursă este compilat într-un limbaj intermediar bytecode;
● Bytecode este executat de către mașina virtuală Java.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 9


Figura 1.2 Compilare și execuție în C

1.2. Limbajul de programare Java


Java este un limbaj de programare de nivel înalt, care sintetizează principalele limbaje existente atunci când a
fost creat în 1995 de către Sun Microsystems. Acesta permite programarea orientată pe obiecte bazându-se
pe încapsulare, moștenire, polimorfism (ca și SmallTalk și, într-o măsură mai mică, C++), programarea
modulară (ca și ADA), și utilizează o sintaxă foarte asemănătoare cu cea a limbajului C.

1.2.1. Compilarea în Java


Java oferă portabilitate, ceea ce înseamnă că programele scrise pe platforma Java trebuie să
ruleze în mod similar pe orice combina ție de hardware/sistem de operare cu suport adecvat
(sintagma de bază: Write once, run anywhere). Acest lucru se realizează prin compilarea
codului sursa Java într-o reprezentare intermediară denumită bytecode Java, ce diferă de codul
mașină specific arhitecturii sistem.
Instrucțiunile de cod intermediar Java bytecode sunt similare cu codul scris în limbaj de asamblare, dar sunt
concepute spre a fi executate de o ma șină virtuală (JVM) scrisă special pentru platforma de rulare a
aplicațiilor. Dacă codul mașină produs de o compilare standard poate fi rulat direct de către procesor doar pe
platforma pe care a fost creat, codul intermediar Java este interpretat de ma șina virtuala și, de aceea, poate fi
rulat pe orice platformă care folose ște mediul de execu ție Java. Programele Java sunt atât interpretate cât și
compilate. Astfel, utilizatorii finali instalează în mod obi șnuit pe calculatorul propriu doar mediul de execu ție
Java Runtime Environment (JRE).
Un compilator Java produce fișierele compilate cu extensia .class care conțin cod intermediar
independent de platforma, dar există și compilatoare care oferă un cod de ma șină optimizat pentru o anumite
combinații hardware/sistem de operare.
La execuție, fișierul ce conține metoda main este încărcat de JRE și mai apoi sunt parcurse trei etape
principale:

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 10


Figura 1.3 Compilare și execuție în Java

● Verificarea codului intermediar – mai întâi codul intermediar Java este inspectat de un verificator.
Acesta verifică dacă instrucțiunile ce vor fi rulate pot efectua ac țiuni care sunt în mod vădit
dăunătoare. Toate clasele încărcate, cu excep ția claselor de sistem, sunt verificate.
● Încărcarea codului intermediar – clasele dintr-o aplica ție Java sunt încărcate folosind clase
speciale derivate din clas java.lang.ClassLoader.
● Just-In-Time (JIT) Compilation – compilatorul JIT este activat în mod implicit în JRE și este activat
atunci când este apelată o metodă Java. Compilatorul JIT compilează codul intermediar al acestei
metode în codul nativ, compilându-l "la timp" pentru a putea fi rulat. Atunci când o metodă a fost
compilată, JVM solicită codul compilat al acelei metode în loc să îl interpreteze si, astfel, permite
ca viteza programului Java să se apropie de cea a unei aplica ții native.

1.2.2. Mediul de lucru Java


Pentru dezvoltarea programelor Java se folosește JDK(Java Development Kit)
ce conține:
● mașina virtuală Java: comanda java
● compilatorul Java: comanda javac
JDK trebuie instalat dacă utilizatorul dore ște să realizeze activitate de programare în Java.
Programele Java se scriu în fișiere cu extensia .java. Compilarea acestora se face prin comanda javac urmată
de unul sau mai multe nume de fișiere con ținând codul sursă din clasa Java. De exemplu, javac MyClass.java
compilează clasa MyClass a cărui cod sursă este localizat în fi șierul MyClass.java. Compilarea necesita
adesea furnizarea anumitor parametri pentru a se efectua corect, mai ales atunci când codul sursă se referă
la anumite clase din directoare altele decât codul compilat. Este nevoie sa adăugam op țiunea - classpath
urmată de directoarele (separate de un ; sub Windows și : sub Unix).
De exemplu, folosind sub Unix:

javac -classpath /programs/ex1:/lecturenotes1 MyClass.java

compilam fișierul MyClass.java care face referin ță la clase din directoarele /programs/ex1/ și /lecturenotes1/.
Rezultatul compilării este un fișier numit MyProg.class care con ține codul intermediar corespunzător sursei
compilate. Acest fișier este creat în mod implicit în directorul în care s-a produs compilarea. Cu toate acestea,
este foarte de dorit să nu amestecați fișierele care con țin codul sursă și cele care con țin codul intermediar. Un
director de destinație în care va fi creat fi șierul MyProg.class poate fi specificat prin op țiunea -d, ca mai jos:

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 11


javac -d /prog/ex1 -classpath /lecturenotes1 MyClass.java

Fișierele compilate pot fi regrupate în arhive JAR (Java Archive) ce conțin în mod tipic
pe lângă fișierele .class și alte fișiere auxiliare (resurse de tipul imaginilor, filmelor etc.). Fiecare
arhivă posedă un singur fișier manifest cu informații referitoare la fișierele împachetate în
arhiva .jar, de exemplu, numele clasei de pornire a aplica ției în care se găse ște o func ție
public static void main(String[] args);. JDK conține utilitarul jar pentru crearea de arhive JAR.
Colectarea fișierelor într-o singură arhivă JAR facilitează transmiterea codului executabil pe
Internet.

Pentru rularea programelor în Java se folosește JRE (Java Runtime Environment)


ce conține mașina virtuală Java (comanda java) și este instalat pe majoritatea
calculatoarelor. Bytecode-urile obținute prin compilare pot fi executate numai
cu ajutorul interpretului JVM. Rularea programului se face prin comanda java,
urmată de numele clasei de execu ție (fără extensie .class). Ca și la compilare, pot fi necesare
clase din alte directoare. Utilizați deci op țiunea -classpath ca în exemplul următor:

java -classpath /programs/ex1:/lecturenotes1 MyClass

Clasele Java sunt organizate în ierarhi de pachete și sub-pachete ce urmează structura de directoare în care
sunt stocate clasele compilate. Toate clasele dintr-un pachet vor fi colectate într-un singur director. Dacă
pachetul conține sub-pachete acestea apar ca subdirectoare ale directorului pachetului.

Fiecare fișier .java începe cu mențiunea package: se indică numele pachetului din care vor face parte clasele.
package javalectures.exercises;

class HelloWorldApp {
public static void main(String[] args) {
System.out.println("Hello World!"); // Display the string.
}
}

Pentru ca o ierarhie de clase sa poată fi vizibila de către clase din afara ei, rădăcinile arborilor de directoare în
care sunt memorate clasele trebuie adăuga ți în variabila sistem CLASSPATH.
C:\Users\Alexandru>echo %CLASSPATH%
C:\Program Files\Java\jdk1.8.2_14\jre\lib;C:\MyApps\lib;
Compilatorul și interpretorul Java caută automat clasele și arhivele ce con țin fi șiere .class în lista specificata
de această variabilă sistem. Directorul curent .\ este întotdeauna prezent în variabila CLASSPATH.

1.2.3. Stilul de scriere a codului în Java


- Creatorii Java recomandă folosirea unei conven ții unice de numire a pachetelor, astfel încât să nu apară
conflicte: utilizarea domeniilor de internet în ordine inversă.
com.sun.eng
com.apple.quicktime.v2
edu.cmu.cs.bovik.cheese

- Numele de clase ar trebui să fie substantive. Numele de clase formate din mai multe cuvinte se fac cu
capitalizarea fiecărui cuvânt din nume.
class Raster {};
class ImageSprite {};

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 12


- Pentru orice altceva (metode, câmpuri, referin țe de obiecte) prima litera e literă mică.
int i;
char c;
float myWidth;

- Numele de metode ar trebui să fie verbe.


void run() {System.out.println("run");}
void runFast() {System.out.println("run fast");}
String getBackground(){return "blue";}

- Numele constantelor de clasă și ale constantelor ANSI ar trebui să fie toate scrise cu majuscule, iar cuvintele
separate de caracterul "_".
static final int MIN_WIDTH = 4;
static final int MAX_WIDTH = 999;
static final String GET_THE_CPU_TYPE = "Intel";

Întrebări recapitulative
Întrebări pentru testul scris:
1. Ce este un compilator?
2. Ce este un interpretor?
3. Ce este un asamblor?
4. Ce este compilarea in doua faze?
5. Explicati diferenta intre codul masina si codul intermediar Java.
6. Cum difera compilarea si executia din C fata de Java?
7. Detaliati caracteristicile celor cinci generatii de limbaje de programare?
8. La ce foloseste variabila CLASSPATH?
9. Ce este JVM? Dar JRE? sau JDK?
10. Prezentati conventiile de numire ale pachetelor, claselor, metodelor, constantelor si variabilelor in
Java.

1.3. Teste de evaluare

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 13


1 Compilarea in doua faze este specifica limbajului de 8 Sintagma de baza a compilarii in Java este:
programare: A. Read once debug anywhere
A. C B. Write once run twice
B. C++ C. Write anywhere run once
C. Java D. Write once run anywhere

2 Codul intermediar Java poarta numele de: 9 Numele unei metode ar trebui sa fie format din:
A. cod obiect A. litere
B. bytecode B. cifre
C. cod masina C. verbe
D. majuscule

3 Java este un limbaj de programare de generatia a: 10 Compilarea fisierelor Java se face prin comanda:
A. cincea A. java
B. treia B. javac
C. doua C. gcc

4 În cadrul compartimentul concierge se regăsește postul de: 11 Java este un limbaj:


A. bagajist A. specializat
B. recepționer B. functional
C. guvernantă C. orientat obiect
D. cameristă

5 In Java, numele de clase sunt : 12 In variabila CLASSPATH este intotdeauna prezent:


A. adverbe A. C:\
B. prepozitii B. C:\Program Files\
C. interjectii C. /etc/bin/
D. substantive D. directorul curent

6 Fisierele compilate cu extensia .class sunt regrupate de 13 Programele scrise in Java sunt:
obicei in fisiere: A. mai rapide decat cele scrise in C
A. ZIP B. mai rapide decat cele scrise in limbaj de
B. GZIP asamblare
C. JAR C. mai sigure decat cele scrise in C
D. Alte tipuri de arhive

7 Limbajul Java a fost creat în anul: 14 Toate clasele dintr-un pachet vor fi colectater:
A. 1989. A. într-un singur director
B. 2001 B. in mai multe directoare
C. 1995 C. intr-un fisier ZIP

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 14


Programare Orientată Obiect

Modulul 1

Referințe bibliografice:

● Bruce Eckel, Thinking in Java, ed. 4-a, Prentice Hall, 2006


● Alfred AHO, Jeffrey ULLMAN, Principles of compilers design, Addison-Wesley, 1977

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 15


Programare Orientă Obiect

Modulul 2

Introducere în programarea obiectulă

I. Cuprins
1. Introducere în programarea
obiectulă 3. Clase interioare
2. Modele de design (Design Patterns)
4. Teste de evaluare
II. Obiective
În acest modul urmează să
prezentăm descrierea, realizarea și
implementarea conceptelor de
programare obiectuală în Java.
III.Aceste
Cuvinteconcepte
cheie sunt detaliate în
curs.obiect, incapsulare, atribute,
Clasa,
metode, compozitie, delegare,
mostenire, polimorfism, upcast,
downcast, modele de design, clase
interioare.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 16


Modulul 2 - Introducere în programarea obiectulă
2.1. Introducere în programarea obiectulă
2.1.1. Praradicma programării obiectuale
Toate limbajele de programare realizează abstractizări. Atunci când scrie un program, programatorul trebuie
să gândească în limbajul înțeles de calculator pentru a rezolva o problemă. Se stabile ște astfel o
corespondență între modelul mașinii (în spa țiul solu țiilor) pe care se rulează programul și modelul problemei
care se rezolvă prin intermediul calculatorului.
Activitatea de programare presupune stabilirea acestei asocia ții pentru o problemă cerută a se rezolva pe
calculator. Pentru simplificarea acestei activită ți de programare, este necesară inventarea unor limbaje de
programare pentru care modelele mașinii (spa țiul solu țiilor) să fie cât mai aproape de percep ția umană despre
spațiul problemei de rezolvat. Programarea obiectuală asociază elementelor din spațiul problemei obiecte în
spațiul soluțiilor.

Principalele caracteristici ale unui limbaj de programare obiectual sunt:


1. Orice element este un obiect. Putem vedea obiectele ca și variabile care memorează
date, dar în plus, putem adresa cereri obiectelor, solicitându-le să- și schimbe starea;
2. Un program este o colecție de obiecte. Obiectele sunt legate unele de
altele, transmițându-și mesaje. Putem vedea un mesaj ca și un apel de
funcție;
3. Fiecare obiect are propriul spațiu de memorie, și este constituit din alte obiecte.
Astfel, se pot crea noi tipuri de obiecte împachetând obiecte existente;
4. Fiecare obiect are un tip;
5. Toate obiectele dintr-un anumit tip pot primi același mesaj.

Light
on()
off()
brighten()
dim()
Nume tip
Interfața

Figura 2.1 Exemplu simplu de clasă reprezentată în UML


(limbaj standardizat pentru modelarea obiectuală)

O clasă descrie un set de obiecte care au caracteristici și funcționalitate


identice. Clasa este un tip de date, așa cum sunt cunoscute tipurile de date în
C. Cererile care pot fi adresate unui obiect definesc interfața obiectului.
Funcțiile care compun interfața unei clase se numesc metode. Figura 2.1 prezintă
exemplul clasei Ligth.

Modul de accesare a unui camp/metoda


objectReference.member

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 17


Metode, argumente și tip de return
ReturnType methodName(/* argument list */) {
/* method body*/
}

Astfel, dacă lt este un obiect de tipul Ligth, atunci putem cere acestui obiect realizarea mesajului on():lt.on();
Implementarea constă în modalitatea concretă prin care se realizează func ționalitatea descrisă de interfa ță.
Astfel, în exemplul prezentat anterior, implementarea va consta în ac țiunile concrete care trebuie realizate
(programate) pentru a realiza opera ția on(). În activitatea de scriere și utilizare a programelor
informatice, se disting 2 tipuri de actori: - programatori de clase noi
- programatori care folosesc clasele create de creatorii de clase. Programele scrise de
aceștia se numesc programe client.
Scopul programatorilor de aplica ții client este de a colecta și utiliza un mediu de lucru care să
conțină cat mai multe clase, pentru a asigura dezvoltarea rapidă aplica țiilor. Scopul
programatorilor creatori de clase este de a construi clase care să furnizeze programatorilor
clienți doar ceea ce este necesar, restul componentelor clasei urmând să rămână ascunse.
Se ajunge astfel la conceptul de “ascundere a implementării”, descris în Java prin cuvintele
cheie public, private, protected. Aceste cuvinte cheie se numesc modificatori de acces.
După ce o clasă a fost creată, testată și începe să fie utilizată în programe client, se pot construi noi clase care
să conțină obiecte din clasa inițială. Astfel, se realizează “ reutilizarea implementării”, una din facilitățile
principale oferite de programarea obiectuală.
Procesul prin care se compune o nouă clasă înglobând de la clase existente se nume ște
compoziție1 (agregare).
Dacă avem creată o clasă, și dorim să creăm o clasă nouă, cu o func ționalitate similară cu a
clasei inițiale (o clasă care, eventual, să con țină aceea și func ționalitate a clasei ini țiale, extinsă
cu proprietăți noi), se folosește conceptul de moștenire. Clasa inițială de la care porne ște
procesul de moștenire se numește clasă de bază.
În cazul în care prin moștenire, clasa nouă are exact aceea și interfa ță ca și clasa de bază, având doar o
funcționalitate diferită pentru metodele din interfa ță, spunem ca obiectele din tipul clasei derivate sunt de tipul
clasei de bază. Un asemenea tip de rela ție este denumită generic rela ție de tip is-a.
Dacă însă, clasa derivată adaugă funcționalitate nouă prin crearea de noi metode în interfa ță, atunci spunem
că obiectele de tipul clasei derivate se aseamănă cu obiectele din clasa de bază. Rela ția nou creată se
numește generic relație de tipul is-like-a.
Prin moștenire, se pot crea ierarhii de clase. Între obiectele de tipul claselor din ierarhie se
pot stabili relații de tipul is-a sau is-like-a. Astfel, putem considera că obiectele din clasele derivate sunt
în același timp și obiecte din tipul clasei de bază. Programarea orientată obiect ne permite să utilizăm obiecte
de tipul claselor derivate în locul obiectelor de tipul clasei de bază. Se ob ține astfel posibilitatea schimbării
obiectelor din clase diferite între ele, realizându-se polimorfism prin interschimbare de obiecte. Prin
polimorfism, mașina virtuala Java va determina în momentul execu ției tipul din care face parte obiectul,
apelând funcționalitatea aferentă metodei solicitate.
Crearea și distrugerea obiectelor reprezintă elemente importante de care trebuie să se țină seama
la scrierea programelor. La creare obiectele necesită resurse (memorie), iar Java utilizează în mod exclusiv
alocarea dinamică a memoriei cu tip efectiv determinat la execuție (necesarul de memorie al fiecărui
obiect în parte nu se poate determina în momentul compilării). Instantierea se realizează prin intermediul
operatorului new și are ca efect crearea efectiva a obiectului cu alocarea spa țiului de memorie corespunzător
în zona de “heap” a programului.
Pentru obiectele alocate static sau pe stivă, compilatorul se ocupă de distrugerea acestora la terminarea
programului sau a unei zone de vizibilitate.

1
Composition în lb. Engleză

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 18


Limbaje Java are un mecanism prin care mediul de execu ție caută și identifică obiectele care nu mai sunt
utilizate în program, și la identificarea lor, le dealocă. Mecanismul care realizează această caracteristică se
numește garbage collector și degrevează programatorul de sarcina de a mai distruge obiectele.
Identificatorii(variabilele) prin care se manipulează obiectele sunt referin țe către obiecte. De exemplu:
String s; // se creează o referință către un obiect de tip
String
String s = new String("asdf"); // referință creata este
inițializată
Dacă variabilele (referințele) create în program nu sunt ini țializate primi valoarea null, cuvânt rezervat ce
semnifică ca aceste variabile nu referă niciun obiect sau masiv.
De multe ori, la execuția programelor, pot apărea situa ții excep ționale, care nu au fost prevăzute la scrierea
programului și care generează erori. Cu cât programatorul este mai vigilent la scrierea programelor, cu atât
mai mult scade probabilitatea ca programul realizat să furnizeze erori. Unele limbaje avansate de programare
furnizează mecanisme de evitare a erorilor, numite “exception handling”. Astfel, în momentul în care apare o
eroare, se generează un obiect numit excep ție care urmează să fie tratat în mod special de program într-un fir
alternativ de execuție.

2.1.2. Formalizarea conceptului de obiect în Java


În C, o structură reprezintă o aglomerare de date, un mod de a împacheta mai multe variabile pentru o
utilizare comună a lor.

În Java, un obiect este o variabilă, adică un spațiu de memorie care are un identificator unic,
care păstrează date și specifică operațiile care se pot executa asupra acestor date.

Se numește încapsulare abilitatea de a împacheta împreună date cu func ții, în vederea


creării de noi tipuri de date. Ne referim la tipurile de date noi create prin sintagma de “ tipuri
abstracte de date” deoarece ele permit abstractizarea unor concepte din spațiul
problemei de rezolvat.

Sintagma

obiect.functieMembra(listaArgumente)

înseamnă pentru JVM apelarea funcției membru pentru un obiect, iar în terminologie orientată obiect
înseamnă transmiterea unui mesaj către obiectul respectiv. Astfel, un program în Java înseamnă creare de
obiecte și transmitere de mesaje către acestea.

2.1.3. Ascunderea implementării


La crearea de noi tipuri de date se dorește separarea interfeței de implementare.
Programarea orientată obiect introduce controlul accesului la membrii unei clase. Acesta este necesar
deoarece:
- programatorul client nu trebuie să poată accesa func ționalită ți care nu îi sunt destinate (cum ar fi
manipulările interne structurii);
- programatorul bibliotecii de obiecte trebuie să poată schimba implementarea internă fără să afecteze
eventualele programe client scrise deja, care utilizează biblioteca.

2.1.3.1. Încapsulare
Controlul accesului mai este denumit și “ascunderea implementării”. Încapsularea și controlul accesului
conduce la definirea unor entită ți care sunt mai mult decât simple structuri C. Astfel, prin încapsulare, putem

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 19


cuprinde împreună date (proprietăți) și func ții (comportament). Prin controlul accesului putem separa interfa ța
de implementare. Astfel, se poate schimba în orice moment implementarea fără a afecta func ționalitatea
programelor care utilizează variabile de tipul respectiv (ele interac ționează doar prin intermediul interfe ței).
Astfel, apare noțiunea de clasă, definită în Java prin cuvântul cheie class.

//Structura unei clase în Java


class NumeClasaJava {
//definiții de proprietăți(câmpuri)

//definiții de metode

//metoda main: punct de pornire a programului


public static void main(String args[]) {
//cod metoda main
} //end main
} // end class

2.1.3.2. Organizarea claselor în pachete și specificatori de acces la clase


Clase sunt grupate împreună într-un singur spațiu de nume logic numit pachet (package).
Orice clasă dintr-un pachet poate face referire directă la o alta clasă din acela și pachet.
Clasele utilizate care fac parte din alt package se importă folosind directiva import. Se pot
importa toate clasele dintr-un pachet, printr-o instrucțiune de forma:

import numePachet.subPachet.*; // sau


import java.util.*; // se importa tot packageul java.util
sau doar o singura clasa cu:
import numePachet.NumeClasa; // sau
import java.util.ArrayList; // se importa clasa ArrayList
Punctul în numele bibliotecii reprezintă un subdirector de pe disc.
Lipsa definiției unui pachet presupune utilizarea unui așa numit pachet default (însă nu se
recomandă utilizarea acestuia deoarece clasele definite aici nu pot fi referite din exterior).

În Java specificatorii de acces la clase sunt:


- public: clasele pot fi referita din afara pachetului lor;
- Fără specificator de acces – acces package, nu pot fi referite din afara pachetului în
care au fost definite.

2.1.3.3. Specificatori de acces la membrii claselor


În Java specificatorii de acces la membrii claselor sunt:
- public: elementele private pot fi accesate doar de creatorul clasei în cadrul metodelor
acelei clase;
- private: elementele publice sunt disponibile oricăror alte clase;
- protected: similar cu private, utilizat în mo ștenire, clasele care mo ștenesc pot accesa
elementele private din clasele de bază;
- Fără specificator de acces – acces package: pot fi referiți din clasele package-ului
curent.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 20


Specificatorii pot să apară în orice număr și ordine în cadrul unei clase. Ace ști specificatori de acces au rolul
de a ascunde implementările de interfe țe, și de a asigura un control eficient și o separare a datelor și
conceptelor din program.

2.1.4. Gestiunea memoriei în Java


Mașina virtuala Java nu permite accesul direct la zona de registru a
procesorului. Zonele de memorie frecvent folosite sunt stiva și heap-ul. Stiva este
utilizată pentru a stoca variabilelor metodelor (argumente și variabile locale).Fiecare metoda
are propria stivă (o zona în stiva procesului), inclusiv metoda speciala main.

Stiva unei metode există doar pe durata de via ța a acestei metode: din momentul apelării până în momentul
terminării funcției (print return sau cu o excepție). Acest comportament se datorează chiar faptului ca stiva
este limitată și spațiul este pus la dispozi ția următoarei metode.

Heap-ul este un spațiu de memorie utilizat de JVM pentru a obține memorie suplimentara
la execuție (run-time) atunci când sunt create obiecte folosind cuvântul cheie new.

Variabilele de pe stivă există atâta timp cât metoda este executată. Valorile din heap există atât timp cat o
referință are ca valoare adresa acestei zone de memorie. Obiectele Java pot fi stocate și în zone de memorie
non-RAM prin operații de serializare.

Variabilele Java pot avea și tipuri primitive neobiectuale (boolean, char, byte, short, int, long, float, double sau
void). Aceste variabile de dimensiune mică sunt salvate direct pe stivă, iar ini țializarea lor nu necesită new la
alocare. Fiecare dintre acestea are un wrapper type pentru construirea obiectului din tipul obiect
corespondent. De exemplu tipului primitiv char îi corespunde tipul obiect Character. Prin autoboxing se
realizează conversia automată de la un tip primitiv la un tip wrapper asociat.

Character ch = 'x';

2.1.5. Membrii statici ai unei clase


Sunt prefixați la declarație de cuvântul cheie static. Dacă datele membre și metodele nestatice pot fi
accesate doar sub calificarea unui obiect existent în memoria programului (creat cu new), un câmp sau o
metodă statică nu este legat de vreun obiect anume dintr-o clasa. Vorbim de date sau de metode de clasă.

public class Bicycle {

private int gear;


private int speed;

// add a class variable for the


// number of Bicycle objects instantiated
private static int numberOfBicycles = 0;

public static int getNumberOfBicycles() {


return numberOfBicycles;
}
}

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 21


Un câmp static există o singură dată pentru clasa în care a fost definit, fiind partajat de toate obiectele clasei
respective. Câmpurile și metodele pot fi referite prin numele clasei (fara a fi necesara existenta unui obiect).

Bicycle.numberOfBicycles++;
Bicycle.getNumberOfBicycles();

Un exemplu recurent de astfel de metodă este main. Metodele statice nu pot accesa câmpuri/metode ne-
statice din clasa respectivă.

2.1.6. Operatori
In Java operatorii produc o valoare prin aplicarea lor. Ca și în limbajul C avem 5 tipuri de
operatori:
- aritmetici (+,+=,-,-=,*,*=,/,/=,%,%=,++,--);
- pe biți (-,&,&=,|,|=,^,^=,>>,>>=,<<,<<=,>>>,>>>=);
- relaționali (==,!=,>,<,>=,>,<=, instanceof);
- operatori logici (!, &&,||,? :).
Prioritatea operatorilor este similara celei existe în limbajul C. Ei pot produce și efecte colaterale în sensul
modificarea valorii operanzilor asupra cărora sunt aplica ți.
Operatorul de atribuire = atribuie unei variabile de tip primitiv valoarea indicată.

int a= 4; // atribuirea unei valori dintr-un tip primitiv unei


variabile

In cazul variabilelor de tip obiect, operatorul de atribuire face ca valoarea referin ței să indice adresa de pe
heap a obiectului. De exemplu, dacă c și d sunt obiecte, după atribuirea

Object c=new Integer(2), d=new String("abc");


c = d;

ambele variabile(c și d) vor referi același obiect (String("abc")).


Aliasing: la transmiterea unui obiect ca și argument într-o metoda, se transmite o referin ță, deci modificarea
valorii în metoda afectează valoarea obiectului din afara metodei. Astfel, transmiterea parametrilor de tipuri
neprimitive se pace prin referință (pe stivă va fi copiată doar adresa din heap la care se găsesc obiectele
referite), iar pentru parametri de tipuri primitive se face prin valoare (o copie a valorii lor va fi pusă pe stivă).
Operatorii ++ și – au forma post-fixată și prefixată (similar cu C/C++). Operatorii == și != aplicați pe
referințe de obiecte, compara referințele, și nu conținutul obiectelor.
Pentru compararea conținutului obiectelor, se folosește metoda equals:

c.equals(d);

Pentru clasa String se pot folosi + și += în sensul de concatenare.

String s1="abc", s2;


s2 = "e" + "fg"; //s2 = "efg"
s1 += s2; //s1="abcdefg"

La crearea unui obiect, Java apelează automat constructorul clasei respective. Constructorul default este cel
ce nu are argumente.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 22


2.1.7. Crearea și distrugerea obiectelor. Supraîncărcare
2.1.7.1. Inițializarea obiectelor
In fiecare clasa se garantează ini țializarea obiectelor prin scrierea unui constructor. Numele
constructorului este identic cu numele clasei. Constructorul este o metodă membră a clasei
care are același nume ca și clasa.
Ca și orice altă metodă, constructorul poate primi argumente, și anume, elemente care să
controleze modul în care este inițializat obiectul. În plus, se pot scrie mai mul ți constructori și
apelul acestora se poate realiza în mai multe modalită ți.
Constructorii nu au tip de retur. Aceasta înseamnă că tipul de retur diferă de void (care înseamnă că metoda
nu returnează nimic). Dacă constructorii ar avea tip de retur (fie și void) atunci ar fi nevoie ca să se realizeze
apelul explicit al acestora, deci constructorii nu s-ar mai putea apela implicit.

public class Actor {


public String firstName;
public String lastName;
public boolean goodActor;

public Actor(String first, String last)


{
firstName = first;
lastName = last;
}

public Actor(String first, String last, boolean good)


{
firstName = first;
lastName = last;
goodActor = good;
}

public static void main(String args[]) {


Actor a = new Actor("Emma", " Watson");
Actor b = new Actor("Natalie", "Portman", true);
}
}

În C, variabilele trebuiau definite întotdeauna la începutul blocului de vizibilitate (de ex. la începutul func țiilor).
Această regulă nu se mai păstrează în Java. Astfel, obiectele pot fi definite oriunde în cod și definirea
acestora se face de regulă, cât mai aproape de locul de utilizare.

Se numește constructor implicit (default), constructorul care nu are argumente. Dacă clasa
nu are nici un constructor, atunci compilatorul va scrie (crea) automat un constructor implicit
pentru clasa respectivă. Dacă într-o clasă a fost definit cel pu țin un constructor cu parametrii,
atunci constructorul implicit (fără parametrii) nu va mai fi creat automat.
În cazul în care dorim să apelăm explicit constructorul unei clase folosim metoda
this(argumente), care apelează constructorul ce corespunde liste. Astfel, nu se repetă
secvențele de cod scrise la constructorii cu mai pu ține argumente.

public class Point {

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 23


private double x;
private double y;

Point () {
this(0,0);
}

Point (double x, double y ) {


this.x=x; this.y=y;
}

Point (double x) {
this(x,0);
}
}

2.1.7.2. Cuvântul cheie this


In fiecare metodă apelată, referința obiectului sub care se apelează metoda este transmisă în metoda sub
forma referinței this. Cuvântul cheie this poate fi utilizat doar în interiorul metodelor nestatice.

public class Point {


public int x = 0;
public int y = 0;

//constructor
public Point(int x, int y) {
this.x = x;
this.y = y;
}

public Point multiplyByScalar(int n) {


this.x *= n;//aici this e facultativ
this.y *= n;
return this;
}
}

In interiorul unei metode dintr-o clasă, la apelul unei metode din clasa curentă, nu este nevoie sa se utilizeze
this. Cuvântul cheie this este frecvent utilizat la retur, pentru a returna obiectul curent.

2.1.7.3. Distrugerea Obiectelor

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 24


Garbage collector-ul (GC) colectează memoria rămasă alocata în obiectele care nu mai sunt
utilizate și compactează heap-ul, rearanjând obiectele alocate. GC este implementat folosind
reference counting: de cate ori se asociază o referința pentru un obiect, contorul de referin țe al
obiectului se incrementează. De cate ori se pierde o referin ță pentru o obiect, contorul de
referințe al obiectului se decrementează. GC scanează șirul de variabile contor și când
întâlnește una egala cu zero, dealocă obiectul respectiv.
In Java pentru un obiect se poate identifica o referin ță fie pe stivă fie în zona statică de date. Pentru
optimizarea utilizării memoriei GC realizează o opera ție de tip Stop-and-copy: GC oprește execuția
programului, scanează toate referin țele de pe stivă și copiază obiectele identificate în noul heap. Ceea ce
rămâne e zona ce trebuie dealocată, iar noul heap e deja compactat.
In procesul dealocării memoriei unui obiect, JVM apelează metoda finalize(). Ea permite realizarea de operații
de ștergere înainte ca obiectele sa fie supuse garbage collector-ului Java nu garantează însă apelarea acestei
metode, iar programatorul nu are control asupra momentului când se apelează colectorul.
class A
{
//overriding finalize() method inherited from Object class.
public void finalize()
{
System.out.println("Object is garbage collected");
}

public static void main(String... ar)


{
Runtime run = Runtime.getRuntime();
A ob;

//Creating 10 objects of class A


for(int i=0; i<10;i++)
{
ob= new A();
}

run.gc(); //Making a request to garbage collector to de-


allocate unreferenced objects.
}
}
Rulare:

Object is garbage collected


Object is garbage collected // here not all of the 10 objects are
garbage collected

Dacă obiectele noastre agregă alte obiecte, colectorul se ocupă de dealocarea obiectelor agregate. Metoda
finalize se utilizează în general pentru a implementa termination conditions pentru un obiect (de
exemplu închiderea unui fișier).
Fiindcă Java nu garantează apelarea metodei finalize, pentru o ștergere corectă a obiectelor atunci când, de
exemplu, programul se termină cu o excep ție, este indicat ca codul de ștergere sa fie pus într-o metoda
dispose. Metoda dispose se apelează pe clauza finally a unui try-catch global.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 25


try
{
// Code and exception handling...
}
finally
{
// aici e finally de la un try-catch global
x.dispose();
/*codul din finally este intotdeauna executat
chiar Dacă în program a aparut o exceptie */
}

Ștergerea obiectelor trebuie să se realizeze în ordine inversă creării lor, sarcină ce incumbă programatorul.

2.1.7.4. Supraîncărcarea metodelor


Metoda(funcția) reprezintă un nume asociat unei ac țiuni (sau unui sir de ac țiuni). De multe ori apare o
problemă la modelarea spațiului problemei, când acela și nume are semnifica ții diferite, în func ție de contextul
de apel. Conceptul are mai multe înțelesuri, este deci “ supraîncărcat”.
În multe limbaje de programare (C, de exemplu) se solicită utilizarea de nume diferite pentru sensuri diferite,
sau în altă formulare, nume unice pentru fiecare în țeles distinct. În Java, această regulă nu se mai păstrează.
Un exemplu de încălcare a ei este utilizarea aceluia și identificator pentru numele clasei și numele
constructorului.
Supraîncărcarea metodelor permite folosirea aceluiași identificator pentru a denumi mai multe func ții. La
supraîncărcarea funcțiilor, se poate folosi acela și nume, dar lista argumentelor trebuie să difere. La
supraîncărcare, primitivele din lista de argumente sunt promovate (promoted) către tipurile mai largi. Nu se
poate realiza supraîncărcarea funcțiilor numai pe tipul returnat.
Deoarece constructorii sunt la rândul lor func ții, ei se pot supraîncărca. Astfel, se pot scrie mai mul ți
constructori pentru o clasă.

2.1.7.5. Crearea obiectelor


Presupunem ca avem o clasa Dog. Crearea unui obiect din aceasta clasă presupune mai multe etape:
● Prima data când un obiect de tip Dog este creat, JVM localizează pe disc fi șierul Dog.class și îl
încarcă;
● La încărcarea Dog.class, se inițializează to ți membrii statici ai clasei. în general, ini țializarea
datelor statice are loc doar Dacă acestea sunt necesar a fi utilizate și precedă întotdeauna
inițializarea datelor non-statice;
● La întâlnirea apelului new Dog() se alocă memorie pe heap pentru noul obiect, aceasta zona de
memorie se umple cu 0;
● atributele de tipuri primitive sunt ini țializate la valoarea implicită (de exemplu 0 pentru tipul int și
false pentru boolean);
● Se realizează toate inițializările explicite la momentul definirii datelor membre în ordinea în care
acestea sunt definite în clasa. Obiectele membre neini țializate, vor avea valoarea null;
● Se executa corpul constructorului care poate la rândul sau inițializa valorile
datelor membru.

Datele membre ale unei clase pot fi ini țializate cu ajutorul blocurilor de ini țializare. Acestea pot fi statice sau
non-statice. Blocul static este apelat o singură dată, la prima creare a unui obiect din clasa respectivă sau la
apelarea unui membru statice din acea clasă. Blocul static poate să ini țializeze doar date statice.
Blocul non-static de inițializare specifică opera ții de ini țializare care trebuie efectuate indiferent de
constructorul apelat. El este necesar pentru a se permite definirea claselor interne anonime.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 26


class Test {

public int valoare;


public static int valoareStatica;

//bloc static de initializare


static
{
System.out.println("Bloc static de initializare din
Test !");
valoareStatica = 23;
//valoare = 50; //eroare de compilare
}
//constructor
public Test(){
System.out.println("Constructor Test !");
}
//bloc de initializare non-static
{
System.out.println("Bloc de initializare non-static din
Test !");
valoare = 50;
}
}

public class MainClass {


public static void main(String[] args) {
Test t1 = new Test();
System.out.println("valoareStatica =
"+Test.valoareStatica);
System.out.println("t1.valoare = "+t1.valoare);
Test t2 = new Test();
}
}
Rulare:
Bloc static de inițializare din Test !
Bloc de inițializare non-static din Test !
Constructor Test !
valoareStatica = 23
t1.valoare = 50
Bloc de inițializare non-static din Test !
Constructor Test !

2.1.8. Programarea cu obiecte din mai multe clase


O buna arhitectură a unei aplica ții orientate obiect presupune reutilizarea facilă a codului existent în diferitele
clase. Reutilizarea codului nu înseamnă în nici un caz copy-paste de cod. Ea este realizată prin doua opera ții
principale: compoziție și moștenire. La realizarea opera țiilor de mo ștenire și compozi ție, se vor importa clasele
referite. Astfel, codul lor sursă va fi astfel reutilizat.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 27


2.1.8.1. Compoziția
Compoziția înseamnă a crea o nouă clasă care con ține ca și date membre obiecte din clase
existente. Se spune că un obiect din noua clasă e compus din obiecte din clasele vechi
(existente). Referințele datelor membre obiectuale nou create se ini țializează la valoarea null,
deci ele trebuie inițializate explicit.

Compoziția este de două tipuri:


- Compoziție propriu-zisă: atunci când obiectul membru (sub-obiectul) este în mod
efectiv parte componentă a obiectului din noua clasa. în compozi ție, sub-obiectul există
și are sens numai ca parte a obiectului compus. De exemplu: o persoană are două
mâini și o inimă; o clădire este compusă din mai multe săli. Astfel, sub obiectele (săli)
sunt distruse de îndată ce obiectul clădire va fi distrus și nu au sens în afara unei
clădiri;
- Agregare: sub obiectul există în afara obiectului agregat, este creat în exterior, deci
este trecut ca un argument pentru constructor sau o metodă de ini țializare/adăugare.
De exemplu, administrația unui oraș poate fi proprietara unui parc de ma șini, însă
mașinile sunt create inițial într-un context diferit după care intră în posesia ora șului; mai
apoi, o mașină poate fi vândută și ieși în proprietatea primăriei.

În descriere UML, structura de clase de mai sus poate fi reprezentată prin figura de mai jos:

Figura 2.1 Compoziția și Agregarea

Rombul plin înseamnă compoziție iar cel gol agregare. Compozi ția/Agregarea este expresia unei rela ții de tipul
“has-a”, și se traduce prin expresia: un obiect din tipul X este membru al unui obiect din
tipul Y, sau un obiect din tipul Y se compune dintr-un obiect din tipul X.

class Person {
private Heart heart;
private List<Hand> hands;
}

class City {
private List<Tree> trees;
private List<Car> cars;
}

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 28


Inițializarea referințelor la realizarea compoziției se poate realiza în patru moduri:
● La momentul definirii obiectului – inițializarea se face înainte de apelarea
constructorului;
● In constructor;
● Chiar înainte de utilizarea obiectului (lazy initialization);
● In bloc non-static de inițializare (instance initialization).

2.1.8.2. Moștenirea
Prin moștenire se creează o clasă nouă (clasa derivată) care se aseamănă cu o clasă
existentă (clasa de bază). Moștenirea este expresia unei relații de tipul “is-like-a”.
Sintaxa moștenirii presupune indicarea clasei de bază după numele clasei care se creează,
înainte de a se trece la corpul defini ției noii clase. Java permite doar mo ștenirea simpla, adică
derivarea unei clase utilizând doar o clasă de bază.
Prin moștenire noua clasă va avea ca și membrii toți membrii clasei de bază. În plus, noua
clasă poate să își definească proprii membrii, sau să rescrie defini țiile membrilor din clasa de bază. Subsetul
de membrii din clasa de bază care vor fi mo șteni ți în clasa derivată se determină utilizând regulile de
combinare a specificatorilor de acces.
În rescriere UML, structura de clase din exemplul de mai sus este reprezentată în figura 2.2.
X
- i : int
+ X ()
+ set(in ii : int) : void
+ read() : int
+ permute() : int

Y
- i : int
+ Y ()
+ change() : int
+ set(in ii : int) : void

Figura 2.2 Reprezentarea grafică a mo ștenirii în UML

Noua clasă Y moștenește toate datele membre ale lui X. De fapt Y con ține un sub-obiect de tipul X ca și cum
am fi realizat agregarea tipul X în tipul Y. Astfel, la apelul sizeof se remarcă faptul că Y ocupă mai mult spa țiu
decât X. Membrii privați ai lui X există în continuare ca și membrii priva ți ai sub-obiectului, însă mecanismul
private funcționează, adică nu putem accesa ace ști membrii din afara obiectului de care apar țin (care este de
tip X). Deci, Y moștenește doar membrii din interfa ța lui X, la care se poate avea acces din afară.
Se observă modul în care metoda set din clasa Y redefinește metoda set din clasa X. în acest caz, nu e
vorba de supraîncărcare, deoarece semnăturile celor 2 func ții (cea din clasa X și cea din clasa Y) sunt
identice. La invocarea metodei set pe un obiect din clasa Y se va apela metoda set redefinită.

In Java, moștenirea se realizează ori de câte ori se creează o clasă: se mo ștene ște din java.lang.Object. De
exemplu, Fiindcă clasa nouă poate extindă comportamentul clasei vechi cuvântul cheie utilizat extends.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 29


Figura 2.3 Exemplu de realizare a moștenirii prin compoziție

Pentru realizarea moștenirii, la crearea obiectului din clasa derivată, se creează un sub obiect din clasa de
bază (ca și cum ar fi realizată o compoziție către acesta). Acest obiect poate fi referit prin cuvântul cheie
super. Inițializarea sub-obiectului din clasa de bază se realizează prin apelul constructorului său.
Constructorul clasei de bază este apelat întotdeauna înaintea constructorului clasei derivate. Pentru apel al
constructorului clasei de bază cu argumente: super (argumente). Din clasa derivată, pentru a se apela o
metodă din clasa de bază se utilizează referin ța super.
class Bicycle {
// bicicleta are 2 atribute
public int gear;
public int speed;

// constructor
public Bicycle(int gear, int speed) {
this.gear = gear;
this.speed = speed;
}

// trei metode
public void applyBrake(int decrement) {
speed -= decrement;
}

public void speedUp(int increment) {


speed += increment;
}

public String toString() {


return("No of gears are " + gear +"\nspeed of bicycle is "
+ speed);
}
}

//derived class
class MountainBike extends Bicycle {
// se adauga un atribut suplimentar

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 30


public int seatHeight;

// constructor
public MountainBike(int gear,int speed,
int startHeight) {
// invocam constructorul clasei de baza
super(gear, speed);
seatHeight = startHeight;
}

// extindem interfata cu o noua metoda


public void setHeight(int newValue) {
seatHeight = newValue;
}

// suprascriem metoda toString()


@Override
public String toString() {
return (super.toString() +
"\nseat height is " + seatHeight);
}
}

public class Test {


public static void main(String args[]) {

MountainBike mb = new MountainBike(5, 70, 25);


System.out.println(mb.toString());
}
}

Rulare:
No of gears are 5
speed of bicycle is 70
seat height is 25

2.1.8.3. Operația de delegare


Delegarea este o operație intermediară intre compozi ție și mo ștenire. Consta în plasarea într-o
clasă a unui obiect membru din a 2-a clasă, iar clasa nouă va expune o parte sau toate
metodele furnizate de obiectul din clasa a 2-a. Delega ția poate fi o alternativă la mo ștenire.
Delegarea presupune transmiterea mesajelor către instan ța din a 2-a clasă. în multe cazuri ea
poate fi mai bună decât moștenirea, deoarece nu obligă acceptarea tuturor metodelor ca și în
cazul super claselor (se pot expune numai metodele care au sens).

class Delegate() {

Result doSomething() {
Result myResult = new Result();

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 31


return myResult;
}

//metoda neexpusa
Result doSomethingElse() {
Result otherResult = new Result();
return otherResult;
}
}

public class Delegator {

private Delegate delegate;

public Result doSomething() {


return delegate.doSomething();
}
}

Principalul avantaj al delegării este flexibilitatea la runtime. Delegatul poate fi modificat cu ușurin ță la execu ție.

2.1.8.4. Combinarea compoziției cu moștenirea


Compoziția și moștenirea pot fi combinate la libera alegere a programatorului. Astfel, se pot crea sisteme
oricât de complexe și extensibile. Figura de mai jos prezintă un exemplu de combinare a agregării cu
moștenirea.

B
-i : int
+B()
+ ~B()
+f() : void

A C
-i : int 1 1 -a : A
+A() +C()
+~A() +~C()
+f() : void +f() : void

Figura 2.4 Agregare și moștenire combinate

Se observă că de multe ori, la crearea obiectelor complexe este nevoie de apel explicit al constructorilor.
Apelul destructorilor acestor obiecte se realizează în mod automat, de către compilator. Sec țiunea următoare
va trata în detaliu ordinea de apel automat a constructorilor și destructorilor, în cazul combinării compozi ției cu
moștenirea.

2.1.8.5. Inițializarea obiectelor în cazul moștenirii


La crearea obiectelor din clase derivate, ordinea de apelare a constructorilor este:
● Se apelează constructorul clasei de bază. Apelul este recursiv, până la vârful
ierarhiei (constructorul tipului din vârf este apelat primul);
● Se inițializează membrii clasei în ordinea declarării acestora;
● Se apelează corpul constructorului clasei derivate.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 32


class Game {
Game(int i) {
System.out.println("Game constructor");
}
}

class BoardGame extends Game { //clasa derivata din Game


BoardGame(int i) {
super(i);
//apel constructor clasa de baza (Game)
System.out.println("BoardGame constructor");
}
}
public class Chess extends BoardGame {
Chess() {
super(11);
//apel constructor clasa de baza BoardGame
System.out.println("Chess constructor");
}
public static void main(String[] args) {
Chess x = new Chess();
}
}
Rulare:
Game constructor
BoardGame constructor
Chess constructor

Chiar dacă în constructor se apelează o metodă presupusă a se lega dinamic, în fapt, se apelează exclusiv
metoda suprascrisă (cea din clasa cu constructorul deoarece în acest caz legarea are loc la compilare).
O metodă suprascrisă dintr-o clasă derivată poate returna un tip derivat dintr-un tip returnat de metoda din
clasa de baza (care este suprascrisă). Aceasta strategie de programare poarta numele de covariant return
types.

//Two classes used for return types.


class A {}
class B extends A {}

class Base {
A method() {
System.out.println("Base method()");
return new A();
}
}

class Derived extends Base {


B method() {
System.out.println("Derived method()");
return new B();

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 33


}
}

public class CovariantReturnTypes {


public static void main(String args[])
{
Base base = new Base();
base.method();

Derived derived = new Derived();


derived.method();
}
}
Rulare:
Base method()
Derived method()

2.1.8.6. Considerente de design a claselor


În această secțiune se vor prezenta câteva considerente care trebuie urmate la alegerea între utilizarea
compoziției și a moștenirii la crearea aplica țiilor orientate obiect.
Compoziția și moștenirea reprezintă două modalită ți prin care programele existente se pot extinde și reutiliza.
Compoziția se alege atunci când interfa ța clasei de baza nu se dore ște a fi expusă în clasa derivată.
Compoziția se face, de obicei, prin agregare private.
Moștenirea se folosește atunci când interfa ța clasei de baza se dore ște preluată și expusă de clasa derivata.
în Java, chiar Dacă toate metodele și atributele sunt mo ștenite și accesibile prin referin ța super, doar
elementele publice și protected pot fi accesate. Protected funcționează ca și private în afara ierarhiei.
Avantajul principal al acestor două modalită ți de reutilizare a codului este posibilitatea de dezvoltare
incrementală a programelor. Astfel, putem scrie cod nou, respective să adăugăm facilită ți noi programelor
existente fără să fie nevoie să rescriem codul deja existent. Astfel evităm introducerea de biguri în programele
existente care probabil sunt deja func ționale. Bug-urile care apar în programe pot fi astfel izolate la nivelul
noului cod scris.
Este important de înțeles că dezvoltarea incrementală a programelor este similară cu procesul uman de
învățare. Astfel, referitor la un domeniu anume, putem realiza o activitate de analiză oricât de cuprinzătoare
dar nu vom reuși să cuprindem toate aspectele domeniului la momentul realizării analizei. Astfel, pentru a
putea continua și a reuși să furnizăm software util, dezvoltarea incrementală devine esen țială.

2.1.9. Cuvântul cheie final


Itemii specificați cu final nu pot fi schimba ți (de exemplu, într-o clasă derivată). Datele membre final sunt
constante la compilare ce nu se schimbă ulterior.
Final poate fi utilizat și atunci când sunt inutilizate variabile cu valori determinate doar la
execuție ce mai apoi nu se dorește a fi schimbate.
final int seed = new java.util.Random().nextInt(10);

Atunci când final este precedat de static datele vor fi stocate în zona de date a programului.

// a final static variable PI


static final double PI = 3.141592653589793;

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 34


Final aplicat la o referință face referin ța constantă: valoarea obiectului poate fi modificată însă obiectul de pe
heap nu poate fi înlocuit cu un alt obiect.

class FinalVariable {
public static void main(String args[]) {
final String s = "abc";
s = "ef"; // Error because s is final.
}
}
//-----------------------------------------------------------------
---------
class SomeObject {
int i = 10;
}

class TestFinalReference {
public static void main(String args[]) {
final SomeObject t = new SomeObject();
t.i = 30; // Works correctly
}
}

Câmpurile declarate final care nu sunt inițializate, trebuie inițializate înainte de utilizare.
// a blank final static variable
static final double PI;

In cazul argumentelor declarate final, corpul metodei nu se poate schimba valoarea către care arată referin ța.
class MyClass {
private int x;
//getters and setters
void setX(int x) {
this.x = x;
}

static void doSomething( final MyClass arg ) {


arg = new MyClass(); // Compiler error: The passed
argument variable arg cannot be reassigned to another
object.
arg.setX(20); // allowed
// We can reassign properties of argument which is marked
as final
}
}

Metode final nu pot fi suprascrise prin moștenire. Metodele private ale unei clase sunt
implicit final. Metodele final sunt tratate inline de către compilator. Codul metodei este copiat de
compilator ceea ce permite creșterea vitezei de execu ție prin evitarea generării codului pentru apel și retur
precum și a codului de introducere și extragere din stivă a parametrilor metodei.
final class A
{

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 35


// methods and fields
}
// The following class is illegal.
class B extends A
{
// COMPILE-ERROR! Can't subclass A
}

In cazul clase declarate final, se inhibă mo ștenirea din clasa respectivă. Aceste clase vor avea toate metodele
implicit finale.

2.1.10. Upcast
Upcast reprezintă una din proprietățile cele mai importante introduse de paradigma obiectuală
prin conceptual de moștenire. Upcast înseamnă că obiectele din tipul claselor derivate sunt în
același timp și din tipul clasei de bază.

Să considerăm o clasă de bază Intrument, și o clasă derivată Wind. Clasa derivată moștenește toate metodele
interfeței clasei de bază. Astfel, dacă vom putea trimite un mesaj unui obiect de tip Instrument, acela și mesaj îl
vom putem trimite și unui obiect de tipul Wind. Astfel, putem spune că un obiect de tip Wind este în același
timp și de tipul Instrument. Clasa Instrument de ține o metodă play care este specifică claselor derivate,
precum clasa Wind.
Oricum, la transmiterea argumentelor pentru o func ția redefinită a clasei Instrument, compilatorul realizează
conversia automată a pointerului de tip Wind în referință de tip
Instrument. Aceasta reprezintă în fapt, opera ția de upcast.
Revenind la întrebarea referitoare la alegerea între compozi ție și mo ștenire, una din modalită țile cele mai bune
de a decide care concept să-l folosim este să încercăm să răspundem la următoarea întrebare: pe parcursul
programului, operația de upcast va fi necesară? În caz afirmativ, se recomandă utilizarea mo ștenirii. În caz
negativ, utilizarea compoziției.

enum Note {
MIDDLE_C, C_SHARP, B_FLAT; // Etc.
} ///:~

class Instrument {
public void play(Note n) {
System.out.println("Instrument.play()");
}
}

// Wind objects are instruments because they have the same


interface:
class Wind extends Instrument {
// Redefine interface method:
public void play(Note n) {
// metoda play este cea suprascrisa
System.out.println("Wind.play() " + n);
}

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 36


}

public class Music { //fisier Music.java


public static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind(); //flute este în acelasi timp și
Instrument
//tune asteapta un argument de tip Instrument
//dar accepta și obiecte de tip flute
tune(flute); // Upcasting – se va executa metoda play din
clasa Wind
}
}

Rulare:

Wind.play() MIDDLE_C

2.1.11. Legarea metodelor și datelor membru în Java


La apelul unei funcții, aducerea în contextul curent și execuția corpului unei
funcții se numește legarea funcțiilor. Atunci când legarea funcțiilor se face înainte de
execuția programului, se spune că avem legare “înainte” ( early binding). Early binding se face
întotdeauna în faza de link-editare a programului. În limbajele procedurale (C) early binding
reprezintă singura modalitate de legare a corpului unei func ții de apelul acesteia.

Alternativa la early binding o reprezintă “legarea întârziată” (late binding) adică legarea
dinamică a corpului funcției de apelul acesteia exact înainte de realizarea apelului. Legarea
întârziată presupune existenta unui mecanism care să determine tipul exact al obiectului înainte
de apelul funcției. Astfel, prin determinarea exactă a tipului înainte de apel, se poate face
legare la versiunea corectă a funcției, și anume la cea redefinită în cazul în care ne situăm într-
un program cu ierarhii de clase.

Redefinirea unei funcții într-o clasă derivată se nume ște “ overriding”. în Java, mecanismul de
legare dinamică și identificarea tipului la execu ție se nume ște RTTI ( Run-Time Type
Identification). JVM utilizează legarea dinamică pentru orice apel de metodă, cu excep ția
metodelor statice și a metodelor final (private e implicit final pentru că o metodă private nu
poate fi moștenită). Legarea constructorilor este realizată și ea la compilare. Prin declararea
unei metode ca fiind final putem inhiba mecanismul de legare dinamică și cre ște eficien ța la
execuție. în ceea ce privește legarea datelor membru statice sau non-statice , acestea sunt legate (rezolvate)
la compilare.

In exemplul analizat, aceasta presupune inserarea cuvântului cheie virtual înainte de


declarația metodei play în clasa Instrument.
În contextul polimorfismului, metodele care se apelează pentru obiecte din clasele de bază (precum metoda
tune) pot fi privite ca și o trimitere de mesaj către un obiect și responsabilizarea obiectului destina ție cu privire

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 37


la modul de tratare a mesajului. Astfel, obiectul (în func ție de tipul său determinat la execu ție) va trata mesajul
într-o manieră proprie, specifică.

2.1.12. Clase abstracte și interfețe


De multe ori în designul unei aplica ții dorim să punem în clasa de bază doar interfa ța viitoarelor obiecte, și nu
să creăm obiecte din clasa respectivă. Clasa de bază va fi folosită doar pentru opera ția de upcast care va
permite astfel tratarea unitară a obiectelor derivate, care au aceea și interfa ță. Pentru a realiza acest lucru,
vom transforma clasa de bază într-o clasă abstractă.

O clasă se numește abstractă dacă are cel pu țin o metoda abstracta, adică se declară doar
semnătura ei, nu și implementarea. Pentru a exprima faptul că o metodă (si clasa care o
conține) este abstractă, Java folosește cuvântul cheie abstract.

abstract class Instrument {


//...
abstract void play();
//...
}

Compilatorul împiedică definirea de obiecte din clasele abstracte.

Instrument i = new Instrument();// produce eroare de compilare !!!


//tipul Instrument neputand fi instantiat

Atunci când se moștenește dintr-o clasă abstractă, clasa derivată trebuie să implementeze toate func țiile
abstracte. În cazul în care în clasa derivată nu este implementată cel pu țin o abstractă, clasa derivată devine
la rândul său o clasă abstractă. Metodele abstracte for țează astfel să fie implementate, în clasele derivate,
care se doresc a fi clase efective.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 38


Figura 2.5 Exemplu de clase abstracte dominând vârful unei ierarhi de clase

În exemplul cu ierarhia de instrumente muzicale, metodele din interfața clasei Instrument sunt
doar de complezență, adică au fost definite deoarece este nevoie de definirea lor pentru ca programul să
funcționeze. Aceste metode nu furnizează un răspuns util. Inten ția clasei Instrument este să se creeze o
interfață pentru clasele care se vor deriva, astfel încât obiectele din clasele derivate să poată fi folosite în mod
unitar. Astfel, clasa Instrument este un bun exemplu de posibilă clasă abstractă. De obicei, se creează o clasă
abstractă în vârful unei ierarhi de clase atunci când dorim să manipulăm obiecte din diverse clase derivate
printr-o interfață comună.
Cu toate că de obicei unele metodele abstracte nu sunt implementate în clasa abstractă, este totu și posibil să
se scrie implementarea acestora. Chiar dacă o clasă abstractă va implementa toate metodele sale,
mecanismul clasei abstracte se păstrează, adică, în continuare, compilatorul împiedică crearea de obiecte din
această clasă.
De multe ori definirea tuturor metodelor unei clase abstracte este convenabilă atunci când dorim să scriem o
bucată de cod care să fie apoi apelată din toate sau din unele clase derivate, eventual din metodele care
redefinesc această metodă.
Clasele abstracte de obicei nu implementează interfa ța respectivă. în Java există conceptual distinct de
interfață. Interfețele duc conceptul de clasa abstractă cu un pas mai departe.

Se poate considera că o interfață este o clasă abstractă pură în care toate metodele sunt
abstracte și nu agregă obiecte. Dacă se poate crea o clasa de bază fără defini ții de metode și
variabile membre, se recomandă să se creeze interfe țe în locul claselor abstracte. O interfa ță
poate conține câmpuri dar acestea sunt în mod implicit static și final (mod convenabil pentru a
defini constante). Câmpurile definite în interfe țe nu pot fi blank finals, ele trebuie inițializate la
definire. Metodele declarate în interfa ță sunt în mod implicit publice.
Interfața descrie un contract între clase: o clasă care implementează o interfa ța va implementa metodele
definite în interfața. Pentru a crea o interfa ța folosim cuvântul cheie interface. Atunci când o clasă
implementează metodele unei interfețe va indica acest fapt print cuvântul cheie implements.

interface Animal {
public void eat();
public void travel();
}

abstract public class Mammal implements Animal {

public void eat() {


System.out.println("Mammal eats");
}

public void travel() {


System.out.println("Mammal travels");
}

abstract public int noOfLegs();


}

Implementarea de interfețe este asemănătoare cu mo ștenirea, cu diferen ța că nu se


moștenește comportament, ci doar “interfața” – orice cod client care utilizează o anume
interfață va ști ce metode se pot apela (si doar atât).

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 39


Interfața nu este doar o formă pură a unei clase abstracte, ci și permite realizarea unei forme de „mo ștenire
multiplă” în Java. în Java se poate extinde o singura clasă și se pot implementa oricâte interfe țe.

class NumeClasa implements Interfata1, Interfata2,...{


// cod
}

Interfețele se pot combina și extinde interfe țele la fel ca și clasele. Astfel, o interfa ță poate extinde oricâte alte
interfețe (numite super-interfețe).

interface NumeInterfata extends Interfata1, Interfata2,...{


// cod
}

Nu apar coliziuni ale numelor de metode la implementarea mai multor interfe țe dacă interfe țele de bază au
același nume de metodă cu semnături diferite. Dacă însă metodele diferă doar prin tip de retur par erori la
compilare.
Interfețele reprezintă tipuri propriu-zise de date, deci pot fi definite variabile de tip interfa ță. Evident, tipul real
al variabilelor va fi cel al unei clase care implementează interfa ța.

Animal myPet = new Dog();

Astfel, mecanismul de upcast este pe deplin aplicabil variabilelor de tip interfa ță: o variabilă
dintr-o anumită clasă va poseda tipul tuturor interfe țelor pe care clasa le implementează dintr-o
ierarhie de interfețe. Astfel, rolul interfe țelor este acela de a putea face upcast la mai mult decât
la o clasă de bază.

2.1.13. Downcast
Downcast reprezintă operația de conversie între un tip din partea de sus a unei ierarhii și un tip
specializat. Dow cast este operația inversă upcastului. La upcast realizarea conversiei este o
chestiune simplă, datorită faptului că se cunoa ște și se determină exact tipul destina ție a
conversiei. La downcast, problema apare datorită faptului că tipul destina ție nu este unic
determinat. La realizarea operației de downcast, programatorul trebuie să știe ca într-adevăr,
la momentul apelului, se referă un obiect din tipul derivat a șteptat. Compilatorul nu are de unde
să cunoască tipul obiectului la momentul realizării apelului. Java verifică orice conversie (cast), și Dacă nu se
poate realiza se aruncă o excepție de tipul ClassCastException

2.2. Modele de design (Design Patterns)


Prin pattern ne referim la o problemă (de programare) care se repetă foarte frecvent în
proiectele pe care trebuie să le realizăm. Datorită acestei frecven țe a repetării, putem să
presupunem că la fiecare întâlnire a problemei respective vom folosi acela și procedeu
rezolvare. Deci, prin pattern putem înțelege atât problema în sine cât și solu ția adoptată generic
pentru rezolvarea acestei probleme. Astfel, un design pattern reprezintă o soluție conceptuală
în termeni de programare obiectuală furnizată unui tip probleme specifice.
Design pattern-urile reprezintă un nivel superior de în țelegere a programării obiectuale și ajută programatorii
să realizeze soluții scalabile, flexibile la modificarea cerin țelor, reutilizabile în condi țiile unor probleme similare.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 40


2.2.1. Modelul de design Strategy
Design pattern-ul Strategy încapsulează algoritmii în clase ce oferă o anumită interfa ță de folosire, și pot fi
selecționați la execuție. Exemplele clasice de utilizare sunt selectarea în mod dinamic a unor algoritmi de
sortare, compresie, criptare etc.
Acest pattern presupune crearea unei metode care să aibă comportament diferit în func ție de tipul
argumentului care se prezintă la intrare. Metoda con ține o parte fixă care este apelata de fiecare data și o
parte care variază (strategia). Strategia se creează ca și o interfa ță, iar clasele care doresc să facă parte din
strategie vor implementa interfață scrisă.

Figura 2.7 Diagramă UML asociată modelului de design Strategy

Strategy permite eliminarea unor cuplări puternice ce nu ar permite aplicarea metodei din
strategie (de exemplu, execute) decât obiectelor dintr-o anumită ierarhie, și nicidecum altor
obiecte din alte ierarhii. în cazul în care nu s-ar utiliza interfe țele, dacă am dori să aplicăm
metoda (strategia) unui obiect dintr-o clasă care nu face parte din ierarhie, nu s-ar putea.
Denumirile uzuale în exemplele acestui pattern sunt: Strategy pentru interfață sau clasa
abstractă, Concrete Strategy pentru implementarea acesteia, Context, pentru clasa care
folosește/execută strategiile.
De exemplu, să presupunem că dorim să sortăm colec ții de obiecte folosind diferite strategii de sortare.
Metoda de sortare va conține o parte fixă, ce nu se va schimba (de exemplu, anumite procesări ale datelor).
Partea variabilă va fi aici strategia de sortare (quicksort, heapsort, bubblesort, shellsort etc…) care poate fi
schimbată la execuție în funcție de natura datelor, pentru a optimiza timpul de sortare.

Figura 2.8 Diagramă UML asociată modelului de design Strategy aplicat unei probleme de sortare

2.2.2 Modelul de design Adapter


Acest patern poate fi folosit atunci când codul nostru depinde de biblioteci externe sau orice altă clasă
predispusă schimbărilor frecvente. Adaptorul este menit sa convertească interfa ța unei clase externe într-o

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 41


interfața stabilă a codului existent, permi țând inter-operabilitatea claselor care altfel nu ar fi compatibile. El
este adesea folosit pentru a integra clase existente cu altele fără a modifica codul existent.

Figura 2.9 Diagramă UML asociată modelului de design Adaptor

Se va scrie cod care preia la intrare interfa ța din biblioteca externă și va produce interfa ța țintă
(Target) de care este nevoie în program nostru. în acest caz, adaptorul este obiectul de tip
wrapper care face posibil acest lucru. El va implementa interfa ța din programul nostru, și va
conține o referință spre un obiect (Adaptee) din librăria externă asupra căreia nu avem control.
Când îi vom apela metoda (operation), el va delega apelul spre metoda corespunzătoare
(specificOperation) a obiectul adaptat(Adaptee) asupra căruia nu avem control.

De exemplu, în figura de mai jos avem o ierarhie proprie în care toate clasele implementează interfa ța
Processor și metoda asociată process. Pe lângă această ierarhie, avem o alta asupra căreia nu avem control
și în care toate obiectele implementează interfa ța Filter, ce conține și ea o metodă asociată process. Dar o
clasă din ierarhia Filter nu este reutilizabilă din simplul motiv că interfa ță acesteia nu se potrive ște cu cea
specifică contextului în care se dore ște utilizată ( Process).
Conceptual putem însă considera că filtrele nu sunt altceva decât ni ște procesoare și am dori să le putem
folosi ca atare. Maniera în care putem atinge acest deziderat constă în crearea unui adaptor FilterAdapter ce
implementează interfața Process, înglobează un filtru propriu-zis, iar în momentul apelului metodei process
acesta delegă apelul către obiectul adaptat filtru.

Figura 2.10 Diagramă UML asociată unui exemplu de utilizare a modelului de design Adaptor

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 42


2.2.3 Modelul de design Factory Method
Prin design paternul Factory Method, în loc să se apeleze un constructor în mod direct (ace știa
sunt în general inaccesibili), se definește o metodă de creare a unui obiect într-o clasă
abstractă/interfață Factory. Decizia privitoare la ce fel de obiect se creează este lăsată
subclaselor/implementărilor clasei abstracte/interfe ței Factory. Tipul concret al obiectului
creat este ales dinamic la execuție, dar se cunoaște interfața acestuia (in
diagrama de mai jos, Service) – operațiile pe care el trebuie să le furnizeze (aici, method1
și method2).

Se realizează astfel decuplarea atât dintre clasa Factory și implementările ei concrete (in diagrama de mai jos,
Implementation1Factory și Implementation2Factory), cat și dintre interfața generică a obiectului produs
(Service) și implementările sale concrete ( Implementation1 și Implementation2). Astfel se vor putea adaugă
foarte ușor noi tipuri de creatori de servicii, cu serviciile lor asociate, fără a impacta implementările deja
existente. în plus, se poate schimba la execu ție în mod dinamic tipul creatorilor de servicii.
Un alt avantaj al acestui model conceptual este legat de faptul ca anumite opera ții de control ale creării
obiectelor pot fi centralizate la nivelul clasei abstracte Factory, în corpul unor metode concrete.

Figura 2.11 Diagramă UML asociată modelului de design Factory Method

2.2.4. Modelul de design Iterator


Un iterator este un obiect asociat unei colecții de obiecte (Aggregate). Prin intermediul iteratorului
putem avea acces la câte un membru al colec ției la un moment dat inhibând-se accesul direct la colec ția
asociată acestuia.
Folosirea iteratorilor este foarte utilă când:
● Colecția se bazează pe o resursă lentă, care nu poate oferi toate elementele dintr-o dată;
● Colecția permite doar o parcurgerea unidirec țională;
● Colecția este generată în mod dinamic pe măsură ce este parcursă;
● Colecția este extrem de lungă și nu poate fi păstrată toată în memorie la un moment dat;
● Colecția este filtrată pe baza unor criterii, și nu este eficient să se creeze o nouă colec ție ce con ține
doar elementele filtrate.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 43


Figura 2.12 Diagramă UML asociată modelului de design Iterator

2.3. Clase interioare


2.3.1. Conceptul de clasă interioară

Clasele interioare sunt declarate în interiorul unei alte clase (Outer Class). Clasa interioară este
un concept diferit de cel de compoziție, ce permite gruparea claselor care sunt legate din punct
de vedere logic și controlul vizibilită ții în cadrul acestora.

Clasele interioare se comporta ca un membru al clasei exterioare, astfel obiectele din aceste
clase pot accesa membrii obiectului clasei exterioare fără ca sa fie nevoie de calificare (au
dreptul de a accesa membrii clasei exterioare inclusiv cei private). Clasa interioară poate avea
modificatorii permiși atât metodelor ( public, final, abstract) cat și datelor membru
(private, protected și static). Clasele interioare cu modificatori private și protected sunt
potrivite pentru a implementa interfețe deoarece ele realizează ascunderea implementării. în
acest caz, interfețele sunt utile pentru a putea asocia claselor interioare un tip, care să ne permită folosirea
instanțelor acestora. Altfel, tipurile lor nu ar fi fost vizibile în exteriorul clasei pentru că a fost declarate private
sau protected.

interface Contents{
public int value();
}

interface Destination{
public String readLabel();
}

class TParcel {
private class PContents implements Contents {
private int i = 11;
public int value() { return i; }
}
protected class PDestination implements Destination {
private String label;
private PDestination(String whereTo) {

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 44


label = whereTo;
}
public String readLabel() {
return label;
}
}
public Destination destination(String s) {
return new PDestination(s);
}
public Contents contents() {
return new PContents();
}
}
public class TestParcel { public static void main(String[] args) {
TParcel p = new TParcel();
Contents c = p.contents();
Destination d = p.destination("Tasmania");
// Illegal -- can’t access private class:
//! TParcel.PContents pc = p.new PContents();
}
}

De obicei, clasa exterioară are o metoda care returnează un obiect din clasa interioară. Tipul
obiectului din clasa interioară se specifică prefixat de numele clasei exterioare:
OuterClass.InnerClass. Obiectul clasei interioare are o referintaa catre obiectul clasei
exterioare care l-a creat. Pentru a se obtine referinta la obiectul clasei exterioare din obiectul
clasei interioare se utilizeaza: OuterType.this. Pentru a crea un obiect din clasa inner pornind
de la un obiect din clasa outer se poate utiliza: obiectOuter.new InnerType(…).

TParcel p = new TParcel();


Parcel4.PContents pc = p.new PContents();

In funcție de maniera de instatiere și rela ția cu clasa exterioară clasele interioare pot fi clasificate în patru
tipuri:
● clase interioare normale
● clase interioare anonime
● clase interioare statice (nested classes)
● clase interioare definite în interiorul metodelor sau a unui domeniu de vizibilitate

Clasele anonime sunt clase interioare fără nume create direct la momentul utilizării lor. Ele sunt
instanțiate într-un singur loc și folosite prin upcasting la o clasă de bază sau interfață. Astfel,
numele clasei nu este important, tipul fiind un subtip al unei clase sau o implementare a unei
interfețe. O clasă internă anonimă poate extinde o clasă sau implementa o singură interfa ță,
dar nu poate face ambele concomitent. De asemenea, ea nici nu poate să implementeze mai
multe interfețe.
Singurele metode care pot fi apelate pe o clasa anonimă sunt cele ale tipului pe care îl extinde sau
implementează. Caracterul ; finalizează construc ția instruc țiunii care con ține defini ția clasei anonime.

public class Parcel {


public Contents contents() {
return new Contents() {

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 45


// definitia unei clase interioare anonime

private int i = 11;


public int value() { return i; }
}; // Semicolon required în this case
}
public static void main(String[] args) {
Parcel p = new Parcel();
Contents c = p.contents();
}
}

Câmpurile din clasele anonime pot fi ini țializate cu valori din domeniul unde se creează clasa. Dacă într-o
clasa anonimă se dorește a fi utilizat un argument (sau o valoare) definit în afara metodei, atunci referin ță
acestuia trebuie sa fie precedată de cuvântul cheie final.

public class Parcel {


// Argument must be final to use inside anonymous inner class:
public Destination destination(final String dest) {
return new Destination() {
private String label = dest;
public String readLabel() { return label; }
};
}
public static void main(String[] args) {
Parcel p = new Parcel();
Destination d = p.destination("Tasmania");
}
}

Clasele anonime, neavând un nume, nu pot avea constructor, dar ini țializările se pot realiza în blocul non-
static de inițializare. în mod implicit, clasă de bază este creată cu constructorul default. Dacă dorim să
invocăm un alt constructor al clasei de bază se vor transmite parametrilor către constructorul clasei de bază
direct la crearea obiectului de tip clasă anonimă.

abstract class Base {


public Base(int i) {
System.out.println("Base constructor, i = " + i);
}
public abstract void f();
}

public class AnonymousConstructor {


public static Base getBase(int i) {
return new Base(i) {
//bloc non-static de initializare
{ System.out.println("Inside instance
initializer"); }

public void f() {//metoda în clasa anonima

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 46


System.out.println("In anonymous f()");
}
};
}

public static void main(String[] args) {


Base base = getBase(47);
base.f();
}
}
Rulare:

Inside instance initializer


In anonymous f()

Clasele interioare pot fi create static (nested classes) dacă nu se dorește utilizarea referin ței
obiectului exterior în interiorul clasei interioare. Nu este nevoie de un obiect al clasei externe
pentru a crea un obiect al clasei interioare statice. Instan țele lor nu pot accesa câmpuri
nestatice ale clasei exterioare. Clasele interioare parte a unei interfe țe devin automat static
public. Rolul lor este acela de a crea cod comun care sa fie utilizat de toate implementările
interfeței.
Oricât de adâncă este imbricarea claselor interioare statice, acestea pot accesa obiectele membre din clasele
exterioare, indiferent de nivelul de imbricare.
Clasele interioare pot fi definite și în interiorul metodelor (sau a unui domeniu de
vizibilitate) deoarece:
● permit implementarea unei interfețe (sau extinderea unei clase). Metoda va crea și returna o
referință ce este folosită prin upcasting la clasa de bază sau interfa ță.
● permit crearea unor clase nepublice ce ajută la rezolvarea unor probleme complicate, clase ce nu au
utilitate în alte zone ale programului.
Aceste clase pot fi accesate doar din interiorul metodei (sau domeniului de vizibilitate) unde a fost definite.
Clasele locale unui domeniu de vizibilitate au acces la toate variabilele domeniului respectiv (inclusiv cele
final), însă aceste clase nu au specificator de acces, singurii modificatori care pot fi aplica ți lor sunt abstract
sau final.
Deoarece constructorul clasei interioare trebuie să fie ata șat de un obiect al clasei exterioare, la mo ștenirea
dintr-o clasa interioară, acest obiect trebuie furnizat la construc ția obiectului din clasa derivată. Acesta va
permite instantierea obiectului incorporat super din super-clasa interioară (outerClassReference.super()).

class WithInner {
class Inner {}
}
public class InheritInner extends WithInner.Inner {
//! InheritInner() {} // Nu va compila
InheritInner(WithInner wi) {
wi.super(); // aici outerclassreference.super()
}
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner îi = new InheritInner(wi);
}
}

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 47


2.3.2. Closures & callbacks
Un obiect closure este un obiect apelabil care retine informație despre domeniul
de vizibilitate în care a fost creat (având acces la variabilele din domeniul de
vizibilitate unde a fost creat). Un obiect al unei clase interioare este un closure
deoarece are o referință către obiectul clasei exterioare și poate sa acceseze inclusiv membrii
privați ai acestuia.
Un obiect callback este un obiect care primește o informa ție care va permite sa apelăm obiectul
inițial la un moment ulterior de timp. în Java, obiectele Callback se implementează folosind clase interioare.

Clasa de tip closure furnizează o poartă spre interiorul clasei exterioare într-o manieră sigură
ce nu necesită modificarea accesul la metodele acestei clase.
Utilitatea obiectelor callback rezidă în flexibilitatea lor. Acestea permit schimbarea metodei
apelate, în mod dinamic, la execuție.

Un exemplu de implementare a obiectelor callback și closure cu ajutorul claselor interioare poate fi văzut în
codul de mai jos.

interface Incrementable {
void increment();
}

// Very simple to just implement the interface:


class Callee1 implements Incrementable {
private int i = 0;
public void increment() {
i++;
System.out.println(i);
}
}

class MyIncrement {
void increment() {
System.out.println("Other operation");
}
static void f(MyIncrement mi) {
mi.increment();
}
}

// If your class must implement increment() in


// some other way, you must use an inner class:
class Callee2 extends MyIncrement {
private int i = 0;
private void incr() {
i++;
System.out.println(i);
}
private class Closure implements Incrementable {
public void increment() {

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 48


incr();
}
}
Incrementable getCallbackReference() {
return new Closure();
}
}

class Caller {
private Incrementable callbackReference;
Caller(Incrementable cbh) {
callbackReference = cbh;
}
void go() {
callbackReference.increment();
}
}

public class Callbacks {


public static void main(String[] args) {
Callee1 c1 = new Callee1();
Callee2 c2 = new Callee2();
MyIncrement.f(c2);
Caller caller1 = new Caller(c1);
Caller caller2 = new Caller(c2.getCallbackReference());
caller1.go();
caller1.go();
caller2.go();
caller2.go();

}
}

In exemplul de mai sus, clasa Calle1 oferă o implementare standard a interfe ței Incrementable, incrementând
prin metoda increment un contor întreg. Clasa Calle2 moștenește deja o metodă increment de la clasa de
bază MyIncrement, ce are un comportament necorelat cu cel așteptat de interfața
Incrementable. Astfel, Calle2 nu poate suprascrie metoda increment pentru a implementa interfața
Incrementable, fiind forțată să ofere o implementare a acesteia într-o clasă interioară privată Closure. Astfel,
interfața clasei exterioare Calle2 rămâne nemodificată.
Accesul la instanța clasei private Closure se face prin upcast la interfața Incrementable, pe care
clasa interioara o implementează în locul clasei exterioare. Astfel, clasa Closure furnizează o poartă de intrare
spre interiorul clasei exterioare Calle2 într-o manieră sigură, referința ei oferind acces prin upcast doar la
metodele interfeței Incrementable.
Clasa Caller primește în constructor o referință către un obiect Closure ce implementează interfața
Incrementable, putând astfel sa apeleze ulterior (call back) metoda acestuia ce modifica starea obiectului
exterior Calle2.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 49


Figura 2.13 Diagramă UML asociată unui exemplu de utilizare a conceptelor de Closure si Callback

2.3.3. Control Frameworks


Un alt domeniu de aplicativitate al claselor interioare sunt arhitecturile de control ( Control Frameworks).
Arhitecturile de control sunt exemple particulare de arhitecturi aplicative. O arhitectură
aplicativă (Application Framework) constă dintr-un set de clase proiectate să rezolve un tip
general de probleme. Aceste clase, ce oferă o solu ție generică pentru tipul de probleme în
cauză, vor fi extinse prin derivare iar unele din metodele lor vor fi suprascrise pentru a
particulariza soluția generică și a o adapta la cerin țele problemei specifice ce dorim a fi
rezolvate (această strategie este un exemplu de utilizare a design pattern-ului Template
Method).

La utilizarea arhitecturii aplicative se mo ștene ște din clasele de bază și se schimbă


implementarea prin suprascriere. Codul suprascris este cel care adaptează solu ția generică la
cazul particular. Design pattern-ul Template Method va conține structura de baza a algoritmului,
iar părțile specifice sunt apeluri la metodele care pot fi suprascrise. Astfel, se separă păr țile
neschimbate ale algoritmului de cele flexibile.
Arhitecturile de control sunt un tip particular de arhitecturară aplicativă marcate de nevoia de a
trata evenimente într-un sistem bazat pe evenimente (event-driven system) – de exemplu, GUIs.
In implementarea arhitecturilor de control, clasele interioare sunt utilizate pentru a exprima
diversele acțiuni (metoda action).
Bibliotecile Java Swing și AWT sunt arhitecturi de control care folosesc extensiv clasele interioare. De
exemplu, în Swing, putem atașa o acțiune specifică evenimentului de apăsare a unui buton cu ajutorul unei
clase interioare anonime ce implementează interfa ța ActionListener, dând corp metodei actionPerformed:

someButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
//do something ...
}
});

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 50


2.3.4. Avantajele folosirii claselor interioare
Clasele interioare sunt foarte utile deoarece:
● Folosind clasele interioare se pot implementa interfețe și returna referințe spre
acestea, ascunzând în același timp implementarea;
● Se pot folosi sau extinde funcționalită ți ale mai multor clase în interiorul aceleia și clase, dacă
aceasta incorporează mai multe clase interioare. Acestea pot mo șteni orice clasă și au, în plus,
acces la obiectul clasei exterioare;
● Fiecare clasă interioară poate să moșteneasca în mod independent de la o altă implementare.
Deci o clasa interioară nu este limitată de faptul că clasa exterioară mo ștene ște sau nu acea
implementare;
● Clasa interioară poate avea instanțe multiple, fiecare cu propria stare, care este independentă de
starea obiectului din clasa exterioară;
● Clasa exterioară poate avea mai multe clase interioare fiecare implementând aceea și interfa ță sau
moștenind din aceeași clasă în maniere diferite;
● La momentul creării obiectului din clasa interioară, acesta nu este legat de crearea obiectului din
clasa exterioară. Clasa interioară este o clasă total diferită de clasa exterioară, nu existând
confuzie de tipul is-a.

Întrebări recapitulative
Întrebări pentru testul scris:
11. Care este diferenta dintre o clasa abstracta si o interfata?
12. Cum difera conceptul de compozitie de cel de agregare?
13. Ce este delegarea?
14. Ce este o clasa interioara?
15. Explicati conceptul de closure.
16. Enumerati valentele cuvantului cheie static.
17. Care sunt etapele crearii unui obiect in Java?
18. Care sunt posibilele avantaje ale utilizarii unui iterator?
19. Explicatii diferentele intre conceptele de downcast si upcast.
20. Care sunt modificatorii posibili ai unei clase interioare?

2.4. Teste de evaluare

1 Cum se numeste o relatie daca obiectul continut este distrus 7 Cuvantul String in Java indica :
atunci cand obiectul ce il contine este distrus: A. O clasa
A. Agregare B. Un object
B. Compozitie C. O variabila
C. Encapsulare D. Un sir de caractere
D. Asociere

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 51


2 Ce specificatori de acces pot fi utilizati intr-o interfata: 8 Când sunt invocaţi constructorii:
A. public A. când se instanţiază un obiect superclasă
B. protected B. când se invocă o metodă a unui obiect
C. private C. când java virtual machine porneşte garbage
D. toti cei mentionati mai sus collection
D. când obiectele necesită să fie colectate de
garbage collector
E. când se instanţiază un nou obiect dintr-o clasă

3 Din ce clasa mostenesc toate clasele din Java: 9 Care din urmatoarele afirmatii este adevarata in ceea ce
priveste clasele inner statice:
A. java.lang.class A. nu are acces la membrii ne-statici ai clasei
B. java.class.inherited exterioare
C. java.class.object B. variabilele si metodele sale trebuie sa fie statice
D. java.lang.Object C. Trebuie sa primeasca o referinta catre o instanta
din clasa exterioara pentru a se putea crea o
instanta dintr-o asemenea clasa
D. trebuie sa extinda clasa exterioara
4 Membrii statici ai unei clase se mostenesc la derivarea 10 Modelul de design iterartor e util cand:
acesteia de catre subclase: A. Colecția se bazează pe o resursă lentă, care nu
A. adevarat poate oferi toate elementele dintr-o dată
B. fals B. Colecția permite doar o parcurgerea
unidirecțională
C. Colecția este generată în mod dinamic pe măsură
ce este parcursă
D. In toate cazurile precedente

5 Ce se intampla daca o clasa implementeaza doua interfete 11 Care din urmatoarele afirmatii este adevarata atunci cand ne
ce au metode cu aceiasi semnatura: referim la o metoda a unei clase interioare locale unui
A. eroare la executie domeniu de vizibilitate:
B. eroare la compilare A. trebuie declarata final
C. nu vor aparea erori B. poate fi declarata abstract
D. prima metoda apelata este executata cu succes C. poate fi declarata static
D. poate fi declarata public

6 Care din urmatoarele afirmatii este adevarata pentru o 12 Cand un obiect este reciclat de Garbage Collector, se va
interfata: executa metoda finalize:
A. variabilele declarate in interfata sunt implicit A. da
public, static si final B. nu
B. cuvantul cheie implements indica faptul ca C. este posibila executia acesteia
interfata mosteneste din alta
C. metodele declarate in interfata sunt implicit private
D. o interfata nu poate sa extinda orice numar de
interfete

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 52


Programare Orientă Obiect

Modulul 2

Referințe bibliografice:

● Bruce Eckel, Thinking in Java, ed. 4-a, Prentice Hall, 2006


● Ioan SALOMIE, Tehnici de programare obiectuală, Ed. Albastră, 1996

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 53


Programare Orientă Obiect

Modulul 3

Alte elemente de programare in limbajul JAVA

I. Cuprins
1. Excepții
4. RTTI
2. Colecții
5. Concurența în Java
II. Obiective
3. Fluxuri de intrare-ieșire
Familarizarea cu
6. Teste de evaluare elemente
conceptuale fundamentale utilizate
în programarea Java: exceptii,
colectii generice, fluxuri de intrare-
ieșire, RTTI, fire
III.de
Cuvinte
executiecheie
exceptii, bloc try/catch, colectii
generice, serializarea obiectelor, RTTI,
reflectie, thread, partajarea resurselor,
monitoare, deadlock

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 54


Modulul 3 - Alte elemente de programare în limbajul Java
3.1. Excepții
Nu toate erorile pot fi prinse la scrierea programului (la compilare). Restul problemelor trebuie rezolvate la
execuție print-un mecanism prin care să ofere programului client informa ția necesară rezolvării situa ției.
Pentru rezolvarea acestor erori Java pune la dispozi ție mecanismul de gestiune a erorilor ( Exception
handling).

3.1.1. Conceptul de Excepție


O excepție este o eroare care se produce în timpul execu ției unui într-un domeniu de vizibilitate
unde de obicei nu știm cum să tratam eroarea, însă știm că nu putem să continuăm fluxul
normal al instrucțiunilor odată eroarea apărută, astfel că transmitem eroarea spre rezolvare
către un domeniu de vizibilitate exterior.

Apariția unei erori este legată de anumite condi ții particulare ale variabilelor din program ( error condition), de
exemplu, o împărțire cu un operand care are valoarea 0.

Când o eroare se produce într-o metodă, ma șina virtuală Java realizează următoarele opera ții:
● creează un obiect excepție pe heap, cu new și îl pasează către sistemul de
runtime. Obiect conține informații despre tipul de excep ție și stiva de apeluri;
● Firul de execuție curent este întrerupt și referin ța la obiectul excep ție este
transmisă în afara contextului curent;
● Mecanismul de gestiune a excepțiilor caută un context potrivit pentru reluarea
execuției programului;
● Se reia execuția programului într-o por țiune de cod specială, destinată gestiunii acesteia numită
exception handler.
Procesul descris în etapele precedente poartă numele de aruncarea unei excep ții. Aruncarea explicita a unei
excepției se face folosind cuvântul cheie throw:

if (t==null)
throw new NullPointerException();

La throw se poate arunca orice obiect dintr-un tip care extind clasa Throwable (clasa rădăcină a ierarhiei
excepțiilor).

3.1.2. Gestiunea erorilor


Exista doua maniere de gestiune a erorilor: termination – dacă nu se știe cum să se rezolve
eroarea și ea este aruncată mai departe, și resumption – se gestionează apariția unei erori prin
inserarea unui bloc try-catch.
Prinderea excepțiilor se realizează cu ajutorul blocurilor try-catch. Instrucțiunilor ce
aparțin fluxului normal de execuție și pot genera erori se pun într-un bloc try la
sfârșitul căruia se precizează condi țiilor de excep ție posibile, în câte un bloc catch. Logica este
următoarea: se execută instrucțiune cu instruc țiune secven ța din blocul try și, la apari ția unei excep ții
semnalate de o instrucțiune, se abandonează restul instruc țiunilor rămase neexecutate și se sare direct la
blocul catch corespunzător, try-catch funcționând ca un switch pe tipul de excepție. Aici, switch-ul
va fi unul particular în sensul în care se face potrivirea pe cea mai apropiata clauza catch care potrive ște cu
tipul excepției. Potrivirea nu trebuie sa fie una perfectă, fiind suficient ca tipul excep ției sa fie un subtip al
excepției din blocul catch.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 55


try {
// codul care este susceptibil sa genereze exceptie
} catch (ExceptionType1 tp1) {
// exceptii de tipul ExceptionType1
} catch (ExceptionType2 tp2) {
// exceptii de tipul ExceptionType2
} catch (ExceptionType3 tp3) {
// exceptii de tipul ExceptionType3
}
//...
finally {
// cod care se executa indiferent de tipul de exceptie
aruncat (sau nu)
}

Blocul finally se dovedește foarte util când în blocurile try-catch se găsesc instrucțiuni return. El
se va executa și în acest caz, exact înainte de execuția instrucțiunii return, aceasta fiind
executată ulterior. în general, în blocul finally se eliberează resurse pentru ca acestea să nu rămână blocate în
cazul terminării intempestive a programului.
Noțiunea de excepție este foarte importantă și datorită faptului ca ea reprezintă premisa
fundamentală a tranzacțiilor. O tranzac ție e formată din una sau mai multe instruc țiuni care
compun o singură unitate logică de lucru (ori toate instrucțiunile reușesc să se execute cu
succes, ori niciuna). Tranzacția începe cu prima instruc țiune executată și se încheie în
momentul în care efectele tranzacției sunt salvate sau eliminate. O executare cu succes a unei
tranzacții înseamnă că instrucțiunile au fost executate fără erori.
Fiecare tranzacție permite două alternative. Dacă instruc țiunile dintr-o tranzac ție sunt finalizate cu succes
(commit), efectele tranzacției devin permanente (de obicei, într-o bază de date). Dacă, însă, dintr-un motiv
oarecare, instrucțiunile nu sunt finalizate cu succes (apare o eroare/excep ție), efectele tranzac ției sunt
eliminate (din baza de date) și tranzacția este derulată înapoi( rollback). Astfel, mecanismul de gestiune
a excepțiilor devine fundamental pentru a putea realiza operațiile de rollback. Folosind blocul
try-catch modelul general de tranzacție devine:

try {
// blocul de instructiuni
// din tranzactie ce pot ridica exceptii
t.begin();
instructiune 1;
...
instructiune n;
// instructiunea commit ce salveaza modificarile
// cand tranzactia se finalizeaza cu succes
t.commit();
} catch (Exception ex) {
// rollback atunci cand apare o eroare
// modificarile tranzactiei fiind pierdute
t.rollback();
}
finally {
// cod care se executa indiferent de tipul de exceptie aruncat
(sau nu)

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 56


}

Clasa Exception desemnează comportamentul specific pentru excep ții, fiind părintele majorită ții claselor
excepție din Java. Ea oferă printre altele metodele:
● getMessage () – moștenită de la Throwable, ce este similară cu toString pentru erori;
● printStackTrace () – ce afișează secvența de metode apelate pentru a se ajunge la instruc țiunea
ce a generat excepția. La afișări de erori se recomandă utilizarea ie șirii de eroare System.err în
loc de System.out.
Putem a loga excepțiile ce apar pe parcursul aplicației folosind java.util.logging. Mesajele
logate vor putea avea diferite nivele de gravitate : FINE, INFO, WARNING, SEVERE etc.
Dacă utilizatorul dorește să își definească propriile excep ții acestea trebuie sa mo ștenească clasa Exception.
Numele excepției utilizator trebuie să fie sugestiv pentru cauza apari ției excep ției.

class MyException extends Exception {


public MyException () {
}

public MyException (String message) {


super (message);
}

public MyException (Throwable cause) {


super (cause);
}

public MyException (String message, Throwable cause) {


super (message, cause);
}
}

public class TestException {


public static void main(String[] args) {
try {
throw new MyException("Some error situation");
}catch(MyException e) {
e.printStackTrace(System.err);
}
}
}
Rulare:

MyException: Some error situation


at MyException.main(MyException.java:21)

Pentru prinderea oricărui tip de excep ția se va folosi în clauza catch tipul Exception.

3.1.3. Ierarhia Excepțiilor in Java


Nu toate excepțiile trebuie prinse cu try-catch:

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 57


Figura 3.1 Ierarhia excepțiilor in Java

● Checked exceptions, obiecte din clasa Exception sau subclasele sale – sunt excep ții pe care o
aplicație corect programată ar trebui să le prindă și să permită continuarea rulării programului. Ele
sunt excepțiile pe care o aplicație se a șteaptă sa le întâlnească. Fiecare subclasă a lui Exception
reprezintă un tip particular de excep ție. De exemplu, dacă într-o aplica ție se dore ște citirea unui
fișier de pe disc, dar acesta nu există, se va arunca o excep ție de tipul FileNotFoundException.
Un cod bine scris va prinde excepția, va afișa n mesaj de eroare, și va permite
probabil reintroducerea unui alt nume de fișier.
● Errors, obiecte din clasa Error sau subclasele sale – sunt folosite pentru erori de sistem sau erori
la compilare ce nu pot fi tratate de aplica ție și induc e șecul execu ției acesteia. De exemplu, dacă
heap-ul aplicației a crescut dincolo de limita admisă de ma șina virtuală și nu se mai poate aloca
spațiu pentru crearea de noi obiecte un OutOfMemoryError se va produce. Aplicația poate să
prindă această excepție pentru a afișa un mesaj de eroare. Mai apoi însă, execu ția programului va
eșua. Erorile sunt aruncate în mod automat de către Java la momentul apari ției lor ( unchecked
exceptions);
● Runtime Exceptions – ca și în cazul erorilor ele induc e șecul execu ției programului însă nu sunt
generate de factori externi ci de diverse bug-uri de programare. Ele pot fi prinse însă solu ția
evidentă constă în eliminarea din cod a bug-urile care le generează (de exemplu, division by
zero).
Excepțiile checked sunt, în general, cele prinse de blocurile try-catch. Excepțiile unchecked
(runtime, error) nu trebuie, în general, prinse în blocurile try-catch.
Excepțiile pot sa nu fie tratate în exteriorul metodei ce le generează. Pentru a indica tipurile de erori netratate
pe care o metoda le poate arunca se utilizează cuvântul cheie throws în declarația metodei. Tipurile de
excepții ce pot fi aruncate de aceasta vor fi separate prin virgulă.

public void method() throws CustomException, AnotherException {


// Method body
}

Compilatorul forțează astfel programatorul să specifice excep țiile. dacă într-o metodă este posibil să apară o
excepția și programatorul nu o specifică prin throws, atunci apare o eroare la compilare. Erorile de tip
RuntimeException nu trebuie specificate prin throws, doar Checked exceptions fiind verificate la compilare.
In contextul paradigmei orientate obiect folosite în Java:
● se recomanda să nu fie aruncate excep ții în constructori, deoarece aceste metode trebuie sa î și
încheie execuția cu succes pentru a aduce obiectele într-o stare sigură;
● Dacă o metodă suprascrie o metodă din clasa de bază ea poate arunca doar excep țiile metodei
suprascrise sau clase de excepții derivate din acestea.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 58


3.1.4. Înlănțuirea Excepțiilor
Dacă o clauza catch nu știe să trateze o excep ția nici în integralitate nici par țial ea rearunca de
obicei excepția către un nivel superior. în multe situa ții însă nivelele intermediare de gestiune a
excepțiilor pot prinde o excepție realiza anumite opera ții necesare rezolvării ei si, mai apoi,
arunca noi tipuri de excepții spre nivelurile superioare. La prinderea unei excep ții și aruncarea
unei alte excepții, se dorește păstrarea lan țului excep țiilor care apar. Înlăn țuirea excep țiilor
permite păstrarea excepției originale ce a fost ini țial aruncată. în acest scop, subclasele
Throwable (Error, Exception și RuntimeException) pot sa utilizeze cauza erorii ca și argument în constructor.
Pentru celelalte clase de erori setarea/ob ținerea cauzei directe a fiecărei excep ții se poate face cu ajutorul
metodelor initCause()/getCause(). Prin înlănțuire, ultima excep ție aruncată va con ține întreg lan țul excep țiilor
ce au dus la apariția sa.

public class InitCauseTest {


public static void main(String[] args) throws Exception {
try {
testException();
}
catch (Throwable e) {
System.out.println("Cause : "
+ e.getCause());
}
}

// method which throws Exception


public static void testException()
throws Exception {
// This exception will be used as a Cause
// of another exception
ArrayIndexOutOfBoundsException
e = new ArrayIndexOutOfBoundsException();

// create a new Exception


Exception iobe = new Exception();
// initialize the cause and throw Exception
ioe.initCause(e);
throw iobe;
}
}

Rulare:

Cause : java.lang.ArrayIndexOutOfBoundsException

3.1.5. Strategii de gestiune a Excepțiilor


Există situații în care cauzele erorilor pot fi pierdute daca, de exemplu, în program apar clauze
finally fără catch, sau dacă în blocul finally se face return. In aceste situații, programatorul poate
pierde orice indicație despre excep ția în cauză, tipul ei sau dacă excep ția s-a produs efectiv.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 59


In ceea ce privește regulile de bună practică pentru scrierea unui cod robust la erori, excep țiile trebuie tratate
în domeniul de vizibilitate potrivit, de aceea nu este de dorit să prindem excep ții decât dacă știm cum să le
gestionăm.
Mai jos vom enumera câteva alternative valide pe care programatorii le pot adopta atunci când gestionează
excepțiile:
● La tratare, de obicei, se rezolvă problema excep ției și se reia execu ția codului care a generat
excepția sau
● Se transmite execuția la un nivel superior fără reluarea codului sau
● Se poate calcula un rezultat alternativ celui produs de codul care a generat
excepția sau
● Se poate executa tot ce e posibil în domeniul curent și se aruncă aceea și excep ție la un nivel
superior sau
● Se poate executa tot ce e posibil în domeniul curent și se arunca o altă excep ție la nivel superior
sau
● Se poate termina programul.

3.2. Colecții
Când ne referim la colecții de obiecte, tablourile(arrays) reprezintă cel mai eficient mod de a păstra
obiecte însă principalul lor neajuns este legat de dimensiunea lor fixată la compilare.

3.2.1 Collections Framework


Pentru a palia dezavantajele lucrului cu masive de obiecte, Java oferă în pachetul java.util
(pachet standard din JRE) o serie de clase și interfe țe ce constituie o arhitectură unificată
pentru reprezentarea și manipularea colec țiilor de obiecte ( Collections Framework). Arhitectura
conține interfețe ce fac colecțiilor independente de implementările propriu zise, clase de
implementare și algoritmi de prelucrare (in general, căutări și sortări de colec ții).

In figura 3.2 se prezintă ierarhia de interfe țe și clase ale arhitecturii. Numele interfe țelor sunt delimitate de linii
punctate iar numele claselor concrete de linii continue.

Figura 3.2 Ierarhia interfețelor și claselor din Collections Framework

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 60


Colecțiile oferă implementări pentru următoarele trei tipuri: java.util.Set (mul țimi de elemente neduplicate),
java.util.List (liste ordonate de elemente) și java.util.Map (mul țimi neordonate de perechi cheie-valoare).
Înainte de versiunea Java Standard Edition 5: containerele Java puteau memora orice tip de obiecte, deci pot
apărea erori la execuție datorită eterogeneită ții tipurilor de obiecte stocate în container.
Astfel, un obiect de tipul java.util.ArrayList (o implementare a unei liste de obiecte cu ajutorul masivelor) avea,
pre Java SE 5, metodele add și get definite utilizând doar clasa java.lang.Object pe post de
parametru și tip de retur.
De aceea, erorile la regăsirea obiectelor din container puteau sa fie identificate doar la execu ție
(ClassCastException).

import java.util.*;

class Apple {
private static long counter;
private final long id = counter++;
public long id() { return id; }
public String toString() {
return "[ Apple id:" + id + " => " + super.toString() +
" ] ";
}
}

class Orange {}

public class ApplesAndOrangesWithoutGenerics {


@SuppressWarnings("unchecked")
public static void main(String[] args) {
ArrayList apples = new ArrayList(); //lista
generica
for(int i = 0; i < 3; i++)
apples.add(new Apple());

// Not prevented from adding an Orange to apples:


apples.add(new Orange());
//se pot adauga obiecte de tipuri diferite

for(int i = 0; i < apples.size(); i++)


((Apple)apples.get(i)).id();
//ultimul element - orange - nu va putea fi
convertit
//la Apple=>Exceptie de tip ClassCastException
// Orange is detected only at run time
}
}

Rulare:

Exception în thread "main" java.lang.ClassCastException: Orange


cannot be cast to Apple
at

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 61


ApplesAndOrangesWithoutGenerics.main(ApplesAndOrangesWithoutGeneric
s.java:23)

Post Java Standard Edition 5 apar containerele generice în care se pot păstra doar obiecte dintr-un tipul de
bază indicat la declararea variabilei.

TipContainer<TipDeBaza> variabilaContainer;

De exemplu:

List<Integer> l = new ArrayList<Integer>();


l.add(5); //autoboxing int 5 => new Integer(5)
l.add(7); //autoboxing int 7 => new Integer(7)

Se obține o eroare la compilare dacă se încearcă stocarea unui obiect dintr-un alt tip decât cel de bază
specificat la declarație. Nu mai este necesară opera ția de cast (conversie) la regăsirea obiectelor ( get).

public class ApplesAndOrangesWithGenerics {


public static void main(String[] args) {
//lista cu generics - stocheaza doar obiecte de tip Apple
ArrayList<Apple> apples = new ArrayList<Apple>();
for(int i = 0; i < 3; i++)
apples.add(new Apple());
// Compile-time error:
// apples.add(new Orange());
for(int i = 0; i < apples.size(); i++)
System.out.println(apples.get(i).id());
// Using foreach:
for(Apple c : apples)
System.out.println(c);
}
}

Rulare:

0
1
2
[ Apple id:0 => Apple@1db9742 ]
[ Apple id:1 => Apple@106d69c ]
[ Apple id:2 => Apple@52e922e ]

Interfața java.util.Collection este implementată de majoritatea claselor ce desemnează colec ții din pachetul
java.util.

import java.util.Collection;

//...
Collection names = new ArrayList();

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 62


names.add("Mihai");
names.add("Andreea");

In colecții se pot adăuga nu doar elemente individuale ( add) ci și grupuri de elemente folosind
metodele Collection.addAll() și Collection.Arrays.asList().

import java.util.*;
public class AddingGroups {
public static void main(String[] args) {
//colectie de intregi direct initializata
Collection<Integer> collection
=new ArrayList<Integer>(Arrays.asList(1, 2,
3, 4, 5));
Integer[] moreInts = { 6, 7, 8, 9, 10 };
//metoda addAll va adauga la colectie elemente
//din vectorul moreInts
collection.addAll(Arrays.asList(moreInts));
// Runs significantly faster, but you can
}
}

List, Set, Queue și Map sunt și ele interfețe, primele trei extinzând interfața Collections.
Interfața List indica o colecție ordonată. Listele pot con ține elemente duplicate. Pe lângă opera țiile mo ștenite
de la Collection, interfața List conține operații bazate pe poziție (index), de exemplu: set,
add(la o anumita poziție), get, remove (de la o anumita poziție).

List<String> fruits = new ArrayList<>(Arrays.asList("Apple",


"Orange", "Grape"));
fruits.add("Apple"); // metodă moștenită
din Collection
fruits.add(2, "Pear"); // [Apple, Orange,
Pear, Grape, Apple]
System.out.println(fruits.get(3)); // Grape
fruits.set(1, "Cherry"); // [Apple, Cherry,
Pear, Grape, Apple]
fruits.remove(2);
System.out.println(fruits); // [Apple, Cherry,
Grape, Apple]

Interfața List posedă două implementări standard:


● ArrayList: lista implementata cu un vector. Permite operații rapide
pentru acces aliator la elemente O(1). Operațiile de inserare și
ștergere de la mijlocul listei sunt costisitoare O(n);
● LinkedList: lista implementata cu listă simplu înlăn țuita. Permite opera ții rapide de
inserție și ștergere de la mijlocul listei O(1), însă opera țiile de regăsirea aliatoare
sunt costisitoare O(n).
Printre algoritmii implementați pe liste se numără: sort ce realizează sortarea unei liste și binarySearch ce
realizează o căutare binară a unei valori într-o listă sortată.

List<Integer> list = new ArrayList<Integer>();


list.add(5); list.add(10); list.add(12);

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 63


list.add(2); list.add(4);

Collections.sort(list);
System.out.println(list);

Rulare:

[2, 4, 5, 10, 12]

Implicit funcția sort ordonează crescător elementele. Dacă dorim să realizăm o sortare particulară pentru un tip
de date complex, Java ne oferă interfa ța Comparator. Aceasta con ține metoda

int compare(Object o1,Object o2)

ce compară obiectele o1 și o2 și întoarce un întreg. Valoarea întoarsă este interpretată astfel:


● număr pozitiv – o2 este mai mare decât o1;
● zero – o2 este egal cu o1;
● număr negativ – o2 este mai mic decât o1.
Sortarea se va realiza prin apelul Collections.sort(List, Comparator)iar obiectele sunt sortate pe baza metodei
compare() din Comparator.

class ByAgeComparator implements java.util.Comparator<Person> {


@Override
public int compare(Person p1, Person p2) {
return p1.age - p2.age;
}
}
//...

List<Person> personsByAge = new ArrayList<Person>();


personsByAge.addAll(persons.values());
Collections.sort(personsByAge, new ByAgeComparator());

Un Set este o mulțime, ce nu poate conține elemente duplicate. Interfața Set


conține doar metodele moștenite din Collection, la care adaugă restricții astfel încât să
nu poată fi adăugate elemente duplicate.
Există trei implementări ale interfe ței Set: HashSet – stochează obiectele într-o tabelă de
dispersie (este implementarea cea mai performantă), TreeSet – stochează obiectele sub
forma unui arbore roșu-negru, și LinkedHashSet stochează obiectele într-o tabelă de dispersie
ce menține și o listă dublu-înlănțuită între toate elementele sale.

Un obiect Map este o structură care mapează chei pe valori. Cheile nu pot fi duplicate, iar
fiecărei chei îi corespunde exact o valoare. Map modelează conceptul de func ție: prime ște în
intrare un obiect (cheia), și returnează un alt obiect (valoarea). Există trei implementări ale
interfeței Map:HashMap, TreeMap și LinkedHashMap cu particularită ți similare implementărilor
interfeței Set.
Map poate fi văzută ca:
● O pereche de colecții formată din colec ția cheilor ( keySet()) și cea a valorilor(values());
● O singură colecție de perechi (cheie, valoare) ( entrySet()).

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 64


import java.util.*;

public class Person {


String name;
int age;

public Person(String name, int age) {


this.name = name;
this.age = age;
}

public String toString() {


return "[" + name + ", " + age + "]";
}

public static void main(String[] args) {


Map<String,Person> persons = new HashMap<String,
Person>();

persons.put("Alex", new Person("Alex", 30));


persons.put("Andrei", new Person("Andrei", 25));
persons.put("Ana", new Person("Ana", 5));

System.out.println(persons.get("Alex"));
// adaugăm un element cu aceeași cheie
System.out.println(persons.put("Andrei", new Person("",
0)));
// put(...) întoarce elementul vechi și îl suprascrie
System.out.println(persons.get("Andrei"));
// remove(...) returnează elementul șters
System.out.println(persons.remove("Andrei"));
// afișăm structura de date
System.out.println(persons);
}
}

Rulare:

[Alex, 30]
[Andrei, 25]
[, 0]
[, 0]
{Alex=[Alex, 30], Ana=[Ana, 5]}

Interfața Map.Entry corespunde unei perechi (cheie, valoare) a unei structuri Map. Metodele sale fiind:
getKey(returnează cheia), getValue( returnează valoare), setValue (setează valoarea).

3.2.2 Parcurgerea Colecțiilor


Colecțiile pot fi parcurse folosind iteratori sau instrucțiunea for-each. Iteratorii permite
parcurgerea element cu element a unei colecții și precum și modificarea, sau ștergerea de

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 65


elemente. Interfața Collections permite obținerea unui iterator prin apelul
metodei iterator().
In cazul colecțiilor generice iteratorul este și el unul generic având ca
parametru tipul colecției pe care o parcurge. Post Java SE 5 Interfața Iterator
are următoarea definiție, unde E este tipul generic asociat colec ției în cauză:

public interface Iterator<E> {


boolean hasNext();
E next();
void remove(); // optional
}

In cazul colecțiilor non-generice metoda next() va produce un rezultat de tipul java.lang.Object.


Un exemplu de utilizare al iterator-ului:

Collection<Integer> medii = new ArrayList<Integer>();


medii.addAll(Arrays.asList(new Integer[]{9,4,6,4,5,7,3,10}));

for (Iterator<Integer> it = medii.iterator();it.hasNext();) {


int medie = it.next();
// apelul it.next() trebuie realizat
//înainte de apelul it.remove()
if (medie < 5) {
it.remove();
}
}
//toate mediile mai mici decât 5 for fi şterse
System.out.println(medii);

Rulare:

[9, 5, 6, 7, 10]

Construcția sintetică for-each permite și ea parcurgerea unei colecții. Ea se bazează, pe un iterator ascuns.
Astfel, elementele colecției nu pot fi șterse în timpul iterării.

for (Integer medie : medii)


System.out.println(medie);

sau

for (Map.Entry<String, Person> entry : persons.entrySet())


System.out.println(entry.getKey() + " is " +
entry.getValue().age+" y/o.");

3.3. Fluxuri de intrare-ieșire (I/O)


Programele pot avea nevoie sa preia date de la surse externe, sau să trimită date către destina ții externe.
Acest schimb de date este realizat de sistemul de Intrare/ Ie șire (I/O în limba engleză). Sistemul de I/O într-un
limbaj de programare trebuie să fie capabil să trateze:

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 66


● Surse diferite de dispozitive de I/O (fișiere, consola, conexiuni de rețea etc.);
● Tipuri diferite de acces la sursele de date (acces secvențial, acces aleator,
acces buffered);
● Tipuri diferite de lucru cu sursa de date (binar, la nivel de caracter, la nivel de linie, la nivel de
cuvânt).
Facilitățile de intrare/ieșire din Java au la baza no țiunea de flux. Un flux este o succesiune de elemente (octe ți
sau caractere), citite sau scrise secven țial. Pentru preluarea datelor programul deschide un flux de intrare de
la o sursa de date și îl citește, iar pentru trimiterea datelor programul deschide un flux de ie șire către o
destinație de date și scrie în aceasta. Fluxurile pentru lucrul cu fi șiere sunt cele des folosite.
Clasele ce oferă facilitați de intrare/ie șire se găsesc în pachetul java.io. Pentru explorarea
sistemului de fișiere se folosește clasa java.io.File. Ea reprezintă un nume de fișier sau de
director. Prin metoda list() se obține numele fișierelor referite de directorul sau
fișierul asociat. Metoda list() poate fi invocată utilizând un obiect Directory Filter, pentru
a returna doar acele fișiere pentru care numele corespunde unor expresii
regulare. în acest caz metoda accept a interfeței FilenameFilter primește la intrare obiectul de tip File
și expresia regulară pentru comparare. Metoda list() din File apelează metoda accept pentru fiecare fișier
asociat, iar acesta este inserat în lista doar dacă accept furnizează true.

import java.io.*; // pentru functionalitati legate de io


import java.util.*;
import java.util.regex.*; // pentru expresii regulare

public class DirList {


public static void main(String[] args) {
File path = new File(".");
// creaza un obiect de tip File care
// reprezinta toate fisierele din directorul curent
String[] list;
if(args.length == 0)
list = path.list();
else
list = path.list(new DirFilter(args[0]));
Arrays.sort(list, String.CASE_INSENSITIVE_ORDER);
for(String dirItem : list)
System.out.println(dirItem);
}
}
class DirFilter implements FilenameFilter {
private Pattern pattern;
public DirFilter(String regex) {
// se creaza un pattern din expresia regulara furnizata
la intrare
pattern = Pattern.compile(regex);
}
public boolean accept(File dir, String name) {
// metoda din interfata care trebuie suprascrisa
return pattern.matcher(name).matches();
}
}

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 67


Obiectul File poate fi utilizat ca sa creăm/ ștergem/redenumim un director/fi șier sau un director/fi șier cu o
întreaga cale. File poate fi utilizat și ca să citim informa țiile referitoare la un fi șier/director (data creării, data
modificării, mărime, proprietar, tip de acces, etc.).

import java.io.*; // pentru functionalitati legate de io


import java.util.*;

public class TestFileClass {


public static void main(String[] args) {
File file = new File(args[2]);
file.mkdirs();
file.exists();
file.isDirectory();
file.isFile();
file.getName();
file.getTotalSpace();
file.canRead();
// ...
}
}

3.3.1. Tipuri de Fluxuri de intrare-ieșire


In funcție de tipul de date transferate, clasele din pachetul java.io se împart în doua
ierarhii:
● fluxurile de octeți (pe 8 biți), având în vârful ierarhiei de clase superclasele
abstracte InputStream (pentru fluxuri de intrare) și OutputStream (pentru fluxuri de
ieșire);
● fluxurile de caractere (pe 16 biți), având în vârful ierarhiei de clase superclasele
abstracte Reader (pentru fluxuri de intrare) și Writer (pentru fluxuri de ieșire).
Astfel, clasele de I/O sunt derivate din aceste patru clase. O excep ție o reprezintă clasa RandomAccessFile,
care poate fi atât sursă de date, cât și destina ție. De aceea, aplica țiile pot citi și scrie în acest tip de obiecte.
Realizarea propriu-zisa a operaților de I/O se bazează pe în șiruiri de obiecte din clasele de I/O (prin folosirea
design pattern-ului Decorator).

3.3.2. Fluxuri binare de intrare-ieșire


Câteva tipuri de fluxuri binare de intrare:
● ByteArrayInputStream: permite ca un buffer de memorie sa fie utilizat ca și un
InputStream (din buffer se extrag octeți);
● StringBufferInputStream: convertește un String într-un InputStream;
● FileInputStream: sursa de intrare este un fi șier. Se furnizează la construirea obiectului fie un
String fie un obiect File;
● PipedInputStream: este asociat cu un PipedOutputStream (care este furnizat la construirea
obiectului). Implementează conceptul de Pipe;
● SequenceInputStream: convertește unul sau mai multe InputStream într-un singur obiect de tip
InputStream (la construire se furnizează fie un InputStream, fie un Enumerator de obiecte
InputStream);
● FilterInputStream: clasă abstractă, interfa ță pentru decoratorii care furnizează tipuri particulare de
citire.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 68


Exemple de fluxuri binare de ieșire:
● ByteArrayOutputStream: creează un buffer de memorie (octe ți). Datele scrise sunt trimise în acest
buffer;
● FileOutputStream: folosit pentru ieșire într-un fișier. Se construiește obiectul pe
baza unui nume de fișier sau a unui obiect File;
● PipedOutputStream: folosit în conjunctie cu PipedInputStream, pentru implementarea conceptului
de Pipe;
● FilterOutputStream: folosit pentru ieșire formatată, se implementează func ționalită ți de scriere.

3.3.3. Folosirea Design Pattern-ul Decorator în ierarhia claselor de intrare-


ieșire
Design pattern-ul Decorator vizează adăugarea dinamică de func ționalită ți obiectelor.
Extinderea la execuție a funcționalită ții obiectelor reprezintă o alternativă flexibilă la conceptul
de derivare. Extinderea funcționalită ții se realizează prin încapsularea unui obiect de un tip dat
Component în interiorul unui Decorator. Decoratorul delegă toate apelurile către obiectul
încapsulat. Decoratorul concret suprascrie orice metodă din clasa Component a cărui
comportament se dorește modificat. Decoratorii sunt utili atunci când derivarea devine
nepractică prin explozia potențială a numărului de clase ce ar trebui definite.

Figura 3.3. Diagramă UML asociată modelului de design Decorator

In cazul claselor de I/O componenta va fi una dintre cele patru clase abstracte (InputStream OutputStream,
Reader sau Writer). Toate implementările claselor concrete au constructori care iau ca parametru un obiect
din unul din aceste patru tipuri abstracte. Aceasta este caracteristica fundamentală a design pattern-ului
Decorator. Astfel, fiecare obiect de tipul de bază InputStream va încapsula un alt obiect de acela și tip, iar
apelurile către metodele din interfața sa vor fi delegate obiectului încapsulat, cu unele modificări în
comportament. în cazul claselor de I/O nivelul de delegare este de cele mai multe destul de adânc, în sensul
în care un decorator se va regăsi în interiorul unui alt decorator care la rândul său se regăse ște în interiorul
unui al treilea decorator etc.
Decoratorul are aceeași interfață cu obiectele decorate; poate însă să extindă interfa ța prin expunerea unor
metode specifice. Clasele Filter (FilterInputStream, FilterOutputStream, FilterReader, FilterWriter) sunt
rădăcina abstractă a claselor decorator din ierarhiile claselor de I/O.

BufferedInputStream bis = new BufferedInputStream(new

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 69


FileInputStream(new File("test.txt")));
while(bis.available()>0)
{
char c = (char)bis.read();
//metoda abstracta
//public abstract int read() din clasa InputStream (Component)
//devine concreta în clasa derivata FileInputStream (Concrete
Component)
// și e decorata prin bafferizare în clasa BufferedInputStream
(Concrete Decorator)
System.out.println("Char: "+c);
}

Figura 3.4 Maniera de implementare a modelului de design Decorator in ierarhia Input/OutputStream

Exemple de filtre de intrare:


● DataInputStream: permite citirea tipurilor primitive (int, char, long etc);
● BufferedInputStream: citire din buffer, se previne accesul la dispozitivul fizic la
fiecare operație de citire;
● LineNumberInputStream: ține evidența numărului de linie citit (cu getLineNumber);
● PushbackInputStream: se poate pune înapoi în flux ultimul octet citit.
Exemple de filtre de ieșire:
● DataOutputStream: permite scrierea tipurilor primitive;
● PrintStream: folosit pentru formatarea afi șării (extinde interfa ța prin metodele print și println);
● BufferedOutputStream: pentru afișare optimizată prin buffer (se evită accesul la dispozitivul fizic la
fiecare afișare).

3.3.4. Fluxuri de compresie a datelor


Java furnizează clase pentru compresia și decompresia fluxurilor de date în ierarhia InputStream și
OutputStream:
● CheckedInputStream: prin metoda GetCheckSum() produce checksum
● CheckedOutputStream: prin metoda GetCheckSum() produce checksum

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 70


● DeflaterOutputStream: varful ierarhiei de clase pentru compresie
● ZipOutputStream: folosit pentru compresia ZIP
● GZIPOutputStream: folosit pentru compresia GZIP
● InflaterInputStream: varful ierarhiei de clase pentru decompresie
● ZipInputStream : folosit pentru decompresia de tip ZIP
● GZIPInputStream : folosit pentru decompresia de tip GZIP
● ZipFile / ZipEntry : sunt folosite pentru a manipula fisiere de tip ZIP.

3.3.5 Clasele Reader și Writer


Clasele Reader și Writer adaugă funcționalitate pentru opera țiile de I/O ce au loc la nivel de caracter sau
Unicode. Aceste clase sunt atașate ierarhiei InputStream și OutputStream, ele nu o înlocuiesc. Adăugarea se
realizează prin intermediul claselor adaptor InputStreamReader și OutputStreamWriter.
Clasele Reader și Writer au fost create pentru a permite interna ționalizarea (Unicode lucrează
cu caractere pe 16 biți), de aceea se recomandă utilizarea lor pe date textuale ori de cate ori
este posibil. Codificarea Unicode este suficientă pentru majoritatea limbilor. Alfabetele grafice
ale limbilor asiatice conțin însă un număr prea mare de pictograme. în acest caz se utilizează
UTF (Universal Character Set Transformation Format). Codificarea UTF folose ște exact
numărul dorit de biți (mai puțini pentru alfabete mici, mai mul ți pentru alfabete mari).
La citiri și scrieri specifice la nivel de octet se recomandă să se utilizeze clasele InputStream și
OutputStream.
Tabelul de mai jos prezintă coresponden ța de clase intre ierarhiile Input/OutputStream și Reader/Writer:

Clase de tip InputStream / Clase de tip Reader / Writer


OutputStream

InputStream Reader cu adapterul InputStreamReader


OutputStream Writer cu adapterul OutputStreamWriter
FileInputStream / FileReader / FileWriter
FileOutputStream
StringBufferedInputStream StringReader / StringWriter
ByteArrayInputStream / CharArrayReader / CharArrayWriter
ByteArrayOutputStream
PipedInputStream / PipedReader / PipedWriter
PipedOutputStream
FilterInputStream / FilterReader / FilterWriter
FilterOutputStream
BufferedInputStream / BufferedReader / BufferedWriter
BufferedOutputStream
PrintStream PrintWriter
StreamTokenizer StreamTokenizer (are constructor cu Reader)
PushbackInputStream PushbackReader

3.3.6. Clasa RandomAccessFile

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 71


Clasa RandomAccessFile permite citirea și scrierea fi șierelor cu acces aleator direct. Deoarece
nu lucrează pe fluxuri secvențiale de date, această clasă nu face parte din ierarhiile
InputStream sau OutputStream. într-un fişier cu acces aleator, putem citi sau scrie date de la/la
o poziţie precizată. Clasa RandomAccessFile este în general utilizată atunci când fi șierul este
compus dintr-un set de înregistrări cu dimensiune cunoscută. Ea oferă metodele seek (pentru
poziționarea pointerul în fișier) și getFilePointer() (pentru obținerea poziției curente din
fișier). Pentru acest tip de surse de date dimensiunea este cunoscută și poate fi ob ținută cu ajutorul metodei
Length(). RandomAccessFile poate fi utilizat și pentru accesarea fișierele de mapare a
memoriei virtuale a sistemului de operare (memory-mapped files).

3.3.7. Serializarea Obiectelor


Serializarea vizează păstrarea obiectelor dincolo de execu ția programelor. Prin serializare se
pot salva în fișiere, ca șiruri de octe ți, instan țele unei clase. Apoi, obiectele pot fi recuperate din
fișierele în care au fost salvate și execu ția repornită de la punctul unde a fost oprită anterior.
Orice obiect dintr-o clasă care implementează interfa ța Serializable poate fi convertit în sir
de octeți și apoi restaurat. Interfața Serializable nu are metode dar specifică faptul că
instanțele unei clase pot fi serializate.
Serializarea asigură persistența obiectelor în sensul în care durata lor de via ță excedă execu ția programului în
care au fost create. în Java, pentru realizarea persisten ței, obiectele sunt serializate / deserializate în mod
explicit (lightweight persistence).
La serializare, se creează un obiect dintr-un tip OutputStream, acesta este încapsulat în interiorul unui
ObjectOutputStream și se utilizează metoda writeObject.

SomeType o = new SomeType();


FileOutputStream fout = new FileOutputStream("file");
ObjectOutputStream sout = new ObjectOutputStream(fout);
sout.writeObject(o);

La deserializare se folosesc ObjectInputStream și metoda readObject. Se obține un java.lang.Object asupra


căruia trebuie făcut downcast pentru tipizare corectă.

FileInputStream fin = new FileInputStream("file");


ObjectInputStream sin = new ObjectInputStream(fin);
o = (SomeType) sin.readObject();

Tipul efectiv al unui obiect deserializat se poate obține prin metoda getClass.

System.out.println(o.getClass() == SomeType.class);

Pe lângă metodele de scriere/citire a obiectelor cele două clase pun la dispozi ție și metode pentru scrierea
tipurilor primitive de date. Serializarea atributelor de tip primitiv din interiorul obiectelor se face automat. Atunci
când un obiect agregat este serializat întregul graf de obiecte ce este referit direct sau indirect de acesta va fi
serializat, cu condiția ca clasele obiectelor din graf să implementeze interfa ța Serializable. Procesul de
serializare este recursiv deoarece obiectele referite pot referi la rândul lor alte obiecte și
așa mai departe.
Există cazuri când dorim ca unele variabile membre sau sub-obiecte ale unui obiect să nu fie salvate automat
în procesul de serializare, sau dorim să recreăm sub-obiecte de la zero. Dacă atributele reprezintă informa ții
confidențiale sau variabile auxiliare nu dorim în general să le salvăm. Personalizarea serializarii anumitor date
se poate face implementând în clasa serializabilă metodele private writeObject și readObject. Acestea sunt

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 72


apelate automat de către metodele writeObject și readObject din clasele ObjectOutputStream și
ObjectInputStream.
Pentru un control complet al procesului de serializare, o clasă trebuie să implementeze interfa ța
Externalizable. Interfața Externalizable extinde interfața Serializable oferind două metode
noi, writeExternal și readExternal, care sunt apelate automat la serializare/deserializare și
conțin cod suplimentar care se execută la aceste opera ții.
La deserializarea cu Externalizable, obiectele sunt reconstruite folosind constructorul implicit
iar, mai apoi, atributelor sunt recuperate prin apelul metodei readExternal. Sub-obiectele
componente trebuie serializate manual (in writeExternal) iar la deserializare, ele trebuie recuperate de pe disc.
La moștenirea dintr-o clasă ce implementează Externalizable, se apelează writeExternal și readExternal al
clasei de bază pentru a se asigura o serializare / deserializare corectă a obiectului în integralitatea sa.
Pentru a evita serializarea anumitor componente ale obiectelor programatorul are la
dispoziție trei metode:
● utilizarea Externalizable, care poate fi nepractică atunci când clasele sunt greu de serializat și
numărul câmpurilor care nu trebuie salvate este mic;
● cuvântul cheie transient indică faptul acel câmp nu va fi serializat ca și parte a procesului automat
de serializare;
transient private String parola;
● Adăugarea la clasele ce trebuie serializate a metodele writeObject și readObject în care se
furnizează codul programatorului pentru serializare.

3.4. RTTI
Există două maniere de identificare a tipurilor variabilelor unui program:
● cea statică, în care identificarea tipurilor variabilelor are loc la compilare (tipul obiectelor trebuie să
fie definit și identificabil la momentul compilării);
● cea dinamică bazată pe reflecție, în care identificarea tipurilor are loc la rulare (clasele ce descriu
aceste tipuri pot fi descoperite și încărcate doar la momentul execu ției).

In Java, prin mecanismul de RTTI (Runtime Type Identification) se poate identifica tipul exact al
unui obiect la execuție (limbajul de programare are metode pentru a realiza acest lucru). Există
însă o restricție în sensul în care tipurile detectabile prin RTTI trebuie să fie cunoscute la
momentul compilării.
RTTI joacă un rol primordial și în implementarea upcast, atunci când este necesară
identificarea automată a tipului variabilei la execu ție pentru a putea invoca polimorfic metoda
suprascrisă în clasa derivată.

3.4.1. Clasa Class


Java realizează RTTI prin intermediul obiectelor Class. Acestea conțin informații referitoare la
tipurile asociate. Pentru fiecare clasă pe care o avem în program, există un obiect Class. La
compilarea unei noi clase, acest obiect este creat și salvat în fișierul .class.
La crearea unui obiect dintr-o clasă, JVM utilizează un subsistem numit class loader. Toate
clasele sunt încărcate în mod dinamic de către JVM la prima utilizare a acestora (anume când
se face prima referință la un membru static al clasei).

La încărcarea unei clase, se verifică dacă obiectul Class al tipului respectiv este încărcat, dacă nu, se
identifică fișierul .class pe disc, și octe ții acestuia sunt verifica ți înainte să fie încărca ți.
Pregătirea obiectelor unei clase pentru utilizare se realizează automat de JVM în 3 pa și:
● Încărcarea clasei: realizată de class loader, se găse ște byte codul pe disc și se creează obiectul
Class corespunzător tipului;

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 73


● Linking: se alocă spațiu de stocare pentru membrii statici și se rezolvă toate referin țele realizate
de clasă către alte clase;
● Inițializare: se execută initializatorii statici și blocurile statice de ini țializare. Ini țializarea este
întârziată până la prima referință la o metodă statică sau un câmp non-static; ini țializarea
obiectului Class este amânata atâta timp cât este posibil.
Obiectul Class oferă un număr de metode foarte utile pentru realizarea RTTI:
● metoda forName(“NumeTip”) : returnează o referință către obiectul Class aferent unui tip. Ea
solicită încărcarea fișierului .class dacă acesta nu este deja încărcat de către class loader;

class Candy {
static { System.out.println("Loading Candy"); }
}
class Gum {
static { System.out.println("Loading Gum"); }
}
class Cookie {
static { System.out.println("Loading Cookie"); }
}
public class SweetShop {
public static void main(String[] args) {
System.out.println("inside main");
new Candy();
System.out.println("After creating Candy");
try {
Class.forName("Gum");
} catch(ClassNotFoundException e) {
System.out.println("Couldn’t find Gum");
}
System.out.println("After
Class.forName(\"Gum\")");
new Cookie();
System.out.println("After creating Cookie");
}
}

● Metoda object.getClass(): returnează obiectul de tip Class aferent unui tip deja încărcat de către
JVM;
● Metode ale obiectului Class: getName, getSimpleName, getCanonicalName, isInterface,
getInterfaces, getSuperclass etc.

class Toy {
// Comment out the following default constructor
// to see NoSuchMethodError from (*1*)
Toy() {}
Toy(int i) {}
}
interface HasBatteries {}
interface Waterproof {}
interface Shoots {}

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 74


class FancyToy extends Toy implements HasBatteries, Waterproof,
Shoots {
FancyToy() { super(1); }
}
public class ToyTest {
static void printInfo(Class cc) {
System.out.println("Class name: " + cc.getName() + "
is interface? ["
+ cc.isInterface() + "]");
System.out.println("Simple name: " +
cc.getSimpleName());
System.out.println("Canonical name : " +
cc.getCanonicalName());
}
public static void main(String[] args) {
Class c = null; try {
c = Class.forName("FancyToy");
} catch(ClassNotFoundException e) {
System.out.println("Can’t find FancyToy");
System.exit(1);
}
printInfo(c);
for(Class face : c.getInterfaces())
printInfo(face);
Class up = c.getSuperclass(); Object obj =
null;
try {
// Requires default constructor:
obj = up.newInstance();
} catch(InstantiationException e) {
System.out.println("Cannot instantiate");
System.exit(1);
} catch(IllegalAccessException e) {
System.out.println("Cannot access");
System.exit(1);
}
printInfo(obj.getClass());
}
}

Referința spre obiectul de tip Class poate fi obținută și printr-un literal astfel: NumeTip.class. Aceasta
referință este sigură în sensul în care validitatea ei poate fi verificată la momentul compilării, spre deosebire de
metoda classForName care va returna o excepție la rulare dacă clasa nu există. Crearea unei referin țe print-
un literal nu inițializează obiectul de tip Class corespunzător.
Pentru tipurile wrapper de tipuri primitive, există atributul TYPE care oferă referin ța spre obiectul class asociat
(astfel, boolean.class == Boolean.TYPE).
Dacă dorim să declarăm doar referințe spre obiectul Class al unui tip anume, variabila în cauză se va declara
de tipul generic: Class<Tip> referință. în acest caz, variabila va putea referi doar obiectul Class al tipului Tip,
nu obiecte Class ale subtipurilor acestuia. Astfel, chiar dacă clasa Integer este derivată din clasa Number,
Class<Interger> nu este subclasă a lui Class<Number>. Pentru a putea salva referințe spre obiecte

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 75


Class ale subtipurilor unui tip dat se va folosi declarația: Class<? Extends Tip> referință. Class<?>
permite referințe spre obiecte class asociate oricărui tip.

public class GenericClassReference {


public static void main(String[] args) {
Class<?> intClass = int.class;
Class<Integer> genericIntClass = int.class;
genericIntClass = Integer.class; // Same thing
intClass = double.class;
Class<? extends Number> genericNumberClass =
int.class;
// genericIntClass = double.class; // Illegal
}
}

RTTI permite realizarea conversiei de tip cu ajutorul obiectului Class prin metoda obiectClass.cast(referinta).
Conversia la compilare se face folosind operatorul de cast (TipConversie)obiect însă ea poate genera
ClassCastException dacă tipurile nu sunt compatibile. Pentru a evita ridicarea unei astfel de excep ții se
utilizează operatorul instanceof de verificare a tipului.

if (x instanceof Dog)
((Dog)x).bark(); //se evita ClassCastException

La compararea obiectelor de tip Class este diferența intre rezultatele produse de


instanceof/isInstance și compararea obiectelor de tip Class cu operatorul == sau cu metoda equals. în
cazul instanceof/isInstance se ține cont de relațiile de moștenire, la compararea cu == sau
equals nu se ține cont de relațiile de moștenire.

3.4.2. Mecanismul de reflecție


Mecanismul de RTTI prezintă dezavantajul de a circumscris lucrului cu obiecte ale căror clase
sunt disponibile la compilare. Pentru a putea lucra cu obiecte ale căror clase nu sunt disponibile
la compilare Java oferă mecanismul de reflecție. Acesta permite inspectarea în mod
dinamic (in timpul rulării) a structurii claselor încărcate la execu ție și de apelare a metodelor
astfel descoperite.
Mecanismul de reflecție este implementat în Java în cadrul pachetului java.lang.reflect.
Structura tipurilor este descrisă cu ajutorul claselor Field, Method, Constructor și obținută prin apelul
metodelor getFields, getMethods, getConstructors.
De exemplu, pentru a descoperi și a rula o metodă prin mecanismul de reflec ție se poate utiliza următoare
secvență de cod:

java.lang.Object dogObject = ...;


java.lang.reflect.Method method = null;

try {
method = dogObject.getClass().getMethod("bark", null);

} catch (NoSuchMethodException | SecurityException e) {


// TODO Auto-generated catch block
e.printStackTrace();

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 76


}

try {
method.invoke(dogObject, null);
} catch (IllegalAccessException | IllegalArgumentException
| InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

In exemplu de mai sus se presupune existenta unui obiect dogObject ce con ține o metodă bark(). Se
utilizează reflecția pentru a apela metoda bark().

3.4.3. Modelul de design Proxy


Un Proxy este un obiect substitut, transmis în locul unui obiect real, pentru a furniza prelucrări
adiționale înainte de a transmite opera țiile către obiectul real. în general el oferă aceea și
interfață cu obiectul real. Astfel, scopul principal al unui obiect Proxy este de a controla accesul
la obiectul real. Controlul accesului poate presupune opera ții de sincronizare, autentificare,
logare, acces la distanță (RPC), inițializare târzie (Hibernate) etc.

Figura 3.5 Diagramă UML asociată modelului de design Proxy

Spre deosebire de un Proxy static, un Proxy dinamic se creează prin reflec ție în momentul execu ției. Clasa
unui Proxy dinamic va implementa o listă de interfe țe specificate la execu ție, iar fiecare apel spre una din
metodele interfeței va fi delegat spre un alt obiect printr-o interfa ță uniformă ce con ține metoda invoke.
Apelurile garantează conformitatea semnăturii metodelor (nume metodă, parametrii și tip de retur).Un Proxy
dinamic poate fi asimilat unei forme de fa țadă ce oferă poten țial implementarea oricărei interfe țe.

3.5. Concurența în Java


3.5.1. Noțiuni de bază
Pana acum am descris programe alcătuite din secven țe de execu ție a instruc țiunilor. Aceste
programe numite secvențiale au un singur punct de execu ție la un moment dat. Un program, ce
dispune de propriul spațiu de adrese, aflat în execu ție se nume ște proces. Un fir de execu ție
(Thread) este o succesiune secvențială de instrucțiuni care se execută în cadrul unui proces.
Un program secvențial are un singur fir de execu ție.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 77


Un sistem de operare mono-tasking (MS-DOS) poate executa un singur proces la un moment dat. Un sistem
de operare multi-tasking (UNIX, Linux, Windows) poate rula mai multe procese la un moment dat alocând
periodic cuante din timpul de lucru al CPU fiecărui proces. No țiunea de fir de execu ție are sens doar în cadrul
unui sistem de operare multi-tasking.
Programele concurente presupun existen ța mai multor fire de execu ție care rulează în paralel.
în Java, modul concurent de execu ție a programelor este foarte larg răspândit (implementarea
aplicațiilor web, interfețe grafice, aplica ții timp-real etc.) Principalele avantaje ale folosirii
programelor concurente sunt legate de cre șterea vitezei de execu ție a programelor în condi țiile
programării multiprocesor și de îmbunătă țirea designului aplicativ.

Unele probleme (precum cele de simulare) sunt concurente prin defini ția lor. Astfel, chiar dacă sistemul pe
care programul se executa are un singur CPU, programele concurente au o claritate mai mare.
Creșterea puterii de calcul este susținută de emergen ța calculatoarelor cu un număr din ce în ce mai mare de
core-uri, în condițiile plafonării vitezei chipurilor. Acestea permit prin distribuirea taskurilor pe procesoare
majorarea numărului de operații elementare pe unitatea de timp ( throughput). Chiar în condițiile sistemelor
mono-core, programele concurente se pot executa mai rapid prin exploatarea situa țiilor în care anumite
procese sunt blocate. Dacă un task (sau thread) nu poate continua (de exemplu, din cauza unor opera ții de
I/O), se spune că acel task este blocat. în acest timp, un sistem de operare multi-tasking va putea aloca
procesorul unor alte taskuri neblocate.
Abordare concurentă este utilă și în cazul programelor bazate pe evenimente permi țând realizarea de interfe țe
grafice reactive, în condițiile în care prelucrările asociate unor evenimente durează perioade de timp
semnificative. Pentru a realiza un program care răspunde inputului utilizatorului, taskul de lunga durată trebuie
rulat într-un nou fir de execuție.
Implementarea unor strategii de execu ție concurentă este foarte utilă nu doar la nivelul sistemului de operare
(prin multi-tasking) ci și la nivelul mediului de programare. Ea presupune partajarea de către firele de execu ție
a unor resurse comune, coordonarea accesului la acestea și transmiterea de informa ții intre diferitele procese
ale sistemului.
Exista doua mari clase de sisteme concurente: distribuite și concurente. Sistemele concurente pot fi clasificate
ca fiind distribuite sau paralele după cum urmează:
● Într-un sistem distribuit, fiecare procesor are memorie proprie (model de memorie distribuită).
Taskurile concurente sunt izolate unele de altele, fiecare func ție concurentă nu
produce efecte colaterale. Schimbul de informa ții se face prin trimiterea de
mesaje între procesoare. Prin messaging se realizează coordonarea taskurilor
fără să fie nevoie de partajarea de resurse;
● Într-un sistem paralel, toate procesoarele au acces la o memorie partajată.
Memorie partajată poate fi utilizat pentru schimbul de informa ții între procesoare. Majoritatea
programelor concurente scrise în C/C++ sau Java folosesc modelul de memorie partajata. în
C/C++ prin primitiva fork se creează procese separate gestionate de către sistemul de operare.
în Java în cadrul unui singur proces gestionat de către sistemul de operare, se creează mai
multe taskuri prin threading.
Java threading este preemptiv în sensul în care mecanismul de scheduling furnizează o felie de timp procesor
fiecărui fir de execuție, prin întreruperea periodică a threadului care se execută și schimbarea contextului de
execuție către un alt thread. JVM suportă un număr limitat de threaduri (de ordinul zecilor) ceea poate fi
limitativ în anumite situații.
Alternativa, neimplementată în Java, ar fi utilizarea multi-threading-ului cooperativ. Acesta nu impune o limită
a taskurilor independente care pot rula la un moment dat, fiecare thread cedând controlul(yield) către alt
thread la un anume moment convenabil.
Chiar dacă firele de execuție din Java dispun de o primitivă yield, apelul acesteia nu garantează schimbarea
contextului de execuție către un alt thread.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 78


3.5.2. Threading în Java
Intr-un sistem multi-threading, un proces poate conține mai multe thread-uri concurente.
Programatorul poate programa fiecare dintre aceste thread-uri ca și cum ar avea întreg
procesorul la dispoziție. în Java, orice fir de execuție este o instanța a clasei Thread sau a
uneia din subclasele sale. Funcția main are alocat propriul thread. Crearea unui thread este o opera ție
costisitoare la nivelul sistemului de operare, bazată pe conceptul de pthread din C, de aceea thread-urile
trebuie gestionate eficient.
In Java, există distincție intre taskul care este executat și thread-ul care execută acest task,
taskul fiind un anume job care trebuie realizat iar thread-ul, mecanismul care îl execută. Pentru
a defini un task trebuie implementată interfa ța Runnable. Aceasta este o interfa ță care con ține
o singură metodă, și anume metoda run în care este implementat codul ce va fi executat de firul
de execuție. în general, metoda run are o buclă care se execută atâta timp cat este necesar.

Pentru a crea un thread, trebuie furnizat un obiect de tip Runnable în constructorul unui obiect de tip Thread.
Metoda start a clasei Thread realizează toate ini țializările necesare și apoi apelează metoda run a obiectului
Runnable agregat. Metoda run este apelată într-un fir nou de execu ție. Mecanismul pentru lansarea thread-
urilor în execuție este non-determinist (nu putem garanta ordinea în care scheduler-ul alocă procesorul
diverselor taskuri).

public class BasicThreads {

public static void main(String[] args) {


//task-ul este executat într-un nou Thread
//clasa Thread asteapta în constructor un obiect
//ce implementeaza interfata Runnable
Thread t = new Thread(new LiftOff());
t.start();//se lanseaza executia în nou fir de executie:
LiftOff.run()
System.out.println("Waiting for LiftOff");
}
}

Prin folosirea metodei yield() se solicită JVM să dea controlul unui alt thread din programul curent prin
invocarea scheduler-ului. Garbage collector-ul nu va colecta un thread ne-referit decât după ce acesta î și
termină execuția propriului run.
Există trei maniere de creare a unui thread:
● Se creează o clasă care implementează Runnable. Un obiect din aceasta clasă este transmis în
constructorul unui obiect de tip Thread;
● Se moștenește din clasa Thread, care ea însă și implementează Runnable, și se suprascrie
metoda run. Clasa care instantazaa fire de execu ție nu poate avea deja o superclasă, fiindcă Java
nu permite moștenirea multiplă;
● Se creează o clasă care implementează Runnable. Această clasă agregă un obiect de tip Thread
construit prin new Thread(this).

In cazul ultimelor doua metode taskurile sunt pornite din constructor (cu start) ceea ce este problematic,
deoarece thread-ul își poate începe execu ția înainte ca constructorul să se termine. Se preferă prima variantă
și utilizarea executorilor.
Clasele care implementează Runnable sau extind Thread pot fi scrise ca și clase interioare (eventual
anonime).
Java furnizează un nivel intermediar intre un client și taskurile pe care acesta le execută. în loc ca clientul să
apeleze taskurile în mod direct, un obiect intermediar (executor) va apela aceste taskuri indirect. Executorii

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 79


gestionează ciclul de viață a taskurilor asincrone fără să fie nevoie să gestionăm explicit ciclul
de viață a unui obiect Thread. Se creează un ExecutorService prin apelarea unei metode
specifice a clasei Executors. Metoda shutdown previne ca alte taskuri sa fie trimise către
executorService spre execuție.
Există trei tipuri de ExecutorService:
● FixedThreadPool: fixează de la început numărul de thread-uri utilizate pentru
execuția taskurilor. Este o metodă deosebit de eficientă pentru că overhead-ul cu crearea thread-
urilor este realizat la crearea obiectului de tip ExecutorService;
● CachedThreadPool: permite crearea un număr variabil de thread-uri. Va opri crearea de threaduri
noi pe măsura ce thread-urile vechi sunt reciclate;
● SingleThreadExecutor: este un FixedThreadPool cu un singur thread (folosit de obicei pentru
thread-uri cu durată de viață lungă –de exemplu, tread-uri care ascultă un socket).

import java.util.concurrent.*; //package-ul pentru concurenta

public class CachedThreadPool {

public static void main(String[] args) {


//creare obiectului ce va executa task-ul
//si va realiza managementul firelor de executie
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
exec.execute(new LiftOff()); //executarea task-ului
}
System.out.println("Waiting for executor to
terminate!");
exec.shutdown(); //terminarea executiei – obiectul exec
nu va mai accepta task-uri noi
}
}

Crearea specializată a thread-urilor se realizează cu ajutorul interfe ței ThreadFactory. Thread-urile pot fi
personalizate prin implementarea metodei newThread. Obiecte implementând ThreadFactory pot fi furnizate
ca și constructori pentru Executori. ExecutorService-ul rezultat va utiliza acest threadFactory pentru crearea
noilor thread-uri (prin metoda newThread).

3.5.3. Prinderea excepțiilor ridicate în taskuri


Dacă apare o excepție în metoda run, aceasta se va propaga pana la consolă, în cazul în care excep ția nu e
rezolvată în run.
Problema e rezolvata folosind Executori. Fiindcă excep țiile aruncate de metoda run a unui thread nu pot fi
prinse cu try-catch în jurul comenzii exec a executorului, pentru prinderea unei excep ții se implementează
interfața Thread.UncaughtExceptionHandler, creându-se o clasa handler de excep ție. Se asignează handlerul
de excepție cu metoda setUncaughtExceptionHandler thread-ului care rulează taskul. Aceasta opera ție poate
fi scrisa în medoda newThread a unui ThreadFactory folosit de executori pentru instan țiile firelor de execu ție.
Se poate asigna handlerul de excep ții implicit al clasei Thread cu setDefaultUncaughtException.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 80


O problemă legată de task-urile ce implementează implementează interfa ța Runnable este
faptul că nu știm cum anume acestea s-au terminat sau ce rezultate au produs. dacă se
dorește ca execuția taskurile să producă rezultate se va folosi interfa ța Callable. Aceasta
poseda metoda call care trebuie sa returneze o valoare. Obiectele Callable trebuie apelate de
metoda submit a unui ExecutorService.

executorService.submit(new Callable<Integer>() {
@Override
public Integer call(){
System.out.println("Starting");
return new Random().nextInt(4000);
}}
);

Metoda submit produce un obiect de tip Future parametrizat cu tipul rezultatului specific returnat din task.
Obiectul Future poate fi interogat cu metoda isDone() pentru a vedea dacă metoda call a produs rezultatul.
Metoda get() a obiectului Future obține rezultatul produs de metoda call. Dacă rezultatul nu este
disponibil, metoda get blochează execuția thread-ului apelant până când rezultatul devine disponibil.

ExecutorService exec = Executors.newCachedThreadPool();


ArrayList<Future<String>> results = new
ArrayList<Future<String>>();
//colectia tine obiecte "VIITOARE" de tip String returnate de
firele paralele
for (int i = 0; i < 10; i++) {
//submit realizeaza executia task-ului
//returneaza un obiect Future<String>
results.add(exec.submit(new TaskWithResult(i)));
}
System.out.println("Waiting for results: ");

for (Future<String> fs : results) {


try {
// get() blocks until completion:
//get() asteapta returnarea rezultatului din task
System.out.println(fs.get());
} catch (InterruptedException e) {
System.out.println(e);
return;
} catch (ExecutionException e) {
System.out.println(e);
} finally {
//inchiderea Executorului indiferent de rezultatul
executiei
exec.shutdown();
}
}

O clasă aparte de thread-uri sunt cele Daemon care sunt thread-uri de serviciu (furnizând un serviciu general
altor fire de execuție). Dacă un thread daemon creează alte thread-uri, acestea devin daemon la rândul lor.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 81


Când se pornește mașina virtuală Java, există un singur fir de execu ție care nu este de tip
Daemon și care apelează metoda main(). Main este executataaa într-o instan ță non-
daemon. într-un program, când toate thread-urile non-daemon î și termină execu ția, programul
este terminat, JVM omorând toate thread-urile daemon. Dacă există thread-uri non-daemon
care se execută, programul nu se termină. Un thread se marchează ca și daemon înainte ca sa
se înceapă execuția acestuia cu start.

3.5.4. Partajarea resurselor intre threaduri


Există situații în care două sau mai multe thread-uri utilizează aceea și resursă sensibilă în
același timp. Problema apare când threadurile consumatoare fără să a ștepte informa țiile de
care au nevoie interferează cu cele producătoare care nu au produs încă aceste informa ții.
Este nevoie de un mecanism, numit excluziune mutuală sau mutex (mutual exclusion), prin
care se serializează accesul taskurilor la resursa partajată pentru a preveni utilizarea ei
concomitentă de către acestea.
Mecanismul de mutex poate fi asimilat punerii unui lacăt pe resursa care trebuie utilizată partajat de către
primul task care are acces la ea, blocând accesul celorlalte taskuri până când acesta î și termină opera ția pe
resursă.
Java implementează excluderea mutuală pe o resursă prin folosirea cuvântului cheie
synchronized. Resursa partajată se încapsulează într-un obiect, iar toate metodele care
utilizează resursa sunt marcate ca și synchronized. Aceste metode trebuie să fie din aceea și
clasă.
Mecanismul synchronized este implementat astfel: Fiecare obiect are un monitor (lacăt). Când
un thread intră în zona de cod synchronized, thread-ul achizi ționează monitorul, și nici un alt
thread nu va mai putea intra într-o zonă synchornized a aceluia și obiect. Dacă primul thread care a
achiziționat monitorul intră într-o noua zona synchornized a aceluia și obiect, se ține un contor al numărului de
monitoare achiziționate de thread. Există și un monitor la nivel de clasă pentru implementarea mecanismului
synchronized la nivelul metodelor statice.
Pe lângă mecanismul implicit de excluziune mutuală realizat prin sec țiunile synchronized, Java
permite implementarea sa explicită prin obiecte ce implementează interfa ța Lock. Conceptual
un obiect Lock este un monitor. Obiectele Lock dispun de metoda lock pentru achizi ționarea
explicită a monitorului și de metoda unlock pentru eliberarea lui. Metodele aociate monitorului
explicit trebuie introduse într-un bloc try/catch/finally pentru a ne asigura că monitorul este
eliberat corespunzător în cazul unor excep ții. în general, în secven ța de linii de cod dintre apelurile lock și
unlock se inserează zona de cod sensibil unde pot să apară coliziuni intre thread-uri (sec țiune critică). La
sincronizare pe secțiuni critice, trebuie să se utilizeze acela și obiect ca și țintă a sincronizării.
Avem nevoie de secțiuni critice dacă partea critică este o bucată de cod dintr-o metoda (spre deosebire de
întreaga metodă). Secțiunea critică poate să fie marcată și de cuvântul cheie synchronized(obiect){cod
secțiune critica}. Monitorul se achizi ționează pentru obiectul specificat exact ca în cazul obiectelor de tip Lock.
Dacă se sincronizează doar secțiuni critice mai mici decât întreaga metodă se poate câ știga timp pre țios la
execuția programului.
Monitoarele explicite Lock au câteva avantaje asupra celor implicite introduse de synchronized:
● in metodele synchronized pot apărea excep ții, și atunci nu există nici o șansă de a readuce
sistemul în stare validă (monitorul nu poate fi eliberat);
● cu Lock se utilizează mecanismul try/finally, și finally se execută în orice situa ție (inclusiv situa țiile
de excepție);
Însă, șansa de a induce erori cu synchronized este mai redusă, și de aceea se vor utiliza monitoarele explicite
Lock doar în cazuri speciale cum ar fi, de exemplu, nevoia de a ob ține un obiect lock pe durate specificate de
timp:
● tryLock(): metodă neblocantă prin care se încearcă achizi ționarea monitorului;
● tryLock(argumente): metodă care așteaptă un anumit timp pentru achizi ționarea monitorului.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 82


3.5.5. Atomicitate și volatilitate
O operație este atomică dacă ea nu poate fi întreruptă de thread scheduler. Exemple de
operații atomice ar fi pe sisteme de 32 de bi ți opera țiile simple executate pe tipuri primitive
altele decât long și double.
Totuși, chiar dacă modificările efectuate de un task sunt atomice ele pot să nu fie vizibile altor
taskuri deoarece valoarea modificată a variabilei poate fi stocata într-un registru temporar.
Mecanismul de sincronizare forțează ca modificările unui task sa fie vizibile în toata aplica ția,
chiar și în cazul sistemelor multiprocesor.
Pentru a asigura vizibilitatea unor operații atomice se va folosi cuvântul cheie volatile. Imediat
ce un thread scrie o valoare într-o variabilă volatile, acea valoare va fi disponibilă în lectură oricărui alt fir de
execuție. Este sigur să utilizam volatile în loc de synchronized, doar dacă clasa are un singur câmp ce poate
fi modificat.

3.5.6. Stări ale unui Thread


Fiecare fir de execuție are propriul său ciclu de viată: este creat, devine activ prin lansarea sa în execu ție si, la
un moment dat, se termină. Un fir de execu ție se poate găsi în una din următoarele patru stări:
● New: starea unui thread este new imediat după ce aceasta a fost creat. Thread-ul devine eligibil
să fie alocat către CPU spre execuție;
● Runnable: thread-ul poate să fie rulat. Scheduler-ul nu este împiedicat de nimic ca să pună
threadul în execuție;
● Blocked: taskul poate și fie executat, dar exista ceva ce previne aceasta;
● Dead: un thread dead sau terminat nu mai poate fi alocat de către scheduler. A realiza return din
metoda run pune thread-ul în starea dead.
Un thread poate ajunge în starea blocked când:
● se apelează metoda sleep;
● se apelează metoda wait. Thread-ul redevine runnable după ce prime ște un notify
sau un signal;
● thread-ul așteaptă ca o operație de I/O să fie finalizată;
● thread-ul apelează o metodă synchornized și se a șteaptă achizi ția monitorului.

3.5.7. Întreruperea thread-urilor


A întrerupe execuția unui task în mijlocul metodei run este similar cu a arunca o excep ție.
Pentru terminarea unui fir de execu ție clasa Thread dispune de metoda interrupt. Prin apelul
metodei interrupt() se setează statusul interrupted pentru acel task. Un thread cu statusul
interrupted va arunca o excepție de tipul InterruptedException dacă acest task este deja blocat
sau încearcă realizarea unei operații blocante. Statusul interrupted va fi resetat la momentul
aruncării excepției sau când taskul apelează Thread.interrupted().
Pentru întreruperea taskurilor rulate cu executori se va folosi metoda shutdownNow() a acestora. Ea apelează
interrupt() pentru fiecare thread pornit de executor. Apelul metodei cancel pe un obiect Future<?> generează
la rândul său un interrupt. Taskurile blocate de opera ții de I/O sau de un synchronized nu se întrerup cu
interrupt. Interrupt() apare doar la închiderea unui socket dar nu și la închiderea altor tipuri de fluxuri de I/O.
Taskurile blocate prin synchornized nu sunt întrerupte cu interrupt, doar sec țiunile critice gardate de monitoare
explicite de tip ReentrantLock pot fi întrerupte.

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 83


3.5.8. Cooperarea între thread-uri
Problema cooperării intre taskuri nu se referă la accesul la o resursa partajată ci la maniera în
care taskurilor pot lucra în comun pentru atingerea aceluia și obiectiv. De exemplu, un task
trebuie realizat înaintea altuia pentru ca al doilea să poată utiliza rezultatele ob ținute de primul.
Problema fundamentală la cooperare este Handshaking sau sincronizarea execu ției acestora.
Se rezolvă prin extinderea mecanismului excluderii mutuale. Un task are posibilitatea de a-si
suspenda execuția până în momentul în care apare un eveniment exterior lui. Punerea unui fir
de execuție în așteptare se realizează cu metoda wait a clasei Object. Wait() permite să se a ștepte realizarea
unor condiții care sunt în afara controlului taskului curent. Wait() poate avea ca și argument un număr de
milisecunde. Wait() suspendă taskul până când apare un notify sau un notifyAll. Wait este diferit de sleep și
yield în sensul în care el eliberează monitorul asociat obiectului respectiv. Este posibil ca taskuri diferite sa
ceara același lock pentru motive diferite. Metoda notifyAll informează toate firele de execu ție care sunt în
așteptare la monitorul obiectului curent îndeplinirea condi ției pe care o a șteptau. Metoda notify informează
doar un singur fir de execuție.
Wait(), notify() și notifyAll() sunt metode ale java.lang.Object, ele pot fi apelate doar din metode synchornized.
Wait() apelat dintr-o metoda ne-synchronized generează IllegalMonitorStateException.

In condițiile accesului la resurse partajate se poate ajunge la situații de


interblocare a execuției taskurilor. Deadlock este o situație în care taskurile așteaptă
indefinit utilizarea unei resurse unul după celălalt.

Un deadlock apare atunci când următoarele 4 condi ții sunt simultan îndeplinite:
● Excluderea mutuală: cel puțin una din resursele folosite de către taskuri nu pot fi utilizate în
comun;
● Cel puțin un task deține o astfel de resursă și a șteaptă sa ac ționeze o altă asemenea resursă
deținută de alt task;
● astfel de resursă nu poate fi luata în mod for țat de la un task;
● poate apărea un wait circular.
Rezolvarea situației de deadlock presupune împiedecarea realizării uneia din cele 4 condi ții de mai sus.

Problema filosofilor (Dijkstra) este un exemplu clasic de program concurent ce poate genera un
deadlock. Ea modelează procese care își dispută accesul exclusiv la un număr limitat de
resurse. La o masă circulară stau N filozofi; în fa ța fiecăruia este o farfurie cu spaghete; între
oricare doi filozofi este o furculi ță; fiecare are nevoie de ambele furculi țe alăturate pentru a
mânca apoi eliberează furculițele utilizate. Filosofii desfă șoară alternant următoarele activită ți:
mănâncă si gândesc. Dacă toți filozofii iau fiecare furculi ța din stânga simultan, niciunul nu va mai putea lua
furculița din dreapta, rezultând o situa ție de deadlock.

Întrebări recapitulative
Întrebări pentru testul scris:

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 84


1. Ce este o colectie generica in Java?
2. Cum se definesc exceptiile utilizator?
3. Prin ce interfete se realizeaza procesul de serializare/deserializare al obiectelor?
4. Cum anume mecanismul de RTTI concura la realizarea upcast-ului in Java?
5. Cum este implementat mecanismul de reflexie in Java?
6. Ce este un fir de executie(thread)?
7. Ce este un task?
8. Ce este un sistem multi-tasking?
9. Ce sunt Executorii?
10. Care sunt diferentele intre interfetele Runnable si Callable?
11. Care sunt etapele ciclului de viata al unui thread?
12. Prezentaţi conceptele de atomicitate și volatilitate.
13. Cum se realizeaza prinderea excepțiilor ridicate în taskuri?
14. Ce primitive faciliteaza cooperarea intre thread-uri in Java?
15. Ce presupune situatia de deadlock?

3.6. Teste de evaluare

1 Cate stari primare contine ciclul de viata al unui thread in 7 Care din clasele de mai jos nu e in legatura cu ierarhia de
Java: clase Input/OutputStream:
A. o stare A. File
B. cinci stari B. Writer
C. trei stari C. InputStream
D. patru stari D. Reader

2 Care dintre urmatoarele clase implementeaza un tablou 8 Ce atribute nu sunt niciodata serializate:
dinamic: A. private
A. AbstractList B. protected
B. LinkedList C. static
C. ArrayList D. public
D. AbstractSet

3 Care dintre urmatoarele reprezinta o interfata de control a 9 Care din următoarele alternative sunt adevărate?:
procesului de serializare si deserializare: A. Fiecare thread are propriul spatiu de adrese
A. Serializable (propriile variabile)
B. Externalization B. Pentru a crea un thread trebuie in mod obligatoriu
C. FileFilter sa extindem clasa Thread
D. ObjectInput C. Threadurile permit unei aplicatii sa execute mai
multe taskuri in acelasi timp
D. Pentru a crea un thread trebuie sa implementam
interfata Runnable
E. niciuna din cele de mai sus

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 85


4 Cum poate un obiect sa devina serializabil: 10 Cu ajutorul carei clase se definesc exceptiile:
A. Daca o clasa extinde clasa java.io.Serializable A. Exception
B. Daca clasa sa sau o superclasa implementeaza B. Throwable
interfata java.io.Serializable C. Abstract
C. Orice obiect e serializabil D. System
D. Nici un obiect nu e serializabil

5 Care dintre următoarele clase nu apartine pachetului java.io: 11 Care din clauzele de mai jos e executata chiar daca nu se
A. String gasesc exceptii:
B. StringReader A. throws
C. Writer B. finally
D. File C. throw
D. catch

6 Care din metodele de mai jos trebuie implementate pentru 12 Cate metode are interfata Externalizable:
interfata Runnable: A. 0
A. stop() B. 1
B. run() C. 5
C. runThread() D. 2
D. stopThread()

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 86


Programare Orientă Obiect

Modulul 3

Referințe bibliografice:

● Bruce Eckel, Thinking in Java, ed. 4-a, Prentice Hall, 2006


● Ioan SALOMIE, Tehnici de programare obiectuală, Ed. Albastră, 1996

Alexandru-Ioan Stan (Anul univ. 2018-2019) Programare Orientată Obiect 87

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