+ All Categories

Java

Date post: 05-Dec-2014
Category:
Upload: igleo89
View: 22 times
Download: 4 times
Share this document with a friend
94
1 Lucrarea 1 Concepte de bază ale programării orientate pe obiecte Cuprins Concepte fundamentale în programarea orientată pe obiecte ................................................................................. 1 Primul program Java............................................................................................................................................... 2 De la sursă la execuţie ............................................................................................................................................ 3 Temă ....................................................................................................................................................................... 5 Un program dezvoltat cu ajutorul tehnologiei obiectuale are drept unitate de construcţie nu subprogramul, ci obiectul. Un obiect înglobează date şi operaţii şi reprezintă o abstracţiune a unei entităţi din lumea reală. Obiectele componente interacţionează, determinând transfor- marea datelor de intrare în date de ieşire, adică rezolvarea problemei. Într-un program dezvoltat în manieră obiectuală NU mai există date globale (sau, în orice caz, foarte puţine), datele fiind repartizate şi înglobate în obiecte. Metoda obiectuală de dezvoltare respectă principii ale ingineriei software precum: — localizarea şi modularizarea: codul sursă corespunzător unui obiect poate fi scris şi actualizat independent de alte obiecte; ascunderea informaţiei: un obiect are o interfaţă publică pe care celelalte obiecte o pot utiliza pentru comunicare. Pe lângă interfaţa publică, un obiect poate include date si operaţii private, „ascunse” faţă de alte obiecte, şi care pot fi modificate oricând, fără a afecta restul obiectelor; reutilizarea codului: clasele de obiecte pot fi definite pe baza claselor existente, preluând automat (prin moştenire) conţinutul acestora din urmă. Concepte fundamentale în programarea orientată pe obiecte Principalele concepte care stau la baza programării orientate pe obiecte sunt: 1) Abstractizarea este una dintre căile fundamentale prin care oamenii ajung să înţeleagă şi să cuprindă complexitatea. Ea scoate în evidenţă toate detaliile semnificative pentru perspectiva din care este analizat un obiect, suprimând sau estompând toate celelalte caracteristici ale obiectului. În contextul programării orientate pe obiecte, toate acestea se transpun astfel: — obiectele şi nu algoritmii sunt blocurile logice fundamentale; — fiecare obiect este o instanţă a unei clase. Clasa este o descriere a unei mulţimi de obiecte caracterizate prin structură şi comportament similare; — clasele sunt legate între ele prin relaţii de moştenire. 2) Încapsularea este conceptul complementar abstractizării. Încapsularea este procesul de compartimentare a elementelor care formează structura şi comportamentul unei
Transcript
Page 1: Java

1

Lucrarea 1

Concepte de bază ale programării orientate pe obiecte Cuprins Concepte fundamentale în programarea orientată pe obiecte ................................................................................. 1 Primul program Java............................................................................................................................................... 2 De la sursă la execuţie ............................................................................................................................................ 3 Temă ....................................................................................................................................................................... 5

Un program dezvoltat cu ajutorul tehnologiei obiectuale are drept unitate de construcţie nu subprogramul, ci obiectul. Un obiect înglobează date şi operaţii şi reprezintă o abstracţiune a unei entităţi din lumea reală. Obiectele componente interacţionează, determinând transfor-marea datelor de intrare în date de ieşire, adică rezolvarea problemei. Într-un program dezvoltat în manieră obiectuală NU mai există date globale (sau, în orice caz, foarte puţine), datele fiind repartizate şi înglobate în obiecte. Metoda obiectuală de dezvoltare respectă principii ale ingineriei software precum:

— localizarea şi modularizarea: codul sursă corespunzător unui obiect poate fi scris şi actualizat independent de alte obiecte;

— ascunderea informaţiei: un obiect are o interfaţă publică pe care celelalte obiecte o pot utiliza pentru comunicare. Pe lângă interfaţa publică, un obiect poate include date si operaţii private, „ascunse” faţă de alte obiecte, şi care pot fi modificate oricând, fără a afecta restul obiectelor;

— reutilizarea codului: clasele de obiecte pot fi definite pe baza claselor existente, preluând automat (prin moştenire) conţinutul acestora din urmă.

Concepte fundamentale în programarea orientată pe obiecte Principalele concepte care stau la baza programării orientate pe obiecte sunt:

1) Abstractizarea este una dintre căile fundamentale prin care oamenii ajung să înţeleagă şi să cuprindă complexitatea. Ea scoate în evidenţă toate detaliile semnificative pentru perspectiva din care este analizat un obiect, suprimând sau estompând toate celelalte caracteristici ale obiectului. În contextul programării orientate pe obiecte, toate acestea se transpun astfel:

— obiectele şi nu algoritmii sunt blocurile logice fundamentale;

— fiecare obiect este o instanţă a unei clase. Clasa este o descriere a unei mulţimi de obiecte caracterizate prin structură şi comportament similare;

— clasele sunt legate între ele prin relaţii de moştenire.

2) Încapsularea este conceptul complementar abstractizării. Încapsularea este procesul de compartimentare a elementelor care formează structura şi comportamentul unei

Page 2: Java

2

abstracţiuni; încapsularea serveşte la separarea interfeţei „contractuale” de imple-mentarea acesteia. Din definiţia de mai sus rezultă că un obiect este format din două părţi distincte:

— interfaţa (protocolul);

— implementarea interfeţei.

Abstractizarea defineşte interfaţa obiectului, iar încapsularea defineşte reprezentarea (structura) obiectului împreună cu implementarea interfeţei. Separarea interfeţei unui obiect de reprezentarea lui permite modificarea reprezentării fără a afecta în vreun fel pe nici unul dintre clienţi, deoarece aceştia depind de interfaţă şi nu de reprezentarea obiectului-server.

3) Modularizarea constă în divizarea programului într-un număr de subunităţi care pot fi compilate separat, dar care sunt cuplate (conectate) între ele. Gradul de cuplaj trebuie să fie mic, pentru ca modificările aduse unui modul să afecteze cât mai puţine module. Pe de altă parte, clasele care compun un modul trebuie să aibă legături strânse între ele pentru a justifica gruparea (modul coeziv). Limbajele care suportă conceptul de modul fac distincţia între:

— interfaţa modulului, formată din elementele (tipuri, variabile, funcţii etc.) „vizibile” în celelalte module;

— implementarea sa, adică elemente vizibile doar în interiorul modulului respectiv.

4) Ierarhizarea reprezintă o ordonare a abstracţiunilor. Cele mai importante ierarhii în paradigma obiectuală sunt:

— ierarhia de clase (relaţie de tip is a);

— ierarhia de obiecte (relaţie de tip part of).

Moştenirea defineşte o relaţie între clase în care o clasă foloseşte structuri şi comportamente definite în una sau mai multe clase (după caz vorbim de moştenire simplă sau multiplă). Semantic, moştenirea indică o relaţie de tip is a („este un/o”). Ea implică o ierarhie de tip generalizare/specializare, în care clasa derivată specializează structura şi comportamentul mai general al clasei din care a fost derivată. Agregarea este relaţia între două obiecte în care unul dintre obiecte aparţine celuilalt obiect. Semantic, agregarea indică o relaţie de tip part of („parte din”).

Primul program Java class PrimulProgram{

public static void main(String[ ] arg) { System.out.print(“Hello world!”);

} }

Referindu-ne strict la structura acestui program, trebuie spus că programele Java sunt constituite ca seturi de clase (în cazul nostru avem o singură clasă). Pentru descrierea unei clase se foloseşte o construcţie sintactică de forma: class nume_clasa {

//continut clasa }

Page 3: Java

3

Aproape orice program, indiferent că este scris într-un limbaj procedural sau obiectual, are o rădăcină sau un punct de plecare. Astfel, în programele Pascal avem ceea ce se numeşte program principal, iar în C avem funcţia main(). În programele Java vom avea o clasă rădăcină care se caracterizează prin faptul că include o funcţie al cărei prototip este:

public static void main(String[ ] arg)

Elementele desemnate prin cuvintele public şi static vor fi lămurite mai târziu. Numele clasei rădăcină este un identificator dat de programator. Parametrul funcţiei main() este de tip tablou de elemente String (şiruri de caractere). Prin intermediul acestui parametru putem referi şi utiliza în program eventualele argumente specificate la momentul lansări în execuţie a programului. Acţiunile executate de programul de mai sus presupun două operaţii de afişare: print() şi println(). Prefixul System.out care însoţeşte numele celor două operaţii reprezintă numele unui obiect: este vorba despre un obiect predefinit care se utilizează atunci când destinaţia unei operaţii de scriere este ecranul monitorului. Deoarece metodele sunt incluse în clase şi, majoritatea, şi în instanţele claselor, apelul presupune precizarea numelui clasei (daca e vorba de metode statice) sau al obiectului „posesor”. Cu alte cuvinte, în Java, apelul unei operaţii se realizează folosind una din notaţiile:

nume_obiect.nume_metoda(parametri_actuali)

sau: nume_clasa.nume_metoda(parametri_actuali)

Într-un program Java vom lucra cu 2 tipuri de clase:

— clase definite de programator;

— clase predefinite, furnizate împreună cu mediul de dezvoltare Java, care formează aşa numita API (Application Programming Interface), adică interfaţa pentru programarea aplicaţiilor.

Revenind la primul nostru program Java, vom spune că metodele print()/println() pot primi ca parametru un şir de caractere dat, fie sub formă de constante, aşa ca în exemplul prezentat, fie sub formă de expresii al căror rezultat este de tip String. Metodele print()/println() mai pot primi ca parametru şi valori ale tipurilor primitive sau referinţe de clase, dar acestea sunt convertite tot la tipul String înainte de afişare. Println() poate fi apelată şi fără parametri, caz în care execută doar un salt la linie nouă: System.out.println( );

De la sursă la execuţie Programul se editează cu un editor de text ASCII simplu (Notepad, editorul programului NortonCommander sau chiar editoarele unor medii integrate de dezvoltare cum sunt cele din gama Borland). Sursa se salvează într-un fişier care va avea OBLIGATORIU extensia .java. De exemplu:

lucrarea1.java.

După editare, programul se compilează cu comanda: javac nume_fisier_sursa.java

de exemplu:

Page 4: Java

4

javac lucrarea1.java

În urma compilării vor rezulta un număr de fişiere egal cu numărul claselor conţinute de sursă. Fiecare dintre fişierele rezultate are extensia .class şi numele identic cu numele clasei căreia îi corespunde. În cazul nostru, va rezulta un fişier cu numele PrimulProgram.class. Un fişier .class conţine cod maşină virtual (Java Byte Code). Acesta este codul maşină al unui calculator imaginar. Pentru a putea fi executat pe o maşină reală, este necesar un interpretor (sau executiv) care să execute fiecare instrucţiune a codului virtual în termenii operaţiilor maşină ai calculatorului real. După compilare, programul în cod virtual obţinut poate fi transportat pe orice maşină pentru care există executivul corespunzător, iar scrierea unui executiv este mult mai uşoară decât a unui compilator întreg.

Lansarea in execuţie a interpretorului se face cu comanda: java nume_clasa_radacina

Argumentul dat executivului Java la apel este numele unui fişier .class, acela corespunzător clasei rădăcină (cea care conţine metoda main). Pentru exemplul nostru, comanda de execuţie va fi:

java PrimulProgram

Am afirmat în paragraful anterior că parametrul metodei main din clasa rădăcină serveşte la accesarea eventualelor argumente date în linia de comandă la lansarea în execuţie a programului. De exemplu, se consideră un program care face prezentarea unui student, împreună cu numele şi prenumele studentului, date ca argumente: class Prezentare{

public static void main(String[ ] arg) { System.out.println(“Sunt studentul “+arg[0]+” “+arg[1]

+” “+”din anul V”); } }

După compilare, programul poate fi lansat în execuţie cu comanda: java Prezentare Dan Popescu

Programul a primit două argumente (Dan şi Popescu). Acestea sunt interpretate ca primele două elemente ale tabloului arg ce figurează ca parametru formal al metodei main() La fel ca şi în C, în Java elementele unui tablou se indexează începând cu 0.

Dacă programului nu i se furnizează cele 2 argumente, rezultatul este oprirea execuţiei cu un mesaj prin care se reclamă depăşirea indicelui de tablou (IndexOutOfBoundsException). Este vorba de tabloul arg care are dimensiunea 0 în cazul lipsei argumentelor. De aceea, e bine să prevedem secvenţe de verificare în program, prin care să testăm dacă utilizatorul a furnizat numărul necesar de parametri: class Prezentare{

public static void main(String[ ] arg) { if (arg.length < 2)

System.out.println(“Număr impropriu de parame-tri!!”);

else System.out.println(“Sunt studentul “+ arg[0] +”

“+arg[1] +” “+”din anul V”); }

Page 5: Java

5

}

Expresia arg.length utilizată în secvenţa de mai sus determină numărul de elemente ale tabloului arg. O altă observaţie este legată de parametrul funcţiei println(). De data acesta am folosit o expresie construită prin aplicarea operatorului „+” asupra unor operanzi de tip String, al cărei rezultat este un şir de caractere obţinut prin concatenarea operanzilor. Spre deosebire de funcţiile printf() din C, care acceptau un număr variabil de parametri, funcţiile print()/ println() din Java acceptă UN SINGUR parametru. Ca urmare, atunci când dorim să scriem printr-o singură instrucţiune mai multe valori, trebuie să le concatenăm, spre a forma un singur şir.

Temă Se cere să se scrie un program Java care să calculeze şi să afişeze perimetru şi aria unui dreptunghi. Valorile pentru lungime şi lăţime se trimit ca parametri la apel.

Indicaţie:

Nu se pot transmite la apel decât parametri de tipul String. Ca urmare, pentru a putea efectua calcule, valorile transmise trebuie convertite de la String la Integer sau Double. Conversia se face astfel: Type x = Type.valueOf(arg[i])

Page 6: Java

1

Lucrarea 2

Componenţa unei clase. Crearea şi iniţializarea obiectelor Cuprins Componenţa unei clase........................................................................................................................................... 1 Crearea obiectelor................................................................................................................................................... 2 Modificatori de acces.............................................................................................................................................. 2 Iniţializarea câmpurilor unui obiect. Constructori .................................................................................................. 3 Membri statici ai claselor........................................................................................................................................ 5 Temă ....................................................................................................................................................................... 6

Componenţa unei clase În limbajele de programare orientate pe obiecte, clasele pot fi considerate ca fiind mecanisme prin care programatorul îşi construieşte propriile sale tipuri de date, pe care, apoi, le va folosi aproape la fel cum foloseşte tipurile predefinite. Fiind un tip de date, unul dintre rolurile fundamentale ale clasei este acela de a servi la declararea variabilelor. Valorile unui tip clasă se numesc obiecte sau instanţe ale clasei respective.

O clasă reprezintă descrierea unei mulţimi de obiecte care au aceeaşi structură şi acelaşi comportament. Prin urmare, o clasă va trebui să conţină definiţiile datelor şi ale operaţiilor ce caracterizează obiectele acelei clase. Datele definite într-o clasă se mai numesc date-membru, variabile-membru, atribute sau câmpuri, iar operaţiile se mai numesc metode sau funcţii membru. Pentru a arăta felul în care se definesc membrii unei clase, va fi utilizat următorul exemplu: class Punct{ private int x; private int y; public void init(int xx, int yy) { x = xx; y = yy; } public void move(int dx, int dy) { x += dx; y += dy; } public int getX( ) { return x; } public int getY( ) { return y; } }

Clasa modelează un punct şi are ca date-membru două variabile de tip întreg: x şi y, reprezentând coordonatele punctului, iar ca metode:

— funcţia init(), iniţializează coordonatele unui punct

Page 7: Java

2

— funcţia move(), deplasează un punct pe o anumită distanţă

— funcţiile getX() şi getY(), returnează coordonatele curente ale unui punct.

Crearea obiectelor În continuare se va considera un mic program Java pentru a vedea cum se creează obiectele clasei Punct descrisă anterior: class Punct{ private int x; private int y; public void init(int xx, int yy) { x = xx; y = yy; } public void move(int dx, int dy) { x += dx; y += dy; } public int getX( ) { return x; } public int getY( ) { return y; } } class ClientPunct { public static void main(String[ ] arg) { Punct p1 = new Punct( ); //se creează o instanţă a

clasei Punct Punct p2 = new Punct( ); // şi încă una p1.init (10,20); p2.init (30,40); //se apelează

metodele init ale instanţelor p1.move(5,5); p2.move(6,-2); //se apelează metodele

move System.out.println(“(x1,y1) = (“+p1.getX()+”,”+

p1.getY( )+”)”); //se afişează coordonatele curente ale primului punct

System.out.println(“(x2,y2) = (“+p2.getX()+”,”+ p2.getY( )+”)”); //se afişează coordonatele curente ale celui de-al 2-lea punct

} }

În Java toate obiectele se creează în mod dinamic. Până nu se iniţializează variabila respectivă cu adresa unei zone de memorie alocată pentru valori ale tipului indicat de pointer, practic nu se poate folosi variabila în nici un fel. În programul de mai sus se observă aplicarea operatorului de alocare dinamică new.

Modificatori de acces Se poate observa că definiţiile datelor şi ale funcţiilor din clasa Punct sunt prefixate de anumite cuvinte cheie (public, private) care se numesc modificatori de acces. Aceştia

Page 8: Java

3

stabilesc drepturile de acces ale clienţilor la membrii unei clase. Când se discută despre drepturile de acces la membrii unei clase trebuie să se abordeze acest subiect din două perspective:

— Interiorul clasei sau, mai concret, metodele clasei. În cadrul metodelor unei clase există acces nerestrictiv la toţi membrii, date sau funcţii. De exemplu, în metodele clasei Punct se face referire la câmpurile x şi y. În interiorul clasei nu se foloseşte notaţia cu punct pentru a referi membrii, aceştia fiind pur si simplu accesaţi prin numele lor. Când o metodă face referire la alţi membri ai clasei, de fapt sunt accesaţi membrii corespunzători ai obiectului receptor, indiferent care ar fi el. De exemplu, când se apelează metoda init() a obiectului referit de p1, are loc iniţializarea membrilor x şi y ai acelui obiect. În legătură cu accesul din interiorul unei clase, trebuie spus că absenţa restricţiilor se aplică şi dacă este vorba despre membrii altui obiect din aceeaşi clasă, dar diferit de cel receptor. De exemplu, dacă în clasa Punct am avea câte o metodă de calcul a distanţei pe verticală/orizontală dintre 2 puncte, unul fiind obiectul receptor, iar celălalt un obiect dat ca parametru, atunci am putea scrie:

class Punct{ //. . . public int distV(Punct p) { return y - p.y; } public int distH(Punct p) { return x - p.x; } //. . . }

Se observă că din interiorul metodelor distV()/distH() putem accesa liber membrii privaţi ai obiectului p dat ca parametru. La fel ar sta lucrurile şi dacă p ar fi o variabilă locală a unei metode din clasa Punct.

— Exteriorul sau clienţii clasei. Clienţii unei clase pot accesa doar acei membri care au ca modificator de acces cuvântul public. Membrii declaraţi cu modificatorul private NU sunt vizibili în afară, sunt ascunşi. Dacă s-ar încerca folosirea, în metoda main() din exemplul considerat, o referinţă de genul:

p1.x

compilatorul ar raporta o eroare. Structura unei clase, sau modul ei de reprezentare, care este dat de variabilele membru, de regulă se ascunde faţă de clienţi. Dacă este necesar ca aceştia să poată consulta valorile datelor membru, se va opta pentru definirea unor metode de genul getValoare(), iar nu pentru declararea ca publice a datelor respective.

Iniţializarea câmpurilor unui obiect. Constructori În exemplul considerat anterior, după crearea obiectelor p1 şi p2, s-a apelat pentru ele metoda init() care avea ca scop iniţializarea câmpurilor x şi y pentru cele două puncte. Acesta este un exemplu de iniţializare a câmpurilor unui obiect, însă are unele dezavantaje printre care şi acela că programatorul ar putea omite apelul metodei, caz în care câmpurile vor avea valori implicite. În acest sens, programatorul poate prevedea la definirea unei clase

Page 9: Java

4

una sau mai multe metode speciale, numite constructori, care servesc tocmai la iniţializarea datelor unui obiect imediat ce el a fost creat. Exemplul următor, descris tot folosind clasa Punct, va reliefa acest lucru: class Punct{ //. . . public Punct(int xx, int yy) { x = xx; y = yy; } public Punct(Punct p) { x = p.x; y = p.y; } //. . . }

Caracteristicile unui constructor sunt:

— este o metodă care are acelaşi nume cu clasa în care este definită

— nu are tip returnat (nici măcar void)

— se apelează în mod automat la crearea unui obiect

Astfel, în locul metodei init(), în funcţia main() a clasei ClientPunct, se poate scrie: class ClientPunct{ public static void main(String[ ] arg) { Punct p1 = new Punct(10,20 ); //se creează o instanţă

a clasei Punct Punct p2 = new Punct(30,40 ); // şi încă una //. . . } }

O clasă poate avea mai mulţi constructori, datorită facilităţii de supraîncărcare a funcţiilor existentă în limbajul Java. Se oferă, astfel, posibilitatea de a defini, în acelaşi domeniu de vizibilitate, mai multe funcţii care au acelaşi nume, dar parametrii diferiţi ca tip şi număr.

Al doilea constructor definit anterior poate fi apelat astfel: Punct p3 = new Punct(p1);

Dacă programatorul nu prevede nici un constructor, atunci compilatorul va prevedea clasa respectivă cu un constructor implicit de tip no-arg, al cărui corp de instrucţiuni este vid. Aşa au stat lucrurile în cazul primei definiţii a clasei Punct când la crearea unui obiect a fost apelat constructorul implicit Punct(). De asemenea, pentru a putea crea obiecte ale claselor descrise, constructorii trebuie să aibă modificatorul de acces public.

Pe lângă constructori, mai există şi alte metode de iniţializare a câmpurilor unei clase:

— Iniţializatori la declarare, care pot fi impliciţi sau expliciţi: class Punct{ private int x; //iniţializator implicit private int y = 1; //iniţializator explicit //se presupune că nu există constructori }

Page 10: Java

5

//. . . Punct p1 = new Punct(); //în acest caz p1.x = 0 si

p1.y=1

Iniţializatorii impliciţi depind de tipul câmpului respectiv: sunt 0 pentru tipurile numerice, false pentru tipul boolean şi null pentru referinţe la obiecte.

— Blocuri de iniţializare: sunt secvenţe de cod cuprinse între acolade, plasate imediat după declaraţia câmpului pe care îl iniţializează, şi care pot fi asimilate cu nişte constructori no-arg. Ele se folosesc atunci când valoarea de iniţializare a unui câmp nu este o constantă simplă, ci trebuie obţinută prin calcule. Ca exemplu, se va presupune că variabila x din clasa Punct trebuie iniţializată cu valoarea sumei primelor 6 numere Fibonacci: class Punct{ private int x=2; //iniţializator explicit { //bloc de iniţializare int a=1,b=1;

for(int i=3;i <= 6;i++) {

b+=a; a=b-a; x+=b; } //. . . }

Deosebirea esenţială dintre constructori şi celelalte tipuri de iniţializări este aceea că, în cazul constructorilor, se putea iniţializa fiecare instanţă cu valori diferite care se transmit ca parametri. Celelalte mecanisme de iniţializare presupun ca toate instanţele clasei respective să aibă aceleaşi valori. Dacă se doreşte crearea şi iniţializarea unui câmp cu comportare echivalentă unei constante din limbajul C++, atunci câmpul este marcat ca final, cuvânt cheie plasat între modificatorul de acces şi tip.

Membri statici ai claselor Până acum, în clasa Punct s-au definit doar membri non-statici. Aceştia se mai numesc şi membri ai instanţelor. Limbajul Java permite definirea unei categorii speciale de membri, numiţi statici sau membri de clasă. Aceşti membri vor exista în exemplare unice pentru fiecare clasă, fiind accesaţi în comun de toate instanţele clasei respective. Mai mult, membrii statici pot fi referiţi chiar şi fără a instanţia clasa, ei nedepinzând de obiecte. Pentru a defini un membru static se utilizează cuvântul cheie static, plasat după modificatorul de acces: modificator_acces static tip_membru nume_membru;

Referirea unui membru static nu se va face prin intermediul numelui obiectului, ci prin intermediul numelui clasei: nume_clasa.nume_membru_static

Pentru exemplificare, se va modifica clasa Punct astfel încât să se poată calcula numărul de apeluri ale metodei move() pentru toate obiectele create într-un program. Pentru aceasta, se va defini o variabilă statică contor şi o metodă statică getContor(): class Punct{

Page 11: Java

6

private int x; private int y; private static int contor=0; //iniţializator implicit public Punct(int xx,int yy ) { x=xx; y=yy; } public void move(int dx, int dy) { x+=xx; y+=yy; contor++;} public static int getContor() {return contor;} //. . . } //metoda main din clasa rădăcină Punct p1 = new Punct(10,20); Punct p2 = new Punct(15,13); p1.move(8,-2); p2.move(6,7); //. . . System.out.println(“S-au executat “+Punct.getContor()+” mutari.”);

În legătură cu membrii statici ai unei clase trebuie făcută următoarea observaţie: într-o metodă statică nu este permisă referirea simplă a unui membru non-static. Acest lucru se explică prin aceea că un membru non-static există doar în interiorul unui obiect, pe când membrii statici există independent de obiecte. Dacă în exemplul dat s-ar fi declarat variabila contor ca non-statică, atunci ea ar fi existat în atâtea exemplare, câte obiecte s-ar fi creat şi ar fi contorizat pentru fiecare obiect în parte numărul de apeluri ale metodei move().

Temă Funcţia cxbxaxf +⋅+⋅= 2)( are ca grafic o parabolă cu vârful de coordonate

⎟⎟⎠

⎞⎜⎜⎝

⎛ +−−

aacb

ab

44;

2

2

.

Se cere să se definească o clasă Parabola ai cărei membri vor fi:

— 3 variabile de tip double care reprezintă coeficienţii a, b şi c

— un constructor cu 3 parametrii de tip double

— un constructor cu un parametru de tip Parabola

— o metodă de afişare a funcţiei sub forma: f(x) = a x^2 + b x + c

— o metodă pentru calculul coordonatelor vârfului

— o metodă statică ce primeşte ca parametri două parabole şi calculează coordonatele mijlocului dreptei care uneşte vârfurile celor două parabole astfel:

2,

22121 yyyxxx +

=+

= , unde ( 1x , 1y ) sunt coordonatele vârfului primei

parabole, iar ( 2x , 2y ) descriu vârful celei de a doua parabole.

Pe lângă clasa Parabola, se va mai defini o clasă ClientParabola care va exemplifica utilizarea metodelor clasei Parabola.

Page 12: Java

1

Lucrarea 3

Gestionarea memoriei dinamice. Tablouri şi şiruri de caractere. Conversii de date. Operaţii de intrare / ieşire

Cuprins Simbolul this.............................................................................................................................................1 Colectorul de reziduuri (Garbage Collector) ...........................................................................................4 Transmiterea parametrilor ........................................................................................................................4 Tablouri în Java ........................................................................................................................................4 Clasa String ..............................................................................................................................................5 Obiecte String şi tablouri de caractere......................................................................................................6 Compararea obiectelor..............................................................................................................................6 Compararea stringurilor............................................................................................................................7 Conversii între tipul String şi tipurile primitive........................................................................................8 Clase înfăşurătoare ...................................................................................................................................9 Operaţii de intrare/ieşire la nivel de linii de caractere ............................................................................11

Operaţii de citire a liniilor de text.......................................................................................................11 Operaţii de scriere a liniilor de text ....................................................................................................12

Temă.......................................................................................................................................................14

În Java, variabilele al căror tip este o clasă sunt reprezentate ca referinţe (adrese) spre obiecte ale clasei. Pentru a putea iniţializa o asemenea referinţă, trebuie generat un obiect nou sau folosită o referinţă la un obiect deja existent. Unei referinţe îi poate fi atribuită valoarea specială null, care se traduce prin faptul că referinţa respectivă nu indică nici un obiect. Valoarea null nu este atribuită automat tuturor variabilelor referinţă la declararea lor, ci conform următoarei reguli: dacă referinţa este o dată-membru a unei clase şi ea nu este iniţializată în nici un fel, la crearea unui obiect al clasei respective referinţa va primi implicit valoarea null. Dacă referinţa este o variabilă locală a unei metode, iniţializarea implicită nu mai funcţionează. De aceea, se recomandă ca programatorul să realizeze ÎNTOTDEAUNA o iniţializare explicită a variabilelor.

Simbolul this Simbolul this este o referinţă care poate fi utilizată doar în cadrul funcţiilor membru non-statice ale unei clase. Din punctul de vedere al unei funcţii membru, acesta este referinţa spre obiectul receptor, „posesor“ al acelei funcţii. Se poate spune că this reprezintă „conştiinţa de sine“ a unui obiect, în sensul că obiectul îşi cunoaşte adresa la care este localizat în memorie. Practic, orice referire a unei date membru non-statice v în interiorul unei metode, poate fi considerată ca echivalentă cu this.v. De exemplu, metoda move() din clasa Punct, definită în lucrarea precedentă, poate fi scrisă sub forma: class Punct {

Page 13: Java

2

//. . . public void move(int dx, int dy) { this.x += dx; this.y += dy; } }

Referinţa this este folosită explicit în cazul în care ar putea exista conflicte de nume cu datele membru ale unui obiect. De exemplu: class Punct { //. . . public Punct(int x, int y){ // parametrii constructorului au nume // identice cu cele ale datelor membru this.x = x; this.y = y; } }

De asemenea, referinţa this se foloseşte atunci când metoda trebuie să returneze o referinţă la obiectul ei receptor: class Rational { // clasa care modelează lucrul cu numere raţionale private int numitor=1; private int numarator=0; //. . . public Rational adauga(Rational q) {

numarator = numarator * q.numitor + numitor * q.numarator;

numitor *= q.numitor; return this; } public Rational adauga(int n) { // se supraîncarcă funcţia adauga numarator += n*numitor; return this; } } // exemplu de utilizare a clasei class ClientRational { public static void main(String[ ] arg){ Rational a = new Rational(); Rational b = new Rational();

a.adauga(5).adauga(6); // putem apela metoda adauga în cascadă,

b.adauga(3).adauga(a);// deoarece ea returnează o referinţă la un obiect Rational

} }

Page 14: Java

3

O altă situaţie când se foloseşte explicit referinţa this este atunci când referinţa la obiectul receptor trebuie transmisă ca parametru la apelul unei alte metode. class Context { private int x; private Algoritm a;

// obiectul Algoritm va executa anumite calcule pentru un obiect Context

public Context(Algoritm a, int x) { this.a = a; this.x = x; } public int Calcul() { x = a.Calcul(this); return x; } public int getX() { return x; } } class Algoritm { public int Calcul(Context c) { return c.getX()*c.getX(); } }

În fine, simbolul this reprezintă mijlocul prin care poate fi apelat un constructor al unei clase din interiorul altui constructor al aceleiaşi clase. class OClasa { //. . . public OClasa(int x, int y) { this.x = x; this.y = y; } public OClasa(OClasa p) { this(p.x, p.y); // se apelează constructorul cu

2 parametri int } }

Apelul unui constructor din interiorul altui constructor al clasei în cauză NU înseamnă crearea unui nou obiect, ci, pentru obiectul receptor curent, se va executa codul constructorului apelat, la fel ca în cazul apelării unei funcţii-membru obişnuite. De asemenea, un constructor nu poate fi apelat cu ajutorul simbolului this DECÂT din interiorul altui constructor şi nu al altor metode. În felul acesta se asigură îndeplinirea condiţiei ca pentru un obiect iniţializarea datelor prin constructor să se execute O SINGURĂ dată. Referinţa this NU poate fi utilizată în interiorul unei funcţii-membru statice. Referinta this nu poate fi modificată (nu poate să apară ca membru stâng într-o atribuire) şi nu poate să apară în afara corpului de instrucţiuni al metodelor non-statice ale unei clase sau în afara blocurilor de iniţializare asociate cu variabilele membru ale unei clase.

Page 15: Java

4

Colectorul de reziduuri (Garbage Collector) Spre deosebire de limbaje precum Pascal sau C, unde programatorul trebuie să elibereze memoria ocupată de obiecte după ce acestea nu mai sunt folosite, în Java programatorul este scutit de această sarcină de care se ocupă o componentă a maşinii virtuale numită Garbage Collector. Principiul de lucru al acestuia este următorul: dacă spre un anumit obiect nu mai există nici o referinţă externă, în nici o funcţie activă, acel obiect devine candidat la eliminarea din memorie.

Pentru utilizator, acţiunea Garbage Collector-ului este transparentă. Există posibili-tatea de a „surprinde“ momentul în care un anumit obiect este eliminat, deoarece Garbage Collector-ul nu acţionează „fără preaviz“. Astfel, dacă în clasa de care aparţine obiectul este definită o metodă numită finalize(), cu prototipul: protected void finalize() throws Throwable;

această metodă va fi executată chiar înainte ca obiectul să dispară. Dacă metoda este definită astfel încât să tipărească pe ecran un anumit mesaj, poate fi detectat momentul în care un obiect este eliminat din memorie.

Transmiterea parametrilor În Java, transmiterea parametrilor la apelul metodelor se face NUMAI prin VALOARE. Modificările aduse parametrilor în interiorul unei funcţii nu se păstrează la revenirea din funcţia respectivă. Dacă unul dintre parametrii unei funcţii are drept tip o clasă, atunci, la apel, funcţia va primi referinţa unui obiect al clasei. În cazul acesta, ceea ce se transmite prin valoare este chiar referinţa, NU obiectul indicat de ea. Funcţia NU VA PUTEA MODIFICA REFERINŢA respectivă, dar VA PUTEA MODIFICA OBIECTUL indicat de ea.

Tablouri în Java Limbajul Java oferă tipul tablou (tip predefinit), cu ajutorul căruia se pot crea colecţii de variabile de acelaşi tip. Componentele tabloului pot fi accesate cu ajutorul unui index. Acesta trebuie să aparţină unui tip întreg, iar numerotarea elementelor într-un tablou începe întotdeauna de la 0, la fel ca şi în C. Un tablou se declară astfel: tip_element[ ] nume_tablou;

În Java, elementele unui tablou se alocă DINAMIC, specificându-se dimensiunea tabloului, iar numele unui tablou este considerat ca o REFERINŢĂ DE OBIECT: tip_element[ ] nume_tablou = new tip_element [numar_ele-

mente];

La crearea unui tablou se memorează dimensiunile sale, programatorul putând în orice moment să le cunoască apelând la atributului length: static void oFunctie(int[ ] tabp) { //. . . for(int i=0; i < tabp.length; i++) { // tabp.length returnează dimensiunea tabloului // prelucrează tabp[ i ] }

Page 16: Java

5

} public static void main(String[ ] arg) { int [ ] tab = new int[10]; //. . . oFunctie(tab); //. . . }

Dacă elementele unui tablou sunt, la rândul lor, de tip clasă, elementele respective vor fi referinţe la obiecte ale acelei clase. La crearea tabloului se rezervă spaţiu DOAR pentru acele referinţe şi ele sunt iniţializate cu valoarea null.

Java acceptă şi declaraţii de tablouri bidimensionale: tip_element[ ] [ ] nume_matrice;

Clasa String Clasa String este una dintre clasele predefinite, fundamentale, care modelează lucrul cu şiruri de caractere. Modul cel mai simplu de manipulare a obiectelor de tip String îl constituie constantele String, care sunt şiruri de caractere încadrate de ghilimele. Orice referire a unei asemenea constante în cadrul unui program Java are ca efect crearea unui obiect String care conţine secvenţa de caractere respectivă.

Clasa String are mai mulţi constructori:

— un constructor care are ca parametru o referinţă la String. Noul obiect creat va avea acelaşi conţinut ca şi obiectul indicat de parametru:

String s1 = "un sir";

String s2 = new String(s1);

— un constructor no-arg, care crează un şir vid: String svid = new String( );

— un constructor care are ca parametru un tablou cu elemente de tip char: char[ ] tc = {'a','b','c'};

String stc = new String(tc);

Clasa String oferă un set de metode pentru prelucrarea unor şiruri existente: String toUpperCase();

String toLowerCase();

String substring(int);

String substring(int, int);

Metodele clasei String includ şi operaţii care permit consultarea şirurilor de caractere, respectiv, obţinerea de informaţii despre conţinutul lor: int length();

char charAt(int);

int indexOf(char);

Page 17: Java

6

int indexOf(String);

int lastIndexOf(char);

int lastIndexOf(String);

O proprietate foarte importantă a metodelor clasei String este aceea că ele nu modifică NICIODATĂ obiectul receptor, ci creează obiecte noi în care este inclus şirul de caractere modificat. În Java, operaţia de concatenare se realizează cu ajutorul operatorului „+“ . Va fi creat unu obiect nou, al cărui conţinut va fi şirul rezultat prin concatenare. String s1 = "codul ";

String s2 = "penal";

String s3 = s1 + s2;

Operaţia de concatenare suportă următoarele precizări:

— clasa String este singura asupra căreia se poate aplica operatorul „+“. Tipurile primitive numerice şi tipul char mai au definit acest operator (cu altă semnificaţie);

— într-o expresie de concatenare pot să apară şi valori ale tipurilor primitive, acestea fiind automat convertite la String;

String s2 = "x = " +8.5;

— într-o expresie de concatenare pot să apară ca operanzi şi obiecte ale altor clase decât String, cu condiţia ca în clasele respective să fie definită metoda toString care să genereze obiecte String.

Obiecte String şi tablouri de caractere Deşi, aparent, un şir de caractere şi un tablou de caractere (tipul char[ ]) modelează acelaşi lucru (o secvenţă de caractere), cele două tipuri se comportă diferit. Unui obiect String nu i se poate modifica conţinutul, unui tablou de caractere i se poate modifica oricare element.

Pentru a obţine caracterul aflat la o anumită poziţie pos într-un String, s, nu putem folosi notaţia s[pos] ca pentru tablouri, ci trebuie utilizată metoda charAt(pos).

Metoda toCharArray() creează un tablou ale cărui elemente sunt caracterele şirului de caractere sursă. Operaţia inversă se realizează cu ajutorul unui constructor al clasei String.

Compararea obiectelor Una dintre operaţiile cele mai frecvente de comparare a obiectelor este testarea egalităţii. În Java această operaţie poate avea două sensuri:

— testarea identităţii: în acest caz ceea ce se compară sunt două referinţe de obiect, iar testul presupune a verifica dacă cele două referinţe indică unul

Page 18: Java

7

şi acelaşi obiect. Testarea identităţii se realizează cu ajutorul operatorului ==;

— testarea echivalenţei: în acest caz operanzii sunt două obiecte propriu-zise şi testul presupune a verifica dacă cele două obiecte au conţinut asemănător. De regulă, pentru testarea echivalentei se utilizează o metodă denumită equals(), operanzii fiind un obiect receptor şi unul dat ca parametru metodei. Această metodă este definită în clasa Object şi este moştenită de toate clasele.

Definiţia metodei equals(), aşa cum apare ea în clasa Object, pentru majoritatea claselor utilizatorului nu este satisfăcătoare. Ca urmare, programatorul va trebui să-şi definească propriile variante de equals(), acolo unde este cazul. Pentru clasa Punct, o posibilă implementare a metodei equals() ar fi: class Punct{ //. . . public boolean equals(Punct p) { return ((x==p.x) && (y==p.y)); } } class ClientPunct {

public void oMetoda( ) { Punct p1 = new Punct(1,2); Punct p2 = new Punct(1,2); Punct p3 = p1;

boolean t1 = (p1==p2); //test de identitate;

rezultat: false boolean t2 = p1.equals(p2); //test de

echivalenţă; rezultat: true boolean t3 = (p1==p3); //test de identitate;

rezultat: true //. . .

} }

Compararea stringurilor Regulile prezentate mai sus se aplică şi asupra clasei String. Ca atare, se va utiliza operatorul == pentru a testa identitatea obiectelor String şi metoda equals() pentru a testa egalitatea şirurilor de caractere conţinute de obiectele String. class oClasa {

public void oMetoda( ) { String s1 = "ababu"; String s2 = "ababu"; Punct s3 = s1; boolean t1 = (s1==s2); //test de identitate;

rezultat: false boolean t2 = s1.equals(s2); //test de

echivalenţă; rezultat: true

Page 19: Java

8

boolean t3 = (s1==s3); //test de identitate; rezultat: true

//. . . }

}

Se observă că referinţele s1 şi s2 indică obiecte distincte, deşi ele au fost iniţializate cu acelaşi şir de caractere. Acest lucru este o consecinţă a faptului că în Java o constantă String este echivalentă cu new String(constantă), ceea ce determină de fiecare dată crearea unui nou obiect.

Pe lângă metoda equals(), clasa String mai conţine şi alte metode de comparare:

— metoda equalsIgnoreCase(), care este similară cu equals(), dar la comparare nu diferenţiază literele mari de cele mici.

— metoda compareTo(), care primeşte ca parametru o referinţă String şi returnează ca rezultat un număr întreg a cărui valoare este:

– negativă, dacă şirul din obiectul receptor este mai mic decât şirul din obiectul parametru;

– nulă, dacă cele două şiruri sunt egale;

– pozitivă dacă şirul din obiectul receptor este mai mare decât şirul din obiectul parametru;

Metoda compareTo() poate fi folosită pentru realizarea sortării alfabetice a şirurilor de caractere.

Conversii între tipul String şi tipurile primitive Într-un program Java, conversiile între tipul String şi tipurile primitive sunt foarte necesare, în primul rând pentru că datele de intrare ale programului sunt receptate în majoritatea cazurilor sub formă de şiruri de caractere, deoarece în Java nu există ceea ce în C se numea funcţie de citire formatată (scanf). În ceea ce priveşte conversiile, în Java există o regulă care spune că întotdeauna tipul spre care se face conversia trebuie să aibă metoda necesară conversiei. În acest sens, clasa String este dotată cu metode numite valueOf() care asigură conversiile de la tipurile primitive spre String. class oClasa {

public void oMetoda( ) { int a = 4; double b = 13.2; boolean c = true; String s1 = String.valueOf(a); String s2 = String.valueOf(b); String s3 = String.valueOf(c); //. . .

} }

Page 20: Java

9

valueOf() este o metodă statică, supraîncărcată astfel încât să admită ca parametri valori ale tuturor tipurilor primitive. Efectul metodei este crearea unui obiect String al cărui conţinut este obţinut prin conversia la şir de caractere a valorii parametrului. Ca rezultat, metoda returnează o referinţă la obiectul String creat. Pentru a realiza conversiile în sens invers, adică de la String spre tipurile primitive se utilizează metode ale unor clase speciale, numite clase înfăşurătoare (wrapper classes).

Clase înfăşurătoare Tuturor tipurilor primitive din limbajul Java le corespunde câte o clasă înfăşurătoare. Aceste clase au fost definite din două motive:

— să înglobeze metode de conversie între valori ale tipurilor primitive şi obiecte ale altor clase, precum şi constante speciale legate de tipurile primare;

— să permită crearea de obiecte care să conţină valori ale tipurilor primare, şi care apoi să poată fi utilizate în cadrul unor structuri unde nu pot să apară decât obiecte, nu date simple.

Clasele înfăşurătoare sunt definite în pachetul java.lang şi ele sunt:

Integer pentru int

Short pentru short

Byte pentru byte

Long pentru long

Float pentru float

Double pentru double

Boolean pentru boolean

Character pentru char

Void pentru void

Exceptând clasa Void, restul claselor înfăşurătoare conţin următoarele metode:

— câte un constructor care acceptă ca parametru o valoare a tipului primitiv corespondent. Integer a = new Integer(5);

Double b = new Double(12.13);

— câte un constructor care acceptă ca parametru o referinţă String (excepţie face clasa Character care nu are acest constructor). Se presupune că şirul respectiv conţine o secvenţă compatibilă cu tipul primitiv corespondent. Integer a = new Integer("5");

Boolean b = new Boolean("true");

— o metodă statică valueOf() care acceptă ca parametru un String şi returnează un obiect al clasei înfăşurătoare, convertind conţinutul şirului;

Page 21: Java

10

apelul acestei metode este echivalent cu a executa un new cu constructorul descris mai înainte. Integer a = Integer.valueOf("5");

Double b = Double.valueOf("-8.12");

— o metodă toString() care realizează practic conversia de la clasa înfăşurătoare la String

De multe ori este necesar ca datele conţinute de obiecte să fie convertite la şiruri de caractere în vederea participării obiectelor respective ca operanzi în expresii de manipulare a şirurilor. De exemplu, funcţiile print()/println() necesită ca parametru o referinţă String. Convertind un obiect la String, am putea să afişăm acel obiect cu ajutorul funcţiilor print()/println(). Programatorul trebuie să-si definească în clasele sale câte o metodă toString(), dacă doreşte să convertească obiectele respective la şiruri. class Punct { //. . . public String toString( ) {

String s = "("+String.valueOf(x)+","+ String.valueOf(y)+")";

return s; } }

— o metodă typeValue(), unde type este numele tipului primitiv corespondent; această metodă returnează conţinutul obiectului ca valoare a tipului primitiv. Integer a = new Integer("5");

int x = a.intValue( );

Double b = new Double("3.14");

double y = b.doubleValue( );

— o metodă equals() care realizează testul echivalenţei obiectelor.

— o metodă compareTo() care acceptă ca parametru un obiect al aceleiaşi clase înfăşurătoare ca şi obiectul receptor. Metoda returnează o valoare de tip int pozitivă, nulă sau negativă după cum conţinutul obiectului receptor este mai mare, egal sau mai mic decât cel al parametrului. Integer a = new Integer(7);

Integer b = new Integer(10);

int x = b.compareTo(a); //val. x va fi pozitivă

Pentru conversiile de la String la tipurile primitive se utilizează metode statice ale claselor înfăşurătoare, si anume:

— în cazul claselor care reprezintă tipuri numerice există metodele parseType(), unde Type este numele tipului primitiv corespondent scris cu prima literă majusculă; metoda acceptă ca parametru şirul care

Page 22: Java

11

trebuie convertit şi returnează valoarea corespunzătoare, în reprezentarea tipului primitiv respectiv;

— în cazul tipului boolean exista metoda getBoolean() care funcţionează similar metodei parseType().

În cazul tipurilor numerice, dacă şirul care se converteşte nu conţine o secvenţă în concordanţă cu tipul primitiv spre care se face conversia, programul va fi întrerupt cu un mesaj de eroare corespunzător (excepţie NumberFormatException).

Operaţii de intrare/ieşire la nivel de linii de caractere Pachetul java.io este un set de clase cu ajutorul cărora se realizează operaţiile de intrare/ieşire într-un program Java. În Java, operaţiile de intrare/ieşire se bazează pe conceptul de flux de date (stream). Un flux de date este o secvenţă de date care se deplasează dinspre o sursă externă spre memorie (flux de intrare — input stream) sau din memorie spre o destinaţie externă (flux de ieşire — output stream). Pentru operaţiile de intrare/ieşire cea mai frecventă sursă externă este tastatura, iar destinaţia este ecranul monitorului. Acestea se mai numesc şi suporturi standard de intrare, respectiv ieşire. Corespunzător suporturilor standard, în Java exista două obiecte predefinite: System.in pentru tastatură şi System.out pentru monitor.

O linie de caractere este o secvenţă terminată cu un caracter special, numit terminator de linie, care, la tastatură, are drept corespondent tasta Enter. Pentru a realiza citiri la nivel de linie vor fi utilizate două subclase ale clasei Reader (BufferReader şi InputStreamReader). Pentru scrierea de linii, se va folosi clasa PrintStream.

Operaţii de citire a liniilor de text

Clasa care modelează citirea unei linii dintr-un flux de intrare este BufferedReader, prin operaţia readLine(). Această operaţie nu are parametri, iar execuţia ei are ca efect citirea din fluxul de intrare a unei secvenţe de caractere până la întâlnirea terminatorului de linie. Operaţia returnează o referinţă la un obiect String care conţine caracterele citite, dar fără a include şi terminatorul. Cu alte cuvinte, şirul returnat conţine doar caracterele utile (semnificative) ale liniei.

Dacă s-a ajuns la sfârşitul fluxului de intrare, operaţia returnează valoarea null. Dacă citirea nu se poate desfăşura, operaţia emite o excepţie de tip IOException. De aceea, semnătura unei funcţii care apelează metoda readLine(), dar nu tratează eventualele erori de citire, trebuie sa conţină clauza throws IOException. Una dintre cele mai frecvente erori într-un program Java este omiterea clauzelor throws din antetul funcţiilor utilizatorului care apelează funcţii predefinite dotate cu această clauză.

Pentru a crea un obiect al clasei BufferedReader este necesar să i se furnizeze constructorului acesteia o referinţă la un obiect al clasei InputStreamReader. Constructorul acesteia din urmă necesită:

— o referinţă la un obiect FileInputStream, dacă citirea se face dintr-un fişier de pe disc

Page 23: Java

12

— referinţă System.in, dacă citirea se face de la tastatură.

Deci, dacă urmează o citire dintr-un fişier al cărui nume este dat de o variabilă String nume_fis, va trebui să se creeze un obiect BufferedReader ca în secvenţa de mai jos: BufferedReader flux_in = new BufferedReader(new InputStreamReader(new FileInputStream(nume_fis)));

Dacă citirea se va face de la tastatură, obiectul BufferedReader se creează astfel: BufferedReader flux_in = new BufferedReader(new InputStreamReader (System.in));

În continuare, citirea se va realiza cu apelul: linie = flux_in.readLine();

unde linie este o referinţă String. În urma apelului, ea va indica spre un obiect care conţine caracterele citite. În funcţie de natura datelor reprezentate de aceste caractere, uneori pot fi necesare conversii de la String la alte tipuri, în vederea utilizării datelor respective în diverse calcule.

Se va prezenta un program care citeşte numere întregi dintr-un fişier text, calculează şi afişează pe ecran suma acestora. Se presupune că fiecare număr din fişier se află pe câte o linie distinctă, iar numele fişierului este dat ca argument la execuţia programului. public class Suma {

public static void main (String[ ] args) throws IOException { BufferedReader flux_in = new BufferedReader (new InputStreamReader (new FileInputStream(args[0])));

int suma = 0; String linie; while ((linie = flux_in.readLine()) != null) suma+=Integer.parseInt(linie); System.out.println("Suma = "+suma); flux_in.close(); //se inchide fisierul (fluxul) } }

Operaţii de scriere a liniilor de text

Afişarea unei linii pe ecran se realizează apelând metodele print()/println() definite în clasa PrintStream, pentru obiectul System.out. Pentru a scrie o linie într-un fişier de pe disc, se vor folosi aceleaşi metode, dar va trebui să se creeze un obiect separat al clasei PrintStream. Pentru aceasta, trebuie furnizată constructorului clasei PrintStream, ca parametru, o referinţă la un obiect FileOutputStream, ca în secvenţa de mai jos: PrintStream flux_out = new PrintStream (new

FileOutputStream(nume_fis));

unde nume_fis este o referinţă la un obiect String ce conţine numele fişierului.

Page 24: Java

13

Se va prezenta un program care citeşte dintr-un fişier text o secvenţă de numere reale, dispuse câte unul pe linie, determină numărul lor, suma, media aritmetică, valoarea minimă/maximă şi tipăreşte aceste informaţii într-un alt fişier. Numele ambelor fişiere implicate se dau ca argumente la execuţia programului. import java.io.*; public class Statist {

public static void main (String[ ] args) throws IOException { BufferedReader flux_in = new BufferedReader (new

InputStreamReader (new FileInputStream(args[0])));

PrintStream flux_out = new PrintStream (new FileOutputStream(args[1]));

double suma = 0.0, min, max, val; int contor=0; String linie; while ((linie = flux_in.readLine()) != null) {

contor++; val = Double.parseDouble(linie); //conversia

String -> double suma+=val; if(contor==1) { //s-a citit primul numar

min = val; max = val; } else {

if(val < min) min = val; if(val > max) max =val;

} } flux_out.println("S-au citit "+contor+" valori"); flux_out.println("Suma lor este "+suma); flux_out.println("Media aritmetica este

"+(contor>0 ? suma/contor : 0.0)); flux_out.println("Element minim: "+min); flux_out.println("Element maxim: "+max); flux_in.close( ); flux_out.close();

} }

În continuare, se va prezenta un program în care va fi ilustrat conceptul de introducere interactivă de date, care presupune că utilizatorul trebuie să introducă anumite date de la tastatură, Pentru aceasta, este necesar ca înaintea operaţiilor de citire, în program să existe operaţii de afişare a unor mesaje de genul: „Introduceţi denumirea:“ sau „Daţi cantitatea:“. Programul considerat realizează completarea unui tablou de numere întregi, citind numerele de la tastatură, apoi afişează pe ecran tabloul ordonat crescător. import java.io.*;

public class Ordonare{

Page 25: Java

14

public static void main (String [] args) throws IOException{

BufferedReader flux_in=new BufferedReader (new InputStreamReader(System.in));

int[] tab = new int[10];

int i, aux;

String linie;

boolean sw = true;

for(i=0;i<tab.length;i++){

System.out.println("Dati elementul de pe pozitia "+(i+1));

linie=flux_in.readLine();

tab[i]=Integer.parseInt(linie);

}

while(sw){

sw=false;

for(i=o;i<tab.length-1;i++)

if(tab[i]>tab[i+1]){

aux=tab[i];

tab[i]=tab[i+1];

tab[i+1]=aux;

sw=true;

}

}

System.out.println("Tabloul ordonat este:");

for(i=0;i<tab.length;i++)

System.out.println(tab[i]+" ");

}

}

Temă Să se scrie un program care citeşte dintr-un fişier text informaţii referitoare la produsele dintr-un magazin. Pe fiecare linie se află: denumirea produsului, preţul (număr real) şi cantitatea (număr real). Cele trei elemente sunt separate prin caracterul ";" (zahăr;1.5;50). Programul va afişa, sub forma unui tabel, conţinutul fişierului şi va determina denumirea, preţul şi cantitatea produsului cu preţ minim, respectiv maxim pe care le va scrie într-un fişier destinaţie. Ambele fişiere vor fi primite ca parametri la apel.

Page 26: Java

15

Indicaţie: Pentru o tratare mai elegantă a subiectului se poate defini o clasă Produs care să conţină ca date membru cele trei elemente ce descriu un produs. Pentru a tipări informaţiile cerute se va prevedea o metodă toString() în clasa Produs.

Page 27: Java

1

Lucrarea 4

Moştenire şi polimorfism

Cuprins Relaţia de moştenire în Java .....................................................................................................................1 Reguli de vizibilitate în contextul relaţiei de moştenire ...........................................................................2 Constructorii şi moştenirea.......................................................................................................................4 Operatorul instanceof ...............................................................................................................................4 Redefinirea metodelor ..............................................................................................................................5 Legarea dinamică şi constructorii.............................................................................................................7 Temă.........................................................................................................................................................8

Tehnologia orientată pe obiecte permite extinderea comportamentului unei clase existente prin definirea unei clase noi, care moşteneşte conţinutul primei clase, adăugând la acesta elementele specifice ei. Clasa moştenită se numeşte clasă de bază sau superclasă, iar clasa care realizează extinderea se numeşte subclasă, clasă derivată, sau clasă descendentă. Relaţia de moştenire este o relaţie de forma este un fel de, adică subclasa este un fel de superclasă. Practic, relaţia de moştenire între clase este o reflectare a ierarhizării existente între entităţile modelate de clasele respective.

Relaţia de moştenire prezintă două aspecte esenţiale:

— reutilizare de cod;

— polimorfism.

Relaţia de moştenire în Java În Java, relaţia de moştenire dintre o superclasă şi o subclasă se exprimă astfel: class nume_subclasa extends nume_superclasa { //conţinut specific subclasei }

Se consideră clasa Poligon şi o clasă Dreptunghi care moşteneşte clasa Poligon: class Poligon { protected double[ ] laturi; public Poligon(int n) { laturi = new double[n] } public double perimetru( ) { double s=0; for(int i=0;i<laturi.length;i++) s+=laturi[i]; return s; } } class Dreptunghi extends Poligon { public Dreptunghi(double L, double h){ super(4);

Page 28: Java

2

laturi[0] = laturi[2] = L; laturi[1] = laturi[3] = h;

} public double aria( ){ return laturi[0]*laturi[1]; } }

Clasa Dreptunghi reutilizează codul definit în clasa Poligon, la care adaugă elemente proprii. Pe lângă Dreptunghi, am putea defini şi alte clase, cum ar fi Triunghi, Patrat, Pentagon etc. care să moştenească Poligon. Superclasa este factorul comun al subclaselor sale sau, altfel spus, codul factorului comun este reutilizat în subclase.

Despre relaţia de moştenire în Java se pot spune următoarele:

— o subclasă poate extinde o singură superclasă (moştenire simplă);

— o superclasă poate fi moştenită de mai multe subclase distincte.

O subclasă poate fi superclasă pentru alte clase. O clasă de bază împreună cu toate descendentele ei formează o ierarhie de clase.

Reguli de vizibilitate în contextul relaţiei de moştenire Regulile de vizibilitate vor fi discutate din două perspective:

— accesul funcţiilor unei subclase la membrii moşteniţi de la superclasele ei;

— accesul clienţilor unei subclase la membrii ei, moşteniţi sau specifici.

Accesul funcţiilor unei subclase la membrii moşteniţi

Pentru a ilustra drepturile de acces vom lua următorul exemplu: class SuperClasa { private int priv; protected int prot; public int pub; } class SubClasa extends SuperClasa { private int priv_local; protected int prot_local; public int pub_local; public void metoda(SubClasa p ) { priv = 1; //eroare prot = 2; //corect pub = 3; //corect priv_local = 4; //corect prot_local = 5; //corect pub_local = 6; //corect p.priv = 1; //eroare p.prot = 2; //corect p.pub = 3; //corect p.priv_local = 4; //corect

Page 29: Java

3

p.prot_local = 5; //corect p.pub_local = 6; //corect } }

În metodele specifice ale unei subclase pot fi referiţi acei membri moşteniţi, care în superclasă au fost precedaţi de modificatorii protected sau public. Membrii moşteniţi, care în superclasă sunt precedaţi de modificatorul private, deşi fizic fac parte din subclasă, nu sunt accesibili în metodele acesteia. Referirea membrilor moşteniţi se face direct, folosind numele lor, la fel ca referirea membrilor specifici. Acest lucru este foarte normal, deoarece şi membrii moşteniţi sunt conţinuţi în subclasă, numai că ei nu au fost declaraţi explicit. Includerea lor se face automat, ca efect al clauzei extends. Acest lucru nu este valabil şi pentru constructori.

Revenind la exemplul cu clasele Poligon — Dreptunghi, trebuie spus că un obiect al clasei Dreptunghi conţine doi constructori: unul moştenit, responsabil cu iniţializarea porţiunii moştenite, şi unul local, responsabil cu iniţializarea porţiunii specifice subclasei. Referirea în subclasă la constructorul moştenit se face folosind cuvântul cheie super, aşa cum se observă în constructorul clasei Dreptunghi. Cuvântul super este un omolog al lui this şi reprezintă referinţa la partea moştenită a unui obiect. În afară de apelul constructorului moştenit, cuvântul super se va utiliza pentru a distinge între un membru local al subclasei şi un membru moştenit, ambii cu acelaşi nume.

Accesul clienţilor unei subclase la membrii acesteia

Exemplului din paragraful anterior i se adaugă secvenţa următoare: class Client { public void oFunctie( ) { SuperClasa sp = new SuperClasa(); SubClasa sb = new SubClasa(); sp.priv = 1; //eroare

sp.prot = 2; //corect, doar dacă Client şi SuperClasa s-ar afla in acelaşi pachet

sp.pub = 3; //corect sb.priv = 1; //eroare

sb.prot = 2; //corect, doar dacă Client şi SubClasa s-ar afla în acelaşi pachet

sb.pub = 3; //corect sb.priv_local = 4; //eroare

sb.prot_local = 5; //corect, doar dacă Client şi SubClasa s-ar afla în acelaşi pachet

sb.pub_local = 6; //corect } }

În clienţii unei subclase pot fi referiţi acei membri, moşteniţi sau locali, care sunt precedaţi de modificatorul public. Membrii declaraţi ca protected sunt accesibili numai în situaţia în care clientul se află în acelaşi pachet cu subclasa

Page 30: Java

4

respectivă. La referirea membrilor unei subclase de către client, nu se face deosebire între membrii moşteniţi şi cei locali.

Constructorii şi moştenirea În exemplul anterior se observă că cele două clase nu au constructori expliciţi, constructorul SubClasa() fiind de forma: public SubClasa() { super( ); }

Constructorul subclasei apelează constructorul no-arg al superclasei. Dacă pentru SuperClasa am fi definit un constructor no-arg explicit, atunci acesta ar fi fost cel apelat de constructorul SubClasa. Dacă ar fi existat un constructor oarecare în SubClasa, iar acesta nu ar fi apelat explicit vreun constructor din SuperClasa, în mod automat se încearcă apelul constructorului no-arg super(), înainte de a se executa instrucţiunile din constructorul SubClasa: public SubClasa(. . .) { super(); //apel implicit //alte instrucţiuni prevăzute de programator }

Dacă SuperClasa nu are constructor no-arg, apelul implicit de mai sus ar genera eroare. Prin urmare, dacă programatorul defineşte constructori expliciţi într-o subclasă, el trebuie să aibă grijă ca aceşti constructori să apeleze explicit constructorii adecvaţi ai superclasei: super (parametri-actuali)

Un asemenea apel trebuie să fie prima instrucţiune din constructorul subclasei. Constructorul unei subclase se defineşte astfel încât lista lui de parametri să se compună dintr-un set de parametri necesari iniţializării câmpurilor moştenite (transmis constructorului super) şi un alt set, necesar iniţializării câmpurilor locale.

Execuţia constructorului se desfăşoară în 3 etape:

— apelul constructorului superclasei;

— iniţializarea câmpurilor cu valori date la declarare şi execuţia blocurilor de iniţializare, unde este cazul;

— execuţia corpului propriu-zis al constructorului.

Operatorul instanceof Prin intermediul unei referinţe de superclasă putem indica şi utiliza şi obiecte ale subclaselor ei. Operatorul instanceof este util atunci când trebuie să cunoaştem clasa concretă de care aparţine un obiect referit prin intermediul unei referinţe a unui supertip. Aplicarea operatorului se face printr-o expresie de forma: referinta_obiect instanceof nume_clasa

Page 31: Java

5

Redefinirea metodelor Relaţia de moştenire poate fi utilizată atunci când o anumită clasă (subclasă) extinde comportamentul altei clase (superclase), în sensul că superclasa înglobează aspectele comune ale unei ierarhii de abstracţiuni, iar subclasele adaugă la această parte comună funcţiuni specifice. De asemenea, o referinţă la superclasă poate indica şi manipula obiecte ale oricărei clase descendente din ea. Manipularea se referă la faptul că, prin intermediul acelei referinţe se pot apela metodele definite în superclasă, indiferent dacă obiectul concret aparţine superclasei sau uneia dintre subclase. Această facilitate constituie aşa-numitul polimorfism parţial. Folosind o referinţă a superclasei nu se poate, însă, apela o metodă locală a subclasei, decât dacă se realizează o conversie explicită (casting) a referinţei, la tipul subclasei. În Java relaţia de moştenire poate fi aplicată şi în cazul în care se doreşte ca, pe lângă extinderea comportamentului, să realizăm şi o adaptare (specializare) a lui, în sensul că unele metode care în superclasă au o anumită implementare, în subclase să aibă o altă implementare, adaptată la cerinţele locale ale subclasei. class Angajat{

protected String nume; protected double sal_ora; public Angajat(String nume, double sal_ora) {

this.nume = nume; this.sal_ora = sal_ora;

} public double calcSalar(int nr_ore) {

return (sal_ora * nr_ore); } public String toString() { return nume; }

} class Sef extends Angajat {

private double proc_cond; //procent de indemnizaţie conducere

public Sef(String nume, double sal_ora, double proc_cond) { super(nume, sal_ora); this.proc_cond = proc_cond;

} public double calcSalar(int nr_ore) {

return ((1 + proc_cond / 100) * sal_ora * nr_ore);

} } class Client {

public void oMetoda { Angajat a1 = new Angajat("Artaxerse Coviltir",

1000); Angajat a2 = new Sef("Al Bundy", 2000, 33.2); int ore_luna = 160; System.out.println("Salar " + a1 + "=" +

a1.calcSalar(ore_luna));

Page 32: Java

6

System.out.println("Salar " + a2 + "=" + a2.calcSalar(ore_luna));

} //. . .

}

Se observă că aceeaşi metodă, calcSalar() apare atât în clasa Angajat, cât şi în clasa Sef, dar cu implementări diferite. Atunci când într-o subclasă apare o metodă având semnătura identică cu o metoda din superclasă, se spune că metoda din subclasă o redefineşte pe omonima ei din superclasa. În acest caz, un obiect al subclasei practic are două exemplare ale metodei respective, unul moştenit şi unul propriu. Ce se întâmplă la nivelul clienţilor ierarhiei? Se observă că în metoda oMetoda() din clasa Client se folosesc două referinţe la Angajat: una indică un obiect al clasei Angajat, iar cealaltă un obiect al clasei Sef. Prin intermediul celor două referinţe se apelează metoda calcSalar(). Se pune problema care dintre cele două forme ale metodei se apelează efectiv? În Java, regula care se aplică în asemenea cazuri este următoarea: Se va executa întotdeauna acel exemplar de metodă care este definit în clasa la care aparţine obiectul concret indicat de referinţa utilizată. Această facilitate, adăugată polimorfismului parţial conduce la polimorfismul total. Cu alte cuvinte, clasa obiectului „dictează“ şi nu tipul referinţei. Astfel, deşi ambele referinţe a1 şi a2 din exemplul considerat sunt de tip Angajat, ele indică, de fapt, obiecte ale unor clase diferite. Deci, apelul a1.calcSalar(...) va declanşa execuţia metodei calcSalar() definită în clasa Angajat, iar apelul a2.calcSalar(...) va declanşa execuţia metodei calcSalar() definită în clasa Sef. Polimorfismul total permite interschimbarea obiectelor indicate de o referinţă a superclasei, într-o manieră transparentă pentru clienţi. În Java, clasa concretă la care aparţine un obiect indicat de o referinţă se cunoaşte, însă, abia la execuţia programului. Deci, un apel de metodă nu este „rezolvat“ (adică pus în corespondenţă cu o implementare anume) la compilare, ci doar la execuţie. Acest procedeu se numeşte legare dinamică sau amânată (late binding).

S-a spus mai sus că un obiect al clasei Sef „posedă“ două metode calcSalar(): cea moştenită de la Angajat şi cea locală. Din cauză că în Java se aplică legarea dinamică, înseamnă că, din perspectiva clienţilor clasei Sef varianta locală o „eclipsează“ pe cea moştenită, neexistând posibilitatea ca un client să forţeze apelul metodei moştenite (decât aplicând o soluţie „ocolitoare“, adică folosind o metodă cu alt nume care să apeleze metoda calcSalar() moştenită). În interiorul subclasei se poate realiza distincţia între cele două variante ale unei metode redefinite, şi anume folosind simbolul super. Astfel, metoda calcSalar() din clasa Sef se poate folosi de omonima ei din clasa Angajat: class Sef extends Angajat {

//. . . public double calcSalar(int nr_ore) {

return ((1 + proc_cond / 100) * super.calcSalar(nr_ore));

} }

Page 33: Java

7

Practic, aceasta este singura situaţie în care nu se aplică legarea dinamică, ci apelul de forma super.numeMetoda(...) este pus în corespondenţă exact cu implemen-tarea din superclasa clasei curente.

Legarea dinamică şi constructorii Deoarece în Java la apelul metodelor non-statice se aplică legarea dinamică, pe de o parte, şi ţinând cont de modul în care se apelează constructorii într-o ierarhie de clase, pe de altă parte, trebuie să avem grijă cum proiectăm constructorii dacă aceştia apelează la rândul lor metode ale claselor respective. class SuperClasa { protected int a; protected int rez; private int x; public SuperClasa( ) { a = 1; rez = 2; x = calcul( ); } public int calcul( ) { return (rez + a); } } class SubClasa extends SuperClasa { protected int b; private int y; public SubClasa( ) { b =3; y = calcul( ); } public int calcul( ) { return (rez * b); } } class Client { public void oMetoda { SubClasa ob = new SubClasa( ); //. . . } }

Să urmărim ce se întâmplă la crearea unui obiect al clasei Subclasa: ţinând cont de ordinea în care au loc iniţializările câmpurilor unui obiect, înseamnă că se execută următorii paşi:

— câmpurile a, rez, x, b şi y se iniţializează cu valorile implicite corespunzătoare tipurilor lor, deci, în cazul nostru cu 0;

— se lansează constructorul SubClasa(); ca primă acţiune a acestuia are loc apelul constructorului no-arg SuperClasa();

— în constructorul SuperClasa() se execută iniţializările lui a şi rez cu 1, respectiv 2, după care se apelează metoda calcul(). În acest moment, apelul se leagă de varianta metodei calcul() definită în clasa SubClasa, de care aparţine obiectul în curs de iniţializare. Efectul este

Page 34: Java

8

că x se va iniţializă cu valoarea 2 * 0, adică cu 0, deoarece la momentul respectiv câmpul b încă nu a apucat să primească o altă valoare;

— se revine în constructorul SubClasa(), se iniţializează b cu 3 şi y cu 2*3 = 6, cât returnează metoda calcul() din SubClasa.

Dacă în constructorul unei superclase se apelează o metodă care este redefinită în subclasă, la crearea unui obiect al subclasei există riscul ca metoda respectivă să refere câmpuri neiniţializate încă la momentul apelului.

Temă Problema propusă încearcă să dea o mână de ajutor în gestionarea produselor unei firme care comercializează echipamente electronice. Fiecare echipament este înregistrat cu un număr de inventar nr_inv, are un preţ pret şi este plasat într-o anumită zonă din magazie zona_mag. Orice echipament poate fi într-una din situaţiile:

— achiziţionat (intrat în magazie);

— expus (expus în magazin);

— vândut (transportat şi instalat la client).

Firma comercializează mai multe tipuri de echipamente. Toate echipamentele care folosesc hârtia drept consumabil sunt caracterizate de numărul de pagini scrise pe minut ppm. Imprimantele sunt caracterizate de rezoluţie (număr de puncte per inch dpi) şi număr de pagini/cartuş p_car. Unei imprimante i se poate seta modul de scriere:

— tipărireColor (selectere a modului color de tipărire);

— tipărireAlbNegru (selectere a modului alb-negru de tipărire).

Copiatoarele sunt caracterizate de numărul de pagini/toner p_ton. Se poate seta formatul de copiere:

— setFormatA4 (setare a formatului A4);

— setFormatA3 (setare a formatului A3).

Sistemele de calcul au un monitor de un anumit tip tip_mon, un procesor de o anumită viteză vit_proc, o capacitate a HDD c_hdd şi li se poate instala:

— instalWin (instalarea unei variante de Windows);

— instalLinux (instalarea unei variante de Linux).

Metodele specificate mai sus vor afişa textul din paranteză. De asemenea, fiecare metodă va afişa numărul de inventar al echipamentului. Se cere să se realizeze ierarhia de clase corespunzătoare modelului prezentat, să se „realizeze“ câteva echipamente şi să se aplice operaţiile posibile asupra lor.

În momentul în care un client cumpără un echipament de la respectiva firmă, trebuie verificată buna funcţionare a echipamentului, prin apelul unei metode numită Functionare(TipEchipament e) unde e poate fi un echipament care foloseşte hârtia sau un sistem de calcul.

Se cere ca firma să vândă diferite tipuri de echipamente, efectuând pentru fiecare verificarea funcţionării astfel:

Page 35: Java

9

— imprimantă: tipărirea unei pagini color şi a unei pagini alb-negru

— copiator: setare format A4

— sistem de calcul: instalare Windows

Page 36: Java

1

Lucrarea 5

Interfeţe

Cuprins Interfeţe Java .......................................................................................................................................................... 1 Declararea unei interfeţe în Java............................................................................................................................. 1 Utilizarea interfeţelor Java...................................................................................................................................... 2 Exemplu de lucru cu interfeţe Java.........................................................................................................................3 Asocierea operaţiilor cu obiectele .......................................................................................................................... 5 Temă ....................................................................................................................................................................... 5

Un obiect include date şi metode (operaţii) care permit modificarea datelor. Obiectul execută o operaţie atunci când primeşte o cerere (un mesaj) de la un client. Mesajele reprezintă singura cale prin care un obiect este determinat să execute o operaţie, iar operaţiile sunt singurul mod de a modifica datele interne ale obiectului. Din cauza acestor restricţii, starea internă a obiectului se spune că este încapsulată: ea nu poate fi accesată direct, iar reprezentarea ei este invizibilă dinspre exteriorul obiectului. Pentru fiecare operaţie declarată într-un obiect se precizează numele, parametrii şi valoarea returnată. Aceste elemente formează semnătura operaţiei.

Mulţimea tuturor semnăturilor corespunzătoare operaţiilor dintr-un obiect, accesibile clienţilor, reprezintă interfaţa obiectului. Aceasta descrie complet setul mesajelor care pot fi trimise spre obiectul respectiv. Un tip este un nume utilizat pentru a referi o anumită interfaţă. Un obiect este de tipul T dacă el acceptă toate mesajele corespunzătoare operaţiilor definite în interfaţa numită T.

Interfeţele sunt elemente fundamentale în sistemele OO. Obiectele sunt cunoscute doar prin intermediul interfeţelor lor. O interfaţa nu dă nici un detaliu relativ la implementarea unui obiect, iar obiecte distincte pot implementa diferit o aceeaşi cerere.

Interfeţe Java Interfeţele Java reprezintă o colecţie de metode abstracte (doar semnătura, nu şi implementarea acestora) şi de constante. Utilizarea unei interfeţe este justificată în cazurile în care se doreşte ca o clasă să poată fi convertită în mai multe tipuri de bază între care nu există relaţie de moştenire. Deoarece Java nu suportă moştenirea multiplă, interfeţele reprezintă o variantă de rezolvare a acestei situaţii.

La fel ca şi o clasă, înainte de a putea utiliza o interfaţă ea trebuie declarată. Deoarece conţine metode abstracte, interfaţa nu poate fi instanţiată. Cu toate acestea, o variabilă poate fi declarată ca fiind de tipul interfeţei.

Declararea unei interfeţe în Java O interfaţă Java este declarată prin cuvântul cheie interface. Structura generală a declaraţiei unei interfeţe este:

Page 37: Java

2

[modificator] interface id_interfata [extends lis-ta_de_interfete]

{ lista_de_metode; listă_de_constante; }

Modificatorul unei interfeţe poate fi public sau abstract. Având în vedere faptul că o interfaţă este abstractă prin definiţie, utilizarea modificatorului abstract este redundantă. Spre deosebire de o clasă, o interfaţă poate moşteni mai multe interfeţe. Moştenirea se face prin clauza extends urmată de lista interfeţelor moştenite (lista_de _interfete) separate prin virgulă. Prin derivare, o interfaţă moşteneşte de la interfaţa de bază atât metodele, cât şi constantele acesteia. public interface Id1 extends Id2, Id3, Id4

În corpul interfeţei pot exista o listă de declaraţii de metode şi o listă de declaraţii de constante. Metodele declarate într-o interfaţă sunt implicit abstracte. Constantele declarate în interfaţă primesc atributele static şi final. Unui câmp final, după ce a fost iniţializat, nu i se mai poate modifica valoarea. public interface Alergator { int lg_pista=1000; int timp_max=3; public void alearga(); public void accelerează(); }

Metodele dintr-o interfaţă nu pot fi statice. Referirea constantelor se va face astfel: Alergator.lg_pista;

În urma compilării unui fişier sursă Java care conţine o interfaţă, va rezulta un fişier cu extensia .class ataşat interfeţei respective.

Utilizarea interfeţelor Java Interfeţele Java pot fi utilizate în declaraţia unei clase, obligând clasa respectivă să implementeze metodele declarate în interfaţă. În Java, acest lucru e realizat prin folosirea cuvântului cheie implements: class Sportiv extends Om implements Alergator, Saritor { … //implementarea metodelor din interfaţa Alergator; //implementarea metodelor din interfaţa Saritor; …\ }

Aşa cum se poate observa, o clasă poate implementa mai multe interfeţe. Dacă o clasă care implementează o interfaţă nu implementează toate metodele declarate în interfaţa respectivă, va trebui să fie declarată abstractă, altfel se va semnala eroare la compilare. Dacă o clasă A implementează o interfaţă X, un obiect al clasei A este de tip X, el putând să fie atribuit unui

Page 38: Java

3

alt obiect de tip X. Numele unei interfeţe poate fi utilizat la fel ca şi numele unui clase, mai puţin la instanţierea prin operatorul new.

Exemplu de lucru cu interfeţe Java Pentru a mai bună înţelegere a conceptelor prezentate în lucrare, vom recurge, în continuare, la un exemplu. interface Cititor { public void citeste(); } interface Scriitor { public void scrie(); } interface Profesor extends Cititor, Scriitor { int nota_max=10; int nota_medie=5; int nota_min=1; public void apreciere(); } interface Scufundator { int h=10; public void scufundare(); } class Inotator implements Scufundator { static final int l=50; public void inot() { System.out.println("Inoata " + l + "metri "); } public void scufundare() { System.out.println("Scufunda " + h + "metri "); } } class ProfesorInot extends Inotator implements Profesor { public void citeste() { System.out.println("Citeste PRO Sport "); } public void scrie() { System.out.println("Nu scrie nimic!"); }

Page 39: Java

4

public void apreciere() {

System.out.println("Slab. Primeşti nota " + nota_medie);

} } class ProfesorJava implements Profesor { public void citeste() {

System.out.println("Citeste Thinking in Java ");

} public void scrie() { System.out.println("Scrie programe Java"); } public void apreciere() {

System.out.println("Excelent. Primeşti nota " + nota_max);

} } public class TestInterfata { public static void main(String [] args) { ProfesorInot pi = new ProfesorInot(); ProfesorJava pj = new ProfesorJava(); ceCiteste(pi); ceScrie(pi); ceCiteste(pj); ceScrie(pj); cumApreciaza(pi); cumApreciaza(pj); pi.inot(); pi.scufundare(); //corect pj.inot(); pj.scufundare(); //incorect } public static void ceCiteste( Cititor c) { c.citeste(); } public static void ceScrie( Scriitor s) { s.scrie(); } public static void cumApreciaza( Profesor p) { p.apreciere(); } }

Page 40: Java

5

Folosind referinţa de interfaţă se pot apela metodele enumerate în interfaţă, iar ceea ce se execută va fi codul corespunzător clasei "implementatoare", la care aparţine obiectul concret indicat de referinţă

Asocierea operaţiilor cu obiectele Când o cerere este trimisă unui obiect, operaţia care se va executa depinde de:

— cerere;

— obiectul care recepţionează cererea.

Obiecte diferite, care pot recepţiona cereri identice, pot avea implementări diferite ale operaţiilor care vor satisface cererile respective. Practic, prin cerere se identifică serviciul dorit, iar prin obiectul receptor se alege o anume implementare a acelui serviciu. Asocierea dintre o operaţie solicitată şi obiectul care va pune la dispoziţie implementarea concretă a operaţiei se numeşte legare (binding). În funcţie de momentul în care ea se realizează, legarea poate fi:

— statică: asocierea se realizează la compilare;

— dinamică: asocierea se realizează la execuţie.

În programele Java, majoritatea apelurilor de metode presupun legare dinamică. Asocierea dinamică permite scrierea de programe în care:

— la emiterea unei cereri să nu ne preocupe ce obiect o va recepţiona, stiindu-se că orice obiect a cărui interfaţă include o semnătură potrivită va fi bun;

— obiecte având interfeţe identice pot fi substituite unul altuia, la execuţie; această substituire se mai numeşte polimorfism.

Temă Se cere să se modeleze activitatea unui ghişeu bancar. Sistemul este format din următoarele entităţi, cu atributele corespunzătoare: Banca

— clienti (tablou de elemente de tip Client)

— codBanca (sir de caractere) ContBancar

— numarCont (String)

— suma(float) Client

— nume (String)

— adresa (String)

— conturi (tablou de elemente de tip ContBancar; un client trebuie să aibă cel puţin un cont, dar nu mai mult de cinci conturi)

Page 41: Java

6

Conturile bancare pot fi în RON şi în EUR. Pentru conturile in RON, dobânda se calculează astfel: 0,3 RON pe zi pentru sume sub 500 RON si 0,8 RON pe zi pentru sume mai mari. Conturile in EUR au o dobândă de 0,1 EUR pe zi.

Operaţiunile posibile asupra unui cont sunt depunere sau extragere a unei sume specificate. Transferuri se pot efectua doar intre conturile in RON. public void transfer(ContBancar contDest, float suma).

Toate conturile implementează o interfaţa Operatiuni care are metodele public float getSumaTotala()

la suma existentă în cont se adaugă dobânda aferentă public float getDobanda()

public void depunere(ContBancar contDest, float suma) public void extragere (ContBancar contDest, float suma)

Toate clasele vor avea constructori şi metode de prelucrare şi de afişare a datelor membru. De asemenea, se va crea o clasă ClientBanca pentru instanţierea de obiecte ale celor trei clase specificate.

Page 42: Java

1

Lucrarea 6

Tratarea excepţiilor

Cuprins Tratarea clasică a excepţiilor ....................................................................................................................1 Mecanismul de emitere-captare a excepţiilor ...........................................................................................3

Instrucţiunea throw ...............................................................................................................................5 Clauza throws .......................................................................................................................................5

Care tipuri de excepţie vor apărea într-o clauză throws?..................................................................5 Blocurile try-catch................................................................................................................................6 Secvenţa finally ..................................................................................................................................10

Excepţii predefinite ale limbajului Java .................................................................................................11 Temă.......................................................................................................................................................12

Pe parcursul execuţiei unui program pot apărea evenimente deosebite care îi modifică parcursul normal. Asemenea evenimente pot fi situaţii de eroare (de exemplu încer-carea de a accesa un element de tablou aflat dincolo de limitele tabloului) sau pur şi simplu situaţii care necesită o altă abordare decât cea obişnuită (de exemplu, la parcurgerea secvenţială a unui fişier de intrare, la un moment dat se atinge sfârşitul fişierului; acest eveniment, fără a fi o eroare, va schimba cursul de până atunci al programului). Evenimentele de genul celor amintite se mai numesc excepţii.

O excepţie este un obiect ce aparţine clasei predefinite Throwable sau unei clase descendente din Throwable. Clasa Throwable este definită în pachetul java.lang. Limbajul Java dispune, de fapt, de o întreagă ierarhie de clase predefinite pentru reprezentarea excepţiilor. Programatorul poate utiliza clasele predefinite sau poate să-şi definească propriile clase de excepţii, cu condiţia ca ele să descindă direct sau indirect din Throwable. Prin convenţie, clasele definite de utilizatori pentru reprezentarea excepţiilor vor fi derivate din clasa Exception.

Definirea unui tip de excepţie de către utilizator se justifică de obicei prin:

— necesitatea reţinerii de date suplimentare şi/sau mesaje mai detaliate despre excepţie;

— necesitatea ca dintr-o clasă mai largă de excepţii să fie captate doar o anumită parte.

Tratarea clasică a excepţiilor Se va considera un exemplu în care se va face abstracţie de faptul că limbajul Java dispune de un mecanism special de tratare a excepţiilor: class Tabel{

public static boolean fan_err = false; public static int Increm(int[ ] tab,int indice,int

val) {

Page 43: Java

2

if(indice>=0 && indice<tab.length){ tab[indice]+=val; fan_err = false; return tab[indice];

} else{

fan_err = true; return 0;

} }

public static int indexOf(int[ ] tab, int val) {

for(int i = 0;i<tab.length;i++) if(tab[i]==val) return i;

return -1; }

} class ClientTabel{

public static void main(String[ ] arg) { int[ ] tab = new int[Integer.parseInt(arg[0])];

//se completeaza tabloul cu valori int suma=0,indice,val; boolean gata=false;

do{

//se citesc de la tastatura un indice si o valoare

suma+=Tabel.Increm(tab,indice,val); if(Tabel.fan_err) //a fost eroare... else //merg mai departe if(Tabel.indexOf(tab,val)<0) //valoarea val

nu exista in tablou else //valoarea val exista //. . .

}while(!gata); }

}

Metoda Tabel.Increm() se poate confrunta cu situaţia în care unul dintre parametrii săi (indice) primeşte o valoare necorespunzătoare. În maniera clasică de tratare a erorilor se poate adopta una dintre următoarele rezolvări:

1) terminarea forţată a programului - nu reprezintă o tratare propriu-zisă a erorii; de multe ori o eroare nu este chiar atât de gravă, iar programul poate şi trebuie să se descurce cu ea într-un mod mai inteligent.

2) returnarea unei valori speciale pe post de „eroare“.

3) poziţionarea unui indicator de eroare şi returnarea unei valori valide, care însa poate implica coruperea stării programului.

Page 44: Java

3

4) apelul unei funcţii prevăzute special pentru cazurile de eroare; această funcţie, la rândul ei are de ales între cele 3 alternative de mai sus.

În exemplul prezentat, metoda indexOf() aplică varianta 2), iar metoda Tabel.Increm() — varianta 3). Cu privire la varianta 2) trebuie spus ca ea nu este întotdeauna uşor de aplicat. Dacă pentru funcţia indexOf(), de exemplu, valoarea -1 ca indicator de eşec este satisfăcătoare (este clar că un indice de tablou nu poate fi negativ în mod normal), în schimb, pentru Increm() se pune întrebarea ce valoare a tipului int ar putea fi considerată ca „ieşită din comun“? Pentru asemenea funcţii o soluţie ar putea fi aceea că, în loc de int, tipul returnat să fie o structură compusă din rezultatul propriu-zis şi un fanion de eroare.

Pe de altă parte, chiar şi acolo unde este aplicabilă, opţiunea 2) implică necesitatea testării de fiecare dată a rezultatului execuţiei funcţiilor respective. Acest lucru poate duce uşor la dublarea dimensiunii codului. Alternativa 3) prezintă dezavantajul că apelantul funcţiei în care a apărut eroarea poate să nu „observe“ că s-a întâmplat ceva. De exemplu, multe funcţii din biblioteca standard C indică apariţia unei erori prin setarea variabilei globale errno. Dar prea puţine programe ale utilizatorilor testează valoarea lui errno suficient de sistematic încât să se obţină o tratare consistentă a erorilor.

În concluzie, metodele clasice de tratare a excepţiilor se caracterizează prin faptul că porţiunile de cod „normale“ se întrepătrund cu cele de tratare a excepţiilor, iar claritatea programelor are de suferit din cauza ramificărilor generate de numeroasele teste ale indicatorilor de eroare.

Mecanismul de emitere-captare a excepţiilor Mecanismul de emitere-captare a excepţiilor are două avantaje esenţiale:

— permite separarea explicită a secvenţelor de cod care tratează situaţiile normale faţă de cele care tratează excepţiile.

— rezolvă „dilema“ de la alternativa 2) de tratare clasică a erorilor, prin aceea că oferă suportul necesar pentru a „forţa“ o funcţie să returneze, în situaţii deosebite, valori ale altui tip decât cel specificat în antet ca tip returnat.

Se va ilustra în secvenţa de mai jos varianta „cu excepţii“ a metodei Tabel.Increm() din programul prezentat în paragraful anterior: class IndiceEronat extends Exception{ //clasa pt.

reprezentarea exceptiei private int lung; private int index_er; public IndiceEronat(int l,int i) {//constructor

lung=l; index_er=i; } public String toString(){

return "Indicele "+index_er+" eronat pentru tabloul de lungime "+ lung;

} } class Tabel{

Page 45: Java

4

public static int Increm(int[ ] tab,int indice,int val) throws IndiceEronat{ if(indice>=0 && indice<tab.length){

tab[indice]+=val; return tab[indice];

} else //emitere exceptie

throw new IndiceEronat(tab.length, indice); } //. . .

} class ClientTabel{

public static void main(String[ ] arg) { int[ ] tab = new int[Integer.parseInt(arg[0])];

//se completeaza tabloul cu valori int suma=0,indice,val; boolean gata=false; do{

//se citesc de la tastatura un indice si o valoare

try { //secventa sensibila la aparitia unei exceptii suma+=Tabel.Increm(tab,indice,val);

} catch(IndiceEronat e){ //tratare exceptie

System.out.println("Exceptie IndiceEronat: "+e);

} //. . .

}while(!gata); }

}

Ideea fundamentală care stă la baza mecanismului de emitere-captare a excepţiilor este aceea că dacă o funcţie oarecare F1 se confruntă cu o situaţie pe care nu o poate rezolva, ea va anunţa acest lucru apelantului ei, prin emiterea unei aşa-numite excepţii, „sperând“ că apelantul va putea (direct sau indirect) să rezolve problema. O funcţie F2 care doreşte să rezolve o situaţie de excepţie trebuie să-si manifeste intenţia de a capta excepţia emisă în situaţia respectivă. În exemplul de mai sus funcţia F1 este Tabel.Increm(), iar F2 este ClientTabel.main().

Limbajul Java pune la dispoziţie trei construcţii de bază pentru lucrul cu excepţii:

— instrucţiunea throw pentru emiterea excepţiilor;

— blocul catch pentru definirea secvenţei de captare şi tratare a unei excepţii; o asemenea secvenţă se mai numeşte handler de excepţii;

— blocul try pentru delimitarea secvenţelor de cod sensibile la apariţia excepţiilor.

Page 46: Java

5

Instrucţiunea throw

Ca sintaxă, această instrucţiune este asemănătoare cu return: throw expresie_exceptie;

unde expresie_exceptie are ca rezultat o referinţă la un obiect al unei clase derivate (direct sau indirect) din Throwable. De obicei, această expresie constă din aplicarea operatorului new pentru a se crea un obiect al clasei alese pentru reprezentarea excepţiei.

O instrucţiune throw poate să apară într-o funcţie numai dacă:

— ea se găseşte în interiorul unui bloc try-catch care captează tipul de excepţie generată de expresia din throw, sau

— definiţia funcţiei este însoţită de o clauza throws în care apare tipul de excepţie respectiv sau

— excepţia generată aparţine claselor RunTimeException sau Error, sau descendenţilor acestora

Dacă instrucţiunea throw nu apare într-un bloc try, efectul ei este asemănător unui return: se creează o excepţie care va fi „pasată“ spre apelantul funcţiei respective şi se produce revenirea din funcţie; funcţia însă NU va transmite NICI UN rezultat chiar dacă tipul returnat care apare în semnătura funcţiei nu este void. De exemplu, dacă funcţia Tabel.Increm() execută instrucţiunea throw, ea nu va returna nici un rezultat de tip int.

Clauza throws Această clauză se ataşează la antetul unei funcţii şi ea precizează tipurile de excepţii care pot fi generate pe parcursul execuţiei funcţiei respective: tip_returnat nume_functie(lista_param) throws tip_ex1,

tip_ex2,...,tip_exn

Dacă în lista de tipuri din clauza throws există cel puţin două tipuri, tip_exi şi tip_exj, derivate dintr-o superclasă comună tip_exsup, limbajul permite înlocuirea celor două cu tip_exsup. Este adevărat că, până la urmă, toate tipurile din lista throws derivă direct sau indirect din Exception (se poate spune chiar din Throwable sau Object), deci, teoretic clauza poate conţine doar această clasă. În practică este, însă, nerecomandat acest lucru, deoarece, aşa cum tip_returnat reprezintă tipul rezultatului returnat de funcţie la o execuţie normală, tot aşa, clauza throws oferă informaţii referitoare la situaţiile de excepţie care pot apărea în funcţia respectivă. Dacă în listă se trece doar Exception, informaţia este foarte vagă pentru cititorul programului.

Care tipuri de excepţie vor apărea într-o clauză throws? Dacă utilizatorul îşi defineşte propriile tipuri de excepţie, atunci este foarte probabil ca el să ştie în ce situaţii urmează să se genereze excepţiile respective şi, ca urmare, le va prevedea în clauze throws sau în secvenţe catch. În cazul excepţiilor

Page 47: Java

6

predefinite ale limbajului Java, acestea trebuie să apară în clauze throws ale funcţiilor care apelează metode din biblioteca Java care, la rândul lor, generează excepţiile respective. Consultând documentaţia claselor predefinite ale limbajului Java se poate constata că la fiecare metodă este indicată, unde e cazul, clauza throws. Pentru excepţiile din clasele RunTimeException şi Error, precum şi din descendenţii acestora nu sunt necesare clauze throws (v. exceptiile neverificate).

Dacă utilizatorul nu prevede clauza throws, şi nici secvenţe try-catch la o funcţie de a sa care apelează funcţii predefinite dotate cu clauze throws, compilatorul va sesiza acest lucru ca eroare. Aceasta este una dintre căile prin care utilizatorul poate afla ce ar fi trebuit să pună în clauza throws a funcţiei sale. O situaţie frecventă, care constituie exemplu în acest sens este cea a utilizării funcţiilor de intrare/ieşire din pachetul java.io, fără a prevedea clauze throws IOException.

Blocurile try-catch

Din punct de vedere sintactic un bloc try-catch are forma de mai jos: try {

//secventa obisnuita de operatii, care poate fi intrerupta

//de aparitia unei exceptii

}

catch(tip_ex1 e){

//tratare exceptie de tipul tip_ex1

}

catch(tip_ex2 e){

//tratare exceptie de tipul tip_ex2

}

//. . .

catch(tip_exn e){

//tratare exceptie de tipul tip_exn

}

finally {

//secventa optionala (despre finally vom vorbi intr-un

//paragraf urmator)

}

//. . .(*)

Page 48: Java

7

Un bloc try este urmat de 0, 1 sau mai multe blocuri catch şi, opţional, de un bloc finally. Vom presupune deocamdată că nu avem bloc finally. Modul de funcţionare al unei asemenea construcţii este următorul:

— dacă în decursul desfăşurării secvenţei de operaţii din blocul try este emisă o excepţie, fie direct (adică prin execuţia unui throw explicit), fie indirect (adică prin apelul unei funcţii care emite excepţia), secvenţa se întrerupe şi se baleiază lista de blocuri catch asociate pentru a-l detecta pe cel corespunzător tipului de excepţie emisă;

— dacă un astfel de bloc există, se va lansa în execuţie secvenţa de tratare respectivă.

— dacă această secvenţă nu trece printr-un return sau un throw, la terminarea ei execuţia programului continuă cu linia care urmează după ultimul bloc catch sau, după caz, finally ataşate blocului try curent (cea notata cu (*) în secvenţa de mai sus).

— tot de la acea linie se reia execuţia şi dacă secvenţa try a rulat fără să fi apărut vreo excepţie.

Când spunem bloc catch corespunzător unei excepţii, aceasta înseamnă că tipul specificat ca parametru după cuvântul catch este fie identic cu tipul excepţiei, fie este o superclasă a tipului excepţiei. Dacă nu există nici un bloc catch corespunzător excepţiei emise, aceasta va fi propulsată spre apelanţii funcţiei curente, căutându-se un eventual bloc try înconjurător. Practic, un bloc catch poate fi asimilat cu o funcţie anonimă, cu un singur parametru specificat imediat după cuvântul catch. Apelul unei asemenea funcţii nu se face explicit, ci automat, dacă în secvenţa try asociată apare o excepţie corespunzătoare. Un bloc catch se poate termina normal (adică prin epuizarea instrucţiunilor care îl compun) sau printr-un throw care să emită o nouă excepţie sau tot pe cea primită. În acest din urmă caz are loc retransmisia excepţiei.

Referitor la drumul parcurs de o excepţie din momentul emiterii sale şi până ajunge să fie tratată, lucrurile stau astfel:

— stiva de apeluri se baleiază dinspre vârf spre bază în căutarea unui bloc try înconjurător în raport cu instrucţiunea throw emitentă. Un bloc try este înconjurător faţă de o instrucţiune throw dacă aceasta se găseşte chiar în interiorul acelui bloc try sau dacă se află într-o funcţie apelată (direct sau indirect) din acel bloc try.

— dacă se găseşte un bloc try înconjurător, se caută între blocurile catch ataşate, unul ce corespunde tipului excepţiei emise. Dacă există un astfel de bloc catch, acesta preia excepţia. În caz contrar, excepţia este propagată mai departe, repetându-se procesul de la pasul anterior. Dacă se ajunge cu căutarea până în funcţia main() şi nici aici nu există un handler adecvat, excepţia este preluată de handler-ul de excepţii al maşinii virtuale care determină abandonul programului, cu afişarea unui mesaj corespunzător.

— dacă excepţia este preluată de un bloc catch şi acesta nu o retransmite, excepţia respectivă se consideră a fi tratată, indiferent de modul în care

Page 49: Java

8

handler-ul operează efectiv (în particular un handler poate avea corpul de instrucţiuni vid).

Procesul de baleiere a stivei de apeluri implică, practic, părăsirea tuturor funcţiilor „întâlnite“ în cale până la găsirea blocului try înconjurător. Cu ajutorul unui exemplu vom ilustra mecanismul de funcţionare a blocurilor try-catch şi a instrucţiunii throw: class oExceptie extends Exception{/*...*/} class altaExceptie extends Exception{/*...*/} class OClasa {

public static void oFunctie( ){ //. . . if(eroare) throw new oExceptie( );

}

public static void altaFunctie( ){ oFunctie( );

}

public static void siIncaUna( ){ //. . . try{ // 1

altaFunctie( ); } catch(oExceptie e){

if(poti_sa_rezolvi) fa_o( ); else throw e; // retransmisia exceptiei e

} catch(altaExceptie e){

if(alta_eroare) throw new altaExceptie(); else tratare_normala( );

} try{ // 2 //presupunem ca aici nu se apeleaza oFunctie( )

si nici altaFunctie( ) } catch(Exception e){ //capteaza orice exceptie derivata din Exception

aparuta in try 2 } //instructiunea imediat urmatoare blocului try-

catch 2 }//end siIncaUna()

public static void main(String[ ] arg) {

//. . . } //. . .

}end class OClasa

Presupunem că la un moment dat lanţul de apeluri este:

Page 50: Java

9

main() -> siIncaUna() -> altaFunctie() -> oFunctie()

şi că oFunctie() execută instrucţiunea throw. Se creează o excepţie de tip oExceptie şi se iese din oFunctie(), revenindu-se în altaFunctie(). Aici se constată că nu există bloc try şi, ca urmare, se iese din altaFunctie() ca şi cum aici ar exista un throw, şi se revine în siIncaUna(). De data aceasta revenirea are loc în interiorul unui bloc try (cel notat cu //1). Se caută printre ramurile sale catch şi se găseşte cea cu parametru de tip oExceptie. Dacă excepţia se poate rezolva, după execuţia secvenţei catch, programul continuă cu blocul try următor (cel notat cu //2). Dacă se ajunge la retransmisia excepţiei, atunci se iese din siIncaUna() şi se verifică dacă apelul la această funcţie nu se află în interiorul unui bloc try din apelant (funcţia main(), adică).

La execuţia unui throw din interiorul unui bloc catch nu se rebaleiază lista curentă de handler-e şi nici nu se trece la următorul bloc try-catch în aceeaşi funcţie, chiar dacă acesta din urmă ar putea capta excepţia emisă (cum ar fi cazul blocului try //2 din exemplul de mai sus). Există şi posibilitatea ca un bloc try-catch să fie inclus în secvenţa try a unui alt bloc try-catch, ca în exemplul următor: class oExceptie extends Exception{/*...*/} class altaExceptie extends Exception{/*...*/} class OClasa {

public static void oFunctie( ) throws oExceptie{ //. . . if(eroare) throw new oExceptie( );

}

public static void altaFunctie( ) throws oExceptie, altaExceptie{

try { //1 try { //2

oFunctie( ); } catch (altaExceptie e){

System.out.println("S-a captat altaExceptie"); //aici tratarea presupune pur si simplu o afisare de mesaj

} } catch(oExceptie e){

if(poti_sa_rezolvi) fa_o( ); else throw e; // retransmisia exceptiei e

} catch(altaExceptie e){

if(alta_eroare) throw new altaExceptie(); else tratare_normala( );

} }

public static void main(String[ ] arg) {

Page 51: Java

10

//. . . } //. . .

}//end class OClasa

În acest exemplu se observă că blocul try-catch notat cu //2 nu va capta excepţii de tipul oExceptie. Blocul try-catch notat cu //1 este considerat ca înconjurător pentru blocul //2 şi, deci, excepţia emisă de oFunctie() va ajunge să fie tratată de handler-ul corespunzător ataşat blocului //1. Astfel, un bloc try este înconjurător pentru un throw dacă secvenţa try

— conţine în mod direct acel throw, sau

— conţine un bloc try-catch în care se află acel throw, sau

— conţine rădăcina apelului la funcţia în care se află acel throw.

Secvenţa finally

Dacă este prezentă, se execută întotdeauna după ce s-a terminat secvenţa try sau secvenţele catch din blocul try-catch curent. Practic, nu există posibilitatea de a determina desfăşurarea execuţiei unui bloc try prevăzut cu ramura finally în sensul „şuntării“ acestei secvenţe. Secvenţa finally reprezintă un mecanism prin care se forţează execuţia unei porţiuni de cod indiferent dacă o excepţie a apărut sau nu. De obicei, ramura finally are rolul de a „face curat“, în sensul de a elibera resurse de genul fişiere deschise accesate prin intermediul unor variabile locale. public boolean Cautare(String nume_fis,String cuvant )

throws IOException{ BufferedReader flux_in = null; try{

flux_in = new (new InputStreamReader(new FileInputStream(nume_fis)));

String linie; while ((linie = flux_in.readLine()) != null)

if(linie.equals(cuvant)) return true; return false; //nu s-a gasit cuvantul in fisier

} finally {

if(flux_in != null) flux_in.close(); }

}

Primul lucru pe care îl constatăm la programul prezentat este absenţa ramurii catch. Aceasta înseamnă că programatorul nu are intenţia să trateze excepţiile de tip IOException în cadrul funcţiei Cautare(). Mai mult, acest program este un exemplu de utilizare a blocului try nu neapărat pentru a rezolva situaţii de eroare, ci, datorită blocului finally, mai mult pentru a determina o anumită înlănţuire a operaţiilor. De altfel, trebuie să ne obişnuim cu ideea că o excepţie nu este întotdeauna un indiciu de eroare, ci, uneori, este pur şi simplu un semn că în desfăşurarea programului a intervenit ceva deosebit care trebuie tratat altfel.

Page 52: Java

11

În secvenţa de mai sus dacă fişierul nu ar fi putut fi deschis (de exemplu nume_fis nu există) atunci referinţa flux_in ar fi rămas pe null. De aceea s-a prevăzut testul din blocul finally. Presupunând că deschiderea fişierului ar fi mers cum trebuie, în continuare, indiferent de modul în care se termină blocul try (pe unul dintre cele două return-uri sau prin apariţia unei excepţii generate de funcţia readLine()) prezenţa ramurii finally asigură închiderea fişierului înainte de părăsirea efectivă a funcţiei Cautare(). Ramura finally se mai poate utiliza şi pentru a forţa execuţia unei porţiuni de cod când în try avem bucle ce conţin combinaţii de break, continue şi return.

Excepţii predefinite ale limbajului Java

Există două clase principale de excepţii predefinite: Error şi Exception. Excepţiile reprezentate de clasele descendente din Error indică, în general, probleme foarte serioase care trebuie să ducă la întreruperea programului şi nu se recomandă captarea lor de către utilizator. Ele sunt generate automat de suportul de execuţie al programelor Java, la întâlnirea situaţiilor de eroare respective. Clasele Error şi RunTimeException, împreună cu descendenţii lor, formează categoria excepţiilor neverificate (unchecked), adică excepţii care pot fi generate în programe, fără obligativitatea ca ele să apară în clauze throws.

Restul excepţiilor sunt verificate (checked), adică la compilare se verifică dacă există clauze throws corespunzătoare. Un exemplu în acest sens este clasa IOException (definită în pachetul java.io), care trebuie specificată fie în clauze throws, fie în ramuri catch, acolo unde se folosesc funcţiile de intrare/ieşire din pachetul java.io.

Programatorul poate să-şi definească anumite clase de excepţii ca descendente din RuntimeException, făcându-le, astfel, neverificate. Acest lucru trebuie evitat, deoarece lipsa clauzei throws din declaraţia unei funcţii care totuşi generează excepţii, este, de fapt, o lipsă de informaţie pentru potenţialii utilizatori ai funcţiei respective.

În exemplul care urmează vom arata cum putem folosi mecanismul excepţiilor pentru a rezolva problema depăşirii limitelor unui tablou. Practic, vom completa elementele unui tablou fără ca, la parcurgerea tabloului, să aplicăm comparaţia clasică a indicelui faţă de lungimea tabloului (i < tab.length). Aici ne vom folosi de o clasă de excepţii predefinită, IndexOutOfBoundsException, din pachetul java.lang: import java.io.*; public class Tablou {

public static void main (String[ ] args) { int [ ] tab = new int[10]; int i; try {

for(i=0;;) tab[i++]=i; //parcurg tabloul fara grija

} catch(IndexOutOfBoundsException e) {

//ajung aici cand i >=tab.length System.out.println(e);

Page 53: Java

12

System.out.println("Gata tabloul!"); }

} }

Se observă că aici nu avem instrucţiune throw, adică nu avem o emitere explicită a excepţiei. Aceasta, deoarece în anumite situaţii, cum este şi depăşirea limitelor tablourilor, excepţiile sunt generate automat, de către suportul de execuţie Java. Excepţia IndexOfBoundException este derivată din RuntimeException, deci este neverificată. Aceasta înseamnă că şi dacă în programul de mai sus nu am fi prevăzut ramura catch pentru ea, nu am fi fost obligaţi să ataşăm funcţiei main() o clauză throws IndexOutOfBoundsException.

Temă Să se scrie un program care citeşte de la tastatură perechi de numere în care primul număr trebuie să fie mai mic decât al doilea. Dacă această condiţie nu este îndeplinită, folosind mecanismul excepţiilor, se va semnala eroare şi se va trata această eroare prin cererea unei alte perechi de numere. Toate perechile de numere care îndeplinesc condiţia vor fi scrise într-un fişier primit ca parametru la apel.

Page 54: Java

1

Lucrarea 7

Colecţii de obiecte Cuprins Interfaţa Collection ......................................................................................................................................... 1 Interfaţa List ................................................................................................................................................... 3 Interfaţa Set..................................................................................................................................................... 3 Iteratori ........................................................................................................................................................... 4 Comparatori.................................................................................................................................................... 5 Temă............................................................................................................................................................... 6

De regulă, un program Java va crea întotdeauna obiecte noi, pe baza unor criterii care sunt cunoscute abia în momentul rulării. Din acest motiv, este necesară posibilitatea de a crea oricâte obiecte, oricând şi oriunde. Pentru a rezolva această problemă, Java pune la dispoziţia programatorilor câteva metode de a reţine obiectele (sau, mai degrabă, referinţele la obiecte). Tipul predefinit în acest scop este tabloul, pe lângă care se mai găseşte un set complet de clase container care pun la dispoziţia programatorului metode sofisticate de a reţine şi chiar de a manipula obiectele.

Biblioteca de containere Java reţine obiectele programatorului şi le divide în două concepte distincte:

a) colecţie (Collection) — un grup de elemente individuale, asupra cărora pot fi aplicate anumite reguli. O listă (List) conţine elemente într-o anumită secvenţă, iar o mulţime (Set) nu poate avea duplicate ale elementelor.

b) hartă (Map) — un grup de perechi de obiecte cheie-valoare. La o primă privire, s-ar părea că o astfel de hartă este o colecţie de perechi, dar când se încearcă implementarea într-o asemenea manieră designul devine dificil. Pe de altă parte, este mai convenabil să se poată lucra cu porţiuni ale unei hărţi, prin crearea unei colecţii care să lucreze cu acea porţiune. Astfel, o hartă poate returna o mulţime a cheilor sale, o colecţie a valorilor sale, sau o mulţime a perechilor sale.

În continuare, vor fi prezentate metodele componente ale interfeţelor Collection, List, Set (care moştenesc interfaţa Collection). Toate acestea se află în pachetul predefinit din Java numit Java.util.

Interfaţa Collection Prezintă următoarele metode: add(element: Object): boolean; addAll(collection: Collection): boolean;

Page 55: Java

2

clear(): void; contains(element: Object): boolean; containsAll(collection: Collection): boolean; equals(object: Object): boolean; hashCode(): int; iterator(): Iterator; remove(object: Object): boolean; removeAll(collection: Collection): boolean; retainAll(collection: Collection): boolean; size(): int; toArray(): Object[]; toArray(array: Object[]): Object[];

Semantica metodelor este următoarea:

— metodele add() şi addAll() adaugă la colecţie obiectul dat ca parametru (colecţia dată ca parametru); rezultatul returnat este true dacă adăugarea a avut loc, colecţia originală modificându-şi conţinutul în acest caz;

— metoda clear() goleşte colecţia;

— metodele contains() şi containsAll() verifică dacă în colecţie se găseşte obiectul dat ca parametru (colecţia dată ca parametru); pentru a verifica existenţa unui element, la comparare se aplică automat metoda equals() specifică fiecărui obiect al colecţiei;

— metoda equals() verifică echivalenţa a două colecţii, astfel: două liste sunt echivalente dacă au aceeaşi lungime şi conţin aceleaşi elemente, în aceeaşi ordine, iar două mulţimi sunt echivalente dacă au aceleaşi elemente, indiferent de ordinea în care ele sunt plasate fizic în colecţii;

— metodele remove() şi removeAll() elimină din colecţie obiectul dat ca parametru (colecţia dată ca parametru), dacă acestea există în colecţia pentru care se execută metodele; verificarea existenţei unui element se face folosind metoda equals(); rezultatul returnat este true dacă eliminarea a avut loc, colecţia iniţială modificându-şi conţinutul;

— metoda retainAll() elimină din colecţie toate elementele, exceptând elementele care se regăsesc şi în colecţia dată ca parametru; rezultatul returnat este true dacă eliminarea a avut loc, colecţia iniţială modificându-şi conţinutul;

— metoda toArray() crează şi returnează un tablou care va conţine elementele colecţiei primită ca parametru;

— metoda toArray(Object[]) va plasa elementele colecţiei în tabloul dat ca parametru dacă tabloul are dimensiunea acoperitoare faţă de mărimea colecţiei şi dacă tipul concret al elementelor tabloului sunt supertip pentru toate elementele colecţiei; dacă a doua condiţie nu este îndeplinită, va fi emisă o excepţie de tip ArrayStoreException, iar dacă doar prima condiţie nu este îndeplinită, metoda creează un nou tablou în care plasează

Page 56: Java

3

elementele colecţiei; metoda va returna o referinţă la tabloul în care au fost depuse elementele colecţiei;

— metoda iterator() creează şi returnează un obiect Iterator, ataşat colecţiei respective;

— metoda hashCode() returnează codul hashing asociat colecţiei curente; codurile hashing asociate obiectelor sunt utilizate atunci când obiectele respective constituie elemente ale unor tabele hashing.

Nu există clase concrete care să implementeze interfaţa Collection, ci doar clase care implementează interfeţele List şi Set.

Interfaţa List Pe lângă metodele moştenite de la interfaţa Collection, interfaţa List prezintă şi următoarele metode proprii: add(index: int, element: Object): boolean; addAll(index: int, collection: Collection): boolean; get(index: int): Object; indexOf(element: Object): int; lastIndexOf(element: Object): int; listIterator(startIndex: int): ListIterator; listIterator(): ListIterator; remove(index: int): Object; set(index: int, element: Object): Object; subList(fromIndex: int, toIndex: int): List;

Clasele predefinite care implementează interfaţa List sunt ArrayList şi LinkedList. Clasa ArrayList este indicată atunci când se doreşte acces aleator pentru consultarea elementelor listei şi nu este necesar ca adăugările/eliminările să se facă la indice specificat. Clasa LinkedList este mai potrivită atunci când operaţiile cele mai frecvente sunt adăugări/eliminări în/din listă. Consultările presupun parcurgeri secvenţiale ale listei. Clasa LinkedList are câteva metode specifice (metode care permit prelucrarea elementelor aflate la cele capetele ale listei), care permit modelarea cu uşurinţă a unor structuri de date precum stiva sau coada: addFirst(element: Object): void ; addLast(element: Object): void; getFirst(): Object; getLast(): Object ; removeFirst(): Object; removeLast(): Object;

Interfaţa Set Este implementată de clasa HashSet şi este extinsă de interfaţa SortedSet care, la rândul ei este implementată de clasa TreeSet. SortedSet este un caz particular de

Page 57: Java

4

mulţime, asupra elementelor fiind definită o relaţie de ordine totală. Pe lângă declaraţiile din Set, SortedSet mai conţine: comparator(): Comparator; first(): Object; headSet(toElement: Object): SortedSet; last(): Object; subSet(fromElement: Object, toElement: Object): SortedSet; tailSet(fromElement: Object): SortedSet;

Metoda comparator() returnează o referinţă la obiectul utilizat pe post de comparator în mulţimea curentă. Rolul comparatorului constă în a indica relaţia de ordine în care se află două elemente ale mulţimii, în vederea plasării lor în mulţimea ordonată. Elementele unei colecţii de tip SortedSet trebuie să implementeze interfaţa Comparable sau trebuie să poată fi comparate între ele cu ajutorul unui obiect care implementează interfaţa Comparator.

Obiectele elemente ale unei structuri de tip Set trebuie să aibă implementată metoda hashCode(). Pentru operaţii obişnuite cu mulţimi se va folosi clasa HashSet, care este mai eficientă. Clasa TreeSet se utilizează atunci când se doreşte extragerea elementelor într-o anumită ordine.

Pentru exemplificare, se prezintă un program care construieşte o mulţime de şiruri folosind clasa HashSet, apoi afişează elementele mulţimii în ordine alfabetică, folosind o structură TreeSet. class Multime{

public static void main(String []arg){ Set oMultime = new HashSet(); oMultime.add("Popescu"); oMultime.add("Ionescu"); oMultime.add("Georgescu"); oMultime.add("Ixulescu"); System.out.println(oMultime); Set multimeOrdonata = new TreeSet(oMultime); System.out.println(multimeOrdonata);

} }

Iteratori Un iterator este un obiect care poate realiza traversarea într-o anumită ordine a colecţiei la care este ataşat. În Java există o interfaţă predefinită, Iterator, care conţine următoarele declaraţii: interface Iterator {

boolean hasNext(); Object next(); remove();

Page 58: Java

5

}

În cazul claselor concrete predefinite, iteratorul ataşat poate parcurge colecţia respectivă în ordinea în care elementele au fost adăugate, respectiv în ordinea indusă de relaţia de ordine existentă în cazul unei colecţii de tip SortedSet. În cazul listelor, pe lângă iteratorii obişnuiţi există şi iteratori de tip ListIterator (un subtip al lui Iterator). Interfaţa ListIterator aduce, în plus, următoarele metode: interface ListIterator extends Iterator {

void add(Object element); boolean hasPrevious(); int nextIndex(); Object previous(); int previousIndex(); void set(Object element);

}

Iteratorii de tip ListIterator pot să parcurgă o listă în ambele sensuri.

Comparatori În cazul colecţiilor sortate (SortedSet sau SortedMap) , atunci când se adaugă elemente, locul lor este stabilit cu ajutorul metodei compareTo() pe care trebuie să o posede obiectul de adăugat. La adăugare se testează dacă obiectul este de tip Comparable, după care se apelează metoda compareTo(). Dacă obiectul nu este de tip Comparable, se va genera o excepţie ClassCastException.

Relaţia de ordine stabilită cu ajutorul metodei compareTo() se numeşte ordonare naturală a clasei care conţine această metodă, iar metodei i se spune metoda de comparare naturală. Se spune că ordonarea naturală a unei clase este consistentă în raport cu equals() dacă şi numai dacă expresia ob1.compareTo((Object)ob2)==0 are aceeaşi valoare booleană cu expresia ob1.equals((Object)ob2) oricare ar fi obiectele ob1 şi ob2 ale clasei implicate.

Pentru clase definite de programator este obligatorie definirea metodei compareTo() şi equals(). Se recomandă ca definiţiile celor două metode să se facă astfel încât să se asigure consistenţa ordonării naturale faţă de equals(). În continuare, este prezentat un exemplu de clasă definită de programator, care implementează interfaţa Comparable, respectând recomandările enunţate mai sus: import java.util.*; class Coordonate implements Comparable {

private double x; private double y; public Coordonate(double x, double y) {

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

} public double distToO( ) {

Page 59: Java

6

//calculeaza distanta fata de origine return Math.sqrt(x*x+y*y);

} public boolean equals (Coordonate c) {

return (x==c.x && y==c.y); } public int compareTo(Object c) {

if(!(c instanceof Coordonate)) //nu pot stabili o relatie de ordine intre

obiecte de clase diferite throw new ClassCastException( );

Coordonate rc=(Coordonate)c; if (equals(rc)) return 0; double d1=distToO(); double d2=rc.distToO() if(d1>d2) return 1; else if(d1<d2) return -1; // cazul d1 == d if(x>rc.x) return 1; else if(x<rc.x) return -1; // cazul d1 == d2 si x == rc. if(y>rc.y) return 1; return -1;

} public String toString() {

return "("+x+","+y+")"; } //alte metode }

class Client { public static void main(String[] args) {

Coordonate c1=new Coordonate(1,2); Coordonate c2=new Coordonate(3,2); Coordonate c3=new Coordonate(2,4); SortedSet s=new TreeSet(); s.add(c1); s.add(c2); s.add(c3); System.out.println(s);

} }

Temă Se cere să se scrie un program care să ţină evidenţa studenţilor dintr-un an de studiu. Fiecare student este înregistrat cu prenume, nume, medie şi grupa din care face parte. Citirea studenţilor se face de la tastatură sau dintr-un fişier şi se vor crea obiecte care vor

Page 60: Java

7

fi incluse într-o colecţie. Se cere să se listeze alfabetic studenţii dintr-o anumită grupă, precum şi studenţii cu media cea mai mică şi cu media cea mai mare din grupa respectivă. De asemenea, se cere să se listeze studenţii cu media mai mare decât o valoare citită de la tastatură. Afişarea se va face astfel:

Prenume NUME Media Grupa

Ion POPESCU 9.50 4

Page 61: Java

1

Lucrarea 8 Pachete

Cuprins Organizarea programelor Java în pachete ..................................................................................................................... 1 Numele pachetelor şi ale claselor dintr-un pachet ........................................................................................................ 1 Accesul la clasele unui pachet ...................................................................................................................................... 2 Accesul la membrii claselor din pachete diferite .......................................................................................................... 3 Gestionarea fişierelor sursă ale programelor structurate în pachete.............................................................................. 4 Temă ............................................................................................................................................................................. 6

Organizarea programelor Java în pachete Necesitatea structurării în pachete apare în condiţiile creşterii nivelului de complexitate al aplicaţiilor scrise în Java. Practic, pachetele reprezintă materializarea la nivelul limbajului Java a unuia dintre conceptele fundamentale ale tehnologiei OO: modularizarea. În cele ce urmează se vor prezenta construcţiile de limbaj care permit modularizarea programelor Java. Un program Java este o colecţie de clase şi interfeţe grupate opţional în pachete (packages). Clasele care formează un pachet, la rândul lor, pot fi repartizate într-unul sau mai multe fişiere sursă.

Faptul că o clasă NumeClasa aparţine unui pachet cu numele numePachet se specifică printr-o clauză package, sub forma: package numePacket;

plasată pe PRIMA linie a fişierului sursă în care se află clasa NumeClasa. Dacă pachetul numePachet este format din mai multe clase repartizate în mai multe fişiere sursă, toate acele fişiere trebuie să conţină pe prima linie o clauză package ca cea de mai sus. Deoarece clauza package nu poate figura decât pe prima linie a unui fişier sursă, este clar că în acelaşi fişier NU pot apărea clase care să aparţină unor pachete diferite. În cazul unui program nestructurat pe pachete (fără clauze package), clasele componente sunt considerate ca aparţinând implicit unui singur pachet anonim (unnamed). Acesta este modul în care au fost scrise toate programele Java de până acum. În general, vor fi grupate mai multe clase într-un pachet dacă acele clase modelează părţi ale aceluiaşi concept (de exemplu, pachetul java.io conţine clasele cu ajutorul cărora se modelează operaţiile de intrare/ieşire, iar pachetul java.awt conţine clasele cu care se pot reprezenta componente ale unei interfeţe grafice).

Numele pachetelor şi ale claselor dintr-un pachet Întrucât până acum am scris doar programe "neîmpachetate", referirea numelui unei clase s-a făcut pur şi simplu folosind identificatorul respectiv, ales de programator. De fapt, numele complet al unei clase în Java este format astfel: numePacket.numeClasa

Page 62: Java

2

În majoritatea situaţiilor se poate evita folosirea numelui complet al unei clase. În ceea ce priveşte numele unui pachet, acesta poate fi un identificator obişnuit sau o secvenţă de mai mulţi identificatori, separaţi între ei prin caracterul punct: PachetulNostru pachetulNostru.pachetulMeu

pachetulNostru.pachetulMeu.pachetelulMeu.

Când numele unui pachet este de forma nume1.nume2 şi există un alt pachet numit nume1, spunem că nume1.nume2 este un subpachet al lui nume1. Spre deosebire de relaţia clasă– subclasă, relaţia pachet–subpachet este doar la nivel de nume, ea NU implică nici un fel de drepturi speciale ale subpachetului asupra pachetului.

Accesul la clasele unui pachet Pentru ca o clasă OClasa dintr-un pachet unPachet să fie accesibilă într-un alt pachet altPachet, este necesar ca definiţia clasei OClasa să fie precedată de modificatorul de acces public. Pentru clasele pe care le-am scris până acum nu ne-a interesat modul de acces deoarece ele oricum nu erau destinate a fi utilizate de alte pachete. Presupunând că OClasa este cu acces public, atunci în cadrul pachetului altPachet ea va putea fi referită utilizând numele ei complet, adică: unPachet.OClasa

Putem să referim OClasa şi pe numele ei, cu condiţia ca în fişierul sursă în care apare referinţa, să existe clauza import: import unPachet.OClasa;

Clauza import se plasează înaintea definiţiilor de clase, dar după o eventuală clauză package: // un fişier sursă package unPachet; public class OClasa {

//. . . } ----------------------------------------- // alt fişier sursă package altPachet; import unPachet.OClasa; class AltaClasa{

//. . . public static void oMetoda() {

OClasa r = new OClasa(); //. . .

} } // Obs: se presupune că în pachetul altPachet nu există o clasa cu numele OClasa; în caz contrar este necesara denumirea lunga unPachet.OClasa pentru a rezolva conflictul.

Page 63: Java

3

Clauza import poate avea şi forma: import unPachet.*;

În acest caz, în fişierul care conţine clauza vor putea fi referite direct toate clasele publice ale pachetului unPachet. Clasele dintr-un pachet care nu sunt publice nu vor putea fi utilizate în afara pachetului; ele nu pot face obiectul clauzelor import, nefiind „subînţelese“ nici de varianta cu * a clauzei. Trebuie spus că TOATE clasele predefinite ale mediului Java sunt grupate în pachete. Pentru multe dintre aceste clase, însă, nu ne-am pus problema să aflăm din ce pachet fac parte, ci le-am folosit pur şi simplu. Majoritatea claselor predefinite folosite până acum şi care constituie clase fundamentale pentru un program Java fac parte dintr-un pachet numit java.lang. Acest pachet este SINGURUL pentru care nu este necesară clauza import, respectiv utilizarea denumirii lungi a claselor. Clasele String, Math, clasele înfăşurătoare sunt exemple de clase din pachetul java.lang.

Putem considera că un pachet, la fel ca şi o clasă, are o interfaţă şi o implementare. Interfaţa este constituită din clasele publice, accesibile altor pachete (clienţii), iar implementarea — din restul claselor care nu sunt accesibile în afara pachetului.

Accesul la membrii claselor din pachete diferite Acest aspect va fi ilustrat cu ajutorul unui exemplu (făcând abstracţie de repartizarea fizică a claselor pe fişiere sursă): package pachet1; public class Clasa11 {

private int Vpriv; int Vpack; // când modificatorul de acces lipseşte, spunem că

membrul respectiv este cu acces de tip package protected int Vprot; public int Vpub; // . . .

} class Clasa12 { Clasa11 ob; public Clasa12(){

ob = new Clasa11(); ob.Vpriv = 1; // eroare ob.Vpack = 2; // acceptabil ob.Vprot = 3; // idem ob.Vpub = 4; // idem

} // . . . } //-----alt fisier sursa --------- package pachet2; import pachet1.*; class Clasa21{ Clasa11 ob;

public Clasa21() { ob = new Clasa11(); ob.Vpriv = 1; // eroare

Page 64: Java

4

ob.Vpack = 2; // idem ob.Vprot = 3; // idem ob.Vpub = 4; // acceptabil

} // . . .

} class Clasa22 extends Clasa11 {

public Clasa22() { Vpriv = 1; // eroare Vpack = 2; // idem Vprot = 3; // acceptabil Vpub = 4; // idem

} //. . .

}

În concluzie, regulile care guvernează drepturile de acces ale unei clase C2 la membrii altei clase C1 sunt:

— dacă C1 şi C2 fac parte din acelaşi pachet, atunci clasa C2 are acces la toţi membrii lui C1, mai puţin la cei privaţi.

— dacă C1 şi C2 se află în pachete distincte, atunci clasa C2 poate accesa doar membrii publici ai lui C1.

— dacă C1 şi C2 se află în pachete distincte, iar C2 moşteneşte clasa C1, atunci C2 poate accesa membrii publici şi protejaţi moşteniţi de la C1.

Aici se impune o observaţie cu privire la subpachete (pachete care au ca nume o secvenţă de identificatori separaţi prin punct), şi anume: din punct de vedere al regulilor de vizibilitate un pachet şi un subpachet al său se comportă ca şi cum ar fi două pachete distincte absolut oarecare. De altfel, exceptând numele, cele două pachete pot să nu aibă nimic în comun. Mai mult: un subpachet poate exista şi de unul singur, în sensul că se poate denumi un pachet sub forma nume1.nume2 fără a fi necesar să existe un alt pachet nume1.

Posibilitatea ca numele unui pachet să fie o concatenare de identificatori a decurs mai degrabă din necesitatea evitării unor conflicte de nume atunci când la elaborarea unor aplicaţii participă mai mulţi proiectanţi, posibil situaţi departe unul de altul din punct de vedere geografic. Atunci când totuşi există un pachet cu mai multe subpachete, se obişnuieşte ca subpachetele să conţină clase care modelează cazuri particulare sau aspecte diferite ale aceluiaşi concept modelat de pachet în ansamblu. De exemplu, pachetul java.awt conţine clase pentru modelarea componentelor grafice ale unei interfeţe utilizator, iar java.awt.event conţine clasele necesare pentru partea de tratare a evenimentelor generate de componentele grafice.

Gestionarea fişierelor sursă ale programelor structurate în pachete

Considerăm următoarea configuraţie de pachete: // un pachet package pachet1; public class Clasa11 {

// . . .

Page 65: Java

5

} class Clasa12 {

// . . . } class Clasa13 {

// . . . } // alt pachet package pachet2; class Clasa21{

// . . . } public class Clasa22 {

// . . . }

Se pune problema cum repartizăm aceste clase în fişiere sursă. În primul rând, va trebui să ţinem cont de o regulă: clasele publice dintr-un pachet trebuie să constituie fiecare câte un fişier sursă SEPARAT, numele fiecărui fişier fiind format din numele clasei urmat de extensia .java. Această regulă nu se aplică în cazul pachetului anonim. Clasele nepublice ale unui pachet pot să se afle toate într-un singur fişier sursă, numele acestuia nefiind impus. Astfel, pentru exemplul de mai sus ar fi necesare următoarele fişiere sursă:

— Clasa11.java pentru clasa pachet1.Clasa11

— un fişier cu un nume la alegere, pentru clasele pachet1.Clasa12 şi pachet1.Clasa13

— Clasa22.java pentru clasa pachet2.Clasa22

— un fişier cu un nume la alegere pentru clasa pachet2.Clasa21

În ceea ce priveşte fişierele .class rezultate din compilare, regula este: aceste fişiere vor fi plasate în directoare ale căror nume coincid cu numele pachetelor din care fac parte clasele respective. Pentru exemplul de mai sus acest lucru ar însemna că în directorul de lucru să se creeze 2 subdirectoare cu numele pachet1 şi pachet2 în care să se depună fişierele .class după cum urmează:

Clasa11.class, Clasa12.class şi Clasa13.class în directorul pachet1

Clasa21.class şi Clasa22.class în directorul pachet2

În cazul în care numele unui pachet ar fi de forma pac1.pac2, atunci pentru fişierele .class corespunzătoare trebuie creat lanţul de subdirectoare pac1/pac2. La prima vedere am putea concluziona că lucrul cu pachetele ne-ar complica teribil viata: pe de o parte ar trebui să dam mai multe comenzi de compilare, deoarece avem mai multe fişiere sursă, iar pe de altă parte ar trebui să mai şi gestionăm o mulţime de directoare şi subdirectoare. În realitate, comenzile de compilare şi lansare în execuţie dispun de parametri care ne uşurează foarte mult munca în contextul pachetelor.

Astfel, admiţând că toate fişierele sursă care compun o aplicaţie se află in acelaşi director şi că acest director este chiar directorul curent de lucru, atunci comanda de compilare va fi:

Page 66: Java

6

javac -d nume_director *.java

Parametrul -d nume_director ne spune că structura de subdirectoare în care vor fi depuse fişierele .class se va crea începând din directorul nume_director. Dacă dorim ca structura de subdirectoare să fie creată începând cu directorul curent de lucru, atunci comanda va fi: javac -d . *.java

Această structură va fi creată automat, folosindu-se numele pachetelor. La execuţie, dacă suntem pozitionaţi în directorul în care s-a creat arborele de directoare cu fişiere .class, vom da comanda: java nume_pachet.nume_clasa_radacina

Cu alte cuvinte, la execuţie se indică numele complet al clasei.

Temă

Să se scrie un program care gestionează o bibliotecă universitară şi care are următorul meniu:

— adăugarea, ştergerea, căutarea unui client, afişarea tuturor clienţilor

— adăugarea, ştergerea unei cărţi, listarea cărţilor unui anumit autor

— împrumutarea şi restituirea unei cărţi, listarea cărţilor împrumutate de un anumit client

În acest scop, se vor implementa trei subsisteme (fiecare în câte un pachet):

— un subsistem pentru evidenţa clienţilor (care pot fi studenţi, cadre didactice sau externi). Pentru fiecare client interesează prenume, nume, vârsta, adresă, facultatea şi anul de studiu (student), facultatea şi gradul didactic (cadru didactic), locul de muncă şi funcţia (extern);

— un subsistem pentru evidenţa cărţilor din bibliotecă. Interesează titlul, autorul, anul apariţiei, editura, numărul de inventar;

— un subsistem care să le integreze pe celelalte două subsisteme şi să ţină evidenţa împrumuturilor şi a restituirilor.

Page 67: Java

1

Lucrarea 9

Interfeţe grafice

Cuprins Crearea ferestrei unei aplicaţii....................................................................................................................................2 Tratarea evenimentelor în Java...................................................................................................................................2 Crearea unor butoane într-o fereastră .........................................................................................................................4 Introducerea textului de la tastatură ...........................................................................................................................6 Temă ...........................................................................................................................................................................9

În cazul programelor pe care le-am făcut până acum, toate mesajele Õi răspunsurile apăreau ca linii de text succesive, care defilau pe ecran (ecranul era folosit în mod text). Un astfel de stil de comunicare nu este atractiv pentru utilizatori, motiv pentru care se preferă dialogul prin interfeţe grafice sau GUI (Graphical User Interface) (ecranul este folosit în mod grafic).

Componentele unei interfeţe grafice se numesc elemente de control (widgets) Õi pot fi activate de către utilizator cu ajutorul mausului sau al tastaturii. Activarea unui element de control determină un anumit răspuns din partea programului, răspuns concretizat prin execuţia unei anumite operaţii. Cele mai răspândite interfeţe grafice sunt cele bazate pe ferestre, adică fiecărei aplicaţii active la un moment dat i se alocă o zonă dreptunghiulară pe ecran, numită fereastră. În interiorul ferestrei respective, se vor găsi toate elementele de control necesare dialogului utilizator-aplicaţie, precum Õi informaţiile afişate de aplicaţie.

Pentru ca o aplicaţie care foloseşte interfaţa grafică să poată funcţiona, este necesară existenţa unei platforme sau server grafic care să determine intrarea monitorului în regim grafic Õi care să ofere funcţiile primitive necesare, între altele, pentru desenarea componentelor GUI Õi pentru receptarea semnalelor generate de maus Õi tastatură. În sprijinul programatorilor au fost create suporturi software care permit ca o aplicaţie grafică să poată fi construită din `pieseA standardizate, gata create.

De regulă, un suport software pune la dispoziţia programatorului următoarele elemente:

C o bibliotecă de elemente de control (bare de defilare, chenare, butoane etc.). Această bibliotecă eliberează programatorul de sarcina de a desena Õi colora de fiecare dată toate elementele GUI ale unei aplicaţii;

C un manager de ferestre: acesta este cel care impune cum `aratăA elementele GUI, dictează modul în care este asamblată Õi echipată o fereastră, precum Õi modul în care va fi manipulată. Mediul Windows are încorporat un astfel de manager de ferestre, care defineşte aspectul general al unei ferestre Windows. Mediul Linux este mai configurabil din acest punct de vedere, permiţând utilizatorului o gamă mai variată de manageri de ferestre: GNOME, KDE, FVWM, etc.;

C o bibliotecă de funcţii sau clase predefinite. Această bibliotecă permite construirea interfeţei grafice, pornind de la fereastra aplicaţiei, conform standardului impus de managerul de ferestre instalat pe maşina pe care rulează aplicaţia. Limbajul Java

Page 68: Java

2

oferă pachetul predefinit java.awt (Abstract Window Toolkit), împreună cu o serie de subpachete ale acestuia, care încorporează clasele necesare pentru a construi aplicaţii grafice.

Crearea ferestrei unei aplicaţii O aplicaţie grafică are o fereastră principală (frame) Õi una sau mai multe ferestre subordonate celei principale. Clasa java.awt.Frame este cea care conţine elementele de bază necesare construirii ferestrei principale. În secvenţa de mai jos se creează Õi se afişează o fereastră principală amplasată în mijlocul ecranului Õi a cărei suprafaţă este 1/4 din suprafaţa ecranului.

import java.awt.*;

class FereastraMea extends Frame {

public FereastraMea(String titlu) {

super(titlu); Toolkit t = Toolkit.getDefaultToolkit(); Dimension d = t.getScreenSize(); int h = d.height; int w = d.width; setSize(w/2, h/2); setLocation(w/4, h/4);

} } class PrimaFereastra {

public static void main(String[] args) {

Frame f = new FereastraMea("Prima fereastra"); f.show();

} } Orice acţionare de către utilizator a unui element GUI prin intermediul mauslui sau al tastaturii determină apariţia unui eveniment, iar elementul GUI acţionat este sursa evenimentului. Pentru că acţionarea unui element GUI să se soldeze cu un anumit răspuns, programatorul trebuie să rezolve partea de tratare a evenimentului. Aceasta înseamnă că cineva trebuie să recepţioneze evenimentul Õi apoi să execute o anumită funcţie.

Tratarea evenimentelor în Java La acţionarea unui element GUI se creează un obiect al unei clase predefinite din pachetul java.awt.Event. Dacă există un obiect receptor (listener) al evenimentului creat Õi acest obiect este cunoscut de către sursa evenimentului, atunci se încearcă apelarea unei metode a receptorului căreia i se transmite ca parametru obiectul eveniment. Dacă programatorul a definit acea metodă, execuţia ei reprezintă tratarea evenimentului.

Page 69: Java

3

Obiectele eveniment În momentul acţionării, fiecare element GUI determină un eveniment specific. De exemplu, diversele acţiuni asupra unei ferestre conduc la crearea unui obiect al clasei java.awt.event.WindowEvent. Apăsarea unui buton duce la crearea unui obiect al clasei java.awt.event.ActionEvent.

Receptorul evenimentului Receptorul de eveniment este un obiect care are metode cu nume prestabilite, ale căror definiţii se găsesc în interfeţe cu nume prestabilite. Există câte o interfaţă specifică fiecărui tip de eveniment. De exemplu, pentru recepţionarea evenimentelor din clasa WindowEvent, trebuie implementată interfaţa WindowListener.

Metodele din interfeţele EvenimentListener au ca parametru o referinţă la evenimentul corespunzător. De exemplu, metodele din interfaţa WindowListener au ca parametru o referinţă la WindowEvent. Metodele din clasa ActionListener au ca parametru o referinţă la ActionEvent.

Numele metodelor reflectă acţiunea executată de utilizator asupra sursei evenimentului. Spre exemplu, interfaţa WindowListener conţine următoarele metode:

public void windowActivated(WindowEvent e); // este apelată în momentul în care fereastra devine fereastra activă

public void windowClosed(WindowEvent e); // este apelată după ce fereastra a fost închisă

public void windowClosing(WindowEvent e); // este apelată în momentul în care utilizatorul închide fereastra

public void windowDeactivated(WindowEvent e); // este apelată în momentul în care fereastra nu mai este activă

public void windowDeiconified(WindowEvent e); // este apelată în momentul în care fereastra minimizată este restaurată

public void windowIconified(WindowEvent e); // este apelată în momentul în care fereastra este minimizată

public void windowOpened(WindowEvent e); // este apelată în momentul în care fereastra este deschisă pentru prima oară

Legarea unui receptor de eveniment de sursa evenimentului se face prin invocarea de către obiectul care reprezintă sursa evenimentului a unei metode predefinite, care primeşte drept parametru referinţa spre obiectul receptor de eveniment. Numele metodei este format din prefixul add la care se adaugă numele interfeţei corespunzătoare evenimentului:

obiectSursaEveniment.addEvenimentListener(obiectReceptorEveniment);

În exemplul prezentat, obiectul clasei FereastraMea este o sursă de eveniment, iar receptorul ei este clasa ReceptorEvenimentFereastra. Programul anterior devine:

import java.awt.*; import java.awt.event.*;

Page 70: Java

4

class ReceptorEvenimentFereastra implements WindowListener {

public void windowClosing(WindowEvent e) {

System.exit(0); } // restul metodelor din WindowListener se vor implementa cu

corpul vid } class FereastraMea extends Frame {

public FereastraMea(String titlu) {

super(titlu); ReceptorEvenimentFereastra r = new ReceptorEvenimentFe-

reastra(); this.addWindowListener(r); Toolkit t = Toolkit.getDefaultToolkit(); Dimension d = t.getScreenSize(); int h = d.height; int w = d.width; setSize(w/2, h/2); setLocation(w/4, h/4);

} } class PrimaFereastra {

public static void main(String[] args) {

Frame f = new FereastraMea("Prima fereastra"); f.show();

} }

Crearea unor butoane într-o fereastră Pentru a adăuga butoane, casete de text, etc. într-o fereastră, trebuie create obiecte ale claselor care modelează acele elemente. Apoi, pentru fiecare element, trebuie apelată metoda add a containerului în care vor fi păstrate elementele. Referinţa la elementul grafic adăugat interfeţei grafice va fi parametrul metodei add. Amplasarea elementelor în spaţiul containerului este făcută de obiecte speciale (plasatori), care aparţin unor clase descendente din LayoutManager. Cele mai cunoscute asemenea clase sunt FlowLayout Õi BorderLayout. Pentru a folosi plasatorul, obiectul care reprezintă containerul (în cazul nostru FereastraMea) va trebui ca înainte de a adăuga elemente GUI, să apeleze metoda setLayout dându-i parametru referinţa la un obiect de tip FlowLayout.

În Java, butoanele sunt modelate de clasa Button. Unul dintre constructorii clasei Button acceptă drept parametru un String, reprezentând eticheta înscrisă pe buton. Un buton poate fi sursa unui eveniment de tip ActionEvent, care apare atunci când utilizatorul a apăsat acel

Page 71: Java

5

buton. Receptorul unui eveniment ActionEvent trebuie să implementeze interfaţa ActionListener, care declară metoda actionPerformed.

Pentru exemplificare, este prezentat programul anterior, actualizat astfel încât în fereastră să apară un buton cu eticheta `Apasa-maA, iar la acţionarea lui să se afişeze valoarea unui contor care numără de câte ori a fost apăsat butonul. Pentru a stăpâni complexitatea relaţiilor dintre elementele GUI ale unei interfeţe grafice, aplicaţiile pot fi structurate conform unui şablon de proiectare numit Mediator. Pentru aplicaţia exemplificată, aceasta presupune definirea următoarelor clase:

C o clasă care să reprezinte fereastra (FereastraMea);

C o clasă care să reprezinte receptorul de evenimente (ReceptorEvenimenteFereastra), cu precizarea că ea se va ocupa Õi de evenimentele generate de buton;

C o clasă cu rol de mediator, care creează elementele GUI (în cazul de faţă un buton Õi o etichetă) Õi le asamblează în fereastră, păstrând referinţele la ele Õi, înştiinţat de către receptorul de evenimente ori de câte ori intervine o modificare a vreunui element GUI, va actualiza starea tuturor elementelor implicate.

import java.awt.*; import java.awt.event.*; class ReceptorEvenimentFereastra extends WindowAdapter implements

ActionListener {

private Mediator m; public ReceptorEvenimentFereastra(Mediator m) {

this.m = m; } public void windowClosing(WindowEvent e) {

System.exit(0); } public void actionPerformed(ActionEvent e) {

m.buttonPressed(); }

} class Mediator {

public Mediator(Frame f) {

this.f = f; } public void assembleFrame() {

ReceptorEvenimentFereastra r = new ReceptorEveniment-Fereastra(this);

f.addWindowListener(r); b = new Button("Apasa-ma"); b.addActionListener(r);

Page 72: Java

6

etic = new Label("0"); f.setLayout(new FlowLayout()); f.add(b); f.add(etic); f.show();

} public void buttonPressed() {

contor++; etic.setText(""+contor);

} private int contor=0; private Frame f; private Button b; private Label etic;

} class FereastraMea extends Frame {

public FereastraMea(String titlu) {

super(titlu); Toolkit t = Toolkit.getDefaultToolkit(); Dimension d = t.getScreenSize(); int h = d.height; int w = d.width; setSize(w/2, h/2); setLocation(w/4, h/4);

} } class PrimaFereastra {

public static void main(String[] args) {

Frame f = new FereastraMea("Prima fereastra"); Mediator m = new Mediator(f); m.assembleFrame();

} }

Introducerea textului de la tastatură Pentru a putea introduce text de la tastatură într-o aplicaţie cu interfaţă grafică, se utilizează un element GUI numit câmp de editare, modelat de clasa TextField. Pentru exemplificare, se consideră următorul program care citeşte elementele unui tablou. Se utilizează:

C un câmp de editare unde se introduce câte un element al tabloului,

C un buton prin care se comandă introducerea efectivă în tablou a textului editat;

C o etichetă care indică indicele elementului editat. import java.awt.*;

Page 73: Java

7

import java.awt.event.*; class Tablou {

private int[] tab; public Tablou(int n) {

tab=new int[n]; }

public void setElem(int i, int val) {

tab[i]=val; } public int getDim() {

return tab.length; }

} class ReceptorEvenimentFereastra extendsWindowAdapter implements

ActionListener {

private Mediator m; public ReceptorEvenimentFereastra(Mediator m) {

this.m=m; } public void windowClosing(WindowEvent e) {

System.exit(0); } public void actionPerformed(ActionEvent e) {

m.buttonPressed(); }

} class Mediator {

public Mediator(Frame f, Tablou t) {

this.f=f; this.t=t;

} public void assembleFrame() {

ReceptorEvenimentFereastra r = new ReceptorEvenimentFe-reastra(this);

f.addWindowListener(r); b = new Button("Adauga"); b.addActionListener(r); etic = new Label("Element 0"); tx = new TextField(15);

Page 74: Java

8

f.SetLayout(new FlowLayout()); f.add(b); f.add(tx); f.add(etic); f.show();

} public void buttonPressed() {

if (contor < t.getDim()) {

try {

int val = Integer.parseInt(tx.getText()); t.setElem(contor,val); contor++;

} catch (NumberFormatException e) {

// daca nu s-a introdus o valoare numerica, nu se actualizeaza tabloul

} if (contor >= t.getDim())

etic.setText("Tablou plin!"); else

etic.setText("Element "+contor); tx.setText("");

} } private int contor=0; private Tablou t; private Frame f; private Button b; private Label etic; private TextField tx;

} class FereastraMea extends Frame {

public FereastraMea(String titlu) {

super(titlu); Toolkit t = Toolkit.getDefaultToolkit(); Dimension d = t.getScreenSize(); int h = d.height; int w = d.width; setSize(w/2, h/2); setLocation(w/4, h/4);

} } class PrimaFereastra {

public static void main(String[] args)

Page 75: Java

9

{ Frame f = new FereastraMea("Prima fereastra"); Tablou t = new Tablou(10); Mediator m = new Mediator(f,t); m.assembleFrame();

} }

În cazul în care există mai multe butoane într-o fereastră, se folosesc următoarele soluţii:

C se construiesc două clase distincte pentru receptorii de evenimente ai celor două butoane, definind corespunzător metodele actionPerformed;

C se utilizează informaţiile furnizate de obiectul eveniment ActionEvent pasat ca parametru metodei actionPerformed. În acest caz, pentru obiectul eveniment, poate fi apelată metoda:

public Object getSource();

care returnează o referinţă la obiectul sursă de eveniment.

Temă Se cere să se realizeze un calculator, a cărui interfaţă să fie cea din figura de mai jos. Acesta trebuie să permită introducerea a doi operanzi Õi efectuarea celor patru operaţii de bază: adunare, scădere, înmulţire Õi împărţire. Fiecare operaţie este rezultat al acţionării cu mausul a butonului corespunzător. Rezultatele vor fi puse într-o listă, care va avea facilitate de golire (prin acţionarea butonului Clear). Cele patru butoane aferente operaţiilor vor fi deselectate în cazul în care cei doi operanzi nu au ambii valori valide. În cazul împărţirii la zero se va afişa un mesaj de eroare.

Page 76: Java

1

Lucrarea 10

Fire de execuţie

Cuprins Definiţii......................................................................................................................................................................... 1 Construcţii de limbaj pentru declararea firelor de execuţie .......................................................................................... 2 Sincronizarea firelor de execuţie................................................................................................................................... 4 Temă ............................................................................................................................................................................. 8

Definiţii Firele de execuţie (threads) reprezintă porţiuni de cod ale aceluiaşi program care se pot executa în paralel una faţă de alta. Programele de până acum au fost de tip single-threaded, adică au fost compuse fiecare din câte un singur fir de execuţie. Firele de execuţie reprezintă modalitatea prin care limbajul Java suportă programarea concurentă. Execuţia paralelă a firelor depinde practic de suportul de execuţie al programelor, hardware şi software. În general, dacă se dispune de un sistem cu mai multe procesoare firele vor fi distribuite pe acestea.

Dacă avem un singur procesor execuţia paralelă se simulează folosind diferite tehnici. În principiu se cunosc două asemenea tehnici care constituie, de fapt, politici de planificare a firelor. Primele trei versiuni ale interpretorului Java (pentru Solaris, Windows 95 şi Windows NT) aplică următoarea tehnică: un fir de execuţie este lăsat să ruleze până când se termină sau până când ajunge în stare de aşteptare, moment în care este preluat un fir cu prioritate mai mare pregătit pentru execuţie. Versiunile de interpretor realizate pentru 32 de biţi aplică tehnica numită time-slicing (divizarea timpului). Practic, fiecărui fir i se alocă câte un interval de timp în care poate ocupa procesorul. În felul acesta, fiecare fir ajunge, prin rotaţie, să ocupe câte puţin procesorul, iar utilizatorul are impresia că firele rulează în paralel.

Divizarea în fire de execuţie este avantajoasă mai ales în cazul operaţiilor care necesită timp mare de rulare sau care forţează procesorul să aştepte pentru a accesa discul sau reţeaua. Constituirea unor asemenea operaţii ca fire separate conduce la:

— creşterea vitezei de execuţie a programului în ansamblu, deoarece procesorul nu mai pierde vremea aşteptând după resurse, ci ia la rând alte fire până când resursele devin disponibile;

— creşterea vitezei de răspuns a programului, deoarece interfaţa utilizator poate avea propriul ei fir de execuţie care să preia intrările utilizatorului chiar în timpul cât celelalte fire sunt "ocupate".

Desigur că toate aceste avantaje au un preţ: întrucât mai multe fire de execuţie aparţin aceluiaşi pogram, rezultă că ele pot accesa în comun anumite structuri de date ale programului. Aici se pune problema sincronizarii accesului, astfel încât să se asigure consistenţa datelor, lucru care complică puţin proiectarea programului.

Page 77: Java

2

Construcţii de limbaj pentru declararea firelor de execuţie Pentru ca o secvenţă de program să fie considerată fir de execuţie distinct, sunt necesare următoarele:

— construirea unei clase care extinde clasa predefinita Thread;

— redefinirea în acea clasă a metodei run() care are prototipul: public void run();

Codul acestei metode constituie un fir de execuţie. Altfel spus, în metoda run() se plasează secvenţa care trebuie rulată în paralel cu alte fire. Se precizează că metoda run() definită în clasa Thread nu face nimic.

— crearea unuia sau a mai multor obiecte ale clasei definite mai înainte şi apelarea pentru ele a metodei start() (moştenită de la Thread); metoda start() creează un nou flux de control (pe baza datelor obiectului) şi lansează în execuţie firul apelând metoda run() a acestuia.

Exemplu considerat creează trei fire de execuţie cu rol de numărătoare: import java.io.*; class Fir extends Thread {

private String nume; public Fir(String nume) {this.nume = nume;} public void run() {

// aici pun codul firului de executie for(long i=0; i <= 10000; i++)

System.out.println("Firul "+nume+" iteratia "+i); } public static void main(String[] args) {

new Fir("Dandanache A.").start(); // se lanseaza in executie un fir

new Fir("Catavencu N.").start(); // se lanseaza in executie al 2-lea fir

new Fir("Coana Joitica").start(); // s.a.m.d System.out.println("Aici se termina metoda main.");

} }

Cele trei fire execută, de fapt, acelaşi lucru, ele fiind create prin intermediul unor obiecte ale aceleiaşi clase, Fir. Programul prezentat are în realitate patru fire de execuţie: metoda main() constituie firul principal care va continua să se execute în paralel cu cele trei fire pe care le lansează. Pe lângă cele patru fire „vizibile“ ale programului mai există un fir care corespunde Garbage Collector-ului.

Dacă politica de planificare a firelor nu este cea de time-slicing, atunci este foarte probabil ca cele trei fire din exemplul de mai sus să nu se execute interclasat, ci unul după altul. Acest lucru se datorează faptului că un fir este lăsat să ruleze până fie se termină, fie ajunge în stare de aşteptare. Firele din exemplu, nu ajung în stare de aşteptare deoarece nu aşteaptă după nici o resursă. În acest caz, ca să putem vedea totuşi o execuţie „paralelă“ va trebui să forţăm noi

Page 78: Java

3

intrarea în aşteptare. Cel mai simplu mod de a simula aşteptarea este acela de a lăsa firul să „doarmă“ o bucată de timp. „Adormirea“ se realizează cu metoda sleep() a clasei Thread, care primeşte ca parametru numărul de milisecunde reprezentând durata „somnului“. Cu acestea, programul de mai sus poate fi rescris astfel: import java.io.*; class Fir extends Thread {

private String nume; public Fir(String nume) {this.nume = nume;} public void run() {

// aici pun codul firului de executie try {

for(long i=0; i <= 10000; i++) {

System.out.println("Firul "+nume+" iteratia "+i); sleep(100); // dorm 100 milisec.

} } catch (InterruptedException e) { return; }

} public static void main(String[] args) {

new Fir("Dandanache").start(); // se lanseaza in executie un fir

new Fir("Catavencu").start(); // se lanseaza in executie al 2-lea fir

new Fir("Coana Joitica").start(); // s.a.m.d System.out.println("Aici se termina metoda main.");

} }

Introducerea blocului try-catch în metoda run() a fost necesară deoarece, pe de o parte, metoda sleep() are o clauză throws InterruptedException, iar pe de altă parte metoda run() nu are clauza throws, deci suntem nevoiţi să captăm eventuala excepţie emisă de metoda sleep(). S-a expus mai sus o variantă prin care putem defini fire de execuţie. Această variantă ar putea fi dezavantajoasă atunci când clasa care conţine metoda run() ar trebui să extindă şi o altă clasă decât Thread. Deoarece Java nu permite moştenire multiplă, în asemenea cazuri putem folosi şi o altă variantă, în care ne bazăm pe interfaţa predefinită Runnable (pe care clasa Thread o implementează de altfel) şi pe faptul că unul dintre constructorii clasei Thread acceptă ca parametru o referinţă la Runnable. Varianta cu Runnable a programului anterior este următoarea: import java.io.*; class Fir implements Runnable {

private String nume; public Fir(String nume) {this.nume = nume;}

Page 79: Java

4

public void run() {

// aici pun codul firului de executie for(long i=; i <= 10000; i++)

System.out.println("Firul "+nume+" iteratia "+i); } public static void main(String[ ] args) {

Fir f1 = new Fir("Dandanache A."); // se creeaza un fir Fir f2 = new Fir("Catavencu N."); // se creaza al 2-lea

fir Fir f3 = new Fir("Coana Joitica"); // s.a.m.d new Thread(f1).start(); // se lanseaza in executie un fir new Thread(f2).start(); // se lanseaza in executie al 2-

lea fir new Thread(f3).start(); // s.a.m.d System.out.println("Aici se termina metoda main().");

} }

Sincronizarea firelor de execuţie În exemplele din paragraful precedent firele de execuţie erau secvenţe absolut independente una de cealaltă, în sensul că nu accesau date comune. Un caz tipic în care două fire de execuţie folosesc în comun structuri de date şi când este necesară sincronizarea lor, este exploatarea în regim producător-consumator a unei zone de date.

Presupunem că avem un tablou de numere întregi, de dimensiune limitată. Unul dintre fire, producătorul depune valori în tablou, câte una o dată. Celălalt fir, consumatorul, citeşte câte o valoare din tablou. Ambele operaţii realizează parcurgerea circulară a tabloului (adică, după ce s-a accesat ultimul element al tabloului, la următorul acces se va prelucra primul element al tabloului). Sincronizarea este necesară, în primul rând, pentru asigurarea consistenţei datelor: producătorul să nu încerce să introducă valori când tabloul este plin, iar consumatorul să nu încerce să citească dintr-un tablou gol.

De asemenea, foarte important este ca atât operaţia de introducere a unei valori cât şi cea de citire a unei valori din tablou să se desfăşoare fără întrerupere sau în regim de excludere mutuală. Ce înseamnă aceasta? Să presupunem că cele două operaţii arată că în secvenţa de mai jos: // introducerea void Pune(int v) {

while(nr_elem == tablou.length) {

//daca tabloul este plin, astept } iP = (iP + 1) % tablou.length; // iP va indica urmatoarea

pozitie libera tablou[ iP ] = v; nr_elem++; // incrementez numarul de elemente

Page 80: Java

5

} // extragerea int Scoate() {

while (nr_elem == 0) {

// daca tabloul este gol, astept } iS = (iS + 1) % tablou.length; // iS indica urmatorul element

de citit nr_elem--; return tablou[ iS ];

}

Să presupunem acum că este în curs de execuţie metoda de introducere. Ea apucă să modifice indicele iP şi, înainte ca valoarea v să se depună în tablou, expiră cuanta de timp a firului producător. În acest moment intră în acţiune firul consumator care, în cel mai bun caz va citi o valoare pe care a mai citit-o o dată, dacă nu cumva se blochează pe motiv că tabloul este gol. Probleme similare pot fi întâlnite şi în cazul extragerii. Soluţia unor astfel de probleme este aceea de a impune ca pe durata execuţiei uneia dintre operaţii nici un alt fir să nu poată accesa tabloul. Limbajul Java oferă în acest sens posibilitatea de a declara anumite metode ca fiind de tip synchronized.

Fie următoarea secvenţă: class Resursa {

public synchronized void oMetoda ( ) { ... } // alte metode

}

Faptul că oMetoda() este declarată ca fiind sincronizată înseamnă că pe durata în care un fir execută această metodă, obiectul receptor implicat intră în starea „blocat“, astfel încât nici un alt fir nu mai poate executa vreo altă metodă sincronizată pentru obiectul respectiv. Dacă un fir apelează o metodă sincronizată pentru un obiect aflat în starea blocat, firul respectiv intră într-o coadă de aşteptare ataşată obiectului. Ieşirea din blocaj a obiectului se realizează la terminarea execuţiei metodei sincronizate.

Sincronizarea metodelor reprezintă mecanismul prin care se asigură accesul firelor la resurse comune în regim de excludere mutuală. Dacă analizăm metodele Pune() şi Scoate() de mai sus constatăm ca operaţiile propriu-zise de scriere/citire se pot efectua doar în anumite condiţii. Spre exemplu, nu putem citi dintr-un tablou gol. Ca urmare, dacă tabloul este gol, firul consumator trebuie să aştepte ca un eventual producător să depună ceva în tablou. Aici intervine problema comunicării între fire. Comunicarea presupune un mecanism prin care un fir care a ţinut ocupată o resursă să anunţe celelalte fire în momentul eliberării resursei.

Intrarea în aşteptare şi anunţarea eliberării unei resurse se realizează cu ajutorul metodelor wait(), respectiv notifyAll() pe care toate clasele le moştenesc de la Object. Metoda wait() permite firului care execută la un moment dat o metodă sincronizată asupra unui obiect să intre în stare de aşteptare, obiectul ieşind din starea de blocaj. Ca urmare, un alt fir poate acţiona asupra obiectului respectiv. Ieşirea din starea de aşteptare se va face când unul dintre

Page 81: Java

6

firele active apelează metoda notifyAll(). Aceasta înştiinţează firele aflate în aşteptare că obiectul dorit de ele iese din blocaj. Modul în care se folosesc metodele wait() şi notifyAll() este ilustrat în secvenţa de mai jos, în care sunt rescrise metodele Pune() şi Scoate(), împreuna cu clasa din care ele fac parte: class Resursa {

private int[] tablou; private int nr_elem=0; private int iP=0; private int iS=0; public Resursa (int n) { tablou=new int[n]; } // introducerea public synchronized void Pune(int v) {

while(nr_elem == tablou.length) {

// tablou plin try {

wait(); // asteapta } catch (InterruptedException e) { System.out.println(e);}

} iP = (iP + 1) % tablou.length; // iP va indica urmatoarea

pozitie libera tablou[ iP ] = v; nr_elem++; // incrementez numarul de elemente notifyAll(); // anunt firele in asteptare ca am eliberat

resursa } // extragerea public synchronized int Scoate() {

while (nr_elem == 0) {

// tablou gol try {

wait(); // asteapta } catch (InterruptedException e)

{System.out.println(e);} } iS = (iS + 1) % tablou.length; // iS indica urmatorul

element de citit nr_elem--; notifyAll(); // anunt firele in asteptare ca am eliberat

resursa return tablou[ iS ];

}

Page 82: Java

7

}

Să vedem acum cum scriem firele producător şi consumator propriu-zise: class Producator extends Thread {

private Resursa r; private int nr_valori; public Producator (Resursa r, int n)

{ this.r = r; nr_valori = n; } public void run() {

for(int i=0; i < nr_valori; i++) {

r.Pune(i); System.out.println("Am pus valoarea "+i);

} }

} class Consumator extends Thread {

private Resursa r; private int nr_valori; public Producator (Resursa r, int n)

{ this.r = r; nr_valori = n; } public void run() {

for(int i=0; i < nr_valori; i++) {

int j = r.Scoate(); System.out.println("Am scos valoarea "+j);

} }

} class Client {

public static void main(String[] args) {

Resursa r = new Resursa(10); Producator p = new Producator(r,40); Consumator c = new Consumator(r,40); p.start(); c.start();

} }

În secvenţa de mai sus am folosit variabila membru nr_valori în ambele fire pentru a reprezenta numărul total de valori care vor fi manipulate în procesul de scriere/citire. Este obligatoriu ca la crearea unei perechi de obiecte producator-consumator care manipulează aceeaşi resursă r să se dea aceeaşi valoare parametrului n din constructor.

Page 83: Java

8

Dacă la consumator nr_valori este mai mare decât la producător, vom ajunge în situaţia în care firul consumator nu mai poate ieşi din starea de aşteptare, după ce a citit ultima valoare creată de producător. O altă variantă ar fi utilizarea unui fanion pe care producătorul să-l seteze când a terminat de creat valorile, iar consumatorul să-şi termine execuţia în funcţie de acest fanion.

Temă Se consideră o parcare pentru maşini, cu n intrări şi o singură ieşire. Activităţile din parcare sunt dirijate de către un paznic. Acesta autorizează intrările (respectiv ieşirile) maşinilor în (respectiv din) parcare şi memorează numărul s de locuri libere. Înainte de a încerca să intre sau să iasă din parcare, fiecare şofer îi comunică paznicului decizia sa. Acesta, fiind un om simplu, nu poate discuta simultan cu mai multe persoane, deci nici un şofer nu se poate adresa paznicului în intervalul de timp în care acesta dialoghează cu un alt şofer. Există două modalităţi prin care paznicul discută cu şoferii:

— un şofer îl anunţă pe paznic că doreşte să intre în parcare. Paznicul verifică dacă există locuri libere şi, în caz afirmativ, îi permite şoferului accesul. Maşina ocupă unul dintre locurile libere, valoarea lui s se micşorează cu o unitate, iar dialogul se încheie. Dacă nu mai sunt locuri în parcare, paznicul îi spune şoferului că nu poate intra deoarece parcarea este plină, dar că a notat numărul maşinii, invitându-l să fie gata să intre în parcare atunci când se va elibera un loc. Dialogul se încheie.

— un şofer doreşte să iasă din parcare şi începe dialogul cu paznicul pentru a-l informa de această intenţie. Dacă există şoferi care aşteaptă să intre în parcare, atunci paznicul îi permite şoferului care a iniţiat dialogul să plece şi, concomitent (pentru operativitate) îl cheamă pe unul dintre şoferii aflaţi pe lista de aşteptare să ocupe locul eliberat. Dialogul se încheie. În caz contrar, paznicul îi permite şoferului, care a iniţiat dialogul, să plece, măreşte cu o unitate valoarea lui s şi încheie dialogul.

Să se scrie un program pentru vizualizarea modului în care se realizează dialogul dintre paznic şi şoferi.

Page 84: Java

1

Lucrarea 11

Applet-uri Java

Limbajul Java a cunoscut o răspândire foarte mare datorită faptului că un program Java poate fi încorporat într-o pagină web, putând fi executat ori de câte ori pagina respectivă este vizitată cu ajutorul unui program de navigare. O pagină web este un fişier care conţine două tipuri de informaţii:

— text obişnuit, reprezentând partea din fişier care este afişată de către programul de navigare;

— construcţii speciale numite tag-uri, care servesc la:

– controlul modului de afişare a textului, privind aspecte de genul: formatul literelor, alinierea pe orizontală sau verticală, prezentarea datelor sub formă de tabele etc.

– includerea de imagini in pagină

– realizarea de legături spre alte pagini.

Ansamblul de reguli care guvernează construirea şi utilizarea tag-urilor se numeşte HyperText Markup Language — HTML. Pentru ca un fişier HTML să fie recunoscut de o aplicaţie de navigare pe Internet, el trebuie să aibă o anumită structură: <HTML> <HEAD> <TITLE> Titlul paginii </TITLE> </HEAD> <BODY> Conţinulul propriu-zis al paginii </BODY> <HTML>

Limbajul HTML este un instrument de organizare a informaţiei astfel încât aceasta să poată fi accesată şi prezentată într-un mod util şi plăcut pentru utilizatori. Una dintre limitările HTML-ului constă în caracterul său pasiv deoarece nu poate fi folosit pentru a crea pagini de Internet interactive. Această limitare este rezolvată cu ajutorul applet-urilor Java. Un applet este un program Java compilat (adică un fişier cu extensia .class) al cărui nume este referit într-o pagină web. Când pagina respectivă este încărcată într-un browser, programul va fi şi el încărcat şi lansat în execuţie. Un asemenea program va fi capabil, pe lângă simpla afişare de informaţii, să citească date şi să le prelucreze. Este important de reţinut faptul că un applet nu poate fi lansat în execuţie de sine stătător, ci doar prin intermediul browser-elor compatibile Java.

Page 85: Java

2

Un applet utilizează date din pachetul java.applet, care realizează controlul execuţiei, precum şi al comunicării dintre un applet şi contextul său. Pe lângă acestea, applet-ul trebuie să conţină elemente de interfaţă grafică pentru dialogul cu utilizatorul (din pachetul java.awt.). Exemplul care urmează prezintă structura celui mai simplu applet, care afişează mesajul Hello World. import java.awt.*; import java.applet.*; public class PrimulApplet extends Applet{

public void paint (Graphics g){ g.drawString(“Hello World”, 20, 20);

} }

Pentru a lansa în execuţie un applet se parcurg următorii paşi:

— se compilează applet-ul obţinând un fişier .class (în cazul prezentat, PrimulApplet.class);

— se inserează o referinţă la acest fişier în pagina web în care se doreşte încorporat applet-ul folosind secvenţa: <APPLET CODE =”nume_fisier.class” WIDTH=dim_w

HEIGHT=dim_h> </APPLET>

unde dim_w şi dim_h sunt numere întregi care reprezintă lungimea, respectiv lăţimea, exprimate în număr de pixeli, ale unui dreptunghi pe ecran, considerat fereastră pentru applet. În interiorul acestui dreptunghi vor fi încadrate toate afişările applet-ului;

— se încarcă pagina web într-un browser compatibil Java.

Pentru exemplul nostru poate fi folosit următorul program HTML: <HTML> <HEAD> <TITLE> Primul applet </TITLE> </HEAD> <BODY>

<B> Primul applet Java </B><BR><BR> <APPLET CODE = “PrimulApplet.class” WIDTH=300

WEIGHT=60></APPLET> </BODY> <HTML>

Fişierul .class trebuie să se afle în acelaşi director cu fişierul HTML, altfel nu este suficient să se scrie doar numele său la atributul CODE. Clasa principală a unui applet trebuie neapărat să extindă clasa predefinită Applet. Metoda paint() primeşte ca parametru o referinţă a unui obiect de tip Graphics (clasă predefinită în java.awt), creat automat în momentul lansării în execuţie a applet-ului. Un obiect Graphics poate

Page 86: Java

3

realiza operaţii de desenare într-o porţiune a ecranului. După ce se operează modificări într-un applet, el trebuie recompilat, rezultând un fişier .class modificat. Pentru ca în pagina web, care include applet-ul respectiv, să fie vizibile modificările, este necesară acţionarea butonului Reload al browser-ului în timp ce se ţine apăsată tasta Shift.

Folosind clasele Font şi Color din pachetul java.awt. se poate face applet-ul mai atractiv. Cu ajutorul obiectelor Color se pot stabili culori pentru umplerea diverselor forme geometrice sau pentru text. O culoare este specificată cu ajutorul a trei numere întregi din intervalul [0-255] care indică nivelul de roşu, verde şi albastru (RGB) prin combinaţia cărora va rezulta culoarea dorită. Spre exemplu, pentru a scrie textul cu violet, applet-ul nostru va deveni: import java.awt.*; import java.applet.*; public class PrimulApplet extends Applet{

public void paint (Graphics g){ Color c=new Color(180,10,120); g.setColor(c); g.drawString(“Hello World”, 20, 20);

}

}

Există un set de obiecte de tip Color definite ca membri statici ai clasei Color: Color.black (negru), Color.blue (albastru), Color.darkGray (gri închis), Color.gray (gri), Color.green (verde), Color.red (roşu), Color.white (alb), Color.yellow (galben) etc.

Pentru specificarea unui anumit font pentru caracterele textului se utilizează clasa Font cu precizarea a trei parametri:

— tipul fontului (“TimesRoman“);

— stilul, sub forma unor constante predefinite: Font.PLAIN (caractere normale), Font.BOLD (caractere îngroşate) sau Font.ITALIC (caractere înclinate). Pentru combinarea stilurilor se utilizează operatorul sau „|“;

— dimensiunea caracterelor, exprimată ca număr de puncte: 10, 12, 14, 18 etc.

Pentru a folosi fonturi Times New Roman, bold şi cu dimensiunea de 18 puncte, exemplul nostru se modifică astfel: import java.awt.*; import java.applet.*; public class PrimulApplet extends Applet{

public void paint (Graphics g){ Color c=new Color(180,10,120); g.setColor(c); Font f=new Font(“TimesRoman”, Font.BOLD, 18); g.drawString(“Hello World”, 20, 20);

}

Page 87: Java

4

}

Dacă într-un applet se execută operaţiile setColor sau setFont, valorile stabilite de către acestea pentru culoare şi font vor rămâne active până la sfârşitul programului sau până la o nouă modificare. Pentru a obţine informaţii despre culoarea/fontul curente se pot folosi următoarele metode:

— getColor returnează un obiect de tip Color reprezentând culoarea curentă. Pentru a afla componentele RGB ale culorii se vor aplica metodele getRed, getGreen şi getBlue.

— getFont care returnează referinţa unui obiect Font, reprezentând fontul curent.

Pe lângă afişarea de text, clasa Graphics include şi metode de desenare a unor forme geometrice:

— drawRect(int x, int y, int width, int height); desenează conturul unui dreptunghi cu colţul din stânga sus în punctul de coordonate (x,y), lungimea egală cu width şi înălţimea egală cu height. Culoarea utilizată pentru contur este culoarea curentă;

— drawLine(int x1, int y1, int x2, int y2); desenează un segment de dreaptă ale cărui capete au coordonatele (x1, y1), respectiv, (x2, y2). Culoarea utilizată pentru linie este culoarea curentă;

— drawOval(int x, int y, int width, int height); desenează conturul unei elipse având cele două diametre principale paralele cu laturile applet-ului. (x, y) reprezintă coordonatele colţului din stânga sus ale dreptunghiului imaginar care circumscrie elipsa, iar width şi hieght reprezintă lărgimea, respectiv înălţimea elipsei. Culoarea utilizată pentru contur este culoarea curentă;

— fillRect(int x, int y, int width, int height); colorează interiorul unui dreptunghi cu culoarea curentă. Semnificaţia parametrilor este aceeaşi ca şi la drawRect;

— fillOval(int x, int y, int width, int height); colorează interiorul unei elipse cu culoarea curentă. Semnificaţia parametrilor este aceeaşi ca şi la drawOval.

Temă

Se cere să se realizeze un applet care trasează graficul funcţiei cosinus pe intervalul [xmin, xmax].

Indiciu

Principiul de lucru este următorul: se parcurge intervalul ales cu un pas stabilit şi, pentru fiecare valoare x a intervalului, se determină coordonata y=cos(x). Graficul va rezulta prin unirea fiecărei perechi de puncte succesive astfel determinate cu o linie dreaptă. Ca

Page 88: Java

5

urmare, cu cât pasul de parcurgerea a domeniului este mai mic, cu atât graficul va avea o precizie mai bună.

Pe de altă parte, ţinând cont de faptul că fereastra applet-ului va avea o anumită dimensiune şi presupunând că vom avea pentru colţul din stânga sus coordonatele (x1,y1), iar pentru colţul din dreapta jos coordonatele (x2,y2), trebuie translatate intervalele [xmin, xmax] şi [ymin,ymax] în [x1,x2] şi [y1,y2]. Astfel, se determină:

— coordonatele originii graficului:

x0 = x1 – [xmin * (x2 – x1 + 1)/(xmax – xmin)]

y0 = y1 + [ymin * (y2 – y1 + 1)/(ymax – ymin)]

— coordonata xt din intervalul [x1,x2], corespunzătoare lui x din intervalul [xmin, xmax]:

a) x > 0 xt = x0 + [x * (x2 – x0 )/xmax]

b) x < 0 xt = x0 – [x * (x0 – x1 )/xmin]

c) x = 0 xt = x0

— coordonata yt din intervalul [y1,y2], corespunzătoare lui y din intervalul [ymin, ymax]:

a) y > 0 yt = y0 – [y * (y0 – y1)/ymax]

b) y < 0 yt = y0 + [y * (y2 – y0)/ymin]

c) y = 0 yt = y0

Page 89: Java

1

Lucrarea 12

Elemente GUI în applet-uri

Biblioteca de clase Java dispune de o gamă foarte largă de mijloace de realizare a comunicării între un applet şi utilizatorii săi, materializate prin elemente GUI: meniuri, butoane, liste etc. Trebuie precizat că la încărcarea unui applet într-un browser, prima metodă care se execută este metoda init(), definită în clasa Applet. Această metodă realizează toate operaţiile de iniţializare, printre care şi dotarea cu elemente de control, dacă este cazul. Abia după finalizarea metodei init() se vor executa celelalte metode ale applet-ului.

Metoda init() este descrisă într-o formă generală în clasa Applet, conţinând toate operaţiile absolut necesare unui applet. Includerea de elemente GUI reprezintă o operaţie particulară şi ea trebuie specificată explicit de către programator prin suprascrierea metodei init(). Un obiect de tip Applet poate fi considerat drept un container (sau o colecţie) care poate conţine elemente simple de control sau chiar alte containere. Din acest motiv, includerea unui element de control într-un applet se traduce prin adăugarea acestuia folosind metoda add(). Spre exemplu, adăugarea unui buton într-un applet se face astfel: import java.awt.*; import java.applet.*; public class Butoane extends Applet{

public void init (){ Button exp = new Button ("Exemplu"); add(exp);

}

}

Se poate observa că, de această dată, nu mai apare metoda paint(). Motivul este următorul: paint(), ca şi init(), este definită în clasa Applet, iar comportarea ei implicită presupune desenarea în fereastra applet-ului a tuturor elementelor de control existente în container după execuţia metodei init(). În cazul în care containerul este gol, atunci este necesară suprascrierea corespunzătoare a metodei paint().

Dacă se doreşte ca un applet să conţină mai multe butoane, atunci trebuie rezolvată problema amplasării acestora în cadrul ferestrei applet-ului. Soluţia constă în crearea unor mici containere de butoane care vor fi, apoi, plasate în containerul mare, care este applet-ul. Pentru a gestiona amplasarea componentelor sale, un container se foloseşte de obiecte ale unor clase speciale, definite în pachetul java.awt, care joacă rolul de „plasatori“. Cele mai des utilizate astfel de clase sunt FlowLayout şi BorderLayout, moştenitoare ale clasei LayoutManager.

Page 90: Java

2

Pentru a specifica modul de plasare a butoanelor, trebuie apelată metoda setLayout() în metoda init() a clasei Applet, având drept parametru un obiect plasator. Dacă parametrul este de tip FlowLayout, toate elementele de control adăugate în applet vor fi dispuse pe orizontală, câte încap pe lungimea ferestrei, distanţate implicit cu 5 pixeli pe orizontală şi pe verticală. Dacă parametrul este de tip BorderLayout, pot fi plasate într-un container maximum cinci elemente, ca in Figura 11.1.

Figura 11.1 Plasarea celor cinci elemente în BorderLayout

Spre deosebire de modul FlowLayout, unde ordinea în care sunt aşezate elementele depinde de ordinea în care sunt scrise comenzile add(), la BorderLayout programatorul trebuie să specifice poziţia unui element: est, vest, nord, sud sau centru: import java.awt.*; import java.applet.*; public class Butoane extends Applet{

public void init(){ setLayout(new BorderLayout()); add("North", new Button("N")); add("South", new Button("S")); add("East", new Button("E")); add("West", new Button("W")); add("Center", new Button("C"));

} }

Dacă se doreşte ca între elementele plasate în mod BorderLayout să rămână spaţii, aceste spaţii vor trebui precizate la crearea obiectului de acest tip: //… setLayout(new BorderLayout(hdist,vdist)); //…

Nu este obligatoriu ca într-un container gestionat în modul BorderLayout să se pună exact cinci elemente. Dacă vor fi puse mai puţine elemente, dimensiunile acestora vor fi ajustate astfel încât ele să ocupe tot spaţiul containerului. Dacă în metoda init() nu se precizează politica de plasare, în mod implicit se consideră că aceasta este de tip FlowLayout. În acest caz, în mod implicit, elementele din container vor fi centrate în cadrul rândului pe care se află. import java.awt.*; import java.applet.*; public class Butoane extends Applet{

Page 91: Java

3

public void init(){ setLayout(new FlowLayout()); add(new Button("butonul1")); add(new Button("butonul2")); add(new Button("butonul3")); add(new Button("butonul4")); add(new Button("butonul5"));

} }

Figura 11.2 Centrarea elementelor cu FlowLayout

Dacă se doreşte o altă aliniere a butoanelor cu FlowLayout, va trebui specificat acest lucru la crearea obiectului de tip FlowLayout. //… setLayout(new FlowLayout(mod_aliniere) //…

unde mod_aliniere poate fi una dintre valorile FlowLayout.LEFT, FlowLayout.RIGHT sau FlowLayout.CENTER. În plus, se poate modifica şi distanţa implicită dintre butoane: //… setLayout(new FlowLayout(mod_aliniere, hdist, vdist) //…

În general, pentru a aplicaţie mai complexă, nu poate fi folosită în exclusivitate una dintre cele două metod, ci se aplică o combinaţie a acestor politici grupând butoanele în subcontainere. În acest sens se foloseşte clasa Panel, definită în pachetul java.awt. care modelează cel mai simplu tip de container. Un astfel de container poate conţine ca elemente şi alte Panel-uri. Astfel, prin încuibarea Panel-urilor se poate rezolva orice combinaţie spaţială de elemente de control. Spre exemplu, pentru realizarea unei interfeţe ca în Figura 11.3 se folosesc Panel-uri imbricate, precum şi ambele metode de plasare.

Figura 1.3 Exemplu de aranjare a butoanelor într-un applet

Page 92: Java

4

import java.awt.*; import java.applet.* ; public class Exemplu extends Applet{

private static Font ff = new Font ("Courier", Font.BOLD,16);

private Button buton (String nume){ Button b = new Button (nume) ; b.setFont(ff); return b;

} private void CreeazaButoane(){

a = buton("A"); b = buton("B"); c = buton("C"); d = buton("D"); e = buton("E"); f = buton("F"); g = buton("G");

} private Panel CreeazaPanel(LayoutManager tip_plasare){

Panel p = new Panel(); p.setLayout(tip_plasare); return p;

} public void init(){

setLayout(new FlowLayout(FlowLayout.CENTER,4,1)); CreeazaButoane(); linie1 = CreeazaPanel(new FlowLayout

(FlowLayout.LEFT, 4,2)); linie1.add(a); linie1.add(b); linie1.add(c); linie1.add(d); add(linie1);

pef = CreeazaPanel(new BorderLayout(2,2)); pef.add("North", e); pef.add("South", f);

pefg = CreeazaPanel(new BorderLayout(2,2)); pefg.add("West", pef); pefg.add("East", g); add(pefg);

}

public void paint(Graphics gg){

Page 93: Java

5

setSize(linie1.getSize().width, 4*a.getSize().height);

validate(); } private Panel linie1, pef, pefg; private Button a, b, c, d, e, f, g;

}

După cum se poate observa, dimensiunea ferestrei applet-ului poate fi ajustată şi din interiorul applet-ului. Această ajustare se face în metoda paint() deoarece abia după execuţia metodei init() se cunosc toate componentele applet-ului şi se poate estima dimensiunea acestora. Metoda folosită este setSize(), ea primind ca parametri lungimea şi înălţimea dorite.

Interacţionarea cu un applet se face prin tratarea evenimentelor care pot să apară. Principiul a fost deja prezentat în Lucrarea 8, la interfeţe grafice în Java. Forma generală a unui applet care tratează evenimente este: import java.awt.*; import java.applet.*; import java.awt.event.*; public class nume_applet extends Applet implements

ActionListener{ public void init(){

b = new Button("nume_buton"); add(b); b.addActionListener(this); //alte elemente de control

} Public void actionPerformed(ActionEvent ev){

//actiuni de executat la apasarea butonului nume_buton

} Private Button b ; //…

}

Când un astfel de applet este încărcat într-un browser, în primul pas se execută metoda init() care realizează popularea applet-ului cu elemente de control. Apoi, programul rămâne în aşteptare până când are loc un eveniment, precum apăsarea unui buton. În acest moment intră în acţiune metoda actionPerformed().

Pentru exemplificare, se consideră următorul applet care înmulţeşte două numere introduse de către un utilizator. import java.awt.*; import java.applet.*; import java.awt.event.*;

Page 94: Java

6

public class Calcul extends Applet implements ActionListener{

public void init(){ calcButon = new Button ("Calcul"); add(calcButon); calcButon.addActionListener(this); add(new Label("Primul numar:")); valnr1 = new TextField(10); add(valnr1); add(new Label("Al doilea numar:")); valnr2 = new TextField(10); add(valnr2); result = new Label("Rezultat = 0000"); add(result);

} public void actionPerformed(ActionEvent ev){

int nr1 = Integer.parseInt(valnr1.getText()); int nr2 = Integer.parseInt(valnr2.getText()); int res = nr1 * nr2; result.setText("Rezultat = " + res); validate();

} private Button calcButon; private TextField valnr1, valnr2; private Label result;

}

Pentru cazurile în care se lucrează cu surse multiple de evenimente, pentru a identifica la un moment dat sursa unui eveniment se foloseşte metoda getActionCommand() din clasa ActionEvent. Dacă evenimentul a constat în apăsarea unui buton, această metodă returnează un String conţinând textul scris pe butonul respectiv. Spre exemplu, dacă applet-ul de mai sus ar avea şi un buton pentru scăderea celor două numere, metoda actionPerformed() ar fi: //… public void actionPerformed(ActionEvent ev){

String s = ev.getActionCommand(); if(s.equals("Calcul"))

//tratarea inmultirii else

//tratarea scaderii //…

}

Temă Să se realizeze sub formă de applet calculatorul prezentat ca temă în Lucrarea 8.


Recommended