Curs Practic de Java

179
1

description

curs

Transcript of Curs Practic de Java

Page 1: Curs Practic de Java

1

Page 2: Curs Practic de Java

2

Cuprins

1 Introducere in Java ......................................................................................... 8

1.1 Ce este Java ? ......................................................................................... 8

1.1.1 Limbajul de programare Java ............................................................ 8

1.1.2 Platforme de lucru Java ..................................................................... 9

1.1.3 Java: un limbaj compilat si interpretat ............................................... 9

1.2 Primul program ...................................................................................... 10

1.3 Structura lexicala a limbajului Java ........................................................ 11

1.3.1 Setul de caractere ........................................................................... 11

1.3.2 Cuvinte cheie .................................................................................. 12

1.3.3 Identificatori ..................................................................................... 12

1.3.4 Literali .............................................................................................. 12

1.3.5 Separatori ........................................................................................ 13

1.3.6 Operatori ......................................................................................... 13

1.3.7 Comentarii ....................................................................................... 14

1.4 Tipuri de date si variabile ....................................................................... 15

1.4.1 Tipuri de date .................................................................................. 15

1.4.2 Variabile .......................................................................................... 15

1.5 Controlul executiei ................................................................................. 17

1.5.1 Instructiuni de decizie if-else ........................................................... 17

1.5.2 Instructiuni de salt for ...................................................................... 17

1.5.3 Instructiuni pentru tratarea exceptiilor ............................................. 18

1.5.4 Alte instructiuni ................................................................................ 18

1.6 Vectori.................................................................................................... 19

1.6.1 Crearea unui vector ......................................................................... 19

1.6.2 Tablouri multidimensionale .............................................................. 20

1.6.3 Dimensiunea unui vector ................................................................. 20

1.6.4 Copierea vectorilor .......................................................................... 20

1.6.5 Sortarea vectorilor – clasa Arrays ................................................... 21

1.6.6 Vectori cu dimensiune variabila si eterogeni ................................... 21

1.7 Siruri de caractere.................................................................................. 21

Page 3: Curs Practic de Java

3

1.8 Folosirea argumentelor din linia de comanda ........................................ 22

1.8.1 Transmiterea argumentelor ............................................................. 22

1.8.2 Primirea argumentelor ..................................................................... 23

1.8.3 Argumente numerice ....................................................................... 24

2 Obiecte si clase ............................................................................................ 26

2.1 Ciclul de viata al unui obiect .................................................................. 26

2.1.1 Crearea obiectelor ........................................................................... 26

2.1.2 Folosirea obiectelor ......................................................................... 27

2.1.3 Distrugerea obiectelor ..................................................................... 28

2.2 Crearea claselor .................................................................................... 29

2.2.1 Declararea claselor ......................................................................... 29

2.2.2 Extinderea claselor .......................................................................... 29

2.2.3 Corpul unei clase............................................................................. 30

2.2.4 Constructorii unei clase ................................................................... 31

2.2.5 Declararea variabilelor .................................................................... 34

2.2.6 this si super ..................................................................................... 35

2.3 Implementarea metodelor ...................................................................... 36

2.3.1 Declararea metodelor ...................................................................... 36

2.3.2 Tipul returnat de o metoda .............................................................. 38

2.3.3 Trimiterea parametrilor catre o metoda ........................................... 39

2.3.4 Metode cu numar variabil de argumente ......................................... 41

2.3.5 Supraincarcarea si supradefinirea metodelor .................................. 41

2.4 Modificatori de acces ............................................................................. 43

2.5 Membri de instanta si membri de clasa.................................................. 43

2.5.1 Variabile de instanta si de clasa ...................................................... 43

2.5.2 Metode de instanta si de clasa ........................................................ 44

2.5.3 Utilitatea membrilor de clasa ........................................................... 45

2.5.4 Blocuri statice de initializare ............................................................ 46

2.6 Clase imbricate ...................................................................................... 47

2.6.1 Definirea claselor imbricate ............................................................. 47

2.6.2 Clase interne ................................................................................... 48

2.6.3 Clase anonime ................................................................................ 49

2.7 Clase si metode abstracte ..................................................................... 50

2.7.1 Declararea unei clase abstracte ...................................................... 50

2.7.2 Metode abstracte............................................................................. 50

2.8 Clasa Obiect .......................................................................................... 52

Page 4: Curs Practic de Java

4

2.8.1 Orice clasa are o superclasa ........................................................... 52

2.8.2 Clasa Object .................................................................................... 53

2.9 Conversii automate intre tipuri ............................................................... 55

2.10 Tipul de date enumerate ..................................................................... 55

3 Exceptii ......................................................................................................... 57

3.1 Ce sunt exceptiile ?................................................................................ 57

3.2 ”Prinderea” si tratarea exceptiilor ........................................................... 58

3.3 ”Aruncarea” exceptiilor ........................................................................... 60

3.4 Avantajele tratarii exceptiilor .................................................................. 63

3.4.1 Separarea codului pentru tratarea erorilor ....................................... 63

3.4.2 Propagarea erorilor ......................................................................... 65

3.4.3 Gruparea erorilor dupa tipul lor ....................................................... 66

3.5 Ierarhia claselor ce descriu exceptii ....................................................... 67

3.6 Exceptii la executie ................................................................................ 67

3.7 Crearea propriilor exceptii ...................................................................... 68

4 Intrari si iesiri ................................................................................................ 70

4.1 Introducere ............................................................................................. 70

4.1.1 Ce sunt fluxurile?............................................................................. 70

4.1.2 Clasificarea fluxurilor ....................................................................... 71

4.1.3 Ierarhia claselor pentru lucrul cu fluxuri ........................................... 71

4.1.4 Metode comune fluxurilor ................................................................ 72

4.2 Folosirea fluxurilor.................................................................................. 72

4.2.1 Fluxuri primitive ............................................................................... 72

4.2.2 Fluxuri de procesare........................................................................ 73

4.2.3 Crearea unui flux ............................................................................. 74

4.2.4 Fluxuri pentru lucrul cu fisiere ......................................................... 75

4.2.5 Citirea si scrierea cu buffer ............................................................. 76

4.2.6 Concatenarea fluxurilor ................................................................... 78

4.2.7 Fluxuri pentru filtrarea datelor ......................................................... 79

4.2.8 Clasele DataInputStream si DataOutputStream .............................. 79

4.3 Intrari si iesiri formatate .......................................................................... 80

4.3.1 Intrari formatate ............................................................................... 80

4.3.2 Iesiri formatate ................................................................................ 80

4.4 Fluxuri standard de intrare si iesire ........................................................ 81

4.4.1 Afisarea informatiilor pe ecran ......................................................... 81

4.4.2 Citirea datelor de la tastatura .......................................................... 81

Page 5: Curs Practic de Java

5

4.4.3 Redirectarea fluxurilor standard ...................................................... 82

4.4.4 Analiza lexicala pe fluxuri (clasa StreamTokenizer) ........................ 83

4.5 Clasa RandomAccesFile (fisiere cu acces direct) \ ................................ 85

4.6 Clasa File ............................................................................................... 86

5 Interfete ........................................................................................................ 88

5.1 Introducere ............................................................................................. 88

5.1.1 Ce este o interfata ? ........................................................................ 88

5.2 Folosirea interfetelor .............................................................................. 88

5.2.1 Definirea unei interfete .................................................................... 88

5.2.2 Implementarea unei interfete ........................................................... 89

5.2.3 Exemplu: implementarea unei stive................................................. 90

5.3 Interfete si clase abstracte ..................................................................... 94

5.4 Mostenire multipla prin interfete ............................................................. 94

5.5 Utilitatea interfetelor ............................................................................... 96

5.5.1 Crearea grupurilor de constante ...................................................... 96

5.5.2 Transmiterea metodelor ca parametri ............................................. 97

5.6 Interfata FilenameFilter .......................................................................... 98

5.6.1 Folosirea claselor anonime ........................................................... 100

5.7 Compararea obiectelor ........................................................................ 101

5.7.1 Interfata Comparable..................................................................... 102

5.7.2 Interfata Comparator ..................................................................... 103

5.8 Adaptori ............................................................................................... 104

6 Organizarea claselor .................................................................................. 105

6.1 Pachete ................................................................................................ 105

6.1.1 Pachetele standard (J2SDK) ......................................................... 105

6.1.2 Folosirea membrilor unui pachet ................................................... 105

6.1.3 Importul unei clase sau interfete ................................................... 106

6.1.4 Importul la cerere dintr-un pachet.................................................. 107

6.1.5 Importul static ................................................................................ 108

6.1.6 Crearea unui pachet ...................................................................... 108

6.1.7 Denumirea unui pachet ................................................................. 109

6.2 Organizarea fisierelor........................................................................... 109

6.2.1 Organizarea fisierelor sursa .......................................................... 109

6.2.2 Organizarea unitatilor de compilare (.class) .................................. 110

6.2.3 Necesitatea organizarii fisierelor ................................................... 111

6.2.4 Setarea caii de cautare (CLASSPATH) ......................................... 112

Page 6: Curs Practic de Java

6

6.3 Arhive JAR ........................................................................................... 113

6.3.1 Folosirea utilitarului jar .................................................................. 113

6.3.2 Executarea aplicatiilor arhivate ..................................................... 114

7 Colectii ....................................................................................................... 115

7.1 Introducere ........................................................................................... 115

7.2 Interfete ce descriu colectii .................................................................. 115

7.3 Implementari ale colectiilor .................................................................. 119

7.4 Folosirea eficienta a colectiilor ............................................................. 119

7.5 Algoritmi polimorfici .............................................................................. 121

7.6 Tipuri generice ..................................................................................... 122

7.7 Iteratori si enumerari ............................................................................ 123

8 Serializarea obiectelor ................................................................................ 125

8.1 Folosirea serializarii ............................................................................. 125

8.1.1 Serializarea tipurilor primitive ........................................................ 126

8.1.2 Serializarea obiectelor ................................................................... 127

8.1.3 Clasa ObjectOutputStream ........................................................... 127

8.1.4 Clasa ObjectInputStream .............................................................. 128

8.2 Obiecte serializabile ............................................................................. 129

8.2.1 Implementarea interfetei Serializable ............................................ 129

8.2.2 Controlul serializarii ....................................................................... 130

8.3 Personalizarea serializarii obiectelor.................................................... 132

8.3.1 Controlul versiunilor claselor ......................................................... 133

8.3.2 Securizarea datelor ....................................................................... 136

8.3.3 Implementarea interfetei Externalizable ........................................ 137

8.4 Clonarea obiectelor .............................................................................. 138

9 Fire de executie .......................................................................................... 140

9.1 Introducere ........................................................................................... 140

9.2 Crearea unui fir de executie ................................................................. 141

9.2.1 Extinderea clasei Thread ............................................................... 141

9.2.2 Implementarea interfetei Runnable ............................................... 143

9.3 Ciclul de viata unui fir de executie ....................................................... 146

9.3.1 Terminarea unui fir de executie ..................................................... 148

9.3.2 Fire de executie de tip ”daemon” ................................................... 150

9.3.3 Stabilirea prioritatilor de executie .................................................. 151

9.3.4 Sincronizarea firelor de executie ................................................... 153

9.3.5 Scenariul producator / consumator................................................ 154

Page 7: Curs Practic de Java

7

9.3.6 Monitoare ...................................................................................... 157

9.3.7 Semafoare ..................................................................................... 159

9.3.8 Probleme legate de sincronizare ................................................... 161

9.4 Gruparea firelor de executie ................................................................ 162

9.5 Comunicarea prin fluxuri de tip ”pipe” .................................................. 164

9.6 Clasele Timer si TimerTask ................................................................. 166

10 Programarea in retea .............................................................................. 169

10.1 Introducere ....................................................................................... 169

10.2 Lucrul cu URL-uri .............................................................................. 170

10.3 Socket-uri ......................................................................................... 172

10.4 Comunicarea prin conexiuni ............................................................. 172

10.5 Comunicarea prin datagrame ........................................................... 175

10.6 Trimiterea de mesaje catre mai multi client ...................................... 178

Page 8: Curs Practic de Java

8

1 Introducere in Java

1.1 Ce este Java ?

Java este o tehnologie inovatoare lansata de compania Sun Microsystems in 1995, care a avut un impact remarcabil asupra intregii comunitati a dezvoltatorilor de software, impunandu-se prin calitati deosebite cum ar fi simplitate, robustete si nu in ultimul rand portabilitate. Denumita initial OAK, tehnologia Java este formata dintr-un limbaj de programare de nivel inalt pe baza caruia sunt construite o serie de platforme destinate implementarii de aplicatii pentru toate segmentele industriei software.

1.1.1 Limbajul de programare Java

Inainte de a prezenta in detaliu aspectele tehnice ale limbajului Java, sa amintim caracteristicile sale principale, care l-au transformat intr-un interval de timp atat de scurt intr-una din cele mai pupulare optiuni pentru dezvoltarea de aplicatii, indiferent de domeniu sau de complexitatea lor:

Simplitate – elimina supraincarcarea operatorilor, mostenirea multipla si toate

”facilitatile” ce pot provoca scrierea unui cod confuz;

Usurinta in crearea de aplicatii complexe ce folosesc programarea in retea, fire de executie, interfata grafica, baze de date, etc.;

Robustete – elimina sursele frecvente de erori ce apar in programare prin renuntarea la pointeri, administrarea automata a memoriei si eliminarea pierderilor de memorie printr-o procedura de colectare a obiectelor care nu mai sunt referite, ce ruleaza in fundal (”garbage collector”);

Complet orientat pe obiecte – elimina complet stilul de programare procedural;

Securitate – este un limbaj de programare foarte sigur, furnizand mecanisme stricte de securitate a programelor concretizate prin: verificarea dinamica a codului pentru detectarea secventelor periculoase, impunerea unor reguli stricte pentru rularea proceselor la distanta, etc.;

Neutralitate arhitecturala – comportamentul unei aplicatii Java nu depinde de arhitectura fizica a masinii pe care ruleaza;

Portabililtate – Java este un limbaj independent de platforma de lucru, aceeasi aplicatie ruland fara nici o modificare si fara a necesita recompilarea ei pe sisteme de operare diferite cum ar fi Windows, Linux, Mac OS, Solaris, etc., lucru care aduce economii substantiale firmelor dezvoltatoare de aplicatii;

Este compilat si interpretat, aceasta fiind solutia eficienta pentru obtinerea portabilitatii;

Performanta -desi mai lent decat limbajele de programare care genereaza executabile native pentru o anumita platforma de lucru, compilatorul Java asigura o performanta ridicata a codului de octeti, astfel incat viteza de lucru putin mai scazuta nu va fi un impediment in dezvoltarea de aplicatii oricat de

Page 9: Curs Practic de Java

9

complexe, inclusiv grafica 3D, animatie, etc.;

Este modelat dupa C si C++, trecerea de la C, C++ la Java facandu-se foarte usor.

1.1.2 Platforme de lucru Java

Limbajul de programare Java a fost folosit la dezvoltarea unor tehnologii dedicate

rezolvarii unor probleme din cele mai diverse domenii. Aceste tehnologii au fost grupate in

asa numitele platforme de lucru, ce reprezinta seturi de librarii scrise in limbajul Java, precum

si diverse programe utilitare, folosite pentru dezvoltarea de aplicatii sau componente

destinate unei anume categorii de utilizatori.

J2SE (Standard Edition) Este platforma standard de lucru ce ofera suport pentru crearea

de aplicatii independente si appleturi;

De asemenea, aici este inclusa si tehnologia Java Web Start ce furnizeaza o modalitate

extrem de facila pentru lansarea si instalarea locala a programelor scrisein Java direct

de pe Web, oferind cea mai comoda solutie pentru distributia si actualizarea aplicatiilor

Java;

J2ME (Micro Edition) Folosind Java, programarea dispozitivelor mobile este extrem de

simpla, platforma de lucru J2ME oferind suportul necesar scrierii de programe dedicate

acestui scop;

J2EE (Enterprise Edition) Aceasta platforma ofera API-ul necesar dezvoltarii de aplicatii

complexe, formate din componente ce trebuie sa rulezein sisteme eterogene, cu

informatiile memorate in baze de date distribuite, etc.

Tot aici gasim si suportul necesar pentru crearea de aplicatii si servicii Web, bazate pe

componente cum ar fi servleturi, pagini JSP, etc.

Toate distributiile Java sunt oferite gratuit si pot fi descarcate de pe Internet de la adresa

”http://java.sun.com”. In continuare, vom folosi termenul J2SDK pentru a ne referi la distributia

standard J2SE 1.5 SDK (Tiger).

1.1.3 Java: un limbaj compilat si interpretat

In functie de modul de executie a aplicatiilor, limbajele de programare se impart in doua

categorii:

Interpretate: instructiunile sunt citite linie cu linie de un program numit interpretor si

traduse in instructiuni masina. Avantajul acestei solutii este simplitatea si faptul ca fiind

interpretata direct sursa programului obtinem portabilitatea. Dezavantajul evident este

viteza de executie redusa. Probabil cel mai cunoscute limbaj interpretat este limbajul

Basic;

Page 10: Curs Practic de Java

10

Compilate: codul sursa al programelor este transformat de compilator intr-un cod ce

poate fi executat direct de procesor, numit cod masina. Avantajul este executia extrem

de rapida, dezavantajul fiind lipsa portabilitatii, codul compilat intr-un format de nivel

scazut nu poate fi rulat decat pe platforma de lucru pe care a fost compilat;

Limbajul Java combina solutiile amintite mai sus, programele Java fiind atat interpretate

cat si compilate. Asadar vom avea la dispozitie un compilator responsabil cu

transformarea surselor programului in asa numitul cod de octeti, precum si un

interpretor ce va executa respectivul cod de octeti.

Codul de octeti este diferit de codul masina. Codul masina este reprezentat de o

succesiune de instructiuni specifice unui anumit procesor si unei anumite platforme de lucru

reprezentate in format binar astfel incat sa poata fi executate fara a mai necesita nici o

prelucrare.

Codurile de octeti sunt seturi de instructiuni care seamana cu codul scris in limbaj de

asamblare si sunt generate de compilator independent de mediul de lucru. In timp ce codul

masina este executat direct de catre procesor si poate fi folosit numai pe platforma pe care a

fost creat, codul de octeti este interpretat de mediul Java si de aceea poate fi rulat pe orice

platforma pe care este instalata mediul de executie Java.

Prin masina virtuala Java (JVM) vom intelege mediul de executie al aplicatiilor Java. Pentru ca

un cod de octeti sa poata fi executat pe un anumit calculator, pe acesta trebuie sa fie instalata

o masina virtuala Java. Acest lucru este realizat automat de catre distributia J2SDK.

1.2 Primul program

Crearea oricarei aplicatii Java presupune efectuarea urmatorilor pasi:

1. Scriererea codului sursa:

class FirstApp {

public static void main( String args[]) {

System.out.println("Hello world!");

}

}

Toate aplicatiile Java contin o clasa principala(primara) in care trebuie sa se gaseasca

metoda main. Clasele aplicatiei se pot gasi fie intr-un singur fisier, fie in mai multe.

2. Salvarea fisierelor sursa

Se va face in fisiere care au obligatoriu extensia java, nici o alta extensie nefiind

acceptata. Este recomandat ca fisierul care contine codul sursa al clasei primare sa aiba

acelasi nume cu cel al clasei, desi acest lucru nu este obligatoriu. Sa presupunem ca am salvat

exemplul de mai sus in fisierul C:\intro\FirstApp.java.

Page 11: Curs Practic de Java

11

3. Compilarea aplicatiei

Pentru compilare vom folosi compilatorul javac din distributia J2SDK. Apelul

compilatorului se face pentru fisierul ce contine clasa principala a aplicatiei sau pentru orice

fisier/fisiere cu extensia java. Compilatorul creeaza cate un fisier separat pentru fiecare clasa a

programului. Acestea au extensia .class si implicit sunt plasate in acelasi director cu fisierele

sursa.

javac FirstApp.java

In cazul in care compilarea a reusit va fi generat fisierul FirstApp.class.

4. Rularea aplicatiei

Se face cu interpretorul java, apelat pentru unitatea de compilare corespunzatoare clasei

principale. Deoarece interpretorul are ca argument de intrare numele clasei principale si nu

numele unui fisier, ne vom pozitiona in directorul ce contine fisierul FirstApp.class si vom apela

interpretorul astfel:

java FirstApp

Rularea unei aplicatii care nu foloseste interfata grafica, se va face intr-o fereastra sistem.

Atentie : Un apel de genul java c:\intro\FirstApp.class este gresit!

1.3 Structura lexicala a limbajului Java

1.3.1 Setul de caractere

Limbajului Java lucreaza in mod nativ folosind setul de caractere Unicode. Acesta este un

standard international care inlocuieste vechiul set de caractere ASCII si care foloseste pentru

reprezentarea caracterelor 2 octeti, ceea ce inseamna ca se pot reprezenta 65536 de semne,

spre deosebire de ASCII, unde era posibila reprezentarea a doar 256 de caractere. Primele

256 caractere Unicode corespund celor ASCII, referirea la celelalte facandu-se prin \uxxxx,

unde xxxx reprezinta codul caracterului.

O alta caracteristica a setului de caractere Unicode este faptul ca intreg intervalul de

reprezentare a simbolurilor este divizat in subintervale numite blocuri, cateva exemple de

blocuri fiind: Basic Latin, Greek, Arabic, Gothic, Currency, Mathematical, Arrows, Musical, etc.

Mai jos sunt oferite cateva exemple de caractere Unicode.

\u0030 -\u0039 : cifre ISO-Latin 0 -9

\u0660 -\u0669 : cifre arabic-indic 0 -9

\u03B1 -\u03C9 : simboluri grecesti α − ω

\u2200 -\u22FF : simboluri matematice (∀, ∃, ∅, etc.)

\u4e00 -\u9fff : litere din alfabetul Han (Chinez, Japonez, Coreean)

Page 12: Curs Practic de Java

12

Mai multe informatii legate de reprezentarea Unicode pot fi obtinute la adresa

”http://www.unicode.org”.

1.3.2 Cuvinte cheie

Cuvintele rezervate in Java sunt, cu cateva exceptii, cele din C++ si au fost enumerate in

tabelul de mai jos. Acestea nu pot fi folosite ca nume de clase, interfete, variabile sau metode.

true, false, null nu sunt cuvinte cheie, dar nu pot fi nici ele folosite ca nume in aplicatii.

Cuvintele marcate prin ∗ sunt rezervate, dar nu sunt folosite.

Incepand cu versiunea 1.5, mai exista si cuvantul cheie enum.

1.3.3 Identificatori

Sunt secvente nelimitate de litere si cifre Unicode, incepand cu o litera. Dupa cum am mai

spus, identificatorii nu au voie sa fie identici cu cuvintele rezervate.

1.3.4 Literali

Literalii pot fi de urmatoarele tipuri:

Intregi : Sunt acceptate 3 baze de numeratie : baza 10, baza 16 (incep cu caracterele 0x)

si baza 8 (incep cu cifra 0) si pot fi de doua tipuri:

Page 13: Curs Practic de Java

13

normali -se reprezinta pe 4 octeti (32 biti)

lungi -se reprezinta pe 8 octeti (64 biti) si se termina cu caracterul L (sau l).

Flotanti Pentru ca un literal sa fie considerat flotant el trebuie sa aiba cel putin o

zecimala dupa virgula, sa fie in notatie exponentiala sau sa aiba sufixul F sau f pentru

valorile normale -reprezentate pe 32 biti, respectiv D sau d pentru valorile duble

-reprezentate pe 64 biti. Exemple: 1.0, 2e2, 3f, 4D.

Logici : Sunt reprezentati de true -valoarea logica de adevar, respectiv false -valoarea

logica de fals.

Caracter : Un literal de tip caracter este utilizat pentru a exprima caracterele codului

Unicode. Reprezentarea se face fie folosind o litera, fie o secventa escape scrisa intre

apostrofuri. Secventele escape permit specificarea caracterelor care nu au reprezentare

grafica si reprezentarea unor caractere speciale precum backslash, apostrof, etc.

Secventele escape pre-definite in Java sunt:

’\b’ : Backspace (BS)

’\t’ : Tab orizontal (HT)

’\n’ : Linie noua (LF)

’\f’ : Pagina noua (FF)

’\r’ : Inceput de rand (CR)

’\"’ : Ghilimele

’\’’ : Apostrof

’\\’ : Backslash

Siruri de caractere

Un literal sir de caractere este format din zero sau mai multe caractere intre ghilimele.

Caracterele care formeaza sirul pot fi caractere grafice sau secvente escape. Daca sirul

este prea lung el poate fi scris ca o concatenare de subsiruri de dimensiune mai mica,

concatenarea sirurilor realizandu-se cu operatorul +, ca in exemplul: "Ana "+" are "+

"mere ". Sirul vid este "".

Dupa cum vom vedea, orice sir este de fapt o instanta a clasei String, definita in

pachetul java.lang.

1.3.5 Separatori

Un separator este un caracter care indica sfarsitul unei unitati lexicale si ınceputul alteia.

In Java separatorii sunt urmatorii: () [];,. . Instructiunile unui program se separa cu punct si

virgula.

1.3.6 Operatori

Operatorii Java sunt, cu mici deosebiri, cei din C++:

atribuirea: =

operatori matematici: +,-, *, /, %,++, --. Este permisa notatia prescurtata de forma lval

Page 14: Curs Practic de Java

14

op= rval: x += 2 n -= 3

Exista operatori pentru autoincrementare si autodecrementare (post si pre): x++, ++x, n--, --n

Evaluarea expresiilor logice se face prin metoda scurtcircuitului: evaluarea se opreste in

momentul in care valoarea de adevar a expresiei este sigur determinata.

operatori logici: &&(and), ||(or), !(not)

operatori relationali: <, <=, >, <=, ==, !=

operatori pe biti: &(and), |(or), ^ (xor), ~ (not)

operatori de translatie: <<, >>, >>> (shift la dreapta fara semn)

operatorul if-else: expresie-logica ? val-true : val-false

operatorul , (virgula) folosit pentru evaluarea secventiala a operatiilor: int x=0, y=1, z=2;

operatorul + pentru concatenarea sirurilor:

String s1="Ana";

String s2="mere";

int x=10;

System.out.println(s1 + " are " + x + " " + s2);

operatori pentru conversii (cast): (tip-de-data)

int a = (int)’a’;

char c = (char)96;

int i =200;

long l = (long)i; //widening conversion

long l2 = (long)200;

int i2 = (int)l2; //narrowing conversion

1.3.7 Comentarii

In Java exista trei feluri de comentarii:

Comentarii pe mai multe linii, inchise intre /* si */.

Comentarii pe mai multe linii care tin de documentatie, inchise intre /** si */. Textul dintre

cele doua secvente este automat mutat in documentatia aplicatiei de catre generatorul

automat de documentatie javadoc.

Comentarii pe o singura linie, care incep cu //. Observatii:

Nu putem scrie comentarii in interiorul altor comentarii.

Nu putem introduce comentarii in interiorul literalilor caracter sau sir de caractere.

Secventele /* si */ pot sa apara pe o linie dupa secventa // dar isi pierd semnificatia.

La fel se intampla cu secventa // in comentarii care incep cu /* sau */.

Page 15: Curs Practic de Java

15

1.4 Tipuri de date si variabile

1.4.1 Tipuri de date

In Java tipurile de date se impart in doua categorii: tipuri primitive si tipuri referinta. Java

porneste de la premiza ca ”orice este un obiect”, prin urmare tipurile de date ar trebui sa fie de

fapt definite de clase si toate variabilele ar trebui sa memoreze instante ale acestor clase

(obiecte). In principiu acest lucru este adevarat, insa, pentru usurinta programarii, mai exista si

asa numitele tipurile primitive de date, care sunt cele uzuale :

aritmetice

intregi: byte (1 octet), short (2), int (4), long (8)

reale: float (4 octeti), double (8)

caracter: char (2 octeti)

logic: boolean (true si false)

In alte limbaje de programare formatul si dimensiunea tipurilor primitive de date pot

depinde de platforma pe care ruleaza programul. In Java acest lucru nu mai este valabil, orice

dependenta de o anumita platforma specifica fiind eliminata.

Vectorii, clasele si interfetele sunt tipuri referinta. Valoarea unei variabile de acest tip

este, spre deosebire de tipurile primitive, o referinta (adresa de memorie) catre valoarea sau

multimea de valori reprezentata de variabila respectiva.

Exista trei tipuri de date din limbajul C care nu sunt suportate de limbajul Java. Acestea

sunt: pointer, struct si union. Pointerii au fost eliminati din cauza ca erau o sursa constanta de

erori, locul lor fiind luat de tipul referinta, iar struct si union nu isi mai au rostul atat timp cat

tipurile compuse de date sunt formate in Java prin intermediul claselor.

1.4.2 Variabile

Variabilele pot fi de tip primitiv sau referinte la obiecte (tip referinta). Indiferent de tipul lor,

pentru a putea fi folosite variabilele trebuie declarate si, eventual, initializate.

Declararea variabilelor: Tip numeVariabila;

Initializarea variabilelor: Tip numeVariabila = valoare;

Declararea constantelor: final Tip numeVariabila;

Evident, exista posibilitatea de a declara si initializa mai multe variabile sau constante de

acelasi tip intr-o singura instructiune astfel:

Tip variabila1[=valoare1], variabila2[=valoare2],...;

Conventia de numire a variabilelor in Java include, printre altele, urmatoarele criterii:

variabilele finale (constante) se scriu cu majuscule;

variabilele care nu sunt constante se scriu astfel: prima litera mica iar daca numele

Page 16: Curs Practic de Java

16

variabilei este format din mai multi atomi lexicali, atunci primele litere ale celorlalti atomi

se scriu cu majuscule.

Exemple:

final double PI = 3.14;

final int MINIM=0, MAXIM = 10;

int valoare = 100;

char c1=’j’, c2=’a’, c3=’v’, c4=’a’;

long numarElemente = 12345678L;

String bauturaMeaPreferata = "apa";

In functie de locul in care sunt declarate variabilele seimpartin urmatoatele categorii:

a. Variabile membre, declarate in interiorul unei clase, vizibile pentru toate metodele clasei

respective cat si pentru alte clase in functie de nivelul lor de acces (vezi ”Declararea

variabilelor membre”).

b. Parametri metodelor, vizibili doar in metoda respectiva.

c. Variabile locale, declarate intr-o metoda, vizibile doar in metoda respectiva.

d. Variabile locale, declarate intr-un bloc de cod, vizibile doar in blocul respectiv.

e. Parametrii de la tratarea exceptiilor (vezi ”Tratarea exceptiilor”).

class Exemplu {

//Fiecare variabila corespunde situatiei data de numele ei //din enumerarea de mai sus

int a;

public void metoda(int b) {

a = b; int c = 10;

for(int d=0; d < 10; d++) {

c --;

}

try {

a = b/c;

} catch(ArithmeticException e) {

System.err.println(e.getMessage());

}

}

}

Observatii:

Variabilele declarate intr-un for, raman locale corpului ciclului:

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

//domeniul de vizibilitate al lui i

}

i = 101;//incorect

Page 17: Curs Practic de Java

17

Nu este permisa ascunderea unei variabile:

int x=1;

{

int x=2; //incorect

}

1.5 Controlul executiei

Instructiunile Java pentru controlul executiei sunt foarte asemanatoare celor din limbajul

C si pot fi impartite in urmatoarele categorii:

Instructiuni de decizie: if-else, switch-case

Instructiuni de salt: for, while, do-while

Instructiuni pentru tratarea exceptiilor: try-catch-finally, throw

Alte instructiuni: break, continue,

1.5.1 Instructiuni de decizie if-else

if (expresie-logica) { ... }

if (expresie-logica) { ... } else { ... }

switch-case

switch (variabila) {

case valoare1: ... break; case valoare2:

return, label:

...

break; ... default: ...

}

Variabilele care pot fi testate folosind instructiunea switch nu pot fi decat de tipuri

primitive.

1.5.2 Instructiuni de salt for

for(initializare; expresie-logica; pas-iteratie) {

//Corpul buclei

}

for(int i=0, j=100 ; i < 100 && j > 0; i++, j--) {

Page 18: Curs Practic de Java

18

...

}

Atat la initializare cat si in pasul de iteratie pot fi mai multe instructiuni despartite prin

virgula.

• while

while (expresie-logica) { ...

}

• do-while

do {

...

}

while (expresie-logica);

1.5.3 Instructiuni pentru tratarea exceptiilor

Instructiunile pentru tratarea exceptiilor sunt try-catch-finally, respectiv throw si vor fi

tratate in capitolul ”Exceptii”.

1.5.4 Alte instructiuni

break: paraseste fortat corpul unei structuri repetitive.

continue: termina fortat iteratia curenta a unui ciclu si trece la urmatoarea iteratie.

return [valoare]: termina o metoda si, eventual, returneaza o valorare.

numeEticheta: : Defineste o eticheta.

Desi in Java nu exista goto, se pot defini totusi etichete folosite in expresii de genul: break

numeEticheata sau continue numeEticheta, utile pentru a controla punctul de iesire dintr-o

structura repetitiva, ca inexemplul de mai jos:

i=0;

eticheta: while (i < 10) {

System.out.println("i="+i);

j=0;

while (j < 10) {

j++;

if (j==5)

continue eticheta;

if (j==7) break eticheta;

System.out.println("j="+j);

}

i++;

Page 19: Curs Practic de Java

19

}

1.6 Vectori

1.6.1 Crearea unui vector

Crearea unui vector presupune realizarea urmatoarelor etape:

Declararea vectorului – Pentru a putea utiliza un vector trebuie, inainte de toate, sa-l

declaram. Acest lucru se face prin expresii de forma:

Tip[] numeVector; sau

Tip numeVector[];

ca in exemplele de mai jos:

int[] intregi;

String adrese[];

Instantierea

Declararea unui vector nu implica si alocarea memoriei necesare pentru retinerea

elementelor. Operatiunea de alocare a memoriei, numita si instantierea vectorului, se

realizeaza intotdeauna prin intermediul operatorului new. Instantierea unui vector se va face

printr-o expresie de genul:

numeVector = new Tip[nrElemente];

unde nrElemente reprezinta numarul maxim de elemente pe care le poate avea vectorul. In

urma instantierii vor fi alocati: nrElemente ∗ dimensiune(T ip) octeti necesari memorarii

elementelor din vector, unde prin dimensiune(T ip) am notat numarul de octeti pe care se

reprezinta tipul respectiv.

v = new int[10]; //aloca spatiu pentru 10 intregi: 40 octeti

c = new char[10]; //aloca spatiu pentru 10 caractere: 20 octeti

Declararea si instantierea unui vector pot fi facute simultan astfel:

Tip[] numeVector = new Tip[nrElemente];

Initializarea (optional) Dupa declararea unui vector, acesta poate fi initializat, adica

elementele sale pot primi niste valori initiale, evident daca este cazul pentru asa ceva. In

acest caz instantierea nu mai trebuie facuta explicit, alocarea memoriei facandu-se

automat in functie de numa rul de elemente cu care se initializeaza vectorul.

String culori[] = {"Rosu", "Galben", "Verde"};

int []factorial = {1, 1, 2, 6, 24, 120};

Page 20: Curs Practic de Java

20

Primul indice al unui vector este 0, deci pozitiile unui vector cu n elemente vor fi cuprinse

intre 0 si n − 1. Nu sunt permise constructii de genul Tip numeVector[nrElemente], alocarea

memoriei facandu-se doar prin intermediul opearatorului new.

int v[10]; //ilegal

int v[] = new int[10]; //corect

1.6.2 Tablouri multidimensionale

In Java tablourile multidimensionale sunt de fapt vectori de vectori. De exemplu, crearea si

instantierea unei matrici vor fi realizate astfel:

Tip matrice[][] = new Tip[nrLinii][nrColoane];

matrice[i] este linia i a matricii si reprezinta un vector cu nrColoane elemente iar matrice[i][j]

este elementul de pe linia i si coloana j.

1.6.3 Dimensiunea unui vector

Cu ajutorul variabilei length se poate afla numarul de elemente al unui vector.

int []a = new int[5];

// a.length are valoarea 5

int m[][] = new int[5][10];

// m[0].length are valoarea 10

Pentru a intelege modalitatea de folosire a lui length trebuie mentionat ca fiecare vector

este de fapt o instanta a unei clase iar length este o variabila publica a acelei clase, in care

este retinut numarul maxim de elemente al vectorului.

1.6.4 Copierea vectorilor

Copierea elementelor unui vector a intr-un alt vector b se poate face, fie element cu

element, fie cu ajutorul metodei System.arraycopy, ca in exemplele de mai jos. Dupa cum vom

vedea, o atribuire de genul b = a are alta semnificatie decat copierea elementelor lui a in b si nu

poate fi folosita in acest scop.

int a[] = {1, 2, 3, 4};

int b[] = new int[4];

// Varianta 1

for(int i=0; i<a.length; i++) b[i] = a[i];

// Varianta 2

System.arraycopy(a, 0, b, 0, a.length);

Page 21: Curs Practic de Java

21

// Nu are efectul dorit b= a;

1.6.5 Sortarea vectorilor – clasa Arrays

In Java s-a pus un accent deosebit pe implementarea unor structuri de date si algoritmi

care sa simplifice proceseul de crearea a unui algoritm, programatorul trebuind sa se

concentreze pe aspectele specifice problemei abordate. Clasa java.util.Arrays ofera diverse

metode foarte utile in lucrul cu vectori cum ar fi:

sort – sorteaza ascendent un vector, folosind un algoritm de tip Quick-Sort performant,

de complexitate O(n log(n)).

int v[]={3, 1, 4, 2};

java.util.Arrays.sort(v); // Sorteaza vectorul v

// Acesta va deveni {1, 2, 3, 4}

binarySearch – cautarea binara a unei anumite valori intr-un vector sortat;

equals – testarea egalitatii valorilor a doi vectori (au aceleasi numar de elemente si

pentru fiecare indice valorile corespunzatoare din cei doi vectori sunt egale)

fill – atribuie fiecarui element din vector o valoare specificata.

1.6.6 Vectori cu dimensiune variabila si eterogeni

Implementari ale vectorilor cu numar variabil de elemente sunt oferite de clase

specializate cum ar fi Vector sau ArrayList din pachetul java.util. Astfel de obiecte descriu

vectori eterogeni, ale caror elemente au tipul Object, si vor fi studiati in capitolul ”Colectii”.

1.7 Siruri de caractere

In Java, un sir de caractere poate fi reprezentat printr-un vector format din elemente de tip

char, un obiect de tip String sau un obiect de tip StringBuffer.

Daca un sir de caractere este constant (nu se doreste schimbarea continutului sa pe

parcursul executiei programului) atunci el va fi declarat de tipul String, altfel va fi declarat de tip

StringBuffer. Diferenta principala intre aceste clase este ca StringBuffer pune la dispozitie

metode pentru modificarea continutului sirului, cum ar fi: append, insert, delete, reverse.

Uzual, cea mai folosita modalitate de a lucra cu siruri este prin intermediul clasei String, care

are si unele particularitati fata de restul claselor menite sa simplifice cat mai mult folosirea

sirurilor de caractere. Clasa StringBuffer va fi utilizata predominant in aplicatii dedicate

procesarii textelor cum ar fi editoarele de texte.

Exemple echivalente de declarare a unui sir:

String s = "abc";

String s = new String("abc");

Page 22: Curs Practic de Java

22

char data[] = {’a’, ’b’, ’c’};

String s = new String(data);

Observati prima varianta de declarare a sirului s din exemplul de mai sus -de altfel, cea

mai folosita -care prezinta o particularitate a clasei String fata de restul claselor Java

referitoare la instantierea obiectelor sale.

1.8 Folosirea argumentelor din linia de comanda

Concatenarea sirurilor de caractere se face prin intermediul operatorului

+ sau, in cazul sirurilor de tip StringBuffer, folosind metoda append.

String s1 = "abc" + "xyz";

String s2 = "123";

String s3 = s1 + s2;

In Java, operatorul de concatenare + este extrem de flexibil, in sensul ca permite

concatenarea sirurilor cu obiecte de orice tip care au o reprezentare de tip sir de caractere. Mai

jos, sunt cateva exemple:

System.out.print("Vectorul v are" + v.length + " elemente");

String x= "a" +1 +"b"

Pentru a lamuri putin lucrurile, ceea ce executa compilatorul atunci cand intalneste o

secventa de genul String x = "a" + 1 + "b" este:

String x = new StringBuffer().append("a").append(1). append("b").toString()

Atentie insa la ordinea de efectuare a operatiilor. Sirul s=1+2+"a"+1+2 va avea valoarea

"3a12", primul + fiind operatorul matematic de adunare iar al doilea +, cel de concatenare a

sirurilor.

1.8.1 Transmiterea argumentelor

O aplicatie Java poate primi oricate argumente de la linia de comanda in momentul

lansarii ei. Aceste argumente sunt utile pentru a permite utilizatorului sa specifice diverse

optiuni legate de functionarea aplicatiei sau sa furnizeze anumite date initiale programului.

Atentie!!!

Programele care folosesc argumente de la linia de comanda nu sunt 100% pure Java,

deoarece unele sisteme de operare, cum ar fi Mac OS, nu au in mod normal linie de comanda.

Argumentele de la linia de comanda sunt introduse la lansarea unei aplicatii, fiind

specificate dupa numele aplicatiei si separate prin spatiu. De exemplu, sa presupunem ca

aplicatia Sortare ordoneaza lexicografic (alfabetic) liniile unui fisier si primeste ca argument de

intrare numele fisierului pe care sa il sorteze. Pentru a ordona fisierul "persoane.txt", aplicatia

va fi lansata astfel:

java Sortare persoane.txt

Asadar, formatul general pentru lansarea unei aplicatii care primeste argumente de la

Page 23: Curs Practic de Java

23

linia de comanda este:

java NumeAplicatie [arg0 arg1 . . . argn]

In cazul in care sunt mai multe, argumentele trebuie separate prin spatii iar daca unul

dintre argumente contine spatii, atunci el trebuie pus intre ghilimele. Evident, o aplicatie poate

sa nu primeasca nici un argument sau poate sa ignore argumentele primite de la linia de

comanda.

1.8.2 Primirea argumentelor

In momentul lansarii unei aplicatii interpretorul parcurge linia de comanda cu care a fost

lansata aplicattia si, in cazul in care exista, transmite programului argumentele specificate sub

forma unui vector de siruri. Acesta este primit de aplicatie ca parametru al metodei main.

Reamintim ca formatul metodei main din clasa principala este:

public static void main (String args[])

Vectorul args primit ca parametru de metoda main va contine toate argumentele

transmise programului de la linia de comanda.

In cazul apelului java Sortare persoane.txt vectorul args va contine un singur element pe

prima sa pozitie: args[0]="persoane.txt".

Vectorul args este instantiat cu un numar de elemente egal cu numarul argumentelor

primite de la linia de comanda. Asadar, pentru a afla numarul de argumente primite de

program este suficient sa aflam dimensiunea vectorului args prin intermediul atributului length:

public static void main (String args[]) {

int numarArgumente = args.length ;

}

In cazul in care aplicatia presupune existenta unor argumente de la linia de comanda,

insa acestea nu au fost transmise programului la lansarea sa, vor aparea exceptii (erori) de

tipul ArrayIndexOutOfBoundsException. Tratarea acestor exceptii este prezentata in capitolul

”Exceptii”. Din acest motiv, este necesar sa testam daca programul a primit argumentele de la

linia de comanda necesare pentru functionarea sa si, in caz contrar, sa afiseze un mesaj de

avertizare sau sa foloseasca niste valori implicite, ca in exemplul de mai jos:

public class Salut {

public static void main (String args[]) {

if (args.length == 0) {

System.out.println("Numar insuficient de argumente!");

System.exit(-1); //termina aplicatia

}

String nume = args[0]; //exista sigur String prenume;

if (args.length >= 1)

prenume = args[1];

Page 24: Curs Practic de Java

24

else prenume = ""; //valoare implicita

System.out.println("Salut " + nume + " " + prenume);

}

}

Spre deosebire de limbajul C, vectorul primit de metoda main nu contine pe prima pozitie

numele aplicatiei, intrucat in Java numele aplicatiei este chiar numele clasei principale, adica a

clasei in care se gaseste metoda main.

Sa considera in continuare un exemplu simplu in care se doreste afisarea pe ecran a

argumentelor primite de la linia de comanda:

public class Afisare {

public static void main (String[] args) {

for (int i = 0; i < args.length; i++)

System.out.println(args[i]);

}

}

Un apel de genul java Afisare Hello Java va produce urmatorul rezultat (aplicatia a primit

2 argumente):

Hello

Java

Apelul java Afisare "Hello Java" va produceinsa alt rezultat (aplicatia a primit un singur

argument):

Hello Java

1.8.3 Argumente numerice

Argumentele de la linia de comanda sunt primite sub forma unui vector de siruri (obiecte

de tip String). In cazul in care unele dintre acestea reprezinta valori numerice ele vor trebui

convertite din siruri in numere. Acest lucru se realizeaza cu metode de tipul parseTipNumeric

aflate in clasa corespunzatoare tipului in care vrem sa facem conversia: Integer, Float, Double,

etc.

Sa consideram, de exemplu, ca aplicatia Power ridica un numar real la o putere intreaga,

argumentele fiind trimise de la linia de comanda sub forma:

java Power "1.5" "2" //ridica 1.5 la puterea 2

Conversia celor doua argumente in numere se va face astfel:

public class Power {

public static void main(String args[]) {

Page 25: Curs Practic de Java

25

double numar = Double.parseDouble(args[0]);

int putere = Integer.parseInt(args[1]);

System.out.println("Rezultat=" + Math.pow(numar, putere));

}

}

Metodele de tipul parseTipNumeric pot produce exceptii (erori) de tipul

NumberFormatException in cazulin care sirul primit ca parametru nu reprezinta un numar de

tipul respectiv. Tratarea acestor exceptii este prezentata in capitolul ”Exceptii”.

Page 26: Curs Practic de Java

26

2 Obiecte si clase

2.1 Ciclul de viata al unui obiect

2.1.1 Crearea obiectelor

In Java, ca in orice limbaj de programare orientat-obiect, crearea obiectelor se realizeaza

prin instantierea unei clase si implica urmatoarele lucruri:

Declararea

Presupune specificarea tipului acelui obiect, cu alte cuvinte specificarea clasei acestuia

(vom vedea ca tipul unui obiect poate fi si o interfata).

NumeClasa numeObiect;

Instantierea

Se realizeaza prin intermediul operatorului new si are ca efect crearea efectiva a

obiectului cu alocarea spatiului de memorie corespunzator.

numeObiect = new NumeClasa();

Initializarea

Se realizeaza prin intermediul constructorilor clasei respective. Initializarea este de fapt

parte integranta a procesului de instantiere, in sensul ca imediat dupa alocarea memoriei ca

efect al operatorului new este apelat constructorul specificat. Parantezele rotunde de dupa

numele clasei indica faptul ca acolo este de fapt un apel la unul din constructorii clasei si nu

simpla specificare a numelui clasei. Mai general, instantierea si initializarea apar sub forma:

numeObiect = new NumeClasa([argumente constructor]);

Sa consideram urmatorul exemplu, in care declaram si instantiem doua obiecte din clasa

Rectangle, clasa ce descrie suprafete grafice rectangulare, definite de coordonatele coltului

stanga sus (originea) si latimea, respectiv inaltimea.

Rectangle r1, r2;

r1 = new Rectangle();

r2 = new Rectangle(0, 0, 100, 200);

In primul caz Rectangle() este un apel catre constructorul clasei Rectangle care este

responsabil cu initializarea obiectului cu valorile implicite. Dupa cum observam in al doilea caz,

initializarea se poate face si cu anumiti parametri, cu conditia sa existe un constructor al clasei

respective care sa accepte parametrii respectivi.

Fiecare clasa are un set de constructori care se ocupa cu initializare obiectelor nou

create. De exemplu, clasa Rectangle are urmatorii constructori:

public Rectangle()

public Rectangle(int latime, int inaltime)

public Rectangle(int x, int y, int latime, int inaltime)

public Rectangle(Point origine)

public Rectangle(Point origine, int latime, int inaltime)

public Rectangle(Point origine, Dimension dimensiune)

Page 27: Curs Practic de Java

27

Declararea, instantierea si initializarea obiectului pot aparea pe aceeasi linie (cazul cel

mai uzual):

Rectangle patrat = new Rectangle(0, 0, 100, 100);

Obiecte anonime

Este posibila si crearea unor obiecte anonime care servesc doar pentru initializarea altor

obiecte, cazin care etapa de declarare a referintei obiectului nu mai este prezenta:

Rectangle patrat = new Rectangle(new Point(0,0), new Dimension(100, 100));

Spatiul de memorie nu este pre-alocat

Declararea unui obiect nu implica sub nici o forma alocarea de spatiu de memorie pentru

acel obiect. Alocarea memoriei se face doar la apelul operatorului new.

Rectangle patrat;

patrat.x = 10;

//Eroare – lipseste instantierea

2.1.2 Folosirea obiectelor

Odata un obiect creat, el poate fi folosit in urmatoarele sensuri: aflarea unor informatii

despre obiect, schimbarea starii sale sau executarea unor actiuni. Aceste lucruri se realizeaza

prin aflarea sau schimbarea valorilor variabilelor sale, respectiv prin apelarea metodelor sale.

Referirea valorii unei variabile se face prin obiect.variabila De exemplu clasa Rectangle

are variabilele publice x, y, width, height, origin. Aflarea valorilor acestor variabile sau

schimbarea lor se face prin constructii de genul:

Rectangle patrat = new Rectangle(0, 0, 100, 200);

System.out.println(patrat.width); //afiseaza 100

patrat.x = 10;

patrat.y = 20; //schimba originea

patrat.origin = new Point(10, 20); //schimba originea

Accesul la variabilele unui obiect se face in conformitate cu drepturile de acces pe care le

ofera variabilele respective celorlalte clase. (vezi ”Modificatori de acces pentru membrii unei

clase”)

Apelul unei metode se face prin obiect.metoda([parametri]).

Rectangle patrat = new Rectangle(0, 0, 100, 200);

patrat.setLocation(10, 20); //schimba originea

patrat.setSize(200, 300); //schimba dimensiunea

Se observa ca valorile variabilelor pot fi modificate indirect prin intermediul metodelor

sale. Programarea orientata obiect descurajeaza folosirea directa a variabilelor unui obiect

deoarece acesta poate fi adus in stari inconsistente (ireale). In schimb, pentru fiecare variabila

care descrie starea obiectului trebuie sa existe metode care sa permita schimbarea/aflarea

valorilor variabilelor sale. Acestea se numesc metode de accesare, sau metode setter -getter

si au numele de forma setVariabila, respectiv getVariabila.

Page 28: Curs Practic de Java

28

patrat.width = -100; //stare inconsistenta

patrat.setSize(-100, -200); //metoda setter

//metoda setSize poate sa testeze daca noile valori sunt

//corecte si sa valideze sau nu schimbarea lor

2.1.3 Distrugerea obiectelor

Multe limbaje de programare impun ca programatorul sa tina evidenta obiectelor create si

sa le distruga in mod explicit atunci cand nu mai este nevoie de ele, cu alte cuvinte sa

administreze singur memoria ocupata de obiectele sale. Practica a demonstrat ca aceasta

tehnica este una din principalele furnizoare de erori ce duc la functionarea defectuoasa a

programelor.

In Java programatorul nu mai are responsabilitatea distrugerii obiectelor sale intrucat, in

momentul rularii unui program, simultan cu interpretorul Java, ruleaza si un proces care se

ocupa cu distrugerea obiectelor care nu mai sunt folosite. Acest proces pus la dispozitie de

platforma Java de lucru se numeste garbage collector (colector de gunoi), prescurtat gc.

Un obiect este eliminat din memorie de procesul de colectare atunci cand nu mai exista

nici o referinta la acesta. Referintele (care sunt de fapt variabile) sunt distruse doua moduri:

natural, atunci cand variabila respectiva iese din domeniul sau de vizibilitate, de

exemplu la terminarea metodei in care ea a fost declarata;

explicit, daca atribuim variabilei respective valoare null.

Cum functioneaza colectorul de gunoaie ?

Colectorul de gunoaie este un proces de prioritate scazuta care se executa periodic,

scaneaza dinamic memoria ocupata de programul Java aflat in executie si marcheaza acele

obiecte care au referinte directe sau indirecte. Dupa ce toate obiectele au fost parcurse, cele

care au ramas nemarcate sunt eliminate automat din memorie.

Apelul metodei gc din clasa System sugereaza masinii virtuale Java sa ”depuna eforturi”

in recuperarea memoriei ocupate de obiecte care nu mai sunt folosite, fara a forta insa

pornirea procesului.

Finalizare

Inainte ca un obiect sa fie eliminat din memorie, procesul gc da acelui obiect posibilitatea

”sa curete dupa el”, apeland metoda de finalizare a obiectului respectiv. Uzual, in timpul

finalizarii un obiect isi inchide fisierele si socket-urile folosite, distruge referintele catre alte

obiecte (pentru a ussura sarcina colectorului de gunoaie), etc.

Codul pentru finalizarea unui obiect trebuie scris intr-o metoda speciala numita finalize a

clasei ce descrie obiectul respectiv. (vezi ”Clasa Object”)

Atentie

Nu confundati metoda finalize din Java cu destructorii din C++. Metoda finalize nu are

rolul de a distruge obiectul ci este apelata automat inainte de eliminarea obiectului respectiv

din memorie.

Page 29: Curs Practic de Java

29

2.2 Crearea claselor

2.2.1 Declararea claselor

Clasele reprezinta o modalitate de a introduce noi tipuri de date intr-o aplicatie Java,

cealalta modalitate fiind prin intermediul interfetelor. Declararea unei clase respecta urmatorul

format general:

[public][abstract][final]class NumeClasa [extends NumeSuperclasa] [implements

Interfata1 [, Interfata2 ... ]] {

// Corpul clasei }

Asadar, prima parte a declaratiei o ocupa modificatorii clasei. Acestia sunt:

public

Implicit, o clasa poate fi folosita doar de clasele aflate in acelasi pachet(librarie) cu clasa

respectiva (daca nu se specifica un anume pachet, toate clasele din directorul curent

sunt considerate a fi in acelasi pachet). O clasa declarata cu public poate fi folosita din

orice alta clasa, indiferent de pachetul in care se gaseste.

abstract

Declara o clasa abstracta (sablon). O clasa abstracta nu poate fi instantiata, fiind

folosita doar pentru a crea un model comun pentru o serie de subclase. (vezi ”Clase si

metode abstracte”)

final

Declara ca respectiva clasa nu poate avea subclase. Declarare claselor finale are doua

scopuri:

securitate: unele metode pot astepta ca parametru un obiect al unei anumite clase si nu

al unei subclase, dar tipul exact al unui obiect nu poate fi aflat cu exactitate decat in

momentul executiei; in felul acesta nu s-ar mai putea realiza obiectivul limbajului Java

ca un program care a trecut compilarea sa nu mai fie susceptibil de nici o eroare.

programare in spririt orientat-obiect: O clasa ”perfecta” nu trebuie sa mai aiba subclase.

Dupa numele clasei putem specifica, daca este cazul, faptul ca respectiva clasa este

subclasa a unei alte clase cu numele NumeSuperclasa sau/si ca implementeaza una sau mai

multe interfete, ale caror nume trebuie separate prin virgula.

2.2.2 Extinderea claselor

Spre deosebire de alte limbaje de programare orientate-obiect, Java permite doar

mostenirea simpla, ceea ce ineamna ca o clasa poate avea un singur parinte (superclasa).

Evident, o clasa poate avea oricati mostenitori (subclase), de unde rezulta ca multimea tuturor

claselor definite in Java poate fi vazuta ca un arbore, radacina acestuia fiind clasa Object.

Asadar, Object este singura clasa care nu are parinte, fiind foarte importanta in modul de lucru

cu obiecte si structuri de date in Java.

Page 30: Curs Practic de Java

30

Extinderea unei clase se realizeaza folosind cuvantul cheie extends:

class B extends A {...} // A este superclasa clasei B // B este o subclasa a clasei A

O subclasa mosteneste de la parintele sau toate variabilele si metodele care nu sunt private.

2.2.3 Corpul unei clase

Corpul unei clase urmeaza imediat dupa declararea clasei si este cuprins intre acolade.

Continutul acestuia este format din:

Declararea si, eventual, initializarea variabilelor de instanta si de clasa (cunoscute

impreuna ca variabile membre).

Declararea si implementarea constructorilor.

Declararea si implementarea metodelor de instanta si de clasa (cunoscute impreuna ca

metode membre).

Declararea unor clase imbricate (interne).

Spre deosebire de C++, nu este permisa doar declararea metodei in corpul clasei, urmand

ca implementare sa fie facuta in afara ei. Implementarea metodelor unei clase trebuie sa se

faca obligatoriu in corpul clasei.

// C++

class A {

void metoda1();

int metoda2() {

// Implementare

}

}

A::metoda1() {

// Implementare

}

// Java

class A {

void metoda1(){

// Implementare

}

void metoda2(){

// Implementare

}

}

Variabilele unei clase pot avea acelasi nume cu metodele clasei, care poate fi chiar

numele clasei, fara a exista posibilitatea aparitiei vreunei ambiguitati din punctul de vedere al

compilatorului. Acest lucru este insa total nerecomandat daca ne gandim din perspectiva

Page 31: Curs Practic de Java

31

lizibilitatii (claritatii) codului, dovedind un stil ineficient de progamare.

class A {

int A;

void A() {}; // Corect pentru compilator // Nerecomandat ca stil de programare

}

Atentie !!! Variabilele si metodele nu pot avea ca nume un cuvant cheie Java.

2.2.4 Constructorii unei clase

Constructorii unei clase sunt metode speciale care au acelasi nume cu cel al clasei, nu

returneaza nici o valoare si sunt folositi pentru initializarea obiectelor acelei clase in momentul

instantierii lor.

class NumeClasa {

[modificatori] NumeClasa([argumente]) { // Constructor

}

}

O clasa poate avea unul sau mai multi constructori care trebuie insa sa difere prin lista de

argumente primite. In felul acesta sunt permise diverse tipuri de initializari ale obiectelor la

crearea lor, in functie de numarul parametrilor cu care este apelat constructorul.

Sa consideram ca exemplu declararea unei clase care descrie notiunea de dreptunghi si

trei posibili constructori pentru aceasta clasa.

class Dreptunghi {

double x, y, w, h;

Dreptunghi(double x1, double y1, double w1, double h1) {

// Cel mai general constructor

x=x1; y=y1; w=w1; h=h1;

System.out.println("Instantiere dreptunghi");

}

Dreptunghi(double w1, double h1) {

// Constructor cu doua argumente

x=0; y=0; w=w1; h=h1;

System.out.println("Instantiere dreptunghi");

}

Dreptunghi() {

// Constructor fara argumente

x=0; y=0; w=0; h=0;

System.out.println("Instantiere dreptunghi");

}

}

Page 32: Curs Practic de Java

32

Constructorii sunt apelati automat la instantierea unui obiect. In cazul in care dorim sa

apelam explicit constructorul unei clase folosim expresia

this( argumente ),

care apeleaza constructorul corespunzator (ca argumente) al clasei respective. Aceasta

metoda este folosita atunci cand sunt implementati mai multi constructori pentru o clasa,

pentru a nu repeta secventele de cod scrise deja la constructorii cu mai multe argumente (mai

generali). Mai eficient, fara a repeta aceleasi secvente de cod in toti constructorii (cum ar fi

afisarea mesajului ”Instantiere dreptunghi”), clasa de mai sus poate fi rescrisa astfel:

class Dreptunghi {

double x, y, w, h;

Dreptunghi(double x1, double y1, double w1, double h1) {

// Implementam doar constructorul cel mai general

x=x1; y=y1; w=w1; h=h1;

System.out.println("Instantiere dreptunghi");

}

Dreptunghi(double w1, double h1) {

this(0, 0, w1, h1); // Apelam constructorul cu 4 argumente

}

Dreptunghi() {

this(0, 0); // Apelam constructorul cu 2 argumente

}

}

Dintr-o subclasa putem apela explicit constructorii superclasei cu expresia

super( argumente ).

Sa presupunem ca dorim sa cream clasa Patrat, derivata din clasa Dreptunghi:

class Patrat extends Dreptunghi {

Patrat(double x, double y, double d) {

super(x, y, d, d); // Apelam constructorul superclasei

}

}

Atentie !!! : Apelul explcit al unui constructor nu poate aparea decat intr-un alt constructor si

trebuie sa fie prima instructiune din constructorul respectiv.

Constructorul implicit

Constructorii sunt apelati automat la instantierea unui obiect. In cazul in care scriem o

clasa care nu are declarat nici un constructor, sistemul ii creeaza automat un constructor

implicit, care nu primeste nici un argument si care nu face nimic. Deci prezenta constructorilor

in corpul unei clase nu este obligatorie. Daca insa scriem un constructor pentru o clasa, care

Page 33: Curs Practic de Java

33

are mai mult de un argument, atunci constructorul implicit (fara nici un argument) nu va mai fi

furnizat implicit de catre sistem. Sa consideram, ca exemplu, urmatoarele declaratii de clase:

class Dreptunghi {

double x, y, w, h;

// Nici un constructor

}

class Cerc {

double x, y, r;

// Constructor cu 3 argumente

Cerc(double x, double y, double r) { ... };

}

Sa consideram acum doua instantieri ale claselor de mai sus:

Dreptunghi d = new Dreptunghi();

// Corect (a fost generat constructorul implicit)

Cerc c;

c = new Cerc();

// Eroare la compilare !

c = new Cerc(0, 0, 100);

// Varianta corecta

In cazul mostenirii unei clase, instantierea unui obiect din clasa extinsa implica

instantierea unui obiect din clasa parinte. Din acest motiv, fiecare constructor al clasei fiu va

trebui sa aiba un constructor cu aceeasi signatura in parinte sau sa apeleze explicit un

constructor al clasei extinse folosind expresia super([argumente]), in caz contrar fiind

semnalata o eroare la compilare.

class A { int x=1; A(int x) { this.x = x;}

}

class B extends A { // Corect B() {super(2);} B(int x) {super.x = x;}

}

class C extends A { // Eroare la compilare ! C() {super.x = 2;} C(int x) {super.x = x;}

}

Constructorii unei clase pot avea urmatorii modificatori de acces: public, protected,

private si cel implicit.

• public : In orice alta clasa se pot crea instante ale clasei respective.

• protected : Doar in subclase pot fi create obiecte de tipul clasei respective.

• private : In nici o alta clasa nu se pot instantia obiecte ale acestei clase. O astfel de clasa

poate contine metode publice (numite ”factory methods”) care sa fie responsabile cu crearea

obiectelor, controland in felul acesta diverse aspecte legate de instantierea clasei respective.

• implicit : Doar in clasele din acelasi pachet se pot crea instante ale clasei respective.

Page 34: Curs Practic de Java

34

2.2.5 Declararea variabilelor

Variabilele membre ale unei clase se declara de obicei inaintea metodelor, desi acest

lucru nu este impus de catre compilator.

class NumeClasa {

// Declararea variabilelor

// Declararea metodelor

}

Variabilele membre ale unei clase se declara in corpul clasei si nu in corpul unei metode,

fiind vizibile in toate metodele respectivei clase. Variabilele declarate in cadrul unei metode

sunt locale metodei respective.

Declararea unei variabile presupune specificarea urmatoarelor lucruri:

• numele variabilei

• tipul de date al acesteia

• nivelul de acces la acea variabila din alte clase

• daca este constanta sau nu

• daca este variabila de instanta sau de clasa

• alti modificatori Generic, o variabila se declara astfel:

[modificatori] Tip numeVariabila [ = valoareInitiala ];

unde un modificator poate fi :

un modificator de acces : public, protected, private (vezi ”Modificatori de acces pentru

membrii unei clase”)

unul din cuvintele rezervate: static, final, transient, volatile Exemple de declaratii de

variabile membre:

class Exemplu {

double x;

protected static int n;

public String s = "abcd";

private Point p = new Point(10, 10);

final static long MAX = 100000L;

}

Page 35: Curs Practic de Java

35

Sa analizam modificatorii care pot fi specificati pentru o variabila, altii decat cei de acces

care sunt tratati intr-o sectiune separata: ”Specificatori de acces pentru membrii unei clase”.

static :

Prezenta lui declara ca o variabila este variabila de clasa si nu de instanta. (vezi ”Membri

de instanta si membri de clasa”)

int variabilaInstanta ; static int variabilaClasa;

final :

Indica faptul ca valoarea variabilei nu mai poate fi schimbata, cu alte cuvinte este folosit

pentru declararea constantelor.

final double PI = 3.14 ; ... PI = 3.141; // Eroare la compilare !

Prin conventie, numele variabilelor finale se scriu cu litere mari. Folosirea lui final aduce o

flexibilitate sporita in lucrul cu constante, in sensul ca valoarea unei variabile nu trebuie

specificata neaparat la declararea ei (ca in exemplul de mai sus), ci poate fi specificata si

ulterior intr-un constructor, dupa care ea nu va mai putea fi modificata.

class Test {

final int MAX;

Test() {

MAX = 100; // Corect

MAX = 200; // Eroare la compilare !

}

}

transient

Este folosit la serializarea obiectelor, pentru a specifica ce variabile membre ale unui obiect nu

participa la serializare. (vezi ”Serializarea obiectelor”)

volatile

Este folosit pentru a semnala compilatorului sa nu execute anumite optimizari asupra

membrilor unei clase. Este o facilitate avansata a limbajului Java.

2.2.6 this si super

Sunt variabile predefinite care fac referinta, in cadrul unui obiect, la obiectul propriu-zis

(this), respectiv la instanta parintelui (super). Sunt folosite in general pentru a rezolva conflicte

de nume prin referirea explicita a unei variabile sau metode membre. Dupa cum am vazut,

utilizate sub forma de metode au rolul de a apela constructorii corespunzatori ca argumente ai

clasei curente, respectiv ai superclasei

class A {

int x;

A() {

this(0);

}

Page 36: Curs Practic de Java

36

A(int x) {

this.x = x;

}

void metoda() {

x ++;

}

}

class B extends A {

B() {

this(0);

}

B(int x) {

super(x);

System.out.println(x);

}

void metoda() {

super.metoda();

System.out.println(x);

}

}

2.3 Implementarea metodelor

2.3.1 Declararea metodelor

Metodele sunt responsabile cu descrierea comportamentului unui obiect. Intrucat Java

este un limbaj de programare complet orientat-obiect, metodele se pot gasi doar in cadrul

claselor. Generic, o metoda se declara astfel:

[modificatori] TipReturnat numeMetoda ( [argumente] ) [throws TipExceptie1,

TipExceptie2, ...] {

// Corpul metodei

}

unde un modificator poate fi :

un specificator de acces : public, protected, private (vezi ”Specificatori de acces pentru

membrii unei clase”)

unul din cuvintele rezervate: static, abstract, final, native, synchronized

Sa analizam modificatorii care pot fi specificati pentru o metoda, altii decat cei de acces care

sunt tratati intr-o sectiune separata.

Page 37: Curs Practic de Java

37

static

Prezenta lui declara ca o metoda este de clasa si nu de instanta. (vezi ”Membri de instanta

si membri de clasa”)

void metodaInstanta();

static void metodaClasa();

abstract

Permite declararea metodelor abstracte. O metoda abstracta este o metoda care nu are

implementare si trebuie obligatoriu sa faca parte dintr-o clasa abstracta. (vezi ”Clase si metode

abstracte”)

final

Specifica faptul ca acea metoda nu mai poate fi supradefinita in subclasele clasei in care ea

este definita ca fiind finala. Acest lucru este util daca respectiva metoda are o implementare

care nu trebuie schimbata sub nici o forma in subclasele ei, fiind critica pentru consistenta

starii unui obiect. De exemplu, studentilor unei universitati trebuie sa li se calculeze media

finala, in functie de notele obtinute la examene, in aceeasi maniera, indiferent de facultatea la

care sunt.

class Student {

...

final float calcMedie(float note[], float ponderi[]) {

...

}

...

}

class StudentInformatica extends Student {

float calcMedie(float note[], float ponderi[]) {

return 10.00;

}

}// Eroare la compilare !

native In cazul in care avem o librarie importanta de functii scrise in alt limbaj de

programare, cum ar fi C, C++ si limbajul de asamblare, acestea pot fi refolosite din

programele Java. Tehnologia care permite acest lucru se numeste JNI (Java Native

Interface) si permite asocierea dintre metode Java declarate cu native si metode native

scrise in limbajele de programare mentionate.

synchronized Este folosit in cazul in care se lucreaza cu mai multe fire de executie iar

metoda respectiva gestioneaza resurse comune. Are ca efect construirea unui monitor

care nu permite executarea metodei, la un moment dat, decat unui singur fir de

executie. (vezi ”Fire de executie”)

Page 38: Curs Practic de Java

38

2.3.2 Tipul returnat de o metoda

Metodele pot sau nu sa returneze o valoare la terminarea lor. Tipul returnat poate fi atat

un tip primitiv de date sau o referinta la un obiect al unei clase. In cazul in care o metoda nu

returneaza nimic atunci trebuie obligatoriu specificat cuvantul cheie void ca tip returnat:

public void afisareRezultat() {

System.out.println("rezultat");

}

private void deseneaza(Shape s) {

...

return;

}

Daca o metoda trebuie sa returneze o valoare acest lucru se realizeaza prin intermediul

instructiunii return, care trebuie sa apara in toate situatiile de terminare a functiei.

double radical(double x) {

if (x >= 0)

return Math.sqrt(x);

else {

System.out.println("Argument negativ !");

// Eroare la compilare

// Lipseste return pe aceasta ramura

}

}

In cazul in care in declaratia functiei tipul returnat este un tip primitiv de date, valoarea

returnata la terminarea functiei trebuie sa aiba obligatoriu acel tip sau un subtip al sau, altfel va

fi furnizata o eroare la compilare. In general, orice atribuire care implica pierderi de date este

tratata de compilator ca eroare.

int metoda() { return 1.2; // Eroare }

int metoda() {

return (int)1.2; // Corect }

double metoda() { return (float)1; // Corect }

Daca valoarea returnata este o referinta la un obiect al unei clase, atunci clasa obiectului

returnat trebuie sa coincida sau sa fie o subclasa a clasei specificate la declararea metodei. De

exemplu, fie clasa Poligon si subclasa acesteia Patrat.

Poligon metoda1( ) {

Poligon p = new Poligon();

Patrat t = new Patrat();

if (...)

return p; // Corect

else

return t; // Corect

}

Page 39: Curs Practic de Java

39

Patrat metoda2( ) {

Poligon p = new Poligon();

Patrat t = new Patrat();

if (...)

return p; // Eroare

else return t; // Corect

}

2.3.3 Trimiterea parametrilor catre o metoda

Signatura unei metode este data de numarul si tipul argumentelor primite de acea

metoda. Tipul de date al unui argument poate fi orice tip valid al limbajului Java, atat tip primitiv

cat si tip referinta.

TipReturnat metoda([Tip1 arg1, Tip2 arg2, ...])

Exemplu:

void adaugarePersoana(String nume, int varsta, float salariu)

// String este tip referinta // int si float sunt tipuri primitive

Spre deosebire de alte limbaje, in Java nu pot fi trimise ca parametri ai unei metode

referinte la alte metode (functii), insa pot fi trimise referinte la obiecte care sa contina

implementarea acelor metode, pentru a fi apelate. Pana la aparitia versiunii 1.5, in Java o

metoda nu putea primi un numar variabil de argumente, ceea ce inseamna ca apelul unei

metode trebuia sa se faca cu specificarea exacta a numarului si tipurilor argumentelor. Vom

analiza intr-o sectiune separata modalitate de specificare a unui numar variabil de argumente

pentru o metoda. Numele argumentelor primite trebuie sa difere intre ele si nu trebuie sa co-

incida cu numele nici uneia din variabilele locale ale metodei. Pot insa sa coincida cu numele

variabilelor membre ale clasei, caz in care diferentierea dintre ele se va face prin intermediul

variabile this.

class Cerc {

int x, y, raza;

public Cerc(int x, int y, int raza) {

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

}

}

In Java argumentele sunt trimise doar prin valoare (pass-by-value). Acest lucruinseamna

ca metoda receptioneaza doar valorile variabilelor primite ca parametri. Cand argumentul are

tip primitiv de date, metoda nu-i poate schimba valoarea decat local (in cadrul metodei); la

revenirea din metoda variabila are aceeasi valoare ca inaintea apelului, modificarile facute in

cadrul metodei fiind pierdute. Cand argumentul este de tip referinta, metoda nu poate schimba

Page 40: Curs Practic de Java

40

valoarea referintei obiectului, insa poate apela metodele acelui obiect si poate modifica orice

variabila membra accesibila.

Asadar, daca dorim ca o metoda sa schimbe starea (valoarea) unui argument primit,

atunci el trebuie sa fie neaparat de tip referinta.

De exemplu, sa consideram clasa Cerc descrisa anterior in care dorim sa implementam o

metoda care sa returneze parametrii cercului.

// Varianta incorecta:

class Cerc {

private int x, y, raza;

public void aflaParametri(int valx, int valy, int valr) {

// Metoda nu are efectul dorit!

valx = x; valy = y; valr = raza;

}

}

Aceasta metoda nu va realiza lucrul propus intrucat ea primeste doar valorile variabilelor

valx, valy si valr si nu referinte la ele (adresele lor de memorie), astfel incat sa le poata modifica

valorile. In concluzie, metoda nu realizeaza nimic pentru ca nu poate schimba valorile

variabilelor primite ca argumente.

Pentru a rezolva lucrul propus trebuie sa definim o clasa suplimentara care sa descrie

parametrii pe care dorim sa-i aflam:

// Varianta corecta

class Param {

public int x, y, raza;

}

class Cerc {

private int x, y, raza;

public void aflaParametri(Param param) {

param.x = x;

param.y = y;

param.raza = raza;

}

}

Argumentul param are tip referinta si, desi nu ii schimbam valoarea (valoarea sa este

adresa de memorie la care se gaseste si nu poate fi schimbata), putem schimba starea

obiectului, adica informatia propriu-zisa continuta de acesta.

Varianta de mai sus a fost data pentru a clarifica modul de trimitere a argumentelor unei

metode. Pentru a aflainsa valorile variabilelor care descriu starea unui obiect se folosesc

metode de tip getter insotite de metode setter care sa permita schimbarea starii obiectului:

Page 41: Curs Practic de Java

41

class Cerc {

private int x, y, raza;

public int getX() {

return x;

}

public void setX(int x) {

this.x = x;

}

...

}

2.3.4 Metode cu numar variabil de argumente

Incepand cu versiunea 1.5 a limbajului Java, exista posibilitate de a declara

metode care sa primeasca un numar variabil de argumente. Noutatea consta in folosirea

simbolului ..., sintaxa unei astfel de metode fiind:

[modificatori] TipReturnat metoda(TipArgumente ... args)

args reprezinta un vector avand tipul specificat si instantiat cu un numar variabil de

argumente, in functie de apelul metodei. Tipul argumentelor poate fi referinta sau primitiv.

Metoda de mai jos afiseaza argumentele primite, care pot fi de orice tip:

void metoda(Object ... args) {

for(int i=0; i<args.length; i++)

System.out.println(args[i]);

}

...

metoda("Hello");

metoda("Hello", "Java", 1.5);

2.3.5 Supraincarcarea si supradefinirea metodelor

Supraincarcarea si supradefinirea metodelor sunt doua concepte extrem de utile ale

programarii orientate obiect, cunoscute si sub denumirea de polimorfism, si se refera la:

supraincarcarea (overloading) : in cadrul unei clase pot exista metode cu acelasi nume

cu conditia ca signaturile lor sa fie diferite (lista de argumente primite sa difere fie prin

numarul argumentelor, fie prin tipul lor) astfel incat la apelul functiei cu acel nume sa se

poata stabili in mod unic care dintre ele se executa.

supradefinirea (overriding): o subclasa poate rescrie o metoda a clasei parinte prin

implementarea unei metode cu acelasi nume si aceeasi signatura ca ale superclasei.

Page 42: Curs Practic de Java

42

class A {

void metoda() {

System.out.println("A: metoda fara parametru");

}

// Supraincarcare

void metoda(int arg) {

System.out.println("A: metoda cu un parametru");

}

}

class B extends A {

// Supradefinire

void metoda() {

System.out.println("B: metoda fara parametru");

}

}

O metoda supradefinita poate sa:

ignore complet codul metodei corespunzatoare din superclasa (cazul de mai sus):

B b = new B(); b.metoda(); // Afiseaza "B: metoda fara parametru";

extinda codul metodei parinte, executand inainte de codul propriu si functia parintelui:

class B extends A {

// Supradefinire prin extensie

void metoda() {

super.metoda();

System.out.println("B: metoda fara parametru");

}

}

...

B b = new B();

b.metoda();

/* Afiseaza ambele mesaje:

"A: metoda fara parametru"

"B: metoda fara parametru" */

O metoda nu poate supradefini o metoda declarata finala in clasa parinte.

Orice clasa care nu este abstracta trebuie obligatoriu sa supradefineasca metodele

abstracte ale superclasei (daca este cazul). In cazul in care o clasa nu supradefineste toate

metodele abstracte ale parintelui, ea insasi este abstracta si va trebui declarata ca atare.

In Java nu este posibila supraincarcarea operatorilor.

Page 43: Curs Practic de Java

43

2.4 Modificatori de acces

Modificatorii de acces sunt cuvinte rezervate ce controleaza accesul celorlate clase la

membrii unei clase. Specificatorii de acces pentru variabilele si metodele unei clase sunt:

public, protected, private si cel implicit (la nivel de pachet).

Asadar, daca nu este specificat nici un modificator de acces, implicit nivelul de acces este

la nivelul pachetului. In cazul in care declaram un membru ”protected” atunci accesul la acel

membru este permis din subclasele clasei in care a fost declarat dar depinde si de pachetul in

care se gaseste subclasa: daca sunt in acelasi pachet accesul este permis, daca nu sunt in

acelasi pachet accesul nu este permis decat pentru obiecte de tipul subclasei.

Exemple de declaratii:

private int secretPersonal;

protected String secretDeFamilie;

public Vector pentruToti;

long doarIntrePrieteni;

private void metodaInterna();

public String informatii();

2.5 Membri de instanta si membri de clasa

O clasa Java poate contine doua tipuri de variabile si metode :

de instanta: declarate fara modificatorul static, specifice fiecarei instante create dintr-o

clasa si

de clasa: declarate cu modificatorul static, specifice clasei.

2.5.1 Variabile de instanta si de clasa

Cand declaram o variabila membra fara modificatorul static, cum ar fi x in exemplul de

mai jos:

class Exemplu {

int x ; //variabila de instanta

}

se declara de fapt o variabila de instanta, ceea ce inseamna ca la fiecare creare a unui

obiect al clasei Exemplu sistemul aloca o zona de memorie separata pentru memorarea

valorii lui x.

Exemplu o1 = new Exemplu(); o1.x = 100;

Exemplu o2 = new Exemplu();

o2.x = 200;

System.out.println(o1.x); // Afiseaza 100

System.out.println(o2.x); // Afiseaza 200

Page 44: Curs Practic de Java

44

Asadar, fiecare obiect nou creat va putea memora valori diferite pentru variabilele sale de

instanta.

Pentru variabilele de clasa (statice) sistemul aloca o singura zona de memorie la care au

acces toate instantele clasei respective, ceea ce inseamna ca daca un obiect modifica

valoarea unei variabile statice ea se va modifica si pentru toate celelalte obiecte. Deoarece nu

depind de o anumita instanta a unei clase, variabilele statice pot fi referite si sub forma:

NumeClasa.numeVariabilaStatica

class Exemplu {

int x ; // Variabila de instanta

static long n; // Variabila de clasa

}

...

Exemplu o1 = new Exemplu();

Exemplu o2 = new Exemplu();

o1.n = 100;

System.out.println(o2.n); // Afiseaza 100

o2.n = 200;

System.out.println(o1.n); // Afiseaza 200

System.out.println(Exemplu.n); // Afiseaza 200

// o1.n, o2.n si Exemplu.n sunt referinte la aceeasi valoare

Initializarea variabilelor de clasa se face o singura data, la incarcarea in memorie a clasei

respective, si este realizata prin atribuiri obisnuite:

class Exemplu {

static final double PI = 3.14;

static long nrInstante = 0;

static Point p = new Point(0,0); }

2.5.2 Metode de instanta si de clasa

Similar ca la variabile, metodele declarate fara modificatorul static sunt metode de instanta

iar cele declarate cu static sunt metode de clasa (statice). Diferenta intre cele doua tipuri de

metode este urmatoarea:

metodele de instanta opereaza atat pe variabilele de instanta cat si pe cele statice ale

clasei;

metodele de clasa opereaza doar pe variabilele statice ale clasei.

Page 45: Curs Practic de Java

45

class Exemplu {

int x ; // Variabila de instanta

static long n; // Variabila de clasa

void metodaDeInstanta() {

n ++; // Corect

x --; // Corect

}

static void metodaStatica() {

n ++; // Corect

x --; // Eroare la compilare !

}

}

Intocmai ca si la variabilele statice, intrucat metodele de clasa nu depind de starea

obiectelor clasei respective, apelul lor se poate face si sub forma:

NumeClasa.numeMetodaStatica

Exemplu.metodaStatica(); // Corect, echivalent cu

Exemplu obj = new Exemplu();

obj.metodaStatica(); // Corect, de asemenea

Metodele de instanta nu pot fi apelate decat pentru un obiect al clasei respective:

Exemplu.metodaDeInstanta(); // Eroare la compilare !

Exemplu obj = new Exemplu();

obj.metodaDeInstanta(); // Corect

2.5.3 Utilitatea membrilor de clasa

Membrii de clasa sunt folositi pentru a pune la dispozitie valori si metode independente

de starea obiectelor dintr-o anumita clasa.

Declararea eficienta a constantelor

Sa consideram situatia cand dorim sa declaram o constanta.

class Exemplu {

final double PI = 3.14; // Variabila finala de instanta

}

La fiecare instantiere a clasei Exemplu va fi rezervata o zona de memorie pentru

variabilele finale ale obiectului respectiv, ceea ce este o risipa intrucat aceste constante au

aceleasi valori pentru toate instantele clasei. Declararea corecta a constantelor trebuie asadar

facuta cu modificatorii static si final, pentru a le rezerva o singura zona de memorie, comuna

tuturor obiectelor:

class Exemplu {

static final double PI = 3.14; // Variabila finala de clasa

}

Page 46: Curs Practic de Java

46

Numararea obiectelor unei clase

Numararea obiectelor unei clase poate fi facuta extrem de simplu folosind

o variabila statica si este utila in situatiile cand trebuie sa controlam diversi parametri legati de

crearea obiectelor unei clase.

class Exemplu {

static long nrInstante = 0;

Exemplu() {

// Constructorul este apelat la fiecare instantiere

nrInstante ++;

}

}

Implementarea functiilor globale

Spre deosebire de limbajele de programare procedurale, in Java nu putem avea functii

globale definite ca atare, intrucat ”orice este un obiect”. Din acest motiv chiar si metodele care

au o functionalitate globala trebuie implementate in cadrul unor clase. Acest lucru se va face

prin intermediul metodelor de clasa (globale), deoarece acestea nu depind de starea partic-

ulara a obiectelor din clasa respectiva. De exemplu, sa consideram functia sqrt care extrage

radicalul unui numar si care se gaseste in clasa Math. Daca nu ar fi fost functie de clasa, apelul

ei ar fi trebuit facut astfel (incorect, de altfel):

// Incorect !

Math obj = new Math();

double rad = obj.sqrt(121);

ceea ce ar fi fost extrem de neplacut... Fiind insa metoda statica ea poate fi

apelata prin:

Math.sqrt(121) .

Asadar, functiile globale necesare unei aplicatii vor fi grupate corespunzator in diverse

clase si implementate ca metode statice.

2.5.4 Blocuri statice de initializare

Variabilele statice ale unei clase sunt initializate la un moment care precede prima

utilizare activa a clasei respective. Momentul efectiv depinde de implementarea masinii

virtuale Java si poarta numele de initializarea clasei. Pe langa setarea valorilor variabilelor

statice, in aceasta etapa sunt executate si blocurile statice de initializare ale clasei. Acestea

sunt secvente de cod de forma:

static {

// Bloc static de initializare;

...

}

Page 47: Curs Practic de Java

47

care se comporta ca o metoda statica apelata automat de catre masina virtuala. Variabilele

referite intr-un bloc static de initializare trebuie sa fie obligatoriu de clasa sau locale blocului:

public class Test {

// Declaratii de variabile statice

static int x = 0, y, z;

// Bloc static de initializare

static {

System.out.println("Initializam...");

int t=1; y = 2; z = x + y + t;

}

Test() {

/* La executia constructorului variabilele de clasa sunt deja initializate

si toate blocurile statice de initializare au fost obligatoriu executate in

prealabil.

*/

...

}

}

2.6 Clase imbricate 2.6.1 Definirea claselor imbricate

O clasa imbricata este, prin definitie, o clasa membra a unei alte clase, numita si clasa de

acoperire. In functie de situatie, definirea unei clase interne se poate face fie ca membru al

clasei de acoperire -caz in care este accesibila tuturor metodelor, fie local in cadrul unei

metode.

class ClasaDeAcoperire{

class ClasaImbricata1 {

// Clasa membru

}

void metoda() {

class ClasaImbricata2 { // Clasa locala metodei }

}

}

Folosirea claselor imbricate se face atunci cand o clasa are nevoie in implementarea ei

de o alta clasa si nu exista nici un motiv pentru care aceasta din urma sa fie declarata de sine

statatoare (nu mai este folosita nicaieri).

O clasa imbricata are un privilegiu special fata de celelalte clase si anume acces

nerestrictionat la toate variabilele clasei de acoperire, chiar daca aces-tea sunt private. O clasa

declarata locala unei metode va avea acces si la variabilele finale declarate in metoda

Page 48: Curs Practic de Java

48

respectiva.

class ClasaDeAcoperire{

private int x=1;

class ClasaImbricata1 {

int a=x;

}

void metoda() {

final int y=2;

int z=3;

class ClasaImbricata2 {

int b=x;

int c=y;

int d=z; // Incorect

}

}

}

O clasa imbricata membra (care nu este locala unei metode) poate fi referita din exteriorul

clasei de acoperire folosind expresia

ClasaDeAcoperire.ClasaImbricata

Asadar, clasele membru pot fi declarate cu modificatorii public, protected, private pentru

a controla nivelul lor de acces din exterior, intocmai ca orice variabila sau metoda mebra a

clasei. Pentru clasele imbricate locale unei metode nu sunt permisi acesti modificatori.

Toate clasele imbricate pot fi declarate folosind modificatorii abstract si final, semnificatia lor

fiind aceeasi ca si in cazul claselor obisnuite.

2.6.2 Clase interne

Spre deosebire de clasele obisnuite, o clasa imbricata poate fi declarata statica sau nu. O

clasa imbricata nestatica se numeste clasa interna.

class ClasaDeAcoperire{

...

class ClasaInterna {

...

}

static class ClasaImbricataStatica {

...

}

}

Page 49: Curs Practic de Java

49

Diferentierea acestor denumiri se face deoarece:

”clasa imbricata” reflecta relatia sintactica a doua clase: codul unei clase apare in

interiorul codului altei clase;

”clasa interna” reflecta relatia dintre instantele a doua clase, in sensul ca o instanta a

unei clase interne nu poate exista decat in cadrul unei instante a clasei de acoperire.

In general, cele mai folosite clase imbricate sunt cele interne.

Asadar, o clasa interna este o clasa imbricata ale carei instante nu pot exista decat in

cadrul instantelor clasei de acoperire si care are acces direct la toti membrii clasei sale de

acoperire.

Dupa cum stim orice clasa produce la compilare asa numitele ”unitati de compilare”, care

sunt fisiere avand numele clasei respective si extensia .class si care contin toate informatiile

despre clasa respectiva. Pentru clasele imbricate aceste unitati de compilare sunt denumite

astfel: numele clasei de acoperire, urmat de simbolul ’$’ apoi de numele clasei imbricate.

class ClasaDeAcoperire{

class ClasaInterna1 {}

class ClasaInterna2 {}

}

Pentru exemplul de mai sus vor fi generate trei fisiere:

ClasaDeAcoperire.class

ClasaDeAcoperire$ClasaInterna1.class

ClasaDeAcoperire$ClasaInterna2.class

In cazul in care clasele imbricate au la randul lor alte clase imbricate (situatie mai putin

uzuala) denumirea lor se face dupa aceeasi regula: adaugarea unui ’$’ si apoi numele clasei

imbricate.

2.6.3 Clase anonime

Exista posibilitatea definirii unor clase imbricate locale, fara nume, utilizate doar pentru

instantierea unui obiect de un anumit tip. Astfel de clase se numesc clase anonime si sunt

foarte utile in situatii cum ar fi crearea unor obiecte ce implementeaza o anumita interfata sau

extind o anumita clasa abstracta.

Exemple de folosire a claselor anonime vor fi date in capitolul ”Interfete”, precum si

extensiv in capitolul ”Interfata grafica cu utilizatorul”.

Fisierele rezultate in urma compilarii claselor anonime vor avea numele de forma

ClasaAcoperire.$1,..., ClasaAcoperire.$n, unde n este numarul de clase anonime definite in

clasa respectiva de acoperire.

Page 50: Curs Practic de Java

50

2.7 Clase si metode abstracte

Uneori in proiectarea unei aplicatii este necesar sa reprezentam cu ajutorul

claselor concepte abstracte care sa nu poata fi instantiate si care sa foloseasca doar la

dezvoltarea ulterioara a unor clase ce descriu obiecte concrete. De exemplu, in pachetul

java.lang exista clasa abstracta Number care modeleaza conceptul generic de ”numar”. Intr-un

program nu avem insa nevoie de nu-mere generice ci de numere de un anumit tip: intregi,

reale, etc. Clasa Number serveste ca superclasa pentru clasele concrete Byte, Double, Float,

Integer, Long si Short, ce implementeaza obiecte pentru descrierea numerelor de un anumit

tip. Asadar, clasa Number reprezinta un concept abstract si nu vom putea instantia obiecte de

acest tip -vom folosi in schimb subclasele sale.

Number numar = new Number(); // Eroare

Integer intreg = new Integer(10); // Corect

2.7.1 Declararea unei clase abstracte

Declararea unei clase abstracte se face folosind cuvantul rezervat abstract:

[public] abstract class ClasaAbstracta [extends Superclasa] [implements Interfata1,

Interfata2, ...] {

// Declaratii uzuale

// Declaratii de metode abstracte

}

O clasa abstracta poate avea modificatorul public, accesul implicit fiind la nivel de pachet,

dar nu poate specifica modificatorul final, combinatia abstract final fiind semnalata ca eroare la

compilare -de altfel, o clasa declarata astfel nu ar avea nici o utilitate.

O clasa abstracta poate contine aceleasi elemente membre ca o clasa obisnuita, la care

se adauga declaratii de metode abstracte -fara nici o implementare.

2.7.2 Metode abstracte

Spre deosebire de clasele obisnuite care trebuie sa furnizeze implementari pentru toate

metodele declarate, o clasa abstracta poate contine metode fara nici o implementare.

Metodele fara nici o implementare se numesc metode abstracte si pot aparea doar in clase

abstracte. In fata unei metode abstracte trebuie sa apara obligatoriu cuvantul cheie abstract,

altfel va fi furnizata o eroare de compilare.

abstract class ClasaAbstracta {

abstract void metodaAbstracta(); // Corect

void metoda(); // Eroare

}

In felul acesta, o clasa abstracta poate pune la dispozitia subclaselor sale un model

complet pe care trebuie sa-l implementeze, furnizand chiar implementarea unor metode

Page 51: Curs Practic de Java

51

comune tuturor claselor si lasand explicitarea altora fiecarei subclase in parte. Un exemplu

elocvent de folosire a claselor si metodelor abstracte este descrierea obiectelor grafice intr-o

maniera orientata-obiect.

Obiecte grafice: linii, dreptunghiuri, cercuri, curbe Bezier, etc.;

Stari comune: pozitia(originea), dimensiunea, culoarea, etc.;

Comportament: mutare, redimensionare, desenare, colorare, etc.

Pentru a folosi starile si comportamentele comune acestor obiecte in avantajul nostru

putem declara o clasa generica GraphicObject care sa fie superclasa pentru celelalte clase.

Metodele abstracte vor fi folosite pentru implementarea comportamentului specific fiecarui

obiect, cum ar fi desenarea iar cele obisnuite pentru comportamentul comun tuturor, cum ar fi

schimbarea originii. Implementarea clasei abstracte GraphicObject ar putea arata astfel:

abstract class GraphicObject {

// Stari comune

private int x, y;

private Color color = Color.black;

...

// Metode comune

public void setX(int x) {

this.x = x;

}

public void setY(int y) {

this.y = y;

}

public void setColor(Color color) {

this.color = color;

}

...

// Metode abstracte

abstract void draw(); ...

}

O subclasa care nu este abstracta a unei clase abstracte trebuie sa furnizeze obligatoriu

implementari ale metodelor abstracte definite in superclasa. Implementarea claselor pentru

obiecte grafice ar fi:

class Circle extends GraphicObject {

void draw() { // Obligatoriu implementarea

...

}

}

Page 52: Curs Practic de Java

52

class Rectangle extends GraphicObject {

void draw() { // Obligatoriu implementarea

...

}

}

Legat de metodele abstracte, mai trebuie mentionate urmatoarele:

clasa abstracta poate sa nu aiba nici o metoda abstracta.

metoda abstracta nu poate aparea decat intr-o clasa abstracta.

Orice clasa care are o metoda abstracta trebuie declarata ca fiind abstracta.

In API-ul oferit de platforma de lucru Java sunt numeroase exemple de ierarhii care

folosesc la nivelele superioare clase abstracte. Dintre cele mai importante amintim:

Number: superclasa abstracta a tipurilor referinta numerice

Reader, Writer: superclasele abstracte ale fluxurilor de intrare/iesire pe caractere

InputStream, OutputStream: superclasele abstracte ale fluxurilor de intrare/iesire pe

octeti

AbstractList, AbstractSet, AbstractMap: superclase abstracte pentru structuri de date

de tip colectie

Component : superclasa abstracta a componentelor folosite in dezvoltarea de aplicatii

cu interfata grafica cu utilizatorul (GUI), cum ar fi Frame, Button, Label, etc.

etc.

2.8 Clasa Obiect

2.8.1 Orice clasa are o superclasa

Dupa cum am vazut in sectiunea dedicata modalitatii de creare a unei clase, clauza

”extends” specifica faptul ca acea clasa extinde (mosteneste) o alta clasa, numita superclasa.

O clasa poate avea o singura superclasa (Java nu suporta mostenirea multipla) si chiar daca

nu specificam clauza ”extends” la crearea unei clase ea totusi va avea o superclasa. Cu alte

cuvinte, in Java orice clasa are o superclasa si numai una. Evident, trebuie sa existe o exceptie

de la aceasta regula si anume clasa care reprezinta radacina ierarhiei formata de relatiile de

mostenire dintre clase. Aceasta este clasa Object.

Clasa Object este si superclasa implicita a claselor care nu specifica o anumita

superclasa. Declaratiile de mai jos sunt echivalente:

class Exemplu {}

class Exemplu extends Object {}

Page 53: Curs Practic de Java

53

2.8.2 Clasa Object

Clasa Object este cea mai generala dintre clase, orice obiect fiind, direct sau indirect,

descendent al acestei clase. Fiind parintele tuturor, Object defineste si implementeaza

comportamentul comun al tuturor celorlalte clase Java, cum ar fi:

posibilitatea testarii egalitatii valorilor obiectelor,

specificarea unei reprezentari ca sir de caractere a unui obiect ,

returnarea clasei din care face parte un obiect,

notificarea altor obiecte ca o variabila de conditie s-a schimbat, etc.

Fiind subclasa a lui Object, orice clasa ii poate supradefini metodele care nu sunt finale.

Metodele cel mai uzual supradefinite sunt: clone, equals/hashCode, finalize, toString.

clone

Aceasta metoda este folosita pentru duplicarea obiectelor (crearea unor clone). Clonarea unui

obiect presupune crearea unui nou obiect de acelasi tip si care sa aiba aceeasi stare (aceleasi

valori pentru variabilele sale).

equals, hashCode Acestea sunt, de obicei, supradefinite impreuna. In metoda equals

este scris codul pentru compararea egalitatii continutului a doua obiecte. Implicit

(implementarea din clasa Object), aceasta metoda compara referintele obiectelor.

Uzual este redefinita pentru a testa daca starile obiectelor coincid sau daca doar o parte

din variabilele lor coincid.

Metoda hashCode returneaza un cod intreg pentru fiecare obiect, pentru a testa

consistenta obiectelor: acelasi obiect trebuie sa returneze acelasi cod pe durata executiei

programului. Daca doua obiecte sunt egale conform metodei equals, atunci apelul metodei

hashCode pentru fiecare din cele doua obiecte ar trebui sa returneze acelasi intreg.

finalize

In aceasta metoda se scrie codul care ”curata dupa un obiect”inainte de a fi eliminat din

memorie de colectorul de gunoaie. (vezi ”Distrugerea obiectelor”)

toString

Este folosita pentru a returna o reprezentare ca sir de caractere a unui obiect. Este utila pentru

concatenarea sirurilor cu diverse obiecte in vederea afisarii, fiind apelata automat atunci cand

este necesara transformarea unui obiect in sir de caractere.

Exemplu

obj = new Exemplu();

System.out.println("Obiect=" + obj);

//echivalent cu

System.out.println("Obiect=" + obj.toString());

Page 54: Curs Practic de Java

54

Sa consideram urmatorul exemplu, in care implementam partial clasa numerelor

complexe, si in care vom supradefini metode ale clasei Object. De asemenea, vom scrie un

mic program TestComplex in care vom testa metodele clasei definite.

class Complex {

private double a; //partea reala

private double b; //partea imaginara

public Complex(double a, double b) {

this.a = a; this.b = b;

}

public Complex() {

this(1, 0);

}

public boolean equals(Object obj) {

if (obj == null)

return false;

if (!(obj instanceof Complex))

return false;

Complex comp = (Complex) obj;

return ( comp.a==a && comp.b==b);

}

public Object clone() {

return new Complex(a, b);

}

public String toString() {

String semn = (b > 0 ? "+" : "-"); return a+ semn +b+"i";

}

public Complex aduna(Complex comp) {

Complex suma = new Complex(0, 0);

suma.a = this.a + comp.a;

suma.b = this.b + comp.b;

return suma;

}

}

public class TestComplex {

public static void main(String c[]) {

Complex c1 = new Complex(1,2);

Complex c2 = new Complex(2,3);

Complex c3 = (Complex) c1.clone();

System .out. println(c1. aduna(c2)); // 3.0 + 5.0i

System .out. println(c1. equals (c2)); // false

System .out. println(c1. equals (c3)); // true

} }

Page 55: Curs Practic de Java

55

2.9 Conversii automate intre tipuri

Dupa cum vazut tipurile Java de date pot fi impartie in primitive si referinta. Pentru fiecare

tip primitiv exista o clasa corespunzatoare care permie lucrul orientat obiect cu tipul respectiv.

Fiecare din aceste clase are un constructor ce permite initializarea unui obiect avand o

anumita valoare primitiva si metode specializate pentru conversia unui obiect in tipul primitiv

corespunzator, de genul tipPrimitivValue:

Integer obi = new Integer(1);

int i = obi.intValue();

Boolean obb = new Boolean(true);

boolean b = obb.booleanValue();

Incepand cu versiunea 1.5 a limbajului Java, atribuirile explicite intre tipuri primitve si

referinta sunt posibile, acest mecanism purtand numele de autoboxing, respectiv

auto-unboxing. Conversia explicita va fi facuta de catre compilator.

// Doar de la versiunea 1.5 !

Integer obi = 1;

int i=obi;

Boolean obb = true;

boolean b = obb;

2.10 Tipul de date enumerate

Incepand cu versiunea 1.5 a limbajului Java, exista posibilitatea de a defini tipuri de date

enumerare prin folosirea cuvantului cheie enum. Acesta solutie simplifica manevrarea

grupurilor de constante, dupa cum reiese din urmatorul exemplu:

public class CuloriSemafor {

public static final int ROSU = -1;

public static final int GALBEN = 0;

public static final int VERDE = 1;

}

...

// Exemplu de utilizare

if (semafor.culoare = CuloriSemafor.ROSU)

semafor.culoare = CuloriSemafor.GALBEN); ...

Page 56: Curs Practic de Java

56

Clasa de mai sus poate fi rescrisa astfel:

public enum CuloriSemafor { ROSU, GALBEN, VERDE };

...

// Utilizarea structurii se face la fel

...

if (semafor.culoare = CuloriSemafor.ROSU)

semafor.culoare = CuloriSemafor.GALBEN); ...

Compilatorul este responsabil cu transformarea unei astfel de structuri intr-o clasa

corespunzatoare.

Page 57: Curs Practic de Java

57

3 Exceptii

3.1 Ce sunt exceptiile ?

Termenul exceptie este o prescurtare pentru ”eveniment exceptional” si poate fi definit ca

un eveniment ce se produce in timpul executiei unui program si care provoaca intreruperea

cursului normal al executiei acestuia.

Exceptiile pot aparea din diverse cauze si pot avea nivele diferite de gravitate: de la erori

fatale cauzate de echipamentul hardware pana la erori ce tin strict de codul programului, cum

ar fi accesarea unui element din afara spatiului alocat unui vector. In momentul cand o

asemenea eroare se produce in timpul executiei va fi generat un obiect de tip exceptie ce

contine:

informatii despre exceptia respectiva;

starea programului in momentul producerii acelei exceptii.

public class Exemplu {

public static void main(String args[]) {

int v[] = new int[10];

v[10] = 0; //Exceptie !

System.out.println("Aici nu se mai ajunge...");

}

}

La rularea programului va fi generata o exceptie, programul se va opri la instructiunea

care a cauzat exceptia si se va afisa un mesaj de eroare de genul:

"Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException :10

at Exceptii.main (Exceptii.java:4)"

Crearea unui obiect de tip exceptie se numeste aruncarea unei exceptii (”throwing an

exception”). In momentulin care o metoda genereaza (arunca) o exceptie sistemul de executie

este responsabil cu gasirea unei secvente de cod dintr-o metoda care sa o trateze. Cautarea

se face recursiv, incepand cu metoda care a generat exceptia si mergand inapoi pe linia

apelurilor catre acea metoda.

Secventa de cod dintr-o metoda care trateaza o anumita exceptie se numeste analizor de

exceptie (”exception handler”) iar interceptarea si tratarea ei se numeste prinderea exceptiei

(”catch the exception”). Cu alte cuvinte, la aparitia unei erori este ”aruncata” o exceptie iar

cineva trebuie sa o ”prinda” pentru a o trata. Daca sistemul nu gaseste nici un analizor pentru o

anumita exceptie, atunci programul Java se opreste cu un mesaj de eroare (in cazul

exemplului de mai sus mesajul ”Aici nu se mai ajunge...” nu va fi afisat).

Atentie!!! In Java tratarea erorilor nu mai este o optiune ci o constrangere. In aproape toate

situatile, o secventa de cod care poate provoca exceptii trebuie sa specifice modalitatea de

tratare a acestora.

Page 58: Curs Practic de Java

58

3.2 ”Prinderea” si tratarea exceptiilor

Tratarea exceptiilor se realizeaza prin intermediul blocurilor de instructiuni try, catch si

finally. O secventa de cod care trateaza anumite exceptii trebuie sa arate astfel:

try {

// Instructiuni care pot genera exceptii

}

catch (TipExceptie1 variabila) {

// Tratarea exceptiilor de tipul 1

} catch (TipExceptie2 variabila) {

// Tratarea exceptiilor de tipul 2

}

...

finally {

// Cod care se executa indiferent daca apar sau nu exceptii

}

Sa consideram urmatorul exemplu: citirea unui fisier octet cu octet si afisarea lui pe ecran.

Fara a folosi tratarea exceptiilor metoda responsabila cu citirea fisierului ar arata astfel:

public static void citesteFisier(String fis) {

FileReader f = null;

// Deschidem fisierul

System.out.println("Deschidem fisierul " + fis);

f = new FileReader(fis);

// Citim si afisam fisierul caracter cu caracter

int c;

while ( (c=f.read()) != -1)

System.out.print((char)c);

// Inchidem fisierul

System.out.println("\\nInchidem fisierul " + fis);

f.close();

}

Aceasta secventa de cod va furniza erori la compilare deoarece in Java tratarea erorilor

este obligatorie. Folosind mecanismul exceptiilor metoda citeste isi poate trata singura erorile

care pot surveni pe parcursul executiei sale. Mai jos este codul complte si corect al unui

program ce afiseaza pe ecran continutul unui fisier al carui nume este primit ca argument de la

linia de comanda. Tratarea exceptiilor este realizata complet chiar de catre metoda citeste.

Page 59: Curs Practic de Java

59

import java.io.*;

public class CitireFisier {

public static void citesteFisier(String fis) {

FileReader f = null;

try {

// Deschidem fisierul

System.out.println("Deschidem fisierul " + fis);

f = new FileReader(fis);

// Citim si afisam fisierul caracter cu caracter

int c;

while ( (c= f.read()) != -1)

System.out. print (( char)c);

} catch (FileNotFoundException e) {

// Tratam un tip de exceptie

System.err.println("Fisierul nu a fost gasit !");

System.err.println("Exceptie: " + e.getMessage());

System . exit (1);

} catch (IOException e) {

// Tratam alt tip de exceptie

System.out.println("Eroare la citirea din fisier!");

e. printStackTrace ();

} finally { if (f != null) {

// Inchidem fisierul

System.out.println("\nInchidem fisierul.");

try {

f. close ();

} catch (IOException e) {

System.err.println("Fisierul nu poate fi inchis!");

e. printStackTrace ();

}

}

}

}

public static void main(String args[]) {

if (args.length > 0)

citesteFisier ( args [0]) ;

}

}

Page 60: Curs Practic de Java

60

Blocul ”try” contine instructiunile de deschidere a unui fisier si de citire dintr-un fisier,

ambele putand produce exceptii. Exceptiile provocate de aceste instructiuni sunt tratate in cele

doua blocuri ”catch”, cate unul pentru fiecare tip de exceptie. Inchiderea fisierului se face in

blocul ”finally”, deoarece acesta este sigur ca se va executa Fara a folosi blocul ”finally”,

inchiderea fisierului ar fi trebuit facuta in fiecare situatie in care fisierul ar fi fost deschis, ceea

ce ar fi dus la scrierea de cod redundant.

try {

...

// Totul a decurs bine.

f.close();

}

...

catch (IOException e) {

...

// A aparut o exceptie la citirea din fisier

f.close(); // cod redundant

}

O problema mai delicata care trebuie semnalata in aceasta situatie este faptul ca metoda

close, responsabila cu inchiderea unui fisier, poate provoca la randul sau exceptii, de exemplu

atunci cand fisierul mai este folosit si de alt proces si nu poate fi inchis. Deci, pentru a avea un

cod complet corect trebuie sa tratam si posibilitatea aparitiei unei exceptii la metoda close.

Atentie !!! Obligatoriu un bloc de instructiuni ”try” trebuie sa fie urmat de unul sau mai multe

blocuri ”catch”, in functie de exceptiile provocate de acele instructiuni sau (optional) de un bloc

”finally”.

3.3 ”Aruncarea” exceptiilor

In cazul in care o metoda nu isi asuma responsabilitatea tratarii uneia sau mai multor

exceptii pe care le pot provoca anumite instructiuni din codul sau atunci ea poate sa ”arunce”

aceste exceptii catre metodele care o apeleaza, urmand ca acestea sa implementeze tratarea

lor sau, la randul lor, sa ”arunce” mai departe exceptiile respective. Acest lucru se realizeaza

prin specificarea in declaratia metodei a clauzei throws:

[modificatori] TipReturnat metoda([argumente]) throws TipExceptie1, TipExceptie2, ... { ... }

Atentie !!! : O metoda care nu trateaza o anumita exceptie trebuie obligatoriu sa o ”arunce”.

In exemplul de mai sus daca nu facem tratarea exceptiilor in cadrul metodei citeste atunci

metoda apelanta (main) va trebui sa faca acest lucru:

Page 61: Curs Practic de Java

61

import java.io.*;

public class CitireFisier {

public static void citesteFisier(String fis) throws FileNotFoundException ,

IOException {

FileReader f = null; f = new FileReader(fis);

int c;

while ( (c =f.read()) != -1)

System .out.print (( char )c);

f. close ();

}

public static void main(String args[]) {

if (args.length > 0) {

try {

citesteFisier ( args [0]) ;

} catch (FileNotFoundException e) {

System.err.println("Fisierul nu a fost gasit !");

System.err.println("Exceptie: " + e);

} catch (IOException e) {

System.out.println("Eroare la citirea din fisier!");

e. printStackTrace ();

}

} else

System.out.println("Lipseste numele fisierului!");

}

}

Observati ca, in acest caz, nu mai putem diferentia exceptiile provocate de citirea din

fisier s de inchiderea fisierului, ambele fiind de tipul IOException. De asemenea, inchiderea

fisierului nu va mai fi facuta in situatia in care apare o exceptie la citirea din fisier. Este situatia

in care putem folosi blocul finally fara a folosi nici un bloc catch:

public static void citesteFisier(String fis) throws FileNotFoundException, IOException {

FileReader f = null;

try {

f = new FileReader(numeFisier);

int c;

while ( (c=f.read()) != -1)

System.out.print((char)c);

} finally {

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

}

Page 62: Curs Practic de Java

62

Metoda apelanta poate arunca la randul sau exceptiile mai departe catre metoda care a

apelat-o la randul ei. Aceasta inlantuire se termina cu metoda main care, daca va arunca

exceptiile ce pot aparea in corpul ei, va determina trimiterea exceptiilor catre masina virtuala

Java.

public void metoda3 throws TipExceptie {

...

}

public void metoda2 throws TipExceptie {

metoda3();

}

public void metoda1 throws TipExceptie {

metoda2();

}

public void main throws TipExceptie {

metoda1();

}

Tratarea exceptiilor de catre JVM se face prin terminarea programului si afisarea

informatiilor despre exceptia care a determinat acest lucru. Pentru exemplul nostru, metoda

main ar putea fi declarata astfel:

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

citeste(args[0]);

}

Intotdeauna trebuie gasit compromisul optimintre tratarea locala a exceptiilor si

aruncarea lor catre nivelele superioare, astfel incat codul sa fie cat mai clar si identificarea

locului in care a aparut exceptia sa fie cat mai usor de facut.

Aruncarea unei exceptii se poate face si implicit prin instructiunea throw ce are formatul:

throw exceptie, ca in exemplele de mai jos:

throw new IOException("Exceptie I/O");

...

if (index >= vector.length)

throw new ArrayIndexOutOfBoundsException();

...

catch(Exception e) {

System.out.println("A aparut o exceptie);

throw e;

}

Aceasta instructiune este folosita mai ales la aruncarea exceptiilor proprii. (vezi ”Crearea

propriilor exceptii”)

Page 63: Curs Practic de Java

63

3.4 Avantajele tratarii exceptiilor

Prin modalitatea sa de tratare a exceptiilor, Java are urmatoarele avantaje fata de mecanismul

traditional de tratare a erorilor:

Separarea codului pentru tratarea unei erori de codul in care ea poate sa apara.

Propagarea unei erori pana la un analizor de exceptii corespunzator.

Gruparea erorilor dupa tipul lor.

3.4.1 Separarea codului pentru tratarea erorilor

In programarea traditionala tratarea erorilor se combina cu codul ce poate produce

aparitia lor producand asa numitul ”cod spaghetti”. Sa consideram urmatorul exemplu: o

functie care incarca un fisier in memorie:

citesteFisier {

deschide fisierul;

determina dimensiunea fisierului;

aloca memorie;

citeste fisierul in memorie;

inchide fisierul;

}

Problemele care pot aparea la aceasta functie, aparent simpla, sunt de genul: ”Ce se

intampla daca: ... ?”

fisierul nu poate fi deschis

nu se poate determina dimensiunea fisierului

nu poate fi alocata suficienta memorie

nu se poate face citirea din fisier

fisierul nu poate fi inchis

Un cod traditional care sa trateze aceste erori ar arata astfel:

int citesteFisier() {

int codEroare = 0;

deschide fisierul;

if (fisierul s-a deschis) {

determina dimensiunea fisierului;

if (s-a determinat dimensiunea) {

aloca memorie;

if (s-a alocat memorie) {

citeste fisierul in memorie;

if (nu se poate citi din fisier) {

codEroare = -1;

Page 64: Curs Practic de Java

64

}

} else {

codEroare = -2;

}

} else {

codEroare = -3;

}

inchide fisierul;

if (fisierul nu s-a inchis && codEroare == 0) {

codEroare = -4;

} else {

codEroare = codEroare & -4;

}

} else {

codEroare = -5;

}

return codEroare;

} // Cod "spaghetti"

Acest stil de progamare este extrem de susceptibil la erori si ingreuneaza extrem de mult

inttelegerea sa. In Java, folosind mecansimul exceptiilor, codul ar arata, schematizat, astfel:

int citesteFisier() {

try {

deschide fisierul;

determina dimensiunea fisierului;

aloca memorie;

citeste fisierul in memorie;

inchide fisierul;

} catch (fisierul nu s-a deschis) {

trateaza eroarea;

} catch (nu s-a determinat dimensiunea) {

trateaza eroarea;

} catch (nu s-a alocat memorie) {

trateaza eroarea

} catch (nu se poate citi din fisier) {

trateaza eroarea;

} catch (nu se poate inchide fisierul) {

trateaza eroarea;

}

}

Diferenta de claritate este evidenta.

Page 65: Curs Practic de Java

65

3.4.2 Propagarea erorilor

Propagarea unei erori se face pana la un analizor de exceptii corespunzator. Sa

presupunem ca apelul la metoda citesteFisier este consecinta unor apeluri imbricate de

metode:

int metoda1() { metoda2(); ...

}

int metoda2() { metoda3; ...

}

int metoda3 { citesteFisier(); ...

}

Sa presupunem de asemenea ca dorim sa facem tratarea erorilor doar in metoda1.

Traditional, acest lucru ar trebui facut prin propagarea erorii produse de metoda citesteFisier

pana la metoda1:

int metoda1() {

int codEroare = metoda2();

if (codEroare != 0)

//proceseazaEroare;

...

}

int metoda2() {

int codEroare = metoda3();

if (codEroare != 0)

return codEroare;

...

}

int metoda3() {

int codEroare = citesteFisier();

if (codEroare != 0)

return codEroare;

...

}

Dupa cum am vazut, Java permite unei metode sa arunce exceptiile aparute in cadrul ei

la un nivel superior, adica functiilor care o apeleaza sau sistemului. Cu alte cuvinte, o metoda

poate sa nu isi asume responsabilitatea tratarii exceptiilor aparute in cadrul ei:

int metoda1() {

try {

metoda2();

}

catch (TipExceptie e) {

//proceseazaEroare;

Page 66: Curs Practic de Java

66

}

...

}

int metoda2() throws TipExceptie { metoda3(); ...

}

int metoda3() throws TipExceptie { citesteFisier(); ...

}

3.4.3 Gruparea erorilor dupa tipul lor

In Java exista clase corespunzatoare tuturor exceptiilor care pot aparea la executia unui

program. Acestea sunt grupate in functie de similaritatile lor intr-o ierarhie de clase. De

exemplu, clasa IOException se ocupa cu exceptiile ce pot aparea la operatii de intrare/iesire si

diferentiaza la randul ei alte tipuri de exceptii, cum ar fi FileNotFoundException,

EOFException, etc. La randul ei, clasa IOException se incadreaza intr-o categorie mai larga de

exceptii si anume clasa Exception. Radacina acestei ierarhii este clasa Throwable (vezi

”Ierarhia claselor ce descriu exceptii”).

Pronderea unei exceptii se poate face fie la nivelul clasei specifice pentru acea exceptie,

fie la nivelul uneia din superclasele sale, in functie de necesitatile programului, insa, cu cat

clasa folosita este mai generica cu atat tratarea exceptiilor programul isi pierde din flexibilitate.

try {

FileReader f = new FileReader("input.dat");

/* Acest apel poate genera exceptie de tipul FileNotFoundException

Tratarea ei poate fi facuta in unul din modurile de mai jos:

*/

} catch (FileNotFoundException e) {

// Exceptie specifica provocata de absenta

// fisierului ’input.dat’

}

// sau

catch (IOException e) {

// Exceptie generica provocata de o operatie IO

}

// sau

catch (Exception e) {

// Cea mai generica exceptie soft

}

//sau

catch (Throwable e) {

// Superclasa exceptiilor

}

Page 67: Curs Practic de Java

67

3.5 Ierarhia claselor ce descriu exceptii

Radacina claselor ce descriu exceptii este clasa Throwable iar cele mai importante

subclase ale sale sunt Error, Exception si RuntimeException, care sunt la randul lor superclase

pentru o serie intreaga de tipuri de exceptii.

Erorile, obiecte de tip Error, sunt cazuri speciale de exceptii generate de

functionarea anormala a echipamentului hard pe care ruleaza un program Java si sunt

invizibile programatorilor. Un program Java nu trebuie sa trateze aparitia acestor erori si este

improbabil ca o metoda Java sa provoace asemenea erori.

Exceptiile, obiectele de tip Exception, sunt exceptiile standard (soft) care trebuie tratate de

catre programele Java. Dupa cum am mai zis tratarea aces-tor exceptii nu este o optiune ci o

constrangere. Exceptiile care pot ”scapa” netratate descind din subclasa RuntimeException si

se numesc exceptii la executie.

Metodele care sunt apelate uzual pentru un obiect exceptie sunt definite in clasa Throwable

si sunt publice, astfel incat pot fi apelate pentru orice tip de exceptie. Cele mai uzuale sunt:

getMessage -afiseaza detaliul unei exceptii;

printStackTrace -afiseaza informatii complete despre exceptie si localizarea ei;

toString -metoda mostenita din clasa Object, care furnizeaza reprezentarea ca sir de

caractere a exceptiei.

3.6 Exceptii la executie

In general, tratarea exceptiilor este obligatoriein Java. De la acest principu se sustrag insa

asa numitele exceptii la executie sau, cu alte cuvinte, exceptiile care provin strict din vina

programatorului si nu generate de o anumita situatie externa, cum ar fi lipsa unui fisier. Aceste

exceptii au o superclasa comuna RuntimeException si in acesata categorie sunt incluse

exceptiile provocate de:

Page 68: Curs Practic de Java

68

operatii aritmetice ilegale (impartirea intregilor la zero);

ArithmeticException

accesarea membrilor unui obiect ce are valoarea null; NullPointerException

accesarea eronata a elementelor unui vector. ArrayIndexOutOfBoundsException

Exceptiile la executie pot aparea uriunde in program si pot fi extrem de numeroare iar

incercarea de ”prindere” a lor ar fi extrem de anevoioasa. Din acest motiv, compilatorul permite

ca aceste exceptii sa ramana netratate, tratarea lor nefiind insa ilegala. Reamintim insa ca, in

cazul aparitiei oricarui tip de exceptie care nu are un analizor corespunzator, programul va fi

terminat.

int v[] = new int[10];

try {

v[10] = 0;

} catch (ArrayIndexOutOfBoundsException e) {

System.out.println("Atentie la indecsi!"); e.printStackTrace();

} // Corect, programul continua

v[11] = 0;

/* Nu apare eroare la compilare dar apare exceptie la executie si programul va fi

terminat.

*/

System.out.println("Aici nu se mai ajunge...");

Impartirea la 0 va genera o exceptie doar daca tipul numerelor impartite este aritmetic

intreg. In cazul tipurilor reale (float si double) nu va fi generata nici o exceptie, ci va fi furnizat ca

rezultat o constanta care poate fi, functie de operatie, Infinity, -Infinity, sau Nan.

int a=1, int b=0;

System.out.println(a/b); // Exceptie la executie !

double x=1, y=-1, z=0;

System.out.println(x/z); // Infinity

System.out.println(y/z); // -Infinity System.out.println(z/z); // NaN

3.7 Crearea propriilor exceptii

Adeseori poate aparea necesitatea crearii unor exceptii proprii pentru a pune in evidenta

cazuri speciale de erori provocate de metodele claselor unei librarii, cazuri care nu au fost

prevazute in ierarhia exceptiilor standard Java. O exceptie proprie trebuie sa se incadreze insa

in ierarhia exceptiilor Java, cu alte cuvinte clasa care o implementeaza trebuie sa fie subclasa

a uneia deja existente in aceasta ierarhie, preferabil una apropiata ca semnificatie, sau

superclasa Exception.

Page 69: Curs Practic de Java

69

public class ExceptieProprie extends Exception {

public ExceptieProprie(String mesaj) {

super(mesaj); // Apeleaza constructorul superclasei Exception

}

}

Sa consideram urmatorul exemplu, in care cream o clasa ce descrie partial

o stiva de numere intregi cu operatiile de adaugare a unui element, respectiv de scoatere a

elementului din varful stivei. Daca presupunem ca stiva poate memora maxim 100 de

elemente, ambele operatii pot provoca exceptii. Pentru a personaliza aceste exceptii vom crea

o clasa specifica denumita ExceptieStiva:

class ExceptieStiva extends Exception {

public ExceptieStiva(String mesaj) { super(mesaj ); }

}

class Stiva {

int elemente[] = new int[100];

int n= 0; // numarul de elemente din stiva

public void adauga(int x) throws ExceptieStiva {

if (n==100)

throw new ExceptieStiva("Stiva este plina!");

elemente[n++] = x;

}

public int scoate() throws ExceptieStiva {

if (n==0)

throw new ExceptieStiva("Stiva este goala!");

return elemente[n--];

}

}

Secventa cheie este extends Exception care specifica faptul ca noua clasa ExceptieStiva

este subclasa a clasei Exception si deci implementeaza obiecte ce reprezinta exceptii.

In general, codul adaugat claselor pentru exceptii proprii este nesemnificativ: unul sau doi

constructori care afiseaza un mesaj de eroare la iesirea standard. Procesul de creare a unei

noi exceptii poate fi dus mai departe prin adaugarea unor noi metode clasei ce descrie acea

exceptie, insa aceasta dezvoltare nu isi are rostul in majoritatea cazurilor. Exceptiile proprii

sunt descrise uzual de clase foarte simple, chiar fara nici un cod in ele, cum ar fi:

class ExceptieSimpla extends Exception { }

Aceasta clasa se bazeaza pe constructorul implicit creat de compilator insa nu are

constructorul ExceptieSimpla(String s).

Page 70: Curs Practic de Java

70

4 Intrari si iesiri

4.1 Introducere 4.1.1 Ce sunt fluxurile?

Majoritatea aplicatiilor necesita citirea unor informatii care se gasesc pe o sursa externa

sau trimiterea unor informatii catre o destinatie externa. Informatia se poate gasi oriunde:

intr-un fisier pe disc, in retea, in memorie sau in alt program si poate fi de orice tip: date

primitive, obiecte, imagini, sunete, etc. Pentru a aduce informatii dintr-un mediu extern, un

progam Java trebuie sa deschida un canal de comunicatie (flux) de la sursa informatiilor (fisier,

memorie, socket, etc) si sa citeasca secvential informatiile respective.

Similar, un program poate trimite informatii catre o destinatie externa deschizand un

canal de comunicatie (flux) catre acea destinatie si scriind secvential informatiile respective.

Indiferent de tipul informatiilor, citirea/scrierea de pe/catre un mediu extern respecta urmatorul

algoritm:

deschide canal comunicatie

while (mai sunt informatii) {

citeste/scrie informatie;

}

inchide canal comunicatie;

Pentru a generaliza, atat sursa externa a unor date cat si destinatia lor sunt vazute ca

fiind niste procese care produc, respectiv consuma informatii.

Definitii:

Un flux este un canal de comunicatie unidirectional intre doua procese. Un proces care

descrie o sursa externa de date se numeste proces producator. Un proces care descrie o

destinatie externa pentru date se numeste proces consumator. Un flux care citeste date se

numeste flux de intrare. Un flux care scrie date se numeste flux de iesire.

Observatii:

Fluxurile sunt canale de comunicatie seriale pe 8 sau 16 biti. Fluxurile sunt

unidirectionale, de la producator la consumator. Fiecare flux are un singur proces producator si

un singur proces consumator. Intre doua procese pot exista oricate fluxuri, orice proces putand

fi atat producator cat si consumator in acelasi timp, dar pe fluxuri diferite. Consumatorul si

producatorul nu comunica direct printr-o interfata de flux ci prin intermediul codului Java de

tratare a fluxurilor.

Clasele si intefetele standard pentru lucrul cu fluxuri se gasesc in pachetul java.io. Deci,

orice program care necesita operatii de intrare sau iesire trebuie sa contina instructiunea de

import a pachetului java.io:

import java.io.*;

Page 71: Curs Practic de Java

71

4.1.2 Clasificarea fluxurilor

Exista trei tipuri de clasificare a fluxurilor:

Dupa directia canalului de comunicatie deschis fluxurile se impart in:

fluxuri de intrare (pentru citirea datelor)

fluxuri de iesire (pentru scrierea datelor)

Dupa tipul de date pe care opereaza:

fluxuri de octeti (comunicarea seriala se realizeaza pe 8 biti)

fluxuri de caractere (comunicarea seriala se realizeaza pe 16 biti)

Dupa actiunea lor:

fluxuri primare de citire/scriere a datelor (se ocupa efectiv cu citirea/scrierea datelor)

fluxuri pentru procesarea datelor

4.1.3 Ierarhia claselor pentru lucrul cu fluxuri

Clasele radacina pentru ierarhiile ce reprezinta fluxuri de caractere sunt:

Reader-pentru fluxuri de intrare si

Writer-pentru fluxuri de iesire.

Acestea sunt superclase abstracte pentru toate clasele ce implementeaza fluxuri

specializate pentru citirea/scrierea datelor pe 16 biti si vor contine metodele comune tuturor.

Ca o regula generala, toate clasele din aceste ierarhii vor avea terminatia Reader sau Writer in

functie de tipul lor, cum ar fiin exemplele: FileReader, BufferedReader, FileWriter,

BufferedWriter, etc. De asemenea, se observa ca o alta regula generala, faptul ca unui flux de

intrare XReader ii corespunde uzual un flux de iesire XWriter, insa acest lucru nu este obliga-

toriu.

Clasele radacina pentru ierarhia fluxurilor de octeti sunt:

InputStream-pentru fluxuri de intrare si

OutputStream-pentru fluxuri de iesire.

Acestea sunt superclase abstracte pentru clase ce implementeaza fluxuri specializate

pentru citirea/scrierea datelor pe 8 biti. Ca si in cazul fluxurilor pe caractere denumirile claselor

vor avea terminatia superclasei lor: FileInputStream, BufferedInputStream, FileOutputStream,

BufferedOutputStream, etc., fiecarui flux de intrare XInputStream corespunzandu-i uzual un

flux de iesire XOutputStream, fara ca acest lucru sa fie obligatoriu.

Pana la un punct, exista un paralelism intre ierarhia claselor pentru fluxuri de caractere si

cea pentru fluxurile pe octeti. Pentru majoritatea programelor este recomandat ca scrierea si

citirea datelor sa se faca prin intermediul fluxurilor de caractere, deoarece acestea permit

manipularea caracterelor Unicode in timp ce fluxurile de octeti permit doar lucrul pe 8 biti -

caractere ASCII.

Page 72: Curs Practic de Java

72

4.1.4 Metode comune fluxurilor

Superclasele abstracte Reader si InputStream definesc metode similare pentru citirea

datelor.

De asemenea, ambele clase pun la dispozitie metode pentru marcarea unei locatii intr-un

flux, saltul peste un numar de pozitii, resetarea pozitiei curente, etc. Acestea sunt insa mai rar

folosite si nu vor fi detaliate.

Superclasele abstracte Writer si OutputStream sunt de asemenea paralele, definind

metode similare pentru scrierea datelor:

Inchiderea oricarui flux se realizeaza prin metoda close. In cazul in care aceasta nu este

apelata explicit, fluxul va fi automat inchis de catre colectorul de gunoaie atunci cand nu va mai

exista nici o referinta la el, insa acest lucru trebuie evitat deoarece, la lucrul cu fluxrui cu zona

tampon de memorie, datele din memorie vor fi pierdute la inchiderea fluxului de catre gc.

Metodele referitoare la fluxuri pot genera exceptii de tipul IOException sau derivate din

aceasta clasa, tratarea lor fiind obligatorie.

4.2 Folosirea fluxurilor

Asa cum am vazut, fluxurile pot fi impartite in functie de activitatea lor in fluxuri care se

ocupa efectiv cu citirea/scrierea datelor si fluxuri pentru procesarea datelor (de filtrare). In

continuare, vom vedea care sunt cele mai importante clase din cele doua categorii si la ce

folosesc acestea, precum si modalitatile de creare si utilizare a fluxurilor.

4.2.1 Fluxuri primitive

Fluxurile primitive sunt responsabile cu citirea/scrierea efectiva a datelor, punand la

dispozitie implementari ale metodelor de baza read, respectiv write, definite in superclase. In

functie de tipul sursei datelor, ele pot fi impartite astfel:

Fisier

FileReader, FileWriter FileInputStream, FileOutputStream Numite si fluxuri fisier, acestea sunt

folosite pentru citirea datelor dintr-un fisier, respectiv scrierea datelor intr-un fisier si vor fi

analizate intr-o sectiune separata (vezi ”Fluxuri pentru lucrul cu fisiere”).

Memorie

CharArrayReader, CharArrayWriter ByteArrayInputStream, ByteArrayOutputStream Aceste

fluxuri folosesc pentru scrierea/citirea informatiilorin/din memorie si sunt create pe un vector

existent deja. Cu alte cuvinte, permit tratarea vectorilor ca sursa/destinatie pentru crearea unor

fluxuri de intrare/iesire.

StringReader, StringWriter Permit tratarea sirurilor de caractere aflatein memorie ca

sursa/destinatie pentru crearea de fluxuri.

Page 73: Curs Practic de Java

73

Pipe

PipedReader, PipedWriter PipedInputStream, PipedOutputStream

Implementeaza componentele de intrare/iesire ale unei conducte de date (pipe). Pipe-urile

sunt folosite pentru a canaliza iesirea unui program sau fir de executie catre intrarea altui

program sau fir de executie.

4.2.2 Fluxuri de procesare

Fluxurile de procesare (sau de filtrare) sunt responsabile cu preluarea datelor de la un

flux primitiv si procesarea acestora pentru a le oferiintr-o alta forma, mai utila dintr-un anumit

punct de vedere. De exemplu, BufferedReader poate prelua date de la un flux FileReader si sa

ofere informatia dintr-un fisier linie cu linie. Fiind primitiv, FileReader nu putea citi decat

caracter cu caracter. Un flux de procesare nu poate fi folosit decat impreuna cu un flux primitiv.

Clasele ce descriu aceste fluxuri pot fi impartite in functie de tipul de procesare pe care il

efectueaza astfel:

”Bufferizare”

BufferedReader, BufferedWriter BufferedInputStream, BufferedOutputStream

Sunt folosite pentru a introduce un buffer in procesul de citire/scriere a informatiilor, reducand

astfel numarul de accesari la dispozitivul ce reprezinta sursa/destinatia originala a datelor.

Sunt mult mai eficiente decat fluxurile fara buffer si din acest motiv se recomanda folosirea lor

ori de cate ori este posibil (vezi ”Citirea si scrierea cu zona tampon”).

Filtrare

FilterReader, FilterWriter FilterInputStream, FilterOutputStream

Sunt clase abstracte ce definesc o interfata comuna pentru fluxuri care filtreaza automat datele

citite sau scrise (vezi ”Fluxuri pentru filtrare”).

Conversie octeti-caractere

InputStreamReader, OutputStreamWriter

Formeaza o punte de legatura intre fluxurile de caractere si fluxurile de octeti. Un flux

InputStreamReader citeste octeti dintr-un flux InputStream si ii converteste la caractere,

folosind codificarea standard a caracterelor sau o codificare specificata de program. Similar,

un flux OutputStreamWriter converteste caractere in octeti si trimite rezutatul catre un flux de

tipul OutputStream.

Concatenare

SequenceInputStream

Concateneaza mai multe fluxuri de intrare intr-unul singur (vezi ”Concatenarea fisierelor”).

Serializare

ObjectInputStream, ObjectOutputStream

Sunt folosite pentru serializarea obiectelor (vezi ”Serializarea obiectelor”).

Conversie tipuri de date

DataInputStream, DataOutputStream

Folosite la scrierea/citirea datelor de tip primitiv intr-un format binar, independent de masina pe

Page 74: Curs Practic de Java

74

care se lucreaza (vezi ”Folosirea claselor DataInputStream si DataOutputStream”).

Numarare

LineNumberReader LineNumberInputStream

Ofera si posibilitatea de numarare automata a liniilor citite de la un flux de intrare.

Citire in avans

PushbackReader PushbackInputStream

Sunt fluxuri de intrare care au un buffer de 1-caracter(octet) in care este citit in avans si

caracterul (octetul) care urmeaza celui curent citit.

Afisare

PrintWriter PrintStream

Ofera metode convenabile pentru afisarea informatiilor.

4.2.3 Crearea unui flux

Orice flux este un obiect al clasei ce implementeaza fluxul respectiv. Crearea

unui flux se realizeaza asadar similar cu crearea obiectelor, prin instructiunea

new si invocarea unui constructor corespunzator al clasei respective:

Exemple:

//crearea unui flux de intrare pe caractere FileReader in = new FileReader("fisier.txt");

//crearea unui flux de iesire pe caractere FileWriter out = new FileWriter("fisier.txt");

//crearea unui flux de intrare pe octeti FileInputStream in = new FileInputStream("fisier.dat");

//crearea unui flux de iesire pe octeti FileOutputStrem out = new FileOutputStream("fisier.dat");

Asadar, crearea unui flux primitiv de date care citeste/scrie informatii de la un dispozitiv

extern are formatul general:

FluxPrimitiv numeFlux = new FluxPrimitiv(dispozitivExtern);

Fluxurile de procesare nu pot exista de sine statatoare ci se suprapun pe un flux primitiv

de citire/scriere a datelor. Din acest motiv, constructorii claselor pentru fluxurile de procesare

nu primesc ca argument un dispozitiv extern de memorare a datelor ci o referinta la un flux

primitiv responsabil cu citirea/scrierea efectiva a datelor:

Exemple:

//crearea unui flux de intrare printr-un buffer

BufferedReader in = new BufferedReader( new FileReader("fisier.txt"));

//echivalent cu

FileReader fr = new FileReader("fisier.txt");

BufferedReader in = new BufferedReader(fr);

//crearea unui flux de iesire printr-un buffer \

BufferedWriter out = new BufferedWriter(

new FileWriter("fisier.txt")));

//echivalent cu

Page 75: Curs Practic de Java

75

FileWriter fo = new FileWriter("fisier.txt");

BufferedWriter out = new BufferedWriter(fo);

Asadar, crearea unui flux pentru procesarea datelor are formatul general:

FluxProcesare numeFlux = new FluxProcesare(fluxPrimitiv);

In general, fluxurile pot fi compuse in succesiuni oricat de lungi:

DataInputStream in = new DataInputStream( new BufferedInputStream( new

FileInputStream("fisier.dat")));

4.2.4 Fluxuri pentru lucrul cu fisiere

Fluxurile pentru lucrul cu fisiere sunt cele mai usor de inteles, intrucat operatia lor de baza

este citirea, respectiv scrierea unui caracter sau octet dintr-un sau intr-un fisier specificat uzual

prin numele sau complet sau relativ la directorul curent.

Dupa cum am vazut deja, clasele care implementeaza aceste fluxuri sunt urmatoarele:

FileReader, FileWriter -caractere FileInputStream, FileOutputStream -octeti

Constructorii acestor clase accepta ca argument un obiect care sa specifice un anume

fisier. Acesta poate fi un sir de caractere, on obiect de tip File sau un obiect de tip FileDesciptor

(vezi ”Clasa File”).

Constructorii clasei FileReader sunt:

public FileReader(String fileName) throws FileNotFoundException

public FileReader(File file) throws FileNotFoundException

public FileReader(FileDescriptor fd)

Constructorii clasei FileWriter:

public FileWriter(String fileName) throws IOException

public FileWriter(File file) throws IOException

public FileWriter(FileDescriptor fd)

public FileWriter(String fileName, boolean append) throws IOException

Cei mai uzuali constructori sunt cei care primesc ca argument numele fisierului. Acestia

pot provoca exceptii de tipul FileNotFoundException in cazul in care fisierul cu numele

specificat nu exista. Din acest motiv orice creare a unui flux de acest tip trebuie facuta intr-un

bloc try-catch sau metoda in care sunt create fluxurile respective trebuie sa arunce exceptiile

de tipul FileNotFoundException sau de tipul superclasei IOException.

Sa consideram ca exemplu un program care copie continutul unui fisier cu numele ”in.txt”

intr-un alt fisier cu numele ”out.txt”. Ambele fisiere sunt considerate in directorul curent.

import java.io.*;

public class Copiere {

public static void main(String[] args) {

try {

FileReader in = new FileReader("in.txt");

Page 76: Curs Practic de Java

76

FileWriter out = new FileWriter("out.txt");

int c;

while ((c = in.read()) != -1)

out. write(c);

in. close ();

out. close ();

} catch(IOException e) {

System.err.println("Eroare la operatiile cu fisiere!");

e. printStackTrace ();

}

}

}

In cazul in care vom lansa aplicatia iar in directorul curent nu exista un fisier cu numele

”in.txt”, va fi generata o exceptie de tipul FileNotFoundException. Aceasta va fi prinsa de

program deoarece, IOException este superclasa pentru FileNotFoundException. Daca exista

fisierul ”in.txt”, aplicatia va crea un nou fisier ”out.txt” in care va fi copiat continutul primului.

Daca exista deja fisierul ”out.txt” el va fi rescris. Daca doream sa facem operatia de

adaugare(append) si nu de rescriere pentru fisierul ”out.txt” foloseam:

FileWriter out = new FileWriter("out.txt", true);

4.2.5 Citirea si scrierea cu buffer

Clasele pentru citirea/scrierea cu zona tampon sunt:

BufferedReader, BufferedWriter -caractere

BufferedInputStream, BufferedOutputStream -octeti

Sunt folosite pentru a introduce un buffer (zona de memorie) in procesul de citire/scriere a

informatiilor, reducand astfel numarul de accesari ale dispozitivului ce reprezinta

sursa/destinatia atelor. Din acest motiv, sunt mult mai eficiente decat fluxurile fara buffer si din

acest motiv se recomanda folosirea lor ori de cate ori este posibil.

Clasa BufferedReader citeste in avans date si le memoreaza intr-o zona tampon. Atunci

cand se executa o operatie de citire, caracterul va fi preluat din buffer. In cazul in care buffer-ul

este gol, citirea se face direct din flux si, odata cu citirea caracterului, vor fi memorati in buffer

si caracterele care ii urmeaza. Evident, BufferedInputStream functioneaza dupa acelasi

principiu, singura diferenta fiind faptul ca sunt cititi octeti.

Similar lucreaza si clasele BufferedWriter si BufferedOutputStream. La operatiile de

scriere datele scrise nu vor ajunge direct la destinatie, ci vor fi memorate jntr-un buffer de o

anumita dimensiune. Atunci cand bufferul este plin, continutul acestuia va fi transferat automat

la destinatie.

Fluxurile de citire/scriere cu buffer sunt fluxuri de procesare si sunt folosite prin

suprapunere cu alte fluxuri, dintre care obligatoriu unul este primitiv.

Page 77: Curs Practic de Java

77

BufferedOutputStream out = new BufferedOutputStream( new FileOutputStream("out.dat"),

1024) //1024 este dimensiunea bufferului

Constructorii cei mai folositi ai acestor clase sunt urmatorii:

BufferedReader(Reader in)

BufferedReader(Reader in, int dim_buffer)

BufferedWriter(Writer out)

BufferedWriter(Writer out, int dim_buffer) BufferedInputStream(InputStream in)

BufferedInputStream(InputStream in, int dim_buffer)

BufferedOutputStream(OutputStream out) BufferedOutputStream(OutputStream

out, int dim_buffer)

In cazul constructorilorin care dimensiunea buffer-ului nu este specificata, aceasta

primeste valoarea implicita de 512 octeti (caractere).

Metodele acestor clase sunt cele uzuale de tipul read si write. Pe langa acestea, clasele

pentru scriere prin buffer mai au si metoda flush care goleste explicit zona tampon, chiar daca

aceasta nu este plina.

BufferedWriter out = new BufferedWriter(

new FileWriter("out.dat"), 1024) ;

//am creat un flux cu buffer de 1024 octeti

for(int i=0; i<1000; i++)

out.write(i);

//bufferul nu este plin, in fisier nu s-a scris nimic

out.flush(); //bufferul este golit, datele se scriu in fisier

Metoda readLine

Este specifica fluxurilor de citire cu buffer si permite citirea linie cu linie a datelor de

intrare. O linie reprezinta o succesiune de caractere terminata cu simbolul pentru sfarsit de

linie, dependent de platforma de lucru. Acesta este reprezentat in Java prin secventa escape

’\n’;

BufferedReader br = new BufferedReader(new FileReader("in")) ;

String linie;

while ((linie = br.readLine()) != null) {

...

//proceseaza linie

}

br.close();

Page 78: Curs Practic de Java

78

4.2.6 Concatenarea fluxurilor

Clasa SequenceInputStream permite unei aplicatii sa combine serial mai multe fluxuri de

intrare astfel incat acestea sa apara ca un singur flux de intrare. Citirea datelor dintr-un astfel

de flux se face astfel: se citeste din primul flux de intrare specificat pana cand se ajunge la

sfarsitul acestuia, dupa care primul flux de intrare este inchis si se deschide automat urmatorul

flux de intrare din care se vor citi in continuare datele, dupa care procesul se repeta pana la

terminarea tuturor fluxurilor de intrare.

Constructorii acestei clase sunt:

SequenceInputStream(Enumeration e)

SequenceInputStream(InputStream s1, InputStream s2)

Primul construieste un flux secvential dintr-o multime de fluxuri de intrare. Fiecare obiect

in enumerarea primita ca parametru trebuie sa fie de tipul InputStream. Cel de-al doilea

construieste un flux de intrare care combina doar doua fluxuri s1 si s2, primul flux citit fiind s1.

Exemplul cel mai elocvent de folosirea a acestei clase este concatenarea a doua sau mai

multor fisiere:

/* Concatenarea a doua fisiere ale caror nume sunt primite de la linia de comanda.

Rezultatul concatenarii este afisat pe ecran. */

import java.io.*;

public class Concatenare {

public static void main(String args[]) {

if (args.length <= 1) {

System.out.println("Argumente insuficiente!");

System .exit (-1);

}

try {

FileInputStream f1 = new FileInputStream(args[0]);

FileInputStream f2 = new FileInputStream(args[1]);

SequenceInputStream s = new SequenceInputStream(f1, f2) ;

int c;

while ((c = s.read()) != -1)

System .out.print (( char )c);

s.close ();

//f1 si f2 sunt inchise automat

} catch (IOException e) {

e. printStackTrace ();

}

}

Page 79: Curs Practic de Java

79

Pentru concatenarea mai multor fisiere exista doua variante:

folosirea unei enumerari -primul constructor (vezi ”Colectii”);

concatenarea pe rand a acestora folosind al 2-lea constructor; concatenarea a 3 fisiere

va construi un flux de intrare astfel:

FileInputStream f1 = new FileInputStream(args[0]);

FileInputStream f2 = new FileInputStream(args[1]);

FileInputStream f3 = new FileInputStream(args[2]);

SequenceInputStream s = new SequenceInputStream(

f1, new SequenceInputStream(f2, f3));

4.2.7 Fluxuri pentru filtrarea datelor

Un flux de filtrare se ataseaza altui flux pentru a filtra datele care sunt citite/scrise de catre

acel flux. Clasele pentru filtrarea datelor superclasele abstracte:

FilterInputStream -pentru filtrarea fluxurilor de intrare si

FilterOutputStream -pentru filtrarea fluxurilor de iesire.

Cele mai importante fluxruri pentru filtrarea datelor sunt implementate de clasele:

DataInputStream, DataOutputStream BufferedInputStream, BufferedOutputStream

LineNumberInputStream PushbackInputStream PrintStream

Observati ca toate aceste clase descriu fluxuri de octeti.

Filtrarea datelor nu trebuie vazuta ca o metoda de a elimina anumiti octeti dintr-un flux ci

de a transforma acesti octeti in date care sa poata fi interpretate sub alta forma. Asa cum am

vazut la citirea/scrierea cu zona tampon, clasele de filtrare BufferedInputStream si

BufferedOutputStream colecteaza datele unui flux intr-un buffer, urmand ca citirea/scrierea sa

se faca prin intermediu acelui buffer.

Asadar, fluxurile de filtrare nu elimina date citite sau scrise de un anumit flux, ci introduc o

noua modalitate de manipulare a lor, ele mai fiind numite si fluxuri de procesare. Din acest

motiv, fluxurile de filtrare vor contine anumite metode specializate pentru citirea/scrierea

datelor, altele decat cele comune tuturor fluxurilor. De exemplu, clasa BufferedInputStream

pune la dispozitie metoda readLine pentru citirea unei linii din fluxul de intrare.

Folosirea fluxurilor de filtrare se face prin atasarea lor de un flux care se ocupa efectiv de

citirea/scrierea datelor: FluxFiltrare numeFlux = new FluxFiltrare(referintaAltFlux);

4.2.8 Clasele DataInputStream si DataOutputStream

Aceste clase ofera metode prin care un flux nu mai este vazut ca o insiruire de octeti, ci de

date primitive. Prin urmare, vor furniza metode pentru citirea si scrierea datelor la nivel de tip

primitiv si nu la nivel de octet. Clasele care ofera un astfel de suport implementeaza interfetele

DataInput, respectiv DataOutput. Acestea definesc metodele pe care trebuie sa le puna la

Page 80: Curs Practic de Java

80

dispozitie in vederea citireii/scrierii datelor de tip primitiv.

Aceste metode au denumirile generice de readXXX si writeXXX, specificate de interfetele

DataInput si DataOutput si pot provoca exceptii de tipul IOException. Denumirile lor sunt

sugestive pentru tipul de date pe care il prelucreaza. mai putin readUTF si writeUTF care se

ocupa cu obiecte de tip String, fiind singurul tip referinta permis de aceste clase.

Scrierea datelor folosind fluxuri de acest tip se face in format binar, ceea ce inseamna ca un

fisier in care au fost scrise informatii folosind metode writeXXX nu va putea fi citit decat prin

metode readXXX.

Transformarea unei valori in format binar se numeste serializare. Clasele

DataInputStream si DataOutputStream permit serializarea tipurilor primitive si a sirurilor de

caractere. Serializarea celorlalte tipuri referinta va fi facuta prin intermediul altor clase, cum ar

fi ObjectInputStream si ObjectOutputStream (vezi ”Serializarea obiectelor”).

4.3 Intrari si iesiri formatate

Incepand cu versiunea 1.5, limbajul Java pune la dispozitii modalitati simplificate pentru

afisarea formatata a unor informatii, respectiv pentru citirea de date formatate de la tastatura.

4.3.1 Intrari formatate

Clasa java.util.Scanner ofera o solutie simpla pentru formatarea unor informatii citite de

pe un flux de intrare fie pe octeti, fie pe caractere, sau chiar dintr-un obiect de tip File. Pentru a

citi de la tastatura vom specifica ca argument al constructorului fluxul System.in:

Scanner s = Scanner.create(System.in);

String nume = s.next();

int varsta = s.nextInt();

double salariu = s.nextDouble();

s.close();

4.3.2 Iesiri formatate

Clasele PrintStream si PrintWriter pun la dispozitiile, pe langa metodele print, println care

ofereau posibilitatea de a afisa un sir de caractere, si metodele format, printf (echivalente) ce

permit afisarea formatata a unor variabile.

System.out.printf("%s %8.2f %2d %n", nume, salariu, varsta);

Formatarea sirurilor de caractere se bazeaza pe clasa java.util.Formatter.

Page 81: Curs Practic de Java

81

4.4 Fluxuri standard de intrare si iesire

Mergand pe linia introdusa de sistemul de operare UNIX, orice program Java are :

intrare standard

iesire standard

iesire standard pentru erori

In general, intrarea standard este tastatura iar iesirea standard este ecranul.

Intrarea si iesirea standard sunt reprezentate de obiecte pre-create ce descriu fluxuri de date

care comunica cu dispozitivele standard ale sistemului. Aceste obiecte sunt definite publice in

clasa System si sunt:

System.in -fluxul standar de intrare, de tip InputStream

System.out -fluxul standar de iesire, de tip PrintStream

System.err -fluxul standar pentru erori, de tip PrintStream

4.4.1 Afisarea informatiilor pe ecran

Am vazut deja numeroase exemple de utilizare a fluxului standard de iesire, el fiind folosit

la afisarea oricaror rezultate pe ecran (in modul consola):

System.out.print (argument);

System.out.println(argument);

System.out.printf (format, argumente...);

System.out.format (format, argumente...);

Fluxul standard pentru afisarea erorilor se foloseste similar si apare uzual in secventele

de tratare a exceptiilor. Implicit, este acelasi cu fluxul standard de iesire.

catch(Exception e) { System.err.println("Exceptie:" + e); }

Fluxurile de iesire pot fi folosite asadar fara probleme deoarece tipul lor este PrintStream,

clasa concreta pentru scrierea datelor. In schimb, fluxul standard de intrare System.out este de

tip InputStream, care este o clasa abstracta, deci pentru a-l putea utiliza eficient va trebui sa-l

folosimimpreuna cu un flux de procesare(filtrare) care sa permita citirea facila a datelor.

4.4.2 Citirea datelor de la tastatura

Uzual, vom dori sa folosim metoda readLine pentru citirea datelor de la tastatura si din

acest motiv vom folosi intrarea standard impreuna cu o clasa de procesare care ofera aceasta

metoda.

Exemplul tipic este:

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

System.out.print("Introduceti o linie:");

String linie = stdin.readLine();

System.out.println(linie);

Page 82: Curs Practic de Java

82

In exemplul urmator este prezentat un program care afiseaza liniile introduse de la

tastatura pana in momentul in care se introduce linia ”exit” sau o linie vida si mentioneazadaca

sirul respectiv reprezinta un numar sau nu.

/* Citeste siruri de la tastatura si verifica daca reprezinta numere sau nu */

import java.io.*;

public class EsteNumar {

public static void main(String[] args) {

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

try {

while(true) {

String s = stdin.readLine();

if (s.equals("exit") || s.length()= =0)

break ;

System .out.print (s);

try {

Double. parseDouble (s);

System.out.println(": DA");

} catch(NumberFormatException e) {

System.out.println(": NU");

}

}

} catch(IOException e) {

System.err.println("Eroare la intrarea standard!");

e. printStackTrace (); }

}

}

Incepand cu versiunea 1.5, varianta cea mai comoda de citire a datelor de la tastatura

este folosirea clasei java.util.Scanner.

4.4.3 Redirectarea fluxurilor standard

Redirectarea fluxurilor standard presupune stabilirea unei alte surse decat tastatura

pentru citirea datelor, respectiv alte destinatii decat ecranul pentru cele doua fluxuri de iesire.

In clasa System exista urmatoarele metode statice care realizeaza acest lucru:

setIn(InputStream) -redirectare intrare

setOut(PrintStream) -redirectare iesire

setErr(PrintStream) -redirectare erori

Redirectarea iesirii este utila in special atunci cand sunt afisate foarte multe date pe

ecran. Putem redirecta afisarea catre un fisier pe care sa-l citim dupa executia programului.

Page 83: Curs Practic de Java

83

Secventa clasica de redirectare a iesirii este catre un fisier este:

PrintStream fis = new PrintStream( new FileOutputStream("rezultate.txt")));

System.setOut(fis);

Redirectarea erorilor intr-un fisier poate fi de asemenea utila si se face intr-o maniera

similara:

PrintStream fis = new PrintStream( new FileOutputStream("erori.txt")));

System.setErr(fis);

Redirectarea intrarii poate fi folositoare pentru un program in mod consola care primeste

mai multe valori de intrare. Pentru a nu le scrie de la tastatura de fiecare data in timpul testarii

programului, ele pot fi puse intrun fisier, redirectand intrarea standard catre acel fisier. In

momentul cand testarea programului a luat sfarsit redirectarea poate fi eliminata, datele fiind

cerute din nou de la tastatura.

import java.io.*;

class Redirectare {

public static void main(String[] args) {

try {

BufferedInputStream in = new BufferedInputStream( new FileInputStream("intrare.txt"));

PrintStream out = new PrintStream( new FileOutputStream("rezultate.txt"));

PrintStream err = new PrintStream( new FileOutputStream("erori.txt"));

System . setIn (in);

System . setOut (out);

System . setErr (err);

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

4.4.4 Analiza lexicala pe fluxuri (clasa StreamTokenizer)

Clasa StreamTokenizer proceseaza un flux de intrare de orice tip siilimparte in ”atomi

lexicali”. Rezultatul va consta in faptul ca in loc sa se citeasca octeti sau caractere, se vor citi,

pe rand, atomii lexicali ai fluxului respectiv. Printr-un atom lexical se in]elege in general:

un identificator (un sir care nu este intre ghilimele)

un numar

un sir de caractere

un comentariu

un separator

Atomii lexicali sunt despartiti intre ei de separatori. Implicit, acesti separatori sunt cei

obisnuti: spatiu, tab, virgula, punct si virgula, etc., insa pot fi schimbati prin diverse metode ale

clasei.

Page 84: Curs Practic de Java

84

Constructorii clasei sunt:

public StreamTokenizer(Reader r)

public StreamTokenizer(InputStream is)

Identificarea tipului si valorii unui atom lexical se face prin intermediul variabilelor:

TT EOF -atom ce marcheaza sfaarsitul fluxului

TT EOL -atom ce marcheaza sfarsitul unei linii

TT NUMBER -atom de tip numar

TT WORD-atom de tip cuvant

ttype-tipul ultimului atom citit din flux

nval-valoarea unui atom numeric

sval -valoarea unui atom de tip cuvant

Citirea atomilor din flux se face cu metoda nextToken(), care returneza tipul atomului

lexical citit si scrie in variabilele nval sau sval valoarea corespunzatoare atomului.

Exemplul tipic de folosire a unui analizor lexical este citirea unei secvente de numere si

siruri aflate intr-un fisier sau primite de la tastatura:

/* Citirea unei secvente de numere si siruri dintr -un fisier specificat si afisarea tipului si

valorii lor */

import java.io.*;

public class CitireAtomi {

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

BufferedReader br = new BufferedReader(new FileReader(" fisier .txt"));

StreamTokenizer st = new StreamTokenizer(br);

int tip = st.nextToken();

//Se citeste primul atom lexical

Asadar, modul de utilizare tipic pentru un analizor lexical este intr-o bucla ”while”, in care

se citesc atomii unul cate unul cu metoda nextToken, pana se ajunge la sfarsitul fluxului (TT

EOF). In cadrul buclei ”while” se determina tipul atomul curent curent (intors de metoda

nextToken) si apoi se afla valoarea numerica sau sirul de caractere corespunzator atomului

respectiv.

In cazul in care tipul atomilor nu ne intereseaza, este mai simplu sa citim fluxul linie cu

linie si sa folosim clasa StringTokenizer, care realizeaza impartirea unui sir de caractere in

atomi lexicali, sau metoda split a clasei String.

Page 85: Curs Practic de Java

85

4.5 Clasa RandomAccesFile (fisiere cu acces direct) \

Dupa cum am vazut, fluxurile sunt procese secventiale de intrare/iesire. Acestea sunt

adecvate pentru lucrul cu medii secventiale de memorare a datelor, cum ar fi banda magnetica

sau pentru transmiterea informatiilor prin retea, desi sunt foarte utile si pentru dispozitive in

care informatia poate fi accesata direct.

Clasa RandomAccesFile are urmatoarele caracteristici:

permite accesul nesecvential (direct) la continutul unui fisier;

este o clasa de sine statatoare, subclasa directa a clasei Object;

se gaseste in pachetul java.io;

implementeaza interfetele DataInput si DataOutput, ceea ce inseamna ca sunt

disponibile metode de tipul readXXX, writeXXX, intocmai ca la clasele DataInputStream

si DataOutputStream;

permite atat citirea cat si scriere din/in fisiere cu acces direct;

permite specificarea modului de acces al unui fisier (read-only, read-write).

Constructorii acestei clase sunt:

RandomAccessFile(StringnumeFisier, StringmodAcces) throws IOException

RandomAccessFile(StringnumeFisier, StringmodAcces) throws IOException

unde modAcces poate fi:

• ”r” -fisierul este deschis numai pentru citire (read-only)

• ”rw” -fisierul este deschis pentru citire si scriere (read-write) Exemple:

RandomAccesFile f1 = new RandomAccessFile("fisier.txt", "r");

//deschide un fisier pentru citire

RandomAccesFile f2 = new RandomAccessFile("fisier.txt", "rw");

//deschide un fisier pentru scriere si citire

Clasa RandomAccesFile suporta notiunea de pointer de fisier. Acesta este un indicator

ce specifica pozitia curenta in fisier. La deschiderea unui fisier pointerul are valoarea 0,

indicand inceputul fisierului. Apeluri la metodele de citire/scrirere deplaseaza pointerul

fisierului cu numarul de octeti cititi sau scrisi de metodele respective.

In plus fatade metodele de tip read si write clasa pune la dispozitie si metode pentru

controlul pozitiei pointerului de fisier. Acestea sunt:

skipBytes -muta pointerul fisierului inainte cu un numar specificat de octeti

seek -pozitioneaza pointerului fisierului inaintea unui octet specificat

getFilePointer -returneaza pozitia pointerului de fisier.

Page 86: Curs Practic de Java

86

4.6 Clasa File

Clasa File nu se refera doar la un fisier ci poate reprezenta fie un fisier anume, fie

multimea fisierelor dintr-un director.

Specificarea unui fisier/director se face prin specificarea caii absolute spre acel fisier sau

a caii relative fata de directorul curent. Acestea trebuie sa respecte conventiile de specificare a

cailor si numelor fisierelor de pe platforma de lucru. Utilitate clasei File consta in furnizarea

unei modalitati de a abstractiza dependentele cailor si numelor fisierelor fatade masina gazda,

precum si punerea la dispozitie a unor metode pentru lucrul cu fisere si directoare la nivelul

sistemului de operare.

Astfel, in aceasta clasa vom gasi metode pentru testarea existentei, stergerea,

redenumirea unui fisier sau director, crearea unui director, listarea fisierelor dintr-un director,

etc.

Trebuie mentionat si faptul ca majoritatea constructorilor fluxurilor care permit accesul la

fisiere accepta ca argument un obiect de tip File in locul unui sir ce reprezinta numele fisierului

respectiv.

File f = new File("fisier.txt");

FileInputStream in = new FileInputStream(f)

Cel mai uzual constructor al clasei File este:

public File(String numeFisier)

Metodele mai importante ale clasei File au denumiri sugestive si vor fi prezentate prin

intermediul exemplului urmator care listeaza fisierele si subdirectoarele unui director specificat

si, pentru fiecare din ele afiseaza diverse informatii:

/* Programul listeaza fisierele si subdirectoarele unui director. Pentru fiecare din ele vor fi

afisate diverse informatii. Numele directorului este primit ca argument de la linia de comanda ,

sau este directorul curent.

*/

import java.io.*;

import java.util.*;

public class ListareDirector {

private static void info(File f) {

//Afiseaza informatii despre un fisier sau director

String nume = f.getName();

if(f. isFile ())

System.out.println("Fisier: " + nume);

else if(f. isDirectory ())

System.out.println("Director: " + nume);

System .out. println ( "Cale absoluta: " + f.getAbsolutePath() + "\n Poate citi: " + f.canRead() +

"\n Poate scrie: " + f.canWrite() + "\n Parinte: " + f.getParent() + "\n Cale: " + f.getPath() + "\n

Lungime: " + f.length() + "\n Data ultimei modificari: " + new Date(f.lastModified()));

Page 87: Curs Practic de Java

87

System .out. println (" --------------");

}

public static void main(String[] args) {

String nume;

if (args.length == 0)

nume = "."; // directorul curent

else nume = args [0];

try {

File director = new File(nume);

File[] continut = director.listFiles();

for(int i = 0; i < continut.length; i++)

info ( continut [i]);

} catch(Exception e) {

e. printStackTrace (); }

}

}

}

Page 88: Curs Practic de Java

88

5 Interfete

5.1 Introducere

5.1.1 Ce este o interfata ?

Interfetele duc conceptul de clasa abstracta cu un pas inainte prin eliminarea oricaror

implementari de metode, punand in practica unul din conceptele programarii orientate obiect si

anume cel de separare a modelului unui obiect (interfata) de implementarea sa. Asadar, o

interfata poate fi privita ca un protocol de comunicare intre obiecte.

O interfata Java defineste un set de metode dar nu specifica nici o implementare pentru

ele. O clasa care implementeaza o interfata trebuie obligatoriu sa specifice implementari

pentru toate metodele interfetei, supunandu-se asadar unui anumit comportament.

Definitie : O interfata este o colectie de metode fara implementare si declaratii de

constante.

Interfetele permit, alaturi de clase, definirea unor noi tipuri de date.

5.2 Folosirea interfetelor

5.2.1 Definirea unei interfete

Definirea unei interfete se face prin intermediul cuvantului cheie interface:

[public] interface NumeInterfata [extends SuperInterfata1, SuperInterfata2...] {

/* Corpul interfetei:

Declaratii de constane

Declaratii de metode abstracte

*/

}

O interfata poate avea un singur modificator si anume public. O interfata publica este

accesibila tuturor claselor, indiferent de pachetul din care fac parte, implicit nivelul de acces

fiind doar la nivelul pachetului din care face parte interfata.

O interfata poate extinde oricate interfete. Acestea se numesc superinterfete si sunt

separate prin virgula. (vezi ”Mostenirea multipla prin intermediul interfetelor”).

Page 89: Curs Practic de Java

89

Corpul unei interfete poate contine:

constante: acestea pot fi sau nu declarate cu modificatorii public, static si final care sunt

impliciti, nici un alt modificator neputand aparea in declaratia unei variabile dintr-o

interfata. Constantele unei interfete trebuie obligatoriu initializate.

interface Exemplu {

int MAX = 100; // Echivalent cu:

public static final int MAX = 100;

int MAX; // Incorect, lipseste initializarea

private int x = 1; // Incorect, modificator nepermis

}

metode fara implementare: acestea pot fi sau nu declarate cu modificatorul public, care

este implicit; nici un alt modificator nu poate aparea in declaratia unei metode a unei

interfete.

interface Exemplu {

void metoda(); // Echivalent cu:

public void metoda();

protected void metoda2(); // Incorect, modificator nepermis

Atentie

Variabilele unei interfete sunt implicit publice chiar daca nu sunt declarate cu

modificatorul public.

Variabilele unei interfete sunt implicit constante chiar daca nu sunt declarate cu

modificatorii static si final.

Metodele unei interfete sunt implicit publice chiar daca nu sunt declarate cu

modificatorul public.

In variantele mai vechi de Java era permis si modificatorul abstract in declaratia

interfetei si in declaratiile metodelor, insa acest lucru nu mai este valabil, deoarece atat

interfata cat si metodele sale nu pot fi altfel decat abstracte.

5.2.2 Implementarea unei interfete

Implementarea uneia sau mai multor interfete de catre o clasa se face prin intermediul

cuvantului cheie implements:

class NumeClasa implements NumeInterfata

sau

class NumeClasa implements Interfata1, Interfata2, ...

Page 90: Curs Practic de Java

90

O clasa poate implementa oricate interfete sau poate sa nu implementeze nici una.

In cazul in care o clasa implementeaza o anumita interfata, atunci trebuie obligatoriu sa

specifice cod pentru toate metodele interfetei. Din acest motiv, odata creata si folosita la

implementarea unor clase, o interfata nu mai trebuie modificata, in sensul ca adaugarea unor

metode noi sau schimbarea signaturii metodelor existente vor duce la erori in compilarea

claselor care o implementeaza. Evident, o clasa poate avea si alte metode si variabile membre

in afara de cele definite in interfata.

Atentie !!! Modificarea unei interfete implica modificarea tuturor claselor care implementeaza

acea interfata.

O interfata nu este o clasa, dar orice referinta de tip interfata poate primi ca valoare o

referinta la un obiect al unei clase ce implementeaza interfata respectiva. Din acest motiv,

interfetele pot fi privite ca tipuri de date si vom spune adesea ca un obiect are tipul X, unde X

este o interfata, daca acesta este o instanta a unei clase ce implementeaza interfata X.

Implementarea unei interfete poate sa fie si o clasa abstracta.

5.2.3 Exemplu: implementarea unei stive

Sa consideram urmatorul exemplu. Dorim sa implementam un nou tip de date numit

Stack, care sa modeleze notiunea de stiva de obiecte. Obiectele de tip stiva, indiferent de

implementarea lor, vor trebui sa contina metodele:

push -adauga un nou element in stıva

pop -elimina elementul din varful stivei

peek -returneaza varful stivei

empty -testeaza daca stiva este vida

toString -returneaza continutul stivei sub forma unui sir de caractere.

Din punctul de vedere al structurii interne, o stiva poate fi implementata folosind un vector

sau o lista inlantuita, ambele solutii avand avantaje si dezavantaje. Prima solutie este mai

simplu de inteles, in timp ce a doua este mai eficienta din punctul de vedere al folosirii

memoriei. Deoarece nu dorim sa legam tipul de date Stack de o anumita implementare

structurala, il vom defini prin intermediul unei interfete. Vom vedea imediat avantajele acestei

abordari.

public interface Stack {

void push(Object item) throws StackException;

void pop() throws StackException;

Object peek() throws StackException;

boolean empty();

String toString();

}

Page 91: Curs Practic de Java

91

Pentru a trata situatiile anormale care pot aparea atunci cand incercam sa punem un

element in stiva si nu este posibil din lipsa de memorie, sau incercam sa accesam varful stivei

si aceasta este vida, vom defini o exceptie proprie StackException:

public class StackException extends Exception {

public StackException() {

super ();

}

public StackException(String msg) {

super(msg);

}

}

In continuare prima implementare a stivei, folosind un vector:

// Implementarea stivei folosind un vector de obiecte.

public class StackImpl1 implements Stack { private Object items[];

//Vectorul ce contine obiectele

private int n= 0; // Numarul curent de elemente din stiva

public StackImpl1(int max) {

// Constructor

items = new Object[max];

}

public StackImpl1() {

this (100) ;

}

public void push(Object item) throws StackException {

if (n == items.length)

throw new StackException("Stiva este plina!");

items[n++] = item;

}

public void pop() throws StackException {

if (empty())

throw new StackException("Stiva este vida!");

items[--n] = null;

}

public Object peek() throws StackException {

if (empty())

throw new StackException("Stiva este vida!");

return items[n-1];

}

Page 92: Curs Practic de Java

92

public boolean empty() {

return (n= =0);

}

public String toString() {

String s="";

for(int i= n-1; i> =0; i--)

s += items[i].toString() + " ";

return s;

}

}

Remarcati ca, desi in interfata metodele nu sunt declarate explicit cu modificatorul public,

ele sunt totusi publice si trebuie declarate ca atare in clasa. Trebuie remarcat si faptul ca

metoda toString este definita deja in clasa Object, deci clasa noastra o are deja implementata

si nu am fi obtinut nici o eroare la compilare daca nu o implementam explicit. Ceea ce facem

acum este de fapt supradefinirea ei.

O alta observatie importanta se refera la faptul ca trebuie sa declaram in cadrul interfetei

si exceptiile aruncate de metode, ce trebuie obligatoriu tratate.

Sa vedem acum modalitatea de implementare a stivei folosind o lista inlantuita:

// Implementarea stivei folosind o lista inlantuita.

public class StackImpl2 implements Stack {

class Node {

//Clasa interna ce reprezinta un nod al listei

Object item; // informatia din nod

Node link; // legatura la urmatorul nod

Node(Object item, Node link) {

this.item = item;

this.link = link;

}

}

private Node top=null; //Referinta la varful stivei

public void push(Object item) {

Node node = new Node(item, top);

top = node;

}

public void pop() throws StackException {

if (empty())

throw new StackException("Stiva este vida!");

top = top.link;

}

Page 93: Curs Practic de Java

93

public Object peek() throws StackException {

if (empty())

throw new StackException("Stiva este vida!");

return top.item;

}

public boolean empty() {

return (top == null);

}

}

Singura observatie pe care o facem aici este ca, desi metoda push din interfata declara

aruncarea unor exceptii de tipul StackException, nu este obligatoriu ca metoda din clasa sa

specifice si ea acest lucru, atat timp cat nu genereaza exceptii de acel tip. Invers este insa

obligatoriu.

In continuare este prezentata o mica aplicatie demonstrativa care foloseste tipul de date

nou creat si cele doua implementari ale sale:

public class TestStiva {

public static void afiseaza(Stack s) {

System.out.println("Continutul stivei este: " + s);

}

public static void main(String args[]){

try {

Stack s1 = new StackImpl1();

s1.push("a");

s1.push("b");

afiseaza (s1);

Stack s2 = new StackImpl2();

s2.push(new Integer(1));

s2.push(new Double(3.14));

afiseaza (s2);

} catch (StackException e) {

System.err.println("Eroare la lucrul cu stiva!");

e. printStackTrace ();

}

}

}

Observati folosirea interfetei Stack ca un tip de date, ce aduce flexibilitate sporita in

manevrarea claselor ce implementeaza tipul respectiv. Metoda afiseaza accepta ca argument

orice obiect al unei clase ce implementeaza Stack.

Page 94: Curs Practic de Java

94

Observatie : In pachetul java.util exista clasa Stack care modeleaza notiune de stiva de

obiecte si, evident, aceasta va fi folosita in aplicatiile ce au nevoie de acest tip de date.

Exemplu oferit de noi nu are legatura cu aceasta clasa si are rol pur demonstrativ.

5.3 Interfete si clase abstracte

La prima vedere o interfata nu este altceva decat o clasa abstracta in care toate metodele

sunt abstracte (nu au nici o implementare). Asadar, o clasa abstracta nu ar putea inlocui o

interfata. Raspunsul la intrebare depinde de situatie, insa in general este ’Nu’. Deosebirea

consta in faptul ca unele clase sunt fortate sa extinda o anumita clasa (de exemplu orice applet

trebuie sa fie subclasa a clasei Applet) si nu ar mai putea sa extinda o alta clasa, deoarece in

Java nu exista decat mostenire simpla. Fara folosirea interfetelor nu am putea forta clasa

respectiva sa respecte diverse tipuri de protocoale.

La nivel conceptual, diferenta consta in:

extinderea unei clase abstracte forteaza o relatie intre clase;

implementarea unei interfete specifica doar necesitatea implementarii unor anumie

metode.

In multe situatii interfetele si clasele abstracte sunt folosite impreuna pentru a implementa

cat mai flexibil si eficient o anumita ierarhie de clase. Un exemplu sugestiv este dat de clasele

ce descriu colectii. Ca sa particularizam, exista:

interfata List care impune protocolul pe care trebuie sa il respecte o clasa de tip lista,

clasa abstracta AbstractList care implementeaza interfata List si ofera implementari

concrete pentru metodele comune tuturor tipurilor de lista,

clase concrete, cum ar fi LinkedList, ArrayList care extind AbstractList.

5.4 Mostenire multipla prin interfete

Interfetele nu au nici o implementare si nu pot fi instantiate. Din acest motiv, nu reprezinta

nici o problema ca anumite clase sa implementeze mai multe interfete sau ca o interfata sa

extinda mai multe interfete (sa aiba mai multe superinterfete)

class NumeClasa implements Interfata1, Interfata2, ...

interface NumeInterfata extends Interfata1, Interfata2, ...

O interfata mosteneste atat constantele cat si declaratiile de metode de la

superinterfetele sale. O clasa mosteneste doar constantele unei interfete si responsabilitatea

implementarii metodelor sale.

Page 95: Curs Practic de Java

95

Sa consideram un exemplu de clasa care implementeaza mai multe interfete:

interface Inotator {

void inoata();

}

interface Zburator {

void zboara();

}

interface Luptator {

void lupta();

}

class Erou implements Inotator, Zburator, Luptator {

public void inoata() {}

public void zboara() {}

public void lupta() {}

}

Exemplu de interfata care extinde mai multe interfete:

interface Monstru {

void ameninta();

}

interface MonstruPericulos extends Monstru {

void distruge();

}

interface Mortal {

void omoara();

}

interface Vampir extends MonstruPericulos, Mortal {

void beaSange();

}

class Dracula implements Vampir {

public void ameninta() {}

public void distruge() {}

public void omoara() {}

public void beaSange() {}

}

Evident, pot aparea situatii de ambiguitate, atunci cand exista constante sau metode cu

aceleasi nume in mai multe interfete, insa acest lucru trebuie intotdeauna evitat, deoarece

scrierea unui cod care poate fi confuz este un stil prost de programare. In cazul in care acest

lucru se intampla, compilatorul nu va furniza eroare decat daca se incearca referirea

constantelor ambigue fara a le prefixa cu numele interfetei sau daca metodele cu acelasi nume

nu pot fi deosbite, cum ar fi situatia cand au aceeasi lista de argumente dar tipuri returnate

incompatibile.

Page 96: Curs Practic de Java

96

interface I1 { int x=1; void metoda();

}

interface I2 { int x=2; void metoda(); //corect //int metoda(); //incorect

}

class C implements I1, I2 {

public void metoda() {

System.out.println(I1.x); //corect

System.out.println(I2.x); //corect

System.out.println(x); //ambiguitate

}

}

Sa recapitulam cateva lucruri legate de clase si interfete:

clasa nu poate avea decat o superclasa.

clasa poate implementa oricate interfete.

clasa trebuie obligatoriu sa trateze metodele din interfetele pe care la implementeaza.

Ierarhia interfetelor este independenta de ierarhia claselor care le implementeaza.

5.5 Utilitatea interfetelor

Dupa cum am vazut, o interfata defineste un protocol ce poate fi implementat de orice

clasa, indiferent de ierarhia de clase din care face parte. Interfetele sunt utile pentru:

definirea unor similaritati intre clase independente fara a forta artificial o legatura intre

ele;

asigura ca toate clasele care implementeaza o interfata pun la dipozitie metodele

specificate in interfata -de aici rezulta posibilitatea implementarii unor clase prin mai

multe modalitati si folosirea lor intr-o maniera unitara;

definirea unor grupuri de constante;

transmiterea metodelor ca parametri;

5.5.1 Crearea grupurilor de constante

Deoarece orice variabila a unei interfete este implicit declarata cu public, static si final,

interfetele reprezinta o metoda convenabila de creare a unor grupuri de constante care sa fie

folosite global intr-o aplicatie:

public interface Luni { int IAN=1, FEB=2, ..., DEC=12; }

Page 97: Curs Practic de Java

97

Folosirea acestor constante se face prin expresii de genul NumeInterfata.constanta, ca in

exemplul de mai jos:

if (luna < Luni.DEC) luna ++ else luna = Luni.IAN;

5.5.2 Transmiterea metodelor ca parametri

Deoarece nu exista pointeri propriu-zisi, transmiterea metodelor ca parametri este

realizata in Java prin intermediul interfetelor. Atunci cand o metoda trebuie sa primeasca ca

argument de intrare o referinta la o alta functie necesara executiei sale, cunoscuta doar la

momentul executiei, atunci argumentul respectiv va fi declarat de tipul unei interfete care

contine metoda respectiva. La executie metoda va putea primi ca parametru orice obiect ce

implementeaza interfata respectiva si deci contine si codul functiei respective, aceasta urmand

sa fie apelata normal pentru a obtine rezultatul dorit.

Aceasta tehnica, denumita si call-back, este extrem de folosita in Java si trebuie neaparat

inteleasa. Sa consideram mai multe exemple pentru a clarifica lucrurile.

Primul exemplu se refera la explorarea nodurilor unui graf. In fiecare nod trebuie sa se

execute prelucrarea informatiei din nodul respectiv prin intermediul unei functii primite ca

parametru. Pentru aceasta, vom defini o interfata Functie care va specifica metoda trimisa ca

parametru.

interface Functie {

public void executa(Nod u);

}

class Graf {

//...

void explorare(Functie f) {

//...

if (explorarea a ajuns in nodul v) {

f.executa(v); //...

}

}

}

//Definim doua functii

class AfisareRo implements Functie {

public void executa(Nod v) {

System.out.println("Nodul curent este: " + v);

}

}

class AfisareEn implements Functie {

public void executa(Nod v) {

System.out.println("Current node is: " + v);

}

}

Page 98: Curs Practic de Java

98

public class TestCallBack {

public static void main(String args[]) {

Graf G = new Graf();

G.explorare(new AfisareRo());

G.explorare(new AfisareEn());

}

}

Al doilea xemplu va fi prezentat in sectiunea urmatoare, intrucat face parte din API-ul

standard Java si vor fi puse in evidenta, prin intermediul sau, si alte tehnici de programare.

5.6 Interfata FilenameFilter

Instantele claselor ce implementeaza aceasta interfata sunt folosite pentru a crea filtre

pentru fisiere si sunt primite ca argumente de metode care listeaza continutul unui director,

cum ar fi metoda list a clasei File. Asadar, putem spune ca metoda list primeste ca argument o

alta functie care specifica daca un fisier va fi returnat sau nu (criteriul de filtrare).

Interfata FilenameFilter are o singura metoda: accept care specifica criteriul de filtrare si

anume, testeaza daca numele fisierului primit ca parametru indeplineste conditiile dorite de

noi.

Definitia interfetei este:

public interface FilenameFilter {

public boolean accept(File dir, String numeFisier);

}

Asadar, orice clasa de specificare a unui filtru care implementeza interfata FilenameFilter

trebuie sa implementeze metoda accept a acestei interfete. Aceste clase mai pot avea si alte

metode, de exemplu un constructor care sa primeasca criteriul de filtrare. In general, o clasa

de specificare a unui filtru are urmatorul format:

class FiltruFisiere implements FilenameFilter {

String filtru;

// Constructorul

FiltruFisiere(String filtru) {

this.filtru = filtru;

}

// Implementarea metodei accept

public boolean accept(File dir, String nume) {

if (filtrul este indeplinit)

return true;

else return false;

}

}

Page 99: Curs Practic de Java

99

Metodele cele mai uzuale ale clasei String folosite pentru filtrarea fisierelor sunt:

endsWith -testeaza daca un sir are o anumita terminatie

indexOf -testeaza daca un sir contine un anumit subsir, returnand pozitia acestuia, sau

0 in caz contrar.

Instantele claselor pentru filtrare sunt primite ca argumente de metode de listare a

continutului unui director. O astfel de metoda este list din clasa File:

String[] list (FilenameFilter filtru)

Observati ca aici interfata este folosita ca un tip de date, ea fiind substituita cu orice clasa

care o implementeaza. Acesta este un exemplu tipic de transmitere a unei functii (functia de

filtrare accept) ca argument al unei metode.

Sa consideram exemplul complet in care dorim sa listam fisierele din directorul curent

care au o anumita extensie.

/* Listarea fisierelor din directorul curent care au anumita extensie primita ca argument. Daca

nu se primeste nici un argument , vor fi listate toate

. */

import java.io.*;

class Listare {

public static void main(String[] args) {

try {

File director = new File(".");

String[] list;

if (args.length > 0)

list = director.list(new Filtru(args[0]));

else

list = director.list();

for(int i = 0; i < list.length; i++)

System.out. println (list [i]);

} catch(Exception e) {

e. printStackTrace (); }

}

}

class Filtru implements FilenameFilter {

String extensie;

Filtru (String extensie) {

this.extensie = extensie;

}

}

public boolean accept (File dir, String nume) {

return ( nume.endsWith("." + extensie) );

}

}

Page 100: Curs Practic de Java

100

5.6.1 Folosirea claselor anonime

In cazul in care nu avem nevoie de clasa care reprezinta filtrul pentru listarea fisierelor

dintr-un director decat o singura data, pentru a evita crearea unei noi clase de sine statatoare

care sa fie folosita pentru instantierea unui singur obiect, putem folosi clasa interna anonima,

aceasta situatie fiind un exemplu tipic de folosire a acestora.

/* Listarea fisierelor din directorul curent folosind o clasa anonima pentru filtru. */

import java.io.*;

class Listare {

public static void main(String[] args) {

try {

File director = new File(".");

String[] list;

if (args.length > 0) {

final String extensie = args[0];

list = director.list(new FilenameFilter() {

// Clasa interna anonima

public boolean accept (File dir , String nume) {

return ( nume.endsWith("." + extensie) );

}

});

} else

list = director.list();

for(int i = 0; i < list.length; i++)

System .out. println (list[i]);

} catch(Exception e) {

e. printStackTrace ();

}

Asadar, o modalitate uzuala de folosire a claselor anonime pentru instantierea unui obiect

care trebuie sa respecte o interfata este:

metoda(new Interfata() { // Implementarea metodelor interfetei });

Page 101: Curs Practic de Java

101

5.7 Compararea obiectelor

Am vazut in primul capitol ca o solutie facila si eficienta de sortare a unui vector este

folosirea metodei sort din clasa java.util.Arrays.

int v[]={3, 1, 4, 2};

java.util.Arrays.sort(v);

// Sorteaza vectorul v

// Acesta va deveni {1, 2, 3, 4}

In cazul in care elementele din vector sunt de tip primitiv, ca in exemplul de mai sus, nu

exista nici o problema in a determina ordinea fireasca a elementelor. Ce se intampla insa

atunci cand vectorul contine referinte la obiecte de un anumit tip ? Sa consideram urmatorul

exemplu, in care dorim sa sortam un vector format din instante ale clasei Persoana, definita

mai jos:

class Persoana {

int cod; String nume;

public Persoana(int cod, String nume) {

this.cod = cod; this.nume = nume;

}

public String toString() { return cod + " \t " + nume; }

}

Programul urmator ar trebui sa sorteze un vector de persoane:

class Sortare {

public static void main(String args[]) {

Persoana p[] = new Persoana[4];

p[0] = new Persoana(3, "Ionescu");

p[1] = new Persoana(1, "Vasilescu");

p[2] = new Persoana(2, "Georgescu");

p[3] = new Persoana(4, "Popescu");

java. util. Arrays . sort(p);

System.out.println("Persoanele ordonate dupa cod:");

for(int i= 0; i<p.length; i++)

System .out. println (p[i]);

}

}

La executia acestei aplicatii va fi obtinuta o exceptie, deoarece metoda sort nu stie care

este ordinea naturala a obiectelor de tip Persoana. Va trebui, intr-un fel sau altul, sa specificam

acest lucru.

Page 102: Curs Practic de Java

102

5.7.1 Interfata Comparable

Interfata Comparable impune o ordine totala asupra obiectelor unei clase ce

o implementeaza. Aceasta ordine se numeste ordinea naturala a clasei si este specificata prin

intermediul metodei compareTo. Definitia interfetei este:

public interface Comparable { int compareTo(Object o); }

Asadar, o clasa ale carei instante trebuie sa fie comparabil va implementa metoda

compareTo care trebuie sa returneze:

valoare strict negativa: daca obiectul curent (this) este mai mic deca obiectul primit ca

argument;

zero: daca obiectul curent este egal deca obiectul primit ca argument;

valoare strict pozitiva: daca obiectul curent este mai mare deca obiectul primit ca

argument.

Reamintim ca metoda equals, mostenita din Object de toate clasele, determina daca

doua obiecte sunt egale (au aceeasi valoare). Spunem ca ordinea naturala a unei clase C este

consitenta cu equals daca si numai daca (e1.compareTo((Object)e2) == 0) are aceeassi

valoare logica cu e1.equals((Object)e2, pentru orice e1, e2 instante ale lui C.

null nu este instanta a nici unei clase si e.compareTo(null) trebuie sa arunce o exceptie

de tip NullPointerException chiar daca e.equals(null) returneaza false.

Sa presupunem ca dorim ca ordinea naturala a persoanelor sa fie dupa codul lor intern.

class Persoana implements Comparable {

int cod;

String nume;

public Persoana(int cod, String nume) {

this.cod = cod;

this.nume = nume;

}

public String toString() { return cod + " \t " + nume; }

public boolean equals(Object o) {

if (!(o instanceof Persoana))

return false;

Persoana p = (Persoana) o;

return (cod == p.cod) && (nume.equals(p.nume));

}

public int compareTo(Object o) {

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

if (!(o instanceof Persoana)) throw new ClassCastException("Nu pot

compara!");

Persoana p = (Persoana) o;

Page 103: Curs Practic de Java

103

return p.cod ==this.cod;

}

}

Observati folosirea operatorului instanceof, care verifica daca un obiect este instanta a

unei anumite clase. Metoda compareTo va arunca o exceptie de tipul ClassCastException

daca se incearca compararea unui obiect de tip Persoana cu un obiect de alt tip. Metoda

equals va returna, pur si simplu, false.

5.7.2 Interfata Comparator

In cazul in care dorim sa sortam elementele unui vector ce contine referinte dupa alt

criteriu decat ordinea naturala a elemenetelor, avem nevoie de o alta solutie. Aceasta este

oferita tot de metoda sort din clasa java.util.Arrays, dar in varianta in care, pe langa vectorul ce

trebuie sortat, vom transmite un argument de tip Comparator care sa specifice modalitatea de

comparare a elementelor.

Interfata java.util.Comparator contine metoda compare, care impune o ordine totala

asupra elementelor unei colectii. Aceasta returneaza un intreg cu aceeasi semnificatie ca la

metoda compareTo a interfetei Comparator si are urmatoarea definitie: int compare(Object o1,

Object o2);

Sa presupunem ca dorim sa sortam persoanele ordonate dupa numele lor. Pentru

definirea comparatorului vom folosi o clasa anonima.

import java.util.*;

class Sortare {

public static void main(String args[]) {

Persoana p[] = new Persoana[4];

p[0] = new Persoana(3, "Ionescu");

p[1] = new Persoana(1, "Vasilescu");

p[2] = new Persoana(2, "Georgescu");

p[3] = new Persoana(4, "Popescu");

Arrays.sort(p, new Comparator() {

public int compare(Object o1, Object o2) {

Persoana p1 = (Persoana)o1;

Persoana p2 = (Persoana)o2;

return (p1.nume.compareTo(p2.nume));

});

}

}

Observati cum compararea a doua siruri de caractere se face tot cu metoda compareTo,

clasa String implemenand interfata Comparable.

Page 104: Curs Practic de Java

104

5.8 Adaptori

In cazul in care o interfata contine mai multe metode si, la un moment dat, avem nevoie

de un obiect care implementeaza interfata respectiv dar nu specifica cod decat pentru o

singura metoda, el trebui totusi sa implementeze toate metodele interfetei, chiar daca nu

specifica nici un cod.

interface X {

void metoda_1();

void metoda_2();

...

void metoda_n();

}

...

// Avem nevoie de un obiect de tip X

// ca argument al unei functii

functie(new X() {

public void metoda_1() {

// Singura metoda care ne intereseaza

...

}

// Trebuie sa apara si celelalte metode chiar daca nu au implementare efectiva

public void metoda_2() {}

public void metoda_3() {}

...

public void metoda_n() {}

});

Aceasta abordare poate fi neplacuta daca avem frecvent nevoie de obiecte ale unor clase

ce implementeaza interfata X. Solutia la aceasta problema este folosirea adaptorilor.

Definitie : Un adaptor este o clasa abstracta care implementeaza o anumita interfata fara a

specifica cod nici unei metode a interfetei.

public abstract class XAdapter implements X {

public void metoda_1() {}

public void metoda_2() {}

...

public void metoda_n() {}

}

In situatia cand avem nevoie de un obiect de tip X vom folosi clasa abstracta,

supradefinind doar metoda care ne intereseaza:

functie(new XAdapter() {

public void metoda_1() { // Singura metoda care ne intereseaza ...

} });

Page 105: Curs Practic de Java

105

6 Organizarea claselor

6.1 Pachete

Definitie : Un pachet este o colectie de clase si interfete inrudite din punctul de

vedere al functionalitatii lor. Sunt folosite pentru gasirea si utilizarea mai usoara a claselor,

pentru a evita conflictele de nume si pentru a controla accesul la anumite clase. In alte limbaje

de programare pachetele se mai numesc librarii sau bibilioteci.

6.1.1 Pachetele standard (J2SDK)

Platforma standard de lucru Java se bazeaza pe o serie de pachete cu ajutorul carora se

pot construi intr-o maniera simplificata aplicatiile. Exista deci un set de clase deja

implementate care modeleaza structuri de date, algoritmi sau diverse notiuni esentiale in

dezvoltarea unui program. Cele mai importante pachete si suportul oferit de lor sunt:

java.lang -clasele de baza ale limbajului Java

java.io -intrari/iesiri, lucrul cu fisiere

java.util -clase si interfete utile

java.applet -dezvoltarea de appleturi

java.awt -interfata grafica cu utilizatorul

java.awt.event -mecanismele de tratare e evenimentelor generate de utilizator

java.beans -scrierea de componente reutilizabile

java.net -programare de retea

java.sql -lucrul cu baze de date

java.rmi -executie la distanta Remote Message Interface

java.security -mecanisme de securitate: criptare, autentificare

java.math -operatii matematice cu numere mari

java.text -lucrul cu texte, date si numere independent de limba

java.lang.reflect -introspectie

javax.swing -interfata grafica cu utilizatorul, mult imbogatita fata de AWT.

...

6.1.2 Folosirea membrilor unui pachet

Conform specificatiilor de acces ale unei clase si ale mebrilor ei, doar clasele publice si

membrii declarati publici ai unei clase sunt accesibili in afara pachetului in care se gasesc.

Dupa cum am vazut in sectiunea ”Specificatori de acces pentru membrii unei clase”, accesul

Page 106: Curs Practic de Java

106

implicit in Java este la nivel de pachet.

Pentru a folosi o clasa publica dintr-un anumit pachet, sau pentru a apela

o metoda publica a unei clase publice a unui pachet, exista trei solutii:

specificarea numelui complet al clasei

importul clasei respective

importul intregului pachet in care se gaseste clasa.

Specificarea numelui complet al clasei se face prin prefixarea numelui scurt al clasei cu

numele pachetului din care face parte: numePachet.NumeClasa.

Aceasta metoda este recomandata doar pentru cazul in care folosirea acelei clase se

face o singura data sau foarte rar. De exemplu, ar fi extrem de neplacut sa scriem de fiecare

data cand vrem sa declaram un obiect grafic secvente de genul:

java.awt.Button b1 = new java.awt.Button("OK");

java.awt.Button b2 = new java.awt.Button("Cancel");

java.awt.TextField tf1 = new java.awt.TextField("Neplacut");

java.awt.TextField tf2 = new java.awt.TextField("Tot neplacut");

In aceste situatii, vom importa in aplicatia noastra clasa respectiva, sau intreg pachetul

din care face parte. Acest lucru se realizeaza prin instructiunea import, care trebuie sa apara

lainceputul fisierelor sursa,inainte de declararea vreunei clase sau interfete.

6.1.3 Importul unei clase sau interfete

Se face prin instructiunea import in care specificam numele complet al clasei sau

interfetei pe care dorim sa o folosim dintr-un anumit pacehet:

import numePachet.numeClasa;

//Pentru exemplul nostru:

import java.awt.Button;

import java.awt.TextField;

Din acest moment, vom putea folosi in clasele fisierului in care am plasat instructiunea de

import numele scurt al claselor Button si TextField:

Button b1 = new Button("OK");

Button b2 = new Button("Cancel");

TextField tf1 = new TextField("Placut");

TextField tf2 = new TextField("Foarte placut");

Aceasta abordare este eficienta si recomandata in cazul in care nu avem nevoie decat de

cateva clase din pachetul respectiv. Daca in exemplul nostru am avea nevoie si de clasele

Line, Point, Rectangle, Polygon, ar trebui sa avem cate o instructiune de import pentru fiecare

dintre ele:

Page 107: Curs Practic de Java

107

import java.awt.Button;

import java.awt.TextField; i

import java.awt.Rectangle;

import java.awt.Line;

import java.awt.Point;

import java.awt.Polygon;

In aceasta situatie ar fi mai simplu sa folosim importul la cerere din intregul pachet si nu al

fiecarei clase in parte.

6.1.4 Importul la cerere dintr-un pachet

Importul la cerere dintr-un anumit pachet se face printr-o instructiune import in care

specificam numele pachetului ale carui clase si interfete dorim sa le folosim, urmat de simbolul

*. Se numeste import la cerere deoareceincarcarea claselor se face dinamic, in momentul

apelarii lor.

import numePachet.*;

//Pentru exemplul nostru:

import java.awt.*;

Din acest moment, vom putea folosi in clasele fisierului in care am plasat instructiunea de

import numele scurt al tuturor claselor pachetului importat:

Button b = new Button("OK"); Point p = new Point(0, 0);

Atentie

* nu are semnificatia uzuala de la fisiere de wildcard (masca) si nu poate fi folosit decat ca

atare. O expresie de genul import java.awt.C*; va produce o eroare de compilare.

In cazul in care sunt importate doua sau mai multe pachete care contin clase (interfete) cu

acelasi nume, atunci referirea la ele trebuie facuta doar folosind numele complet, in caz contrar

fiind semnalata o ambiguitate de catre compilator.

import java.awt.*;

// Contine clasa List

import java.util.*;

// Contine interfata List

... List x; //Declaratie ambigua

java.awt.List a = new java.awt.List(); //corect

java.util.List b = new ArrayList(); //corect

Sunt considerate importate automat, pentru orice fisier sursa, urmatoarele pachete:

• pachetul java.lang import java.lang.*; // Poate sau nu sa apara // Mai bine nu...

pachetul curent

pachetul implicit (fara nume)

Page 108: Curs Practic de Java

108

6.1.5 Importul static

Aceasta facilitate, introdusa incepand cu versiunea 1.5, permite referirea constantelor

statice ale unei clase fara a mai specifica numele complet al acesteia si este implementata prin

adaugarea cuvantului cheie static dupa cel de import:

import static numePachet.NumeClasa.*;

Astfel, in loc sa ne referim la constantele clasei cu expresii de tipul

NumeClasa.CONSTANTA, putem folosi doar numele constantei.

// Inainte de versiuna 1.5

import java.awt.BorderLayout.*;

...

fereastra.add(new Button(), BorderLayout.CENTER);

// Incepand cu versiunea 1.5

import java.awt.BorderLayout.*;

import static java.awt.BorderLayout.*;

... fereastra.add(new Button(), CENTER);

Atentie !!! Importul static nu importa decat constantele statice ale unei clase, nu si clasa in sine.

6.1.6 Crearea unui pachet

Toate clasele si interfetele Java apartin la diverse pachete, grupate dupa functionalitatea

lor. Dupa cum am vazut clasele de baza se gasesc in pachetul java.lang, clasele pentru

intrari/iesiri sunt in java.io, clasele pentru interfata grafica in java.awt, etc.

Crearea unui pachet se realizeaza prin scriere la inceputul fisierelor sursa ce contin

clasele si interfetele pe care dorim sa le grupam intr-un pachet a instructiunii: package

numePachet;

Sa consideram un exemplu: presupunem ca avem doua fisiere sursa Graf.java si Arbore.java.

//Fisierul Graf.java

package grafuri;

class Graf {...}

class GrafPerfect extends Graf {...}

//Fisierul Arbore.java

package grafuri;

class Arbore {...}

class ArboreBinar extends Arbore {...}

Clasele Graf, GrafPerfect, Arbore, ArboreBinar vor face parte din acelasi pachet grafuri.

Instructiunea package actioneaza asupraintregului fisier sursa lainceputul caruia apare. Cu

alte cuvinte nu putem specifica faptul ca anumite clase dintr-un fisier sursa apartin unui pachet,

iar altele altui pachet.

Page 109: Curs Practic de Java

109

Daca nu este specificat un anumit pachet, clasele unui fisier sursa vor face parte din

pachetul implicit (care nu are nici un nume). In general, pachetul implicit este format din toate

clasele si intefetele directorului curent de lucru. Este recomandat insa ca toate clasele si

intefetele sa fie plasate in pachete, pachetul implicit fiind folosit doar pentru aplicatii mici sau

prototipuri.

6.1.7 Denumirea unui pachet

Exist a posibilitatea ca doi programatori care lucreaza la un proiect comun sa

foloseasca acelasi nume pentru unele din clasele lor. De asemenea, se poate ca una din

clasele unei aplicatii sa aiba acelasi nume cu o clasa a mediului Java. Acest lucru este posibil

atat timp cat clasele cu acelasi nume se gasescin pachete diferite, ele fiind diferentiate prin

prefixarea lor cu numele pachetelor.

Ce se intampla insa cand doi programatori care lucreaza la un proiect comun folosesc

clase cu acelasi nume, ce se gasesc in pachete cu acelasi nume ?

Pentru a evita acest lucru, companiile folosesc inversul domeniului lor Internet in

denumirea pachetelor implementate in cadrul companiei, cum ar fi ro.companie.numePachet.

In cadrul aceleiasi companii, conflictele de nume vor fi rezolvate prin diverse conventii de uz

intern, cum ar fi folosirea numelui de cont al programatorilor in denumirea pachetelor create de

acestia. De exemplu, programatorul cu numele Ion al companiei XSoft, avand contul

[email protected], isi va prefixa pachetele cu ro.xsoft.ion, pentru a permite identificareain mod unic

a claselor sale, indiferent de contextulin care acestea vor fi integrate.

6.2 Organizarea fisierelor

6.2.1 Organizarea fisierelor sursa

Orice aplicatie nebanala trebuie sa fie construita folosind o organizare ierarhica a

componentelor sale. Este recomandat ca strategia de organizare a fisierelor sursa sa respecte

urmatoarele conventii:

Codul sursa al claselor si interfetelor sa se gaseasca in fisiere ale caror nume sa fie

chiar numele lor scurt si care sa aiba extensia .java.

Atentie!!! Este obligatoriu ca o clasa/interfata publica sa se gaseasca intr-un fisier avand

numele clasei(interfetei) si extenisa .java, sau compilatorul va furniza o eroare. Din acest

motiv, intr-un fisier sursa nu pot exista doua clase sau interfete publice. Pentru clasele care nu

sunt publice acest lucru nu este obligatoriu, ci doar recomandat. Intr-un fisier sursa pot exista

oricate clase sau interfete care nu sunt publice.

Fisierele sursa trebuie sa se gaseasca in directoare care sa reflecte numele pachetelor

in care se gasesc clasele si interfetele din acele fisiere. Cu alte cuvinte, un director va

Page 110: Curs Practic de Java

110

contine surse pentru clase si interfete din acelasi pachet iar numele directorului va fi

chiar numele pachetului. Daca numele pachetelor sunt formate din mai multe unitati

lexicale separate prin punct, atunci acestea trebuie de asemenea sa corespunda unor

directoare ce vor descrie calea spre fisierele sursa ale caror clase si interfete fac parte

din pachetele respective.

Vom clarifica modalitatea de organizare a fisierelor sursa ale unei aplicatii printr-un

exemplu concret. Sa presupunem ca dorim crearea unor componente care sa reprezinte

diverse notiuni matematice din domenii diferite, cum ar fi geometrie, algebra, analiza, etc.

Pentru a simplifica lucrurile, sa presupunem ca dorim sa cream clase care sa descrie

urmatoarele notiuni: poligon, cerc, poliedru, sfera, grup, functie. O prima varianta ar fi sa

construim cate o clasa pentru fiecare si sa le plasam in acelasi director impreuna cu un

program care sa le foloseasca, insa, avand in vedere posibila extindere a aplicatiei cu noi

reprezentari de notiuni matematice, aceasta abordare ar fi ineficienta.

O abordare eleganta ar fi aceea in care clasele care descriu notiuni din acelasi domeniu

sa se gaseasca in pachete separate si directoare separate. Ierarhia fisierelor sursa ar fi:

/matematica /surse /geometrie

/plan Poligon.java Cerc.java

/spatiu Poliedru.java Sfera.java

/algebra Grup.java /analiza Functie.java

Matematica.java

Clasele descrise in fisierele de mai sus trebuie declarate in pachete denumite

corespunzator cu numele directoarelor in care se gasesc:

// Poligon.java package geometrie.plan; public class Poligon { . . . }

// Cerc.java package geometrie.plan; public class Cerc { . . . }

// Poliedru.java package geometrie.spatiu; public class Poliedru { . . . }

// Sfera.java package geometrie.spatiu; public class Sfera { . . . }

// Grup.java package algebra; public class Grup { . . . }

// Functie.java package analiza; public class Functie { . . . }

Matematica.java este clasa principala a aplicatiei. Dupa cum se observa, numele lung al

unei clase trebuie sa descrie calea spre acea clasa in cadrul fisierelor sursa, relativ la

directorul in care se gaseste aplicatia.

6.2.2 Organizarea unitatilor de compilare (.class)

In urma compilarii fisierelor sursa vor fi generate unitati de compilare pentru fiecare clasa

si interfata din fisierele sursa. Dupa cum stim acestea au extensia .class si numele scurt al

clasei sau interfetei respective.

Spre deosebire de organizarea surselor, un fisier .class trebuie sa se gaseasca intr-o

ierarhie de directoare care sa reflecte numele pachetului din care face parte clasa respectiva.

Implicit, in urma compilarii fisierele sursa si unitatile de compilare se gasesc in acelasi director,

insa ele pot fi apoi organizate separat. Este recomandatinsa ca aceasta separare sa fie facuta

Page 111: Curs Practic de Java

111

automat la compilare.

Revenind la exemplul de mai sus, vom avea urmatoarea organizare:

/matematica /clase /geometrie

/plan Poligon.class Cerc.class

/spatiu

Poliedru.class Sfera.class /algebra Grup.class /analiza Functie.class

Matematica.class

Crearea acestei structuri ierarhice este facuta automat de catre compilator. In directorul

aplicatiei (matematica) cream subdirectorul clase si dam comanda:

javac -sourcepath surse surse/Matematica.java -d clase

sau

javac -classpath surse surse/Matematica.java -d clase

Optiunea -d specifica directorul radacina al ierarhiei de clase. In lipsa lui, fiecare

unitate de compilare va fi plasata in acelasi director cu fisierul sau sursa. Deoarece compilam

clasa principala a plicatiei, vor fi compilate in cascada toate clasele referite de aceasta, dar

numai acestea. In cazul in care dorim sa compilam explicit toate fisierele java dintr-un anumit

director, de exemplu surse/geometrie/plan, putem folosi expresia:

javac surse/geometrie/plan/*.java -d clase

6.2.3 Necesitatea organizarii fisierelor

Organizarea fisierelor sursa este necesara deoarece in momentul cand compilatorul

intalneste un nume de clasa el trebuie sa poata identifica acea clasa, ceea ce inseamna ca

trebuie sa gaseasca fiserul sursa care o contine.

Similar, unitatile de compilare sunt organizate astfel pentru a da posibilitatea

interpretorului sa gaseasca si sa incarce in memorie o anumita clasa in timpul executiei

programului.

Insa aceasta organizare nu este suficienta deoarece specifica numai partea finala din

calea catre fisierele .java si .class, de exemplu

/matematica/clase/geometrie/plan/Poligon.class. Pentru aceasta, atat la compilare cat si la

interpretare trebuie specificata lista de directoare radacina in care se gasesc fisierele

aplicatiei. Aceasta lista se numeste cale de cautare (classpath).

Definitie: O cale de cautare este o lista de directoare sau arhive in care vor fi cautate fisierele

necesare unei aplicatii. Fiecare director din calea de cautare este directorul imediat superior

structurii de directoare corespunzatoare numelor pachetelorin care se gasesc clasele din

directorul respectiv, astfelincat compilatorul si interpretorul sa poata construi calea completa

spre clasele aplicatiei. Implicit, calea de cautare este formata doar din directorul curent.

Page 112: Curs Practic de Java

112

Sa consideram clasa principala a aplicatiei Matematica.java:

import geometrie.plan.*;

import algebra.Grup;

import analiza.Functie;

public class Matematica {

public static void main(String args[]) {

Poligon a = new Poligon();

geometrie.spatiu.Sfera = new geometrie.spatiu.Sfera();

//...

}

}

Identificarea unei clase referite in program se face in felul urmator:

La directoarele aflate in calea de cautare se adauga subdirectoarele specificate in

import sau in numele lung al clasei

In directoarele formate este cautat un fisier cu numele clasei. In cazul in care nu este

gasit nici unul sau sunt gasite mai multe va fi semnalata eroare.

6.2.4 Setarea caii de cautare (CLASSPATH)

Setarea caii de cautare se poate face in doua modalitati:

Setarea variabilei de mediu CLASSPATH -folosind aceasta varianta toate aplicatiile

Java de pe masina respectiva vor cauta clasele necesare in directoarele specificate in

variabila CLASSPATH.

UNIX: SET CLASSPATH = cale1:cale2:... DOS shell (Windows 95/NT/...): SET

CLASSPATH = cale1;cale2;...

Folosirea optiunii -classpath la compilarea si interpretarea programelor -directoarele

specificate astfel vor fi valabile doar pentru comanda curenta:

javac -classpath <cale de cautare> <surse java>

java -classpath <cale de cautare> <clasa principala>

Lansarea in executie a aplicatiei noastre, din directorul matematica, se va face astfel:

java -classpath clase Matematica

In concluzie, o organizare eficienta a fisierelor aplicatiei ar arata astfel:

/matematica

/surse

/clase

compile.bat

(javac -sourcepath surse surse/Matematica.java -d clase)

run.bat

(java -classpath clase Matematica)

Page 113: Curs Practic de Java

113

6.3 Arhive JAR

Fisierele JAR (Java Archive) sunt arhive in format ZIP folosite pentru impachetarea

aplicatiilor Java. Ele pot fi folosite si pentru comprimari obisnuite, diferenta fata de o arhiva ZIP

obisnuita fiind doar existenta unui director denumit META-INF, ce contine diverse informatii

auxiliare legate de aplicatia sau clasele arhivate.

Un fisier JAR poate fi creat folosind utilitarul jar aflat in distributia J2SDK, sau metode ale

claselor suport din pachetul java.util.jar. Dintre beneficiile oferite de arhivele JAR amintim:

portabilitate -este un format de arhivare independent de platforma;

compresare -dimensiunea unei aplicatii in forma sa finala este redusa;

minimizarea timpului de incarcare a unui applet: daca appletul (fisiere class, resurse,

etc)estecompresatintr-oarhivaJAR,elpoatefiincarcat intr-o singura tranzactie HTTP,

fara a fi deci nevoie de a deschide cate o conexiune noua pentru fiecare fisier;

securitate -arhivele JAR pot fi ”semnate” electronic

mecanismul pentru lucrul cu fisiere JAR este parte integrata a platformei Java.

6.3.1 Folosirea utilitarului jar

Arhivatorul jar se gaseste in subdirectorul bin al directorului in care este instalat kitul

J2SDK. Mai jos sunt prezentate pe scurt operatiile uzuale:

Crearea unei arhive

jar cf arhiva.jar fisier(e)-intrare

Vizualizare continutului

jar tf nume-arhiva

Extragerea continutului

jar xf arhiva.jar

Extragerea doar a unor fisiere

jar xf arhiva.jar fisier(e)-arhivate

Executarea unei aplicatii

java -jar arhiva.jar

Deschiderea unui applet arhivat

<applet code=A.class archive="arhiva.jar" ...>

Exemple:

Arhivarea a doua fisiere class:

jar cf classes.jar A.class B.class

arhivarea tuturor fisierelor din directorul curent:

jar cvf allfiles.jar *

Page 114: Curs Practic de Java

114

6.3.2 Executarea aplicatiilor arhivate

Pentru a rula o aplicatie impachetata intr-o arhiva JAR trebuie sa facem cunoscuta

interpretorului numele clasei principale a aplicatiei. Sa consideram urmatorul exemplu, in care

dorim sa arhivam clasele aplicatiei descrise mai sus, in care clasa principala era

Matematica.java.

Din directorul clase vom lansa comanda:

jar cvf mate.jar geometrie analiza algebra Matematica.class

In urma acestei comenzi vom obtine arhiva mate.jar. Daca vom incerca sa lansam in

executie aceasta arhiva prin comanda java -jar mate.jar vom obtine urmatoarea eroare: ”Failed

to load Main-Class manifest from mate.jar”. Aceasta inseamna ca in fiserul Manifest.mf ce se

gaseste in directorul META-INF trebuie sa inregistram clasa principala a aplicatiei.

Acest lucru il vom face in doi pasi:

se creeaza un fisier cu un nume oarecare, de exemplu manifest.txt, in care vom scrie:

Main-Class: Matematica

adaugam aceasta informatie la fisierul manifest al arhivei mate.jar:

jar uvfm mate.jar manifest.txt

Ambele operatii puteau fi executate intr-un singur pas:

jar cvfm mate.jar manifest.txt geometrie analiza algebra Matematica.class

Pe sistemele Win32, platforma Java 2 va asocia extensiile .jar cu interpretorul Java,

ceea ce inseamna ca facand dublu-click pe o arhiva JAR va fi lansata in executie aplicatia

impachetata in acea arhiva (daca exista o clasa principala).

Page 115: Curs Practic de Java

115

7 Colectii

7.1 Introducere

O colectie este un obiect care grupeaza mai multe elemente intr-o singura unitate. Prin

intermediul colectiilor vom avea acces la diferite tipuri de date cum ar fi vectori, liste inlantuite,

stive, multimi matematice, tabele de dispersie, etc. Colectiile sunt folosite atat pentru

memorarea si manipularea datelor, cat si pentru transmiterea unor informatii de la o metoda la

alta.

Tipul de date al elementelor dintr-o colectie este Object, ceea ceinseamna ca multimile

reprezentate sunt eterogene, putand include obiecte de orice tip.

Incepand cu versiunea 1.2, in Java colectiile sunt tratate intr-o maniera unitara, fiind

organizateintr-o arhitectura foarte eficienta si flexibila ce cuprinde:

Interfete: tipuri abstracte de date ce descriu colectiile si permit utilizarea lor independent

de detaliile implementarilor.

Implementari: implementari concrete ale interfetelor ce descriu colectii. Aceste clase

reprezinta tipuri de date reutilizabile.

Algoritmi: metode care efectueaza diverse operatii utile cum ar fi cautarea sau sortarea,

definite pentru obiecte ce implementeaza interfetele ce descriu colectii. Acesti algoritmi

se numesc si polimorfici deoarece pot fi folositi pe implementari diferite ale unei colectii,

reprezentand elementul de functionalitate reutilizabila.

Utilizarea colectiilor ofera avantaje evidente in procesul de dezvoltare a unei aplicatii. Cele

mai importante sunt:

Reducerea efortului de programare: prin punerea la dispozitia programatorului a unui

set de tipuri de date si algoritmi ce modeleaza structuri si operatii des folosite in aplicatii.

Cresterea vitezei si calitatii programului: implementarile efective ale colectiilor sunt de

inalta performanta si folosesc algoritmi cu timp de lucru optim.

Astfel, la scrierea unei aplicatii putem sa ne concentram eforturile asupra problemei in sine

si nu asupra modului de reprezentare si manipulare a informatiilor.

7.2 Interfete ce descriu colectii

Interfetele reprezinta nucleul mecanismului de lucru cu colectii, scopul lor fiind de a

permite utilizarea structurilor de date independent de modul lor de implementare.

Collection modeleaza o colectie la nivelul cel mai general, descriind un grup de obiecte

numite si elementele sale. Unele implementari ale acestei interfete permit existenta

Page 116: Curs Practic de Java

116

elementelor duplicate, alte implementari nu. Unele au elementele ordonate, altele nu.

Platforma Java nu ofera nici o implementare directa a acestei interfete, ci exista doar

implementari ale unor subinterfete mai concrete, cum ar fi Set sau List.

public interface Collection {

// Metode cu caracter general

int size();

boolean isEmpty();

void clear();

Iterator iterator();

// Operatii la nivel de element

boolean contains(Object element);

boolean add(Object element);

boolean remove(Object element);

// Operatii la nivel de multime

boolean containsAll(Collection c);

boolean addAll(Collection c);

boolean removeAll(Collection c);

boolean retainAll(Collection c);

// Metode de conversie in vector

Object[] toArray();

Object[] toArray(Object a[]);

}

Set modeleaza notiunea de multime in sens matematic. O multime nu poate avea

elemente duplicate, mai bine zis nu poate contine doua obiecte o1 si o2 cu proprietatea

o1.equals(o2). Mosteneste metodele din Collection, fara a avea alte metode specifice. Doua

dintre clasele standard care ofera implementari concrete ale acestei interfete sunt HashSet si

TreeSet.

SortedSet este asemanatoare cu interfata Set, diferenta principala constand in faptul ca

elementele dintr-o astfel de colectie sunt ordonate ascendent. Pune la dispozitie operatii care

beneficiaza de avantajul ordonarii elementelor. Ordonarea elementelor se face conform ordinii

lor naturale, sau conform cu ordinea data de un comparator specificat la crearea colectiei si

este mentinuta automat la orice operatie efectuata asupra multimii. Singura condittie este ca,

pentru orice doua obiecte o1,o2 ale colectiei, apelul o1.compareT o(o2) (sau

comparator.compare(o1,o2), daca este folosit un comparator) trebuie sa fie valid si sa nu

provoace exceptii.

Fiind subclasa a interfetei Set, mosteneste metodele acesteia, oferind metode

suplimentare ce tin cont de faptul ca multimea este sortata:

Page 117: Curs Practic de Java

117

public interface SortedSet extends Set {

// Subliste

SortedSet subSet(Object fromElement, Object toElement);

SortedSet headSet(Object toElement);

SortedSet tailSet(Object fromElement);

// Capete

Object first();

Object last();

Comparator comparator();

}

Clasa care implementeaza aceasta interfata este TreeSet.

List descrie liste (secvente) de elemente indexate. Listele pot contine duplicate si permit

un control precis asupra pozitiei unui element prin intermediul indexului acelui element. In plus,

fatade metodele definite de interfata Collection, avem metode pentru acces pozitional, cautare

si iterare avansata. Definitia interfetei este:

public interface List extends Collection {

// Acces pozitional

Object get(int index);

Object set(int index, Object element);

void add(int index, Object element);

Object remove(int index);

abstract boolean addAll(int index, Collection c);

// Cautare

int indexOf(Object o);

int lastIndexOf(Object o);

// Iterare

ListIterator listIterator();

ListIterator listIterator(int index);

// Extragere sublista

List subList(int from, int to);

}

Clase standard care implementeaza aceasta interfata sunt: ArrayList, LinkedList, Vector.

Map descrie structuri de date ce asociaza fiecarui element o cheie unica, dupa care poate fi

regasit. Obiectele de acest tip nu pot contine chei duplicate si fiecare cheie este asociata la un

singur element. Ierarhia interfetelor derivate din Map este independenta de ierarhia derivata

din Collection.

Page 118: Curs Practic de Java

118

Definitia interfetei este prezentata mai jos:

public interface Map {

// Metode cu caracter general

int size();

boolean isEmpty();

void clear();

// Operatii la nivel de element

Object put(Object key, Object value);

Object get(Object key);

Object remove(Object key);

boolean containsKey(Object key);

boolean containsValue(Object value);

// Operatii la nivel de multime

void putAll(Map t);

// Vizualizari ale colectiei

public Set keySet();

public Collection values();

public Set entrySet();

// Interfata pentru manipularea unei inregistrari

public interface Entry {

Object getKey();

Object getValue();

Object setValue(Object value);

}

}

Clase care implementeaza interfata Map sunt HashMap, TreeMap si Hashtable.

SortedMap este asemanatoare cu interfata Map, la care se adauga faptul ca multimea cheilor

dintr-o astfel de colectie este mentinuta ordonata ascendent conform ordinii naturale, sau

conform cu ordinea data de un comparator specificat la crearea colectiei. Este subclasa a

interfetei Map, oferind metode suplimentare pentru: extragere de subtabele, aflarea

primei/ultimei chei, aflarea comparatorului folosit pentru ordonare. Definitia interfetei este data

mai jos:

public interface SortedMap extends Map {

// Extragerea de subtabele

SortedMap subMap(Object fromKey, Object toKey);

SortedMap headMap(Object toKey);

SortedMap tailMap(Object fromKey);

// Capete

Object first();

Object last();

// Comparatorul folosit pentru ordonare

Comparator comparator();

Page 119: Curs Practic de Java

119

}

Clasa care implementeaza aceasta interfata este TreeMap.

7.3 Implementari ale colectiilor

Inainte de versiunea 1.2, exista un set de clase pentru lucrul cu colectii, insa acestea nu

erau organizate pe ierarhia de interfete prezentata in sectiunea anterioara. Aceste clase sunt

in continuare disponibile si multe dintre ele au fost adaptate in asa fel incat sa se integreze in

noua abordare. Pe langa acestea au fost create noi clase corespunzatoare interfetelor definite,

chiar daca functionalitatea lor era aproape identica cu cea a unei clase anterioare.

implementarea interfetelor este explicit realizata la nivelul superclaselor abstracte,

acestea oferind de altfel si implementari concrete pentru multe din metodele definite de

interfete.

In general, clasele care descriu colectii au unele trasaturi comune, cum ar fi:

permit elementul null;

sunt serializabile;

au definita metoda clone;

au definita metoda toString, care returneaza o reprezentare ca sir de caractere a

colectiei respective;

permit crearea de iteratori pentru parcurgere;

au atat constructor fara argumente cat si un constructor care accepta ca argument o

alta colecti;

exceptand clasele din arhitectura veche, nu sunt sincronizate (vezi ”Fire de executie).

7.4 Folosirea eficienta a colectiilor

Dupa cum am vazut, fiecare interfata ce descrie o colectie are mai multe implementari.

De exemplu, interfata List este implementata de clasele ArrayList si LinkedList, prima fiind in

general mult mai folosita. De ce exista atunci si clasa LinkedList ? Raspunsul consta in faptul

ca folosind reprezentari diferite ale multimii gestionate putem obtine performante mai bune in

functie de situatie, prin realizarea unor compromisuri intre spatiul necesar pentru memorarea

datelor, rapiditatea regasirii acestora si timpul necesar actualizarii colectiei in cazul unor

modificari.

Sa consideram un exemplu ce creeaza o lista folosind ArrayList, respectiv LinkedList si

executa diverse operatii pe ea, cronometrand timpul necesar realizarii acestora:

Page 120: Curs Practic de Java

120

import java.util.*;

public class TestEficienta {

final static int N = 100000;

public static void testAdd(List lst) {

long t1 = System.currentTimeMillis();

for(int i= 0; i < N; i++)

lst.add(new Integer(i));

long t2 = System.currentTimeMillis();

System.out.println("Add: " + (t2 -t1));

}

public static void testGet(List lst) {

long t1 = System.currentTimeMillis();

for(int i=0; i < N; i++)

lst.get(i);

long t2 = System.currentTimeMillis();

System.out.println("Get: " + (t2 -t1));

}

public static void testRemove(List lst) {

long t1 = System.currentTimeMillis();

for(int i=0; i < N; i++)

lst. remove (0);

long t2 = System.currentTimeMillis();

System.out.println("Remove: " + (t2 -t1));

}

public static void main(String args[]) {

System .out. println (" ArrayList ");

List lst1 = new ArrayList();

testAdd ( lst1);

testGet ( lst1);

testRemove ( lst1);

System .out. println (" LinkedList ");

List lst2 = new LinkedList();

testAdd ( lst2);

testGet ( lst2);

testRemove ( lst2);

}

}

adaugarea elementelor este rapida pentru ambele tipuri de liste. ArrayList ofera acces in

timp constant la elementele sale si din acest motiv folosirea lui ”get” este rapida, in timp ce

pentru LinkedList este extrem de lenta, deoarece intr-o lista inlantuita accesul la un element se

face prin parcurgerea secventiala a listei pana la elementul respectiv. La operatiunea de

Page 121: Curs Practic de Java

121

eliminare, folosirea lui ArrayList este lenta deoarece elementele ramase sufera un proces de

reindexare (shift la stanga), in timp ce pentru LinkedList este rapida si se face prin simpla

schimbare a unei legaturi. Deci, ArrayList se comporta bine pentru cazuri in care avem nevoie

de regasirea unor elemente la pozitii diferite in lista, iar LinkedList functioneaza eficient atunci

cand facem multe operatii de modificare (stergeri, inserari).

Concluzia nu este ca una din aceste clase este mai ”buna” decat cealalta, ci ca exista

diferente substantialein reprezentarea si comportamentul diferitelor implementari si ca

alegerea unei anumite clase pentru reprezentarea unei multimi de elemente trebuie sa se faca

in functie de natura problemei ce trebuie rezolvata.

7.5 Algoritmi polimorfici

Algoritmii polimorfici descrisi in aceasta sectiune sunt metode definite in clasa Collections

care permit efectuarea unor operatii utile cum ar fi cautarea, sortarea, etc. Caracterisiticile

principale ale acestor algoritmi sunt:

sunt metode de clasa (statice);

au un singur argument de tip colectie;

apelul lor general va fi de forma: Collections.algoritm(colectie, [argumente]);

majoritatea opereaza pe liste dar si pe colectii arbitrare.

Metodele mai des folosite din clasa Collections sunt:

sort -sorteaza ascendent o lista referitor la ordinea sa naturala sau la ordinea data de un

comparator;

shuffle -amesteca elementele unei liste -opusul lui sort;

binarySearch -efectueaza cautarea eficienta (binara) a unui element intr-o lista

ordonata;

reverse -inverseaza ordinea elementelor dintr-o lista;

fill -populeaza o lista cu un anumit element repetat de un numar de ori;

copy -copie elementele unei liste in alta;

min -returneaza minimul dintr-o colectie;

max -returneaza maximul dintr-o colectie;

swap -interschimba elementele de la doua pozitii specificate ale unei liste;

enumeration -returneaza o enumerare a elementelor dintr-o colectie;

unmodifiableTipColectie -returneaza o instanta care nu poate fi modificata a colectiei

respective;

synchronizedTipColectie -returneaza o instanta sincronizata a unei colectii (vezi ”Fire

de executie”).

Page 122: Curs Practic de Java

122

7.6 Tipuri generice

Tipurile generice, introduse in versiunea 1.5 a limbajului Java, simplifica lucrul cu colectii,

permitand tipizarea elementelor acestora. Definirea unui tip generic se realizeaza prin

specificareaintre paranteze unghiulare a unui tip de date Java, efectul fiind impunerea tipului

respectiv pentru toate elementele colectiei: <TipDate>. Sa consideram un exemplu de utilizare

a colectiilor inainte si dupa introducerea tipurilor generice:

// Inainte de 1.5

ArrayList list = new ArrayList();

list.add(new Integer(123));

int val = ((Integer)list.get(0)).intValue();

In exemplul de mai sus, lista definita poate contine obiecte de orice tip, desi am dori ca

elementele sa fie doar numere intregi. Mai mult, trebuie sa facem cast explicit de la tipul Object

la Integer atunci cand preluam valoarea unui element.

Folosind tipuri generice, putem rescrie secventa astfel:

// Dupa 1.5, folosind tipuri generice

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

list.add(new Integer(123));

int val = list.get(0).intValue();

Daca utilizam si mecanismul de autoboxing, obtinem o varianta mult simplificata a

secventei initiale:

// Dupa 1.5, folosind si autoboxing

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

list.add(123);

int val = list.get(0);

In cazul folosirii tipurilor generice, incercarea de a utiliza in cadrul unei colectii a unui

element necorespunzator ca tip va produce o eroare la compilare, spre deosebire de varianta

anterioara ce permitea doara aruncarea unor exceptie de tipul ClassCastException in cazul

folosirii incorecte a tipurilor.

Page 123: Curs Practic de Java

123

7.7 Iteratori si enumerari

Enumerarile si iteratorii descriu modalitati pentru parcurgerea secventiala a unei colectii,

indiferent daca aceasta este indexata sau nu. Ei sunt descrisi de obiecte ce implementeaza

interfetele Enumeration, respectiv Iterator sau ListIterator. Toate clasele care implementeaza

colectii au metode ce returneaza o enumerare sau un iterator pentru parcurgerea elementelor

lor. Deoarece functionalitatea interfetei Enumeration se regaseste in Iterator, aceasta din urma

este preferata in noile implementari ale colectiilor.

Metodele uzuale ale acestor interfete sunt prezentate mai jos, impreuna cu modalitatea

lor de folosire, semnificatiile lor fiind evidente:

• Enumeration: hasMoreElements, nextElement

// Parcurgerea elementelor unui vector v

Enumeration e = v.elements;

while (e.hasMoreElements()) {

System.out.println(e.nextElement());

}

// sau, varianta mai concisa

for (Enumeration e = v.elements(); e.hasMoreElements();) {

System.out.println(e.nextElement());

}

• Iterator: hasNext, next, remove

// Parcurgerea elementelor unui vector

// si eliminarea elementelor nule

for (Iterator it = v.iterator(); it.hasNext();) {

Object obj = it.next();

if (obj == null)

it.remove();

}

• ListIterator: hasNext, hasPrevious, next, previous, remove, add, set

// Parcurgerea elementelor unui vector

// si inlocuirea elementelor nule cu 0

for (ListIterator it = v.listIterator(); it.hasNext();) {

Object obj = it.next();

if (obj == null)

it.set(new Integer(0));

}

Iteratorii simpli permit eliminarea elementului curent din colectia pe care o parcurg, cei de

tip ListIterator permit si inserarea unui element la pozitia curenta, respectiv modificarea

elementului curent, precum si iterarea in ambele sensuri. Iteratorii sunt preferati enumerarilor

datorita posibilitatii lor de a actiona asupra colectiei pe care o parcurg prin metode de tip

remove, add, set dar si prin faptul ca denumirile metodelor sunt mai concise.

Page 124: Curs Practic de Java

124

Atentie : Deoarece colectiile sunt construite peste tipul de date Object, metodele de tip next

sau prev ale iteratorilor vor returna tipul Object, fiind responsabilitatea noastra de a face

conversie (cast) la alte tipuri de date, daca este cazul.

In exemplul de mai jos punem intr-un vector numerele de la 1 la 10, le amestecam, dupa

care le parcurgem element cu element folosind un iterator, inlocuind numerele pare cu 0.

import java.util.*;

class TestIterator {

public static void main(String args[]) {

ArrayList a = new ArrayList();

// Adaugam numerele de la 1 la 10

for(int i=1; i< =10; i++)

a.add(new Integer(i));

// Amestecam elementele colectiei

Collections . shuffle (a);

System.out.println("Vectorul amestecat: " + a);

// Parcurgem vectorul

for(ListIterator it= a.listIterator(); it.hasNext(); ) {

Integer x = (Integer) it.next();

// Daca elementul curent este par , il facem 0

if (x.intValue() % 2 == 0)

it.set(new Integer(0));

}

System.out.print("Rezultat: " + a);

}

}

Incepand cu versiunea 1.5 a limbajului Java, exista o varianta simplificata de utilizare a

iteratorilor. Astfel, o secventa de genul:

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

for (Iterator i = list.iterator(); i.hasNext();) {

Integer val=(Integer)i.next();

// Proceseaza val ...

}

poate fi rescrisa astfel:

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

for (Integer val : list) {

// Proceseaza val ...

}

Page 125: Curs Practic de Java

125

8 Serializarea obiectelor

8.1 Folosirea serializarii

Definitie: Serializarea este o metoda ce permite transformarea unui obiect intr-o secventa de

octeti sau caractere din care sa poata fi refacut ulterior obiectul original.

Cu alte cuvinte, serializarea permite salvarea intr-o maniera unitara a tuturor informatiilor

unui obiect pe un mediu de stocare extern programului. Procesul invers, de citire a unui obiect

serializat pentru a-i reface starea originala, se numeste deserializare. Intr-un cadru mai larg,

prin serializare se intelege procesul de scriere/citire a obiectelor.

Tipurile primitive pot fi de asemenea serializate.

Utilitatea serializarii consta in urmatoarele aspecte:

Asigura un mecanism simplu de utilizat pentru salvarea si restaurarea a datelor.

Permite persistenta obiectelor, ceea ce inseamna ca durata de viata a unui obiect nu

este determinata de executia unui program in care acesta este definit -obiectul poate

exista si intre apelurile programelor care il folosesc. Acest lucru se realizeaza prin

serializarea obiectului si scrierea lui pe disc inainte de terminarea unui program, apoi, la

relansarea programului, obiectul va fi citit de pe disc si starea lui refacuta. Acest tip de

persistenta a obiectelor se numeste persistenta usoara, intrucat ea trebuie efectuata

explicit de catre programator si nu este realizata automat de catre sistem.

Compensarea diferentelor intre sisteme de operare -transmiterea unor informatii intre

platforme de lucru diferite se realizeaza unitar, independent de formatul de

reprezentare a datelor, ordinea octetilor sau alte detalii specifice sistemelor repective.

Transmiterea datelor in retea -Aplicatiile ce ruleaza in retea pot comunicaintre ele

folosind fluxuri pe care sunt trimise, respectiv receptionate obiecte serializate.

RMI (Remote Method Invocation) -este o modalitate prin care metodele unor obiecte de

pe o alta masina pot fi apelate ca si cum acestea ar exista local pe masina pe care

ruleaza aplicatia. Atunci cand este trimis un mesaj catre un obiect ”remote” (de pe alta

masina), serializarea este utilizata pentru transportul argumentelor prin retea si pentru

returnarea valorilor.

Java Beans -sunt componente reutilizabile, de sine statatoare ce pot fi utilizate in medii

vizuale de dezvoltare a aplicatiilor. Orice componenta Bean are o stare definita de

valorile implicite ale proprietatilor sale, stare care este specificata in etapa de design a

aplicatiei. Mediile vizuale folosesc mecanismul serializarii pentru asigurarea

persistentei componentelor Bean.

Un aspect important al serializarii este ca nu salveaza doar imaginea unui obiect ci si

toate referintele la alte obiecte pe care acesta le contine. Acesta este un proces recusiv de

salvare a datelor, intrucat celelalte obiectele referite de obiectul care se serializeaza pot referi

Page 126: Curs Practic de Java

126

la randul lor alte obiecte, si asa mai departe. Asadar referintele care construiesc starea unui

obiect formeaza o intreaga retea, ceea ce inseamna ca un algoritm general de salvare a starii

unui obiect nu este tocmai facil.

In cazul in care starea unui obiect este formata doar din valori ale unor variabile de tip

primitiv, atunci salvarea informatiilor incapsulate in acel obiect se poate face si prin salvarea pe

rand a datelor, folosind clasa DataOutputStream, pentru ca apoi sa fie restaurate prin metode

ale clasei DataInputStream, dar, asa cum am vazut, o asemenea abordare nu este in general

suficienta, deoarece pot aparea probleme cum ar fi: variabilele membre ale obiectului pot fi

instante ale altor obiecte, unele campuri pot face referinta la acelasi obiect, etc.

Serializarea in format binar a tipurilor primitive si a obiectelor se realizeaza prin

intermediul fluxurilor definite de clase specializate in acest scop cu ar fi: ObjectOutputStream

pentru scriere si ObjectInputStream pentru restaurare.

In continuare, prin termenul serializare ne vom referi doar la serializarea in format binar.

8.1.1 Serializarea tipurilor primitive

Serializarea tipurilor primitive poate fi realizata fie prin intermediul fluxurilor

DataOutputStream si DataInputStream, fie cu ObjectOutputStream si ObjectInputStream.

Acestea implementeaza interfetele DataInput, respectiv DataOutput ce declara metode de

tipul readTipPrimitiv, respectiv writeTipPrimitiv pentru scrierea/citirea datelor primitive si a

sirurilor de caractere.

Mai jos este prezentat un exemplu de serializare folosind clasa DataOutputStream:

FileOutputStream fos = new FileOutputStream("test.dat");

DataOutputStream out = new DataOutputStream(fos);

out.writeInt(12345);

out.writeDouble(12.345);

out.writeBoolean(true);

out.writeUTF("Sir de caractere");

out.flush();

fos.close();

Citirea informatiilor scrise in exemplul de mai sus se va face astfel:

FileInputStream fis = new FileInputStream("test.dat");

DataInputStream in = new DataInputStream(fis);

int i = in.readInt();

double d = in.readDouble();

boolean b = in.readBoolean();

String s = in.readUTF();

fis.close();

Page 127: Curs Practic de Java

127

8.1.2 Serializarea obiectelor

Serializarea obiectelor se realizeaza prin intermediul fluxurilor definite de clasele

ObjectOutputStream (pentru salvare) si ObjectInputStream (pentru restaurare). Acestea sunt

fluxuri de procesare, ceea ce inseamna ca vor fi folosite impreuna cu alte fluxuri pentru

scrierea/citirea efectiva a datelor pe mediul extern pe care va fi salvat, sau de pe care va fi

restaurat un obiect serializat.

Mecanismul implicit de serializare a unui obiect va salva numele clasei obiectului,

signatura clasei si valorile tuturor campurile serializabile ale obiectului. Referintele la alte

obiecte serializabile din cadrul obiectului curent vor duce automat la serializarea acestora iar

referintele multiple catre un acelasi obiect sunt codificate utilizand un algoritm care sa poata

reface ”reteaua de obiecte” la aceeasi stare ca atunci cand obiectul original a fost salvat.

Clasele ObjectInputStream si ObjectOutputStream implementeaza interfetele ObjectInput,

respectiv ObjectOutput care extind DataInput, respectiv DataOutput, ceea ce inseamna ca, pe

langa metodele dedicate serializarii obiectelor, vor exista si metode pentru scrierea/citirea

datelor primitive si a sirurilor de caractere.

Metodele pentru serializarea obiectelor sunt:

writeObject, pentru scriere si

readObject, pentru restaurare.

8.1.3 Clasa ObjectOutputStream

Scrierea obiectelor pe un flux de iesire este un proces extrem de simplu, secventa uzuala

fiind cea de mai jos:

ObjectOutputStream out = new ObjectOutputStream(fluxPrimitiv);

out.writeObject(referintaObiect);

out.flush();

fluxPrimitiv.close();

Exemplul de mai jos construieste un obiect de tip Date si il salveaza in fisierul test.ser,

impreuna cu un obiect de tip String. Evident, fisierul rezultat va contine informatiile

reprezentate in format binar.

FileOutputStream fos = new FileOutputStream("test.ser");

ObjectOutputStream out = new ObjectOutputStream(fos);

out.writeObject("Ora curenta:");

out.writeObject(new Date());

out.flush();

fos.close();

Deoarece implementeaza interfata DataOutput, pe langa metoda de scriere a

obiectelor, clasa pune la dispozitie si metode de tipul writeTipPrimitiv pentru serializarea

Page 128: Curs Practic de Java

128

tipurilor de date primitive si a sirurilor de caractere, astfel incat apeluri ca cele de mai jos sunt

permise :

out.writeInt(12345);

out.writeDouble(12.345);

out.writeBoolean(true);

out.writeUTF("Sir de caractere");

Metoda writeObject arunca exceptii de tipul IOException si derivate din aceasta, mai

precis NotSerializableException daca obiectul primit ca argument nu este serializabil, sau

InvalidClassException daca sunt probleme cu o clasa necesara in procesul de serializare.

Vom vedea in continuare ca un obiect este serializabil daca este instanta a unei clase ce

implementeaza interfata Serializable.

8.1.4 Clasa ObjectInputStream

Odata ce au fost scrise obiecte si tipuri primitive de date pe un flux, citirea acestora si

reconstruirea obiectelor salvate se va face printr-un flux de intrare de tip ObjectInputStream.

Acesta este de asemenea un flux de procesare si va trebui asociat cu un flux pentru citirea

efectiva a datelor, cum ar fi FileInputStream pentru date salvate intr-un fisier. Secventa uzuala

pentru deserializare este cea de mai jos:

ObjectInputStream in = new ObjectInputStream(fluxPrimitiv);

Object obj = in.readObject();

//sau

TipReferinta ref = (TipReferinta)in.readObject();

fluxPrimitiv.close();

Citirea informatiilor scrise in exemplul de mai sus se va face astfel:

FileInputStream fis = new FileInputStream("test.ser");

ObjectInputStream in = new ObjectInputStream(fis);

String mesaj = (String)in.readObject();

Date data = (Date)in.readObject();

fis.close();

Trebuie observat ca metoda readObject are tipul returnat Object, ceea ce inseamna ca

trebuie realizata explicit conversia la tipul corespunzator obiectului citit:

Date date = in.readObject(); // gresit

Date date = (Date)in.readObject(); // corect

Atentie : Ca si la celelalte fluxuri de date care implemeteaza interfata DataInput citirea dintr-un

flux de obiecte trebuie sa se faca exact in ordinea in carea acestea au fost scrise, altfel vor

aparea evident exceptii in procesul de deserializare.

Clasa ObjectInputStream implementeaza interfata DataInput deci, pe langa metoda de

citire a obiectelor, clasa pune la dispozitie si metode de tipul readTipPrimitiv pentru citirea

tipurilor de date primitive si a sirurilor de caractere.

Page 129: Curs Practic de Java

129

int i = in.readInt();

double d = in.readDouble();

boolean b = in.readBoolean();

String s = in.readUTF();

8.2 Obiecte serializabile

Un obiect este serializabil daca si numai daca clasa din care face parte implementeaza

interfata Serializable. Asadar, daca dorim ca instantele unei clase sa poata fi serializate, clasa

respectiva trebuie sa implementeze, direct sau indirect, interfata Serializable.

8.2.1 Implementarea interfetei Serializable

Interfata Serializable nu contine nici o declaratie de metoda sau constanta, singurul ei

scop fiind de a identifica clasele ale caror obiecte sunt serializabile. Definitia sa completa este:

package java.io; public interface Serializable { // Nimic ! }

Declararea claselor ale caror instante trebuie sa fie serializate este asadar extrem de

simpla, fiind facuta prin simpla implementare a interfetei Serializable:

public class ClasaSerializabila implements Serializable { // Corpul clasei }

Orice subclasa a unei clase serializabile este la randul ei serializabila, intrucat

implementeaza indirect interfata Serializable. In situatia in care dorim sa declaram o clasa

serializabila dar superclasa sa nu este serializabila, atunci trebuie sa avem in vedere

urmatoarele lucruri:

Variabilele accesibile ale superclasei nu vor fi serializate, fiind responsabilitatea clasei

curente de a asigura un mecanism propriu pentru salvarea/restaurarea lor. Acest lucru

va fi discutat in sectiunea referitoare la personalizarea serializarii.

Superclasa trebuie sa aiba obligatoriu un constructor accesibil fara argumente, acesta

fiind utilizat pentru initializarea variabilelor mostenite in procesul de restaurare al unui

obiect.

Variabilele proprii vor fi initializate cu valorile de pe fluxul de intrare. In lipsa unui

constructor accesibil fara argumente pentru superclasa, va fi generata o exceptie la executie.

In procesul serializarii, daca este intalnit un obiect care nu implementeaza interfata

Serializable atunci va fi generata o exceptie de tipul NotSerializableException ce va identifica

respectiva clasa neserializabila.

Page 130: Curs Practic de Java

130

8.2.2 Controlul serializarii

Exista cazuri cand dorim ca unele variabile membre ale unui obiect sa nu fie salvate

automat in procesul de serializare. Acestea sunt cazuri comune atunci cand respectivele

campuri reprezinta informatii confidentiale, cum ar fi parole, sau variabile temporare pe care nu

are rost sa le salvam. Chiar declarate private in cadrul clasei aceste campuri participa la

serializare. Pentru ca un camp sa nu fie salvat in procesul de serializare el trebuie declarat cu

modificatorul transient si trebuie sa fie ne-static. De exemplu, declararea unei variabile

membre temporare ar trebui facuta astfel:

transient private double temp; // Ignorata la serializare

Modificatorul static anuleaza efectul modificatorului transient. Cu alte cuvinte, variabilele de

clasa participa obligatoriu la serializare.

static transient int N; // Participa la serializare

In exemplele urmatoare campurile marcate ’DA’ participa la serializare, cele

marcate ’NU’, nu participa iar cele marcate cu ’Exceptie’ vor provoca exceptii de tipul

NotSerializableException.

import java.io.*;

public class Test1 implements Serializable {

int x= 1; // DA

transient int y= 2; // NU

transient static int z= 3; // DA

static int t= 4; // DA

public String toString() { return x+", "+y+", "+z+", "+t; }

}

Daca un obiect ce trebuie serializat are referinte la obiecte neserializabile, atunci va fi

generata o exceptie de tipul NotSerializableException.

import java.io.*;

class A { int x= 1; }

class B implements Serializable { int y= 2; }

public class Test2 implements Serializable{

A a = new A(); // Exceptie

B b = new B(); // DA

public String toString() { return a.x + ", " + b.y; }

}

Atunci cand o clasa serializabila deriva dintr-o alta clasa, salvarea campurilor clasei parinte se

va face doar daca si aceasta este serializabila. In caz contrar, subclasa trebuie sa salveze

explicit si campurile mostenite.

Page 131: Curs Practic de Java

131

import java.io.*;

class C {

int x= 0;

// Obligatoriu constructor fara argumente

}

class D extends C implements Serializable { int y= 0; }

public class Test3 extends D {

public Test3() {

x = 1; // NU

y = 2; // DA

}

public String toString() { return x+", "+y; }

}

Mai jos este descrisa o aplicatie care efectueaza salvarea si restaurarea unor obiecte din

cele trei clase prezentate mai sus.

import java.io.*;

public class Exemplu {

public static void test(Object obj) throws IOException {

// Salvam

FileOutputStream fos = new FileOutputStream("fisier.ser") ;

ObjectOutputStream out = new ObjectOutputStream(fos);

out. writeObject (obj);

out.flush ();

fos.close ();

System.out.println("A fost salvat obiectul: " + obj);

// Restauram

FileInputStream fis = new FileInputStream("fisier.ser");

ObjectInputStream in = new ObjectInputStream(fis);

try {

obj = in.readObject();

} catch(ClassNotFoundException e) {

e. printStackTrace ();

}

fis.close ();

System.out.println("A fost restaurat obiectul: " + obj);

}

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

test(new Test1());

try {

test(new Test2());

Page 132: Curs Practic de Java

132

} catch(NotSerializableException e) {

System.out.println("Obiect neserializabil: " + e);

}

test(new Test3());

}

}

Rezultatul acestui program va fi :

A fost salvat obiectul: 1, 2, 3, 4 A fost restaurat obiectul: 1, 0, 3, 4 Obiect neserializabil:

java.io.NotSerializableException: A A fost salvat obiectul: 1, 2 A fost restaurat obiectul: 0, 2

8.3 Personalizarea serializarii obiectelor

Dezavantajul mecanismului implicit de serializare este ca algoritmul pe care se

beazeaza, fiind creat pentru cazul general, se poate comporta ineficient in anumite situatii:

poate fi mult mai lent decat este cazul sau reprezentarea binara generata pentru un obiect

poate fi mult mai voluminoasa decat ar trebui. In aceste situatii, putem sa inlocuim algoritmul

implicit cu unul propriu, particularizat pentru o clasa anume. De asemenea, este posibil sa

extindem comportamentul implicit, adaugand si alte informatii necesare pentru serializarea

unor obiecte. In majoritatea cazurilor mecanismul standard este suficient insa, dupa cum am

spus, o clasa poate avea nevoie de mai mult control asupra serializarii.

Personalizarea serializarii se realizeaza prin definirea (intr-o clasa serializabila!) a

metodelor writeObject si readObject avand exact signatura de mai jos:

private void writeObject (java.io.ObjectOutputStream stream) throws

IOException

private void readObject (java.io.ObjectInputStream stream) throws

IOException, ClassNotFoundException

Metoda writeObject controleaza ce date sunt salvate iar readObject controleaza modulin

care sunt restaurate obiectele, citind informatiile salvate si, eventual, modifcand starea

obiectelor citite astfel incat ele sa corespunda anumitor cerinte. In cazul in care nu dorim sa

inlocuim complet mecanismul standard, putem sa folosim metodele defaultWriteObject,

respectiv defaultReadObject care descriu procedurile implicite de serializare.

Forma generala de implementare a metodelor writeObject si readObject este:

private void writeObject(ObjectOutputStream stream) throws IOException {

// Procesarea campurilor clasei (criptare, etc.)

...

// Scrierea obiectului

curent stream.defaultWriteObject();

// Adaugarea altor informatii suplimentare

...

Page 133: Curs Practic de Java

133

}

private void readObject(ObjectInputStream stream) throws

IOException,ClassNotFoundException {

// Restaurarea obiectului

curent stream.defaultReadObject();

// Actualizarea starii obiectului (decriptare, etc.)

// si extragerea informatiilor suplimentare ...

}

Metodele writeObject si readObject sunt responsabile cu serializarea clasei in care sunt

definite, serializarea superclasei sale fiind facuta automat (si implicit). Daca insa o clasa

trebuie sa-si coordoneze serializarea proprie cu serializarea superclasei sale, atunci trebuie sa

implementeze interfata Externalizable.

8.3.1 Controlul versiunilor claselor

Sa presupunem ca dorim sa realizam o aplicatie care sa tina evidenta angajatilor unei

companii. Evident, vom avean nevoie de o clasa care sa reprezinte notiunea de angjat. O

varianta simplificata a acesteia ar putea arata astfel:

import java.io.*;

class Angajat implements Serializable {

public String nume;

public int salariu;

private String parola;

public Angajat(String nume, int salariu, String parola) {

this.nume = nume;

this.salariu = salariu;

this.parola = parola;

}

public String toString() { return nume + " (" + salariu + ")"; }

}

Mai jos este prezentata o mica aplicatie care permite introducerea de angajati si salvarea

lor intr-un fisier. La fiecare pornire a aplicatiei, vor fi citite datele din fisier astfel incat programul

va actualiza in permanenta lista angajatilor cu noi persoane.

import java.io.*;

import java.util.*;

public class GestiuneAngajati {

//Lista angajatilor

ArrayList ang = new ArrayList();

public void citire() throws IOException {

Page 134: Curs Practic de Java

134

FileInputStream fis = null;

try {

fis = new FileInputStream("angajati.ser");

ObjectInputStream in = new ObjectInputStream(fis);

ang = (ArrayList)

in.readObject();

} catch(FileNotFoundException e) {

System.out.println("Fisierul nou...");

} catch(Exception e) {

System.out.println("Eroare la citirea datelor...");

e. printStackTrace ();

}finally {

if (fis != null)

fis. close ();

}

System.out.println("Lista angajatilor:\n" + ang);

}

public void salvare() throws IOException {

FileOutputStream fos = new FileOutputStream("angajati.ser");

ObjectOutputStream out = new ObjectOutputStream(fos);

out. writeObject (ang);

}

public void adaugare() throws IOException {

BufferedReader stdin = new BufferedReader( new

InputStreamReader(System.in));

while (true) {

System .out.print ("\ nNume :");

String nume = stdin.readLine();

System .out.print (" Salariu :");

int salariu = Integer.parseInt(stdin.readLine());

System .out.print (" Parola :");

String parola = stdin.readLine();

ang.add(new Angajat(nume, salariu, parola));

System.out.print("Mai adaugati ? (D/N)");

String raspuns = stdin.readLine().toUpperCase();

if (raspuns.startsWith("N"))

break;

}

}

Page 135: Curs Practic de Java

135

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

GestiuneAngajati app = new GestiuneAngajati();

// Incarcam angajatii din fisier

app. citire ();

// Adaugam noi angajati

app. adaugare ();

//Salvam angajatii inapoi fisier

app. salvare ();

}

}

Problema care se pune acum este urmatoarea. Dupa introducerea unui numar suficient

de mare de angajati in fisier, clasa Angajat este modificata prin adaugarea unei noi variabila

membre care sa retina adresa. La executia aplicatiei noastre, procedura de citire a angajatilor

din fisier nu va mai functiona, producand o exceptie de tipul InvalidClassException.

Aceastaproblema ar fi aparut chiar daca variabila adaugata era declarata de tip transient. De

ce se intampla acest lucru ?

Explicatia consta in faptul ca mecanismul de serializare Java este foarte atent cu

signatura claselor serializate. Pentru fiecare obiect serializat este calculat automat un numar

reprezentat pe 64 de biti, care reprezinta un fel de ”amprenta” a clasei obiectului. Acest numar,

denumit serialVersionUID, este generat pornind de la diverse informatii ale clasei, cum ar fi

variabilele sale membre, (dar nu numai) si este salvat in procesul de serializare impreuna cu

celelalte date. In plus, orice modificare semnificativa a clasei, cum ar fi adaugarea unui nou

camp, va determina modificarea numarului sau de versiune.

La restaurarea unui obiect, numarul de versiune salvat in forma serializata va fi regasit si

comparat cu noua semnatura a clasei obiectului. In cazul in care acestea nu sunt egale, va fi

generata o exceptie de tipul InvalidClassException si deserializarea nu va fi facuta.

Aceasta abordare extrem de precauta este foarte utila pentru prevenirea unor anomalii ce

pot aparea cand doua versiuni de clase sunt incompatibile, dat poate fi suparatoare atunci

cand modificarile aduse clasei nu strica compatibilitatea cu vechea versiune. In aceasta

situatie trebuie sa comunicam explicit ca cele doua clase sunt compatibile. Acest lucru se

realizeaza prin setarea manuala a variabilei serialVersionUID in cadrul clasei dorite, adaugand

pur si simplu campul:

static final long serialVersionUID = /* numar_serial_clasa */;

Prezenta variabilei serialVersionUID printre membrii unei clase va in-forma algoritmul de

serialzare ca nu mai calculeze numarul de serie al clasei, ci sa-l foloseasca pe cel specificat de

noi. Cum putem afla insa numarul de serie al vechii clase Angajat care a fost folosita anterior la

salvarea angajatilor ?

Utilitarul serialVer permite generarea numarului serialVersionUID pentru o clasa

specificata. Asadar, trebuie sa recompilam vechea clasa Angajat si sa-i aflam numarul de serie

astfel: serialVer Angajat. Rezultatul va fi:

Angajat: static final long serialVersionUID = 5653493248680665297L;

Vom rescrie noua clasa Angajat astfel incat sa fie compatibila cu cea veche astfel:

Page 136: Curs Practic de Java

136

import java.io.*;

class Angajat implements Serializable {

static final long serialVersionUID = 5653493248680665297L;

public String nume, adresa;

public int salariu;

private String parola;

public Angajat(String nume, int salariu, String parola) {

this.nume = nume;

this.adresa = "Iasi";

this.salariu = salariu;

this.parola = parola;

}

public String toString() {

return nume + " (" + salariu + ")";

}

}

Aplicatia noastra va functiona acum, insa rubrica adresa nu va fi initializata in nici un fel

(va fi null), deoarece ea nu exista in formatul original. La noua

salvare a datelor, vor fi serializate si informatiile legate de adresa (evident, trebuie insa sa le

citim de la tastatura...)

8.3.2 Securizarea datelor

Dupa cum am vazut membrii privati, cum ar fi parola din exemplul de mai sus, participa la

serializare. Problema consta in faptul ca, desi in format binar, informatiile unui obiect serializat

nu sunt criptate in nici un fel si pot fi regasite cu usurinta, ceea ce poate reprezenta un

inconvenient atunci cand exista campuri confidentiale.

Rezolvarea acestei probleme se face prin modificarea mecanismului implicit de

serializare, implementand metodele readObject si writeObject, precum si prin utilizarea unei

functii de criptare a datelor. Varianta securizata a clasei Angajat din exemplul anterior ar putea

arata astfel:

import java.io.*;

class Angajat implements Serializable {

static final long serialVersionUID = 5653493248680665297L;

public String nume, adresa;

public int salariu;

private String parola;

public Angajat(String nume, int salariu, String parola) {

this.nume = nume;

this.adresa = "Iasi";

Page 137: Curs Practic de Java

137

this.salariu = salariu;

this.parola = parola;

}

public String toString() { return nume + " (" + salariu + ")"; }

static String criptare(String input, int offset) {

StringBuffer sb = new StringBuffer();

for (int n= 0; n<input.length(); n++)

sb. append (( char)( offset + input. charAt (n)));

return sb.toString();

}

private void writeObject(ObjectOutputStream stream) throws IOException {

parola = criptare(parola, 3);

stream . defaultWriteObject ();

parola = criptare(parola, -3);

}

private void readObject(ObjectInputStream stream) throws IOException ,

ClassNotFoundException {

stream . defaultReadObject ();

parola = criptare(parola, -3);

}

}

8.3.3 Implementarea interfetei Externalizable

Pentru un control complet, explicit, al procesului de serializare, o clasa trebuie sa

implementeze interfata Externalizable. Pentru instante ale acestor clase doar numele clasei

este salvat automat pe fluxul de obiecte, clasa fiind responsabila cu scrierea si citirea

membrilor sai si trebuie sa se coordoneze cu superclasele ei.

Definitia interfetei Externalizable este:

package java.io;

public interface Externalizable extends Serializable {

public void writeExternal(ObjectOutput out) throws IOException;

public void readExternal(ObjectInput in) throws IOException,

ClassNotFoundException;

}

Asadar, aceste clase trebuie sa implementeze obligatoriu metodele write-External si

readExternalin care se va face serializarea completa a obiectelor si coordonarea cu

superclasa ei.

Uzual, interfata Externalizable este folosita in situatii in care se doreste imbunatatirea

performantelor algoritmului standard, mai exact cresterea vitezei procesului de serializare.

Page 138: Curs Practic de Java

138

Mai jos este prezentata o clasa simpla si modalitatea de rescriere a sa folosind interfata

Externalizable:

import java.io.*;

class Persoana implements Serializable {

int cod; String nume;

public Persoana(String nume, int cod) {

this.nume = nume; this.cod = cod;

}

}

Serializare proprie

import java.io.*;

class Persoana implements Externalizable {

int cod; String nume;

public Persoana(String nume, int cod) {

this.nume = nume; this.cod = cod;

}

public void writeExternal(ObjectOutput s) throws IOException {

s. writeUTF ( nume);

s. writeInt (cod);

}

public void readExternal(ObjectInput s) throws ClassNotFoundException ,

IOException {

nume = s.readUTF();

cod = s.readInt();

}

}

8.4 Clonarea obiectelor

Se stie ca nu putem copia valoarea unui obiect prin instructiunea de atribuire. O secventa

de forma:

TipReferinta o1 = new TipReferinta(); TipReferinta o2 = o1;

nu face decat sa declare o noua variabila o2 ca referinta la obiectul referit de o1 si nu creeaza

sub nici o forma un nou obiect.

O posibilitate de a face o copie a unui obiect este folosirea metodei clone definita in clasa

Object. Aceasta creeaza un nou obiect si initializeaza toate variabilele sale membre cu valorile

Page 139: Curs Practic de Java

139

obiectului clonat.

TipReferinta o1 = new TipReferinta(); TipReferinta o2 = (TipReferinta) o1.clone();

Deficienta acestei metode este ca nu realizeaza duplicarea intregii retele de obiecte

corespunzatoare obiectului clonat. In cazul in care exista campuri referinta la alte obiecte,

obiectele referite nu vor mai fi clonate la randul lor.

O metoda clone care sa realizeze o copie efectiva a unui obiect, impreuna cu copierea

tuturor obiectelor referite de campurile acelui obiect poate fi implementata prin mecanismul

serializarii astfel:

public Object clone() { try {

ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(baos); out.writeObject(this); out.close(); byte[] buffer = baos.toByteArray();

ByteArrayInputStream bais = new ByteArrayInputStream(buffer);

ObjectInputStream in = new ObjectInputStream(bais);

Object ret = in.readObject(); in.close();

return ret; } catch (Exception e) { }

}

Page 140: Curs Practic de Java

140

9 Fire de executie

9.1 Introducere

Firele de executie fac trecerea de la programarea secventiala la programarea

concurenta. Un program secvential reprezinta modelul clasic de program: are un inceput, o

secventa de executie a instructiunilor sale si un sfarsit. Cu alte cuvinte, la un moment dat

programul are un singur punct de executie. Un program aflat in executie se numeste proces.

Un sistem de operare monotasking, cum ar fi fi MS-DOS, nu este capabil sa execute decat un

singur proces la un moment dat, in timp ce un sistem de operare multitasking, cum ar fi UNIX

sau Windows, poate rula oricate procese in acelasi timp (concurent), folosind diverse strategii

de alocare a procesorului fiecaruia dintre acestea. Am reamintit acest lucru deoarece notiunea

de fir de executie nu are sens decat in cadrul unui sistem de operare multitasking.

Un fir de executie este similar unui proces secvential, in sensul ca are un inceput, o

secventa de executie si un sfarsit. Diferenta dintre un fir de executie si un proces consta in

faptul ca un fir de executie nu poate rula independent ci trebuie sa ruleze in cadrul unui proces.

Definitie : Un fir de executie este o succesiune sceventiala de instructiuni care se executa in

cadrul unui proces.

Program (proces)

Un program isi poate defini insa nu doar un fir de executie ci oricate, ceea ce inseamna

ca in cadrul unui proces se pot executa simultan mai multe fire de executie, permitand

efectuarea concurenta a sarcinilor independente ale acelui program.

Un fir de executie poate fi asemanat cu o versiune redusa a unui proces, ambele ruland

simultan si independent pe o structura secventiala formata de instructiunile lor. De asemenea,

executia simultana a firelor in cadrul unui proces este similara cu executia concurenta a

proceselor: sistemul de operare va aloca procesorul dupa o anumita strategie fiecarui fir de

executie pana la terminarea lor. Din acest motiv firele de executie mai sunt numite si procese

usoare.

Care ar fi insa deosebirile intre un fir de executie si un proces ? In primul, rand

deosebirea majora consta in faptul ca firele de executie nu pot rula decat in cadrul unui proces.

O alta deosebire rezulta din faptul ca fiecare proces are propria sa memorie (propriul sau

Page 141: Curs Practic de Java

141

spatiu de adrese) iar la crearea unui nou proces (fork) este realizata o copie exacta a

procesului parinte: cod si date, in timp ce la crearea unui fir nu este copiat decat codul

procesului parinte, toate firele de executie avand acces la aceleasi date, datele procesului

original. Asadar, un fir mai poate fi privit si ca un context de executie in cadrul unui proces.

Firele de executie sunt utile in multe privinte, insa uzual ele sunt folosite pentru

executarea unor operatii consumatoare de timp fara a bloca procesul principal: calcule

matematice, asteptarea eliberarii unei resurse, desenarea componentelor unei aplicatii GUI,

etc. De multe ori ori, firele isi desfasoara activitatea in fundal insa, evident, acest lucru nu este

obligatoriu.

9.2 Crearea unui fir de executie

Ca orice alt obiect Java, un fir de executie este o instanta a unei clase. Firele de executie

definite de o clasa vor avea acelasi cod si, prin urmare, aceeasi secventa de instructiuni.

Crearea unei clase care sa defineasca fire de executie poate fi facuta prin doua modalitati:

prin extinderea clasei Thread;

prin implementarea interfetei Runnable.

Orice clasa ale carei instante vor fi executate separat intr-un fir propriu trebuie declarata

ca fiind de tip Runnable. Aceasta este o interfata care contine o singura metoda si anume

metoda run. Asadar, orice clasa ce descrie fire de executie va contine metoda run in care este

implementat codul ce va fi rulat. Interfata Runnable este conceputa ca fiind un protocol comun

pentru obiectele care doresc sa execute un anumit cod pe durata existentei lor.

Cea mai importanta clasa care implementeaza interfata Runnable este Thread. Aceasta

implementeaza un fir de executie generic care, implicit, nu face nimic; cu alte cuvinte, metoda

run nu contine nici un cod. Orice fir de executie este o instanta a clasei Thread sau a unei

subclase a sa.

9.2.1 Extinderea clasei Thread

Cea mai simpla metoda de a crea un fir de executie care sa realizeze o anumita actiune

este prin extinderea clasei Thread si supradefinirea metodei run a acesteia. Formatul general

al unei astfel de clase este:

Page 142: Curs Practic de Java

142

public class FirExcecutie extends Thread {

public FirExcecutie(String nume) {

// Apelam constructorul superclasei

super(nume);

}

public void run() {

// Codul firului de executie

...

}

}

Prima metoda a clasei este constructorul, care primeste ca argument un sir ce va

reprezenta numele firului de executie. In cazul in care nu vrem sa dam nume firelor pe care le

cream, atunci putem renunta la supradefinirea acestui constructor si sa folosim constructorul

implicit, fara argumente, care creeaza un fir de executie fara nici un nume. Ulterior, acesta

poate primi un nume cu metoda setName. Evident, se pot defini si alti constructori, acestia

fiinde utili atunci cand vrem sa trimitem diversi parametri de initializare firului nostru. A doua

metoda este metoda run, ”inima” oricarui fir de executie, in care scriem efectiv codul care

trebuie sa se execute.

Un fir de executie creat nu este automat pornit, lansarea sa fiind realizeaza de metoda

start, definita in clasa Thread.

// Cream firul de executie

FirExecutie fir = new FirExecutie("simplu");

// Lansam in executie

fir.start();

Sa consideram in continuare un exemplu in care definim un fir de executie ce afiseaza

numerele intregi dintr-un interval, cu un anumit pas.

class AfisareNumere extends Thread {

private int a, b, pas;

public AfisareNumere(int a, int b, int pas) {

this.a = a; this.b = b; this.pas = pas;

}

public void run() {

for(int i=a; i <= b; i+= pas)

System.out.print(i + " " );

}

}

Page 143: Curs Practic de Java

143

public class TestThread {

public static void main(String args[]) {

AfisareNumere fir1 , fir2;

fir1 = new AfisareNumere(0, 100, 5);

// Numara de la 0 la 100 cu pasul 5

fir2 = new AfisareNumere(100, 200, 10);

// Numara de la 100 la 200 cu pasul 10

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

// Pornim firele de executie

// Ele vor fi distruse automat la terminarea lor

}

}

Gandind secvential, s-ar crede ca acest program va afisa prima data numerele de la 0 la

100 cu pasul 5, apoi numerele de la 100 la 200 cu pasul 10, intrucat primul apel este catre

contorul fir1, deci rezultatul afisat pe ecran ar trbui sa fie:

0 5101520 25 30 354045 50 55606570 75808590 95 100 100 110 120 130 140

150 160 170 180 190 200

In realitate insa, rezultatul obtinut va fi o intercalare de valori produse de cele doua fire ce

ruleaza simultan. La rulari diferite se pot obtine rezultate diferite deoarece timpul alocat fiecarui

fir de executie poate sa nu fie acelasi, el fiind controlat de procesor intr-o maniera ”aparent”

aleatoare. Un posibil rezultat al programului de mai sus:

0 100 5 110 10 120 15 130 20 140 25 150 160 170 180 190 200 3035 40 45505560

65 707580 85 90 95100

9.2.2 Implementarea interfetei Runnable

Ce facem insa cand dorim sa cream o clasa care instantiaza fire de executie dar aceasta

are deja o superclasa, stiind cain Java nu este permisa mostenirea multipla ?

class FirExecutie extends Parinte, Thread // incorect !

In acest caz, nu mai putem extinde clasa Thread ci trebuie sa implementam direct

interfata Runnable. Clasa Thread implementeaza ea insasi interfata Runnable si, din acest

motiv, la extinderea ei obtineam implementarea indirecta a interfetei. Asadar, interfata

Runnable permite unei clase sa fie activa, fara a extinde clasa Thread.

Interfata Runnable se gasestein pachetul java.lang si este definita astfel:

public interface Runnable { public abstract void run(); }

Page 144: Curs Practic de Java

144

Prin urmare, o clasa care instantiaza fire de executie prin implementarea interfetei

Runnable trebuie obligatoriu sa implementeze metoda run. O astfel de clasa se mai numeste

clasa activa si are urmatoarea structura:

public class ClasaActiva implements Runnable {

public void run() {

//Codul firului de executie

...

}

}

Spre deosebire de modalitatea anterioara, se pierde insa tot suportul oferit de clasa

Thread. Simpla instantiere a unei clase care implemeneaza interfata Runnable nu creeaza nici

un fir de executie, crearea acestora trebuind facuta explicit. Pentru a realiza acest lucru trebuie

sa instantiem un obiect de tip Thread ce va reprezenta firul de executie propriu zis al carui cod

se gaseste in clasa noastra. Acest lucru se realizeaza, ca pentru orice alt obiect, prin

instructiunea new, urmata de un apel la un constructor al clasei Thread, insa nu la oricare

dintre acestia. Trebuie apelat constructorul care sa primeasca drept argument o instanta a

clasei noastre. Dupa creare, firul de executie poate fi lansat printr-un apel al metodei start.

ClasaActiva obiectActiv = new ClasaActiva();

Thread fir = new Thread(obiectActiv);

fir.start();

Aceste operatiuni pot fi facute chiar in cadrul clasei noastre:

public class FirExecutie implements Runnable {

private Thread fir = null;

public FirExecutie(){

if (fir == null) {

fir = new Thread(this);

fir.start();

}

}

public void run() {

//Codul firului de executie ...

}

}

Specificarea argumentului this in constructorul clasei Thread determina crearea unui fir

de executie care, la lansarea sa, va apela metoda run din clasa curenta. Asadar, acest

constructor accepta ca argument orice instanta a unei clase ”Runnable”. Pentru clasa

FirExecutie data mai sus, lansarea firului va fi facuta automat la instantierea unui obiect al

clasei:

Page 145: Curs Practic de Java

145

FirExecutie fir = new FirExecutie();

Atentie : Metoda run nu trebuie apelata explicit, acest lucru realizandu-se automat la apelul

metodei start. Apelul explicit al metodei run nu va furniza nici o eroare, insa aceasta va fi

executata ca orice alta metoda, si nu separat intr-un fir.

Sa consideram urmatorul exemplu in care cream doua fire de executie folosind interfata

Runnable. Fiecare fir va desena figuri geometrice de un anumit tip, pe o suprafata de desenare

de tip Canvas. Vom porni apoi doua fire de executie care vor rula concurent, desenand figuri

diferite, fiecare pe suprafata sa.

import java.awt.*;

import java.awt.event.*;

class Plansa extends Canvas implements Runnable {

// Deoarece Plansa extinde Canvas ,

// nu mai putem extinde clasa Thread

Dimension dim = new Dimension(300, 300);

Color culoare; String figura; int x=0, y=0, r= 0;

public Plansa(String figura, Color culoare) {

this.figura = figura; this.culoare = culoare;

}

public Dimension getPreferredSize() { return dim; }

public void paint(Graphics g) {

// Desenam un chenar

g. setColor ( Color.black );

g.drawRect(0, 0, dim.width-1, dim.height-1);

// Desenam figura la coordonatele calculate // de firul de executie

g. setColor ( culoare ); if (figura.equals("patrat"))

g.drawRect(x, y, r, r); else if (figura.equals("cerc"))

g.drawOval(x, y, r, r);

}

public void update(Graphics g) {

paint (g);

// Supradefinim update ca sa nu mai // fie stearsa suprafata de desenare

}

public void run() {

/* Codul firului de executie: Afisarea a 100 de figuri geometrice la pozitii si

dimensiuni calculate aleator. Intre doua afisari , facem o pauza de 50 ms

*/

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

x = (int)(Math.random() * dim.width);

y = (int)(Math.random() * dim.height);

r = (int)(Math.random() * 100);

Page 146: Curs Practic de Java

146

try {

Thread . sleep (50) ;

} catch(InterruptedException e) {}

repaint ();

}

}

}

class Fereastra extends Frame {

public Fereastra(String titlu) { super(titlu );

this.addWindowListener(new WindowAdapter() {

public void windowClosing(WindowEvent e) { System . exit (0); } });

// Cream doua obiecte active de tip Plansa

Plansa p1 = new Plansa("patrat", Color.blue);

Plansa p2 = new Plansa("cerc", Color.red);

// Acestea extind Canvas , le plasam pe fereastra

setLayout(new GridLayout(1, 2)); add(p1); add(p2); pack ();

// Pornim doua fire de executie , care vor // actualiza desenul celor doua planse

new Thread(p1).start(); new Thread(p2).start(); }

}

public class TestRunnable {

public static void main(String args[]) {

Fereastra f = new Fereastra("Test Runnable");

f.show ();

}

}

9.3 Ciclul de viata unui fir de executie

Fiecare fir de executie are propriul sau ciclu de viata: este creat, devine activ prin

lansarea sa si, la un moment dat, se termina. In continuare, vom analiza mai indeaproape

starile in care se poate gasi un fir de executie. Diagrama de mai jos ilustreaza generic aceste

stari precum si metodele care provoaca tranzitia dintr-o stare in alta:

Page 147: Curs Practic de Java

147

Asadar, un fir de executie se poate gasi in una din urmatoarele patru stari:

• ”New Thread”

• ”Runnable”

• ”Not Runnable”

• ”Dead”

Starea ”New Thread”

Un fir de executie se gaseste in aceasta stare imediat dupa crearea sa, cu alte cuvinte

dupa instantierea unui obiect din clasa Thread sau dintr-o subclasa a sa.

Thread fir = new Thread(obiectActiv);

// fir se gaseste in starea "New Thread"

In aceasta stare firul este ”vid”, el nu are alocate nici un fel de resurse sistem si singura

operatiune pe care o putem executa asupra lui este lansarea in executie, prin metoda start.

Apelul oricarei alte metodein afara de start nu are nici un sens si va provoca o exceptie de tipul

IllegalThreadStateException.

Starea ”Runnable”

Dupa apelul metodei start un fir va trece in starea ”Runnable”, adica va fi in executie.

fir.start();

//fir se gaseste in starea "Runnable"

Metoda start realizeza urmatoarele operatiuni necesare rularii firului de executie:

Aloca resursele sistem necesare.

Planifica firul de executie la procesor pentru a fi lansat.

Apeleaza metoda run a obiectului activ al firului.

Un fir aflat in starea ”Runnable” nu inseamna neaparat ca se gaseste efectiv in executie,

adica instructiunile sale sunt interpretate de procesor. Acest lucru se intampla din cauza ca

majoritatea calculatoarelor au un singur procesor iar acesta nu poate rula simultan toate firele

de executie care se gasesc in starea ”Runnable”. Pentru a rezolva aceasta problema exista o

planificare care sa partajeze dinamic si corect procesorul intre toate firele de executie care

sunt in starea ”Runnable”. Asadar, un fir care ”ruleaza” poate sa-si astepte de fapt randul la

procesor.

Starea ”Not Runnable”

Un fir de executie poate ajunge in aceaata stare in una din urmatoarele situatii:

Este ”adormit” prin apelul metodei sleep;

A apelat metoda wait, asteptand ca o anumita conditie sa fie satisfacuta;

Este blocat intr-o operatie de intrare/iesire.

Metoda sleep este o metoda statica a clasei Thread care provoaca o pauza in timpul

rularii firului curent aflat in executie, cu alte cuvinte il ”adoarme” pentru un timp specificat.

Lungimea acestei pauze este specificata in milisecunde si chiar nanosecunde. Intrucat poate

provoca exceptii de tipul InterruptedException, apelul acestei metode se face intr-un bloc de tip

Page 148: Curs Practic de Java

148

try-cacth:

try {

// Facem pauza de o secunda

Thread.sleep(1000);

} catch (InterruptedException e) {

...

}

Observati ca metoda fiind statica apelul ei nu se face pentru o instanta anume a clasei

Thread. Acest lucru este foarte normal deoarece, la un moment dat, un singur fir este in

executie si doar pentru acesta are sens ”adormirea” sa.

In intervalul in care un fir de executie ”doarme”, acesta nu va fi execut chiar daca procesorul

devine disponibil. Dupa expirarea intervalului specificat firul revine in starea ”Runnable” iar

daca procesorul este in continuare disponibil isi va continua executia.

Pentru fiecare tip de intrare in starea ”Not Runnable”, exista o secventa specifica de iesire

din starea repectiva, care readuce firul de executie in starea ”Runnable”. Acestea sunt:

Daca un fir de executie a fost ”adormit”, atunci el devine ”Runnable” doar dupa

scurgerea intervalului de timp specificat de instructiunea sleep.

Daca un fir de executie asteapta o anumita conditie, atunci un alt obiect trebuie sa il

informeze daca acea conditie este indeplinita sau nu; acest lucru se realizeaza prin

instructiunile notify sau notifyAll (vezi ”Sincronizarea firelor de executie”).

Daca un fir de executie este blocat intr-o operatiune de intrare/iesire atunci el redevine

”Runnable” atunci cand acea operatiune s-a terminat.

Starea ”Dead”

Este starea in care ajunge un fir de executie la terminarea sa. Un fir nu poate fi oprit din

program printr-o anumita metoda, ci trebuie sa se termine in mod natural laincheierea metodei

run pe care o executa. Spre deosebire de versiunile curente ale limbajului Java, in versiunile

mai vechi exista metoda stop a clasei Thread care termina fortat un fir de executie, insa

aceasta a fost eliminata din motive de securitate. Asadar, un fir de executie trebuie sa-si

”aranjeze” singur propria sa ”moarte”.

9.3.1 Terminarea unui fir de executie

Dupa cum am vazut, un fir de executie nu poate fi terminat fortat de catre program ci

trebuie sa-si ”aranjeze” singur terminarea sa. Acest lucru poate fi realizat in doua modalitati:

1. Prin scrierea unor metode run care sa-si termine executia in mod natural. La

terminarea metodei run se va termina automat si firul de executie, acesta

intrand in starea Dead. Ambele exemple anteriorare se incadreaza in aceasta

categorie.

Page 149: Curs Practic de Java

149

// Primul exemplu

public void run() {

for(int i=a;i <=b; i+= pas)

System.out.print(i + " " );

}

Dupa afisarea numerelor din intervalul specificat, metoda se termina si, odata cu ea, se

va termina si firul de executie repsectiv.

2. Prin folosirea unei variabile de terminare. In cazul cand metoda run trebuie

sa execute o bucla infinita atunci aceasta trebuie controlata printr-o variabila

care sa opreasca ciclul atunci cand dorim ca firul de executie sa se termine.

Uzual, vom folosi o variabila membra a clasei care descrie firul de executie

care fie este publica, fie este asociata cu o metoda publica care permite

schimbarea valorii sale.

Sa consideram exemplul unui fir de executie care trebuie sa numere secundele

scurse pana la apasarea tastei Enter.

import java.io.*;

class NumaraSecunde extends Thread {

public int sec = 0;

// Folosim o variabila de terminare

public boolean executie = true;

public void run() {

while (executie) {

try {

Thread. sleep (1000) ;

sec ++;

System.out. print(".");

} catch(InterruptedException e){}

}

}

}

public class TestTerminare {

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

NumaraSecunde fir = new NumaraSecunde();

fir.start ();

System.out.println("Apasati tasta Enter");

System .in.read ();

// Oprim firul de executie

fir.executie = false;

Page 150: Curs Practic de Java

150

System.out.println("S-au scurs " + fir.sec + " secunde");

}

}

Nu este necesara distrugerea explicita a unui fir de executie. Sistemul Java de colectare

a ”gunoiului” se ocupa de acest lucru. Setarea valorii null pentru variabila care referea instanta

firului de executie va usura insa activitatea procesului gc.

Metoda System.exit va oprit fortat toate firele de executie si va termina aplicatia curenta.

Pentru a testa daca un fir de executie a fost pornit dar nu s-a terminat inca putem folosi metoda

isAlive. Metoda returneaza:

true -daca firul estein una din starile ”Runnable” sau ”Not Runnable”;

false -daca firul este in una din starile ”New Thread” sau ”Dead”.

Intre starile ”Runnable” sau ”Not Runnable”, repectiv ”New Thread” sau ”Dead” nu se

poate face nici o diferentiere.

NumaraSecunde fir = new NumaraSecunde();

// isAlive retuneaza false (starea este New Thread)

fir.start();

// isAlive retuneaza true (starea este Runnable)

fir.executie = false;

// isAlive retuneaza false (starea este Dead)

9.3.2 Fire de executie de tip ”daemon”

Un proces este considerat in executie daca contine cel putin un fir de executie activ. Cu

alte cuvinte, la rularea unei aplicatii, masina virtuala Java nu se va opri decat atunci cand nu

mai exista nici un fir de executie activ. De multe ori insa dorim sa folosim fire care sa realizeze

diverse activitati, eventual periodic, pe toata durata de executie a programului iarin momentul

terminarii acestuia sa se termine automat si firele respective. Aceste fire de executie se

numesc demoni.

Dupa crearea sa, un fir de executie poate fi facut demon, sau scos din aceasta stare, cu

metoda setDaemon.

class Beeper implements Runnable {

public void run() {

while (true) {

java.awt. Toolkit . getDefaultToolkit ().beep ();

try {

Thread.sleep (1000);

} catch(InterruptedException e) {}

}

}

}

Page 151: Curs Practic de Java

151

public class TestDaemon {

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

Thread t = new Thread(new Beeper());

t. setDaemon ( true);

t. start ();

System.out.println("Apasati Enter..."); System .in.read ();

// "Demonul" se termina automat la terminarea aplicatiei

}

}

9.3.3 Stabilirea prioritatilor de executie

Majoritatea calculatoarelor au un sigur procesor, ceea ce inseamna ca firele de executie

trebuie sa-si imparta accesul la acel procesor. Executia intr-o anumita ordine a mai multor fire

de executie pe un numar limitat de procesoare se numeste planificare (scheduling). Sistemul

Java de executie a programelor implementeaza un algoritm simplu, determinist de planificare,

cunoscut sub numele de planificare cu prioritati fixate.

Fiecare fir de executie Java primeste la crearea sa o anumita prioritate. O prioritate este

de fapt un numar intreg cu valori cuprinse intre MIN PRIORITY si MAX PRIORITY. Implicit,

prioritatea unui fir nou creat are valoarea NORM PRIORITY. Aceste trei constante sunt definite

in clasa Thread astfel:

public static final int MAX_PRIORITY = 10;

public static final int MIN_PRIORITY = 1;

public static final int NORM_PRIORITY= 5;

Schimbarea ulterioara a prioritatii unui fir de executie se realizeaza cu metoda setPriority

a clasei Thread.

La nivelul sistemului de operare, exista doua modele de lucru cu fire de executie:

Modelul cooperativ, in care firele de executie decid cand sa cedeze procesorul;

dezavantajul acestui model este ca unele fire pot acapara procesorul, nepermitand si

executia altora pana la terminarea lor.

Modelul preemptiv, in care firele de executie pot fi intrerupte oricand, dupa ce au fost

lasate sa ruleze o perioada, urmand sa fie reluate dupa ce si celelalte fire aflate in

executie au avut acces la procesor; acest sistem se mai numeste cu ”cuante de timp”,

dezavantajul sau fiind nevoia de a sincroniza accesul firelor la resursele comune.

Asadar, im modelul cooperativ firele de executie sunt responsabile cu partajarea timpului

de executie, in timp ce in modelul preemptiv ele trebuie sa partajeze resursele comune.

Deoarece specificatiile masinii virtuale Java nu impun folosirea unui anumit model, programele

Java trebuie scrise astfel incat sa functioneze corect pe ambele modele. In continuare, vom

mai detalia putin aceste aspecte.

Planificatorul Java lucreaza in modul urmator: daca la un moment dat sunt mai multe fire

Page 152: Curs Practic de Java

152

de executie in starea ”Runnable”, adica sunt pregatite pentru a fi rulate, planificatorul il va

alege pe cel cu prioritatea cea mai mare pentru a-l executa. Doar cand firul de executie cu

prioritate maxima se termina, sau este suspendat din diverse motive, va fi ales un fir cu o

prioritate mai mica. In cazul in care toate firele au aceeasi prioritate ele sunt alese pe rand,

dupa un algoritm simplu de tip ”round-robin”. De asemenea, daca un fir cu prioritate mai mare

decat firul care se executa la un moment dat solicita procesorul, atunci firul cu prioritate mai

mare este imediat trecut in executie iar celalalt trecut in asteptare. Planificatorul Java nu va

intrerupe insa un fir de executie in favoarea altuia de aceeasi prioritate, insa acest lucru il poate

face sistemul de operare in cazul in care acesta aloca procesorul in cuante de timp (un astfel

de SO este Windows).

Asadar, un fir de executie Java cedeaza procesorul in una din situatiile:

un fir de executie cu o prioritate mai mare solicita procesorul;

metoda sa run se termina;

face explicit acest lucru apeland metoda yield;

timpul alocat pentru executia sa a expirat (pe SO cu cuante de timp).

Atentie!!! In nici un caz corectitudinea unui program nu trebuie sa se bazeze pe

mecansimul de planificare a firelor de executie, deoarece acesta poate fi diferit de la un sistem

de operare la altul.

Un fir de executie de lunga durata si care nu cedeaza explicit procesorul la anumite

intervale de timp astfel incat sa poata fi executate si celelalte fire de executie se numeste fir de

executie egoist. Evident, trebuie evitata scrierea lor intrucat acapareaza pe termen nedefinit

procesorul, blocand efectiv executia celorlalte fire de executie pana la terminarea sa. Unele

sistemele de operare combat acest tip de comportament prin metoda alocarii procesorului in

cuante de timp fiecarui fir de executie, insa nu trebuie sa ne bazam pe acest lucru la scrierea

unui program. Un fir de executie trebuie sa fie ”corect” fatade celelalte fire si sa cedeze

periodic procesorul astfel incat toate sa aiba posibilitatea de a se executa.

class FirEgoist extends Thread {

public FirEgoist(String name) {

super ( name);

}

public void run() {

int i=0;

while (i < 100000) { // Bucla care acapareaza procesorul

i ++;

if (i % 100 == 0)

System.out.println(getName() + " a ajuns la " + i);

// yield();

}

}

}

Page 153: Curs Practic de Java

153

public class TestFirEgoist {

public static void main(String args[]) {

FirEgoist s1, s2;

s1 = new FirEgoist("Firul 1");

s1.setPriority (Thread.MAX _PRIORITY);

s2 = new FirEgoist("Firul 2");

s2.setPriority (Thread.MAX _PRIORITY);

s1.start ();

s2.start ();

}

}

Firul de executie s1 are prioritate maxima si pana nu-si va termina executia nu-i va

permite firului s2 sa execute nici o instructiune, acaparand efectiv procesorul. Rezultatul va

arata astfel:

Firul 1 a ajuns la 100 Firul 1 a ajuns la 200 Firul 1 a ajuns la 300 ... Firul 1 a ajuns la 99900

Firul 1 a ajuns la 100000 Firul 2 a ajuns la 100 Firul 2 a ajuns la 200 ... Firul 2 a ajuns la 99900

Firul 2 a ajuns la 100000

Rezolvarea acestei probleme se face fie prin intermediul metodei statice yield a clasei

Thread, care determina firul de executie curent sa se opreasca temporar, dand ocazia si altor

fire sa se execute, fie prin ”adormirea” temporara a firului curent cu ajutorul metodei sleep. Prin

metoda yield un fir de executie nu cedeaza procesorul decat firelor de executie care au

aceeasi prioritate cu a sa si nu celor cu prioritati mai mici. Decomentand linia in care apelam

yeld din exemplul anterior, executia celor doua fire se va intercala.

Firul 1 a ajuns la 31900 Firul 1 a ajuns la 32000 Firul 2 a ajuns la 100 Firul 1 a ajuns la

32100 Firul 2 a ajuns la 200 Firul 2 a ajuns la 300 ...

9.3.4 Sincronizarea firelor de executie

Pana acum am vazut cum putem crea fire de executie independente si asincrone, cu alte

cuvinte care nu depind in nici un fel de executia sau de rezultatele altor fire. Exista insa

numeroase situatii cand fire de executie separate, dar care ruleaza concurent, trebuie sa

comunice intre ele pentru a accesa diferite resurse comune sau pentru a-si transmite dinamic

rezultatele ”muncii” lor. Cel mai elocvent scenariu in care firele de executie trebuie sa se

comunice intre ele este cunoscut sub numele de problema producatorului/consumatorului, in

care producatorul genereaza un flux de date care este preluat si prelucrat de catre

consumator.

Sa consideram de exemplu o aplicatie Java in care un fir de executie (producatorul) scrie

date intr-un fisier in timp ce alt fir de executie (consumatorul) citeste date din acelasi fisier

pentru a le prelucra. Sau, sa presupunem ca producatorul genereaza niste numere si le

plaseaza, pe rand, intr-un buffer iar consumatorul citeste numerele din acel buffer pentru a le

procesa. In ambele cazuri avem de-a face cu fire de executie concurente care folosesc o

Page 154: Curs Practic de Java

154

resursa comuna: un fisier, respectiv o zona de memorie si, din acest motiv, ele trebuie

sincronizate intr-o maniera care sa permita decurgerea normala a activitatii lor.

9.3.5 Scenariul producator / consumator

Pentru a intelege mai bine modalitatea de sincronizare a doua fire de executie sa

implementam efectiv o problema de tip producator/consumator. Sa consideram urmatoarea

situatie:

Producatorul genereaza numerele intregi de la 1 la 10, fiecare la un interval neregulat

cuprins intre 0 si 100 de milisecunde. Pe masura ce le genereaza incearca sa le

plaseze intr-o zona de memorie (o variabila intreaga) de unde sa fie citite de catre

consumator.

Consumatorul va prelua, pe rand, numerele generate de catre pro-ducator si va afisa

valoarea lor pe ecran.

Pentru a fi accesibila ambelor fire de executie, vom incapsula variabila ce va contine

numerele generate intr-un obiect descris de clasa Buffer si care va avea doua metode put

(pentru punerea unui numar in buffer) si get (pentru obtinerea numarului din buffer).

Fara a folosi nici un mecanism de sincronizare clasa Buffer arata astfel:

class Buffer {

private int number = -1;

public int get() { return number; }

public void put(int number) { this.number = number; }

}

Vom implementa acum clasele Producator si Consumator care vor descrie cele doua fire

de executie. Ambele vor avea o referinta comuna la un obiect de tip Buffer prin intermediul

caruia isi comunica valorile.

class Producator extends Thread {

private Buffer buffer;

public Producator(Buffer b) { buffer = b; }

public void run() {

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

buffer .put(i);

System.out.println("Producatorul a pus:\t" + i);

try {

sleep((int)(Math.random() * 100));

} catch (InterruptedException e) { }

}

Page 155: Curs Practic de Java

155

}

}

class Consumator extends Thread {

private Buffer buffer;

public Consumator(Buffer b) { buffer = b; }

public void run() {

int value = 0;

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

value = buffer.get();

System.out.println("Consumatorul a primit:\t" + value);

}

}

}

public class TestSincronizare1 {

public static void main(String[] args) {

Buffer b = new Buffer();

Producator p1 = new Producator(b);

Consumator c1 = new Consumator(b);

p1. start ();

c1. start ();

}

}

Dupa cum ne asteptam, rezultatul rularii acestui program nu va rezolva nici pe departe

problema propusa de noi, motivul fiind lipsa oricarei sincronizari intre cele doua fire de

executie. Mai precis, rezultatul va fi ceva de forma:

Consumatorul a primit: -1 Consumatorul a primit: -1 Producatorul a pus: 0 Consumatorul

a primit: 0 Consumatorul a primit: 0 Consumatorul a primit: 0 Consumatorul a primit: 0

Consumatorul a primit: 0 Consumatorul a primit: 0 Consumatorul a primit: 0 Consumatorul a

primit: 0 Producatorul a pus: 1 Producatorul a pus: 2 Producatorul a pus: 3 Producatorul a pus:

4 Producatorul a pus: 5 Producatorul a pus: 6 Producatorul a pus: 7 Producatorul a pus: 8

Producatorul a pus: 9

Ambele fire de executie acceseaza resursa comuna, adica obiectul de tip Buffer, intr-o

maniera haotica si acest lucru se intampla din dou motive :

Consumatorul nu asteapta inainte de a citi ca producatorul sa genereze un numar si va

prelua de mai multe ori acelasi numar.

Producatorul nu asteapta consumatorul sa preia numarul generatinainte de a produce

un altul, in felul acesta consumatorul va ”rata” cu siguranta unele numere (in cazul

nostru aproape pe toate).

Page 156: Curs Practic de Java

156

Problema care se ridica in acest moment este: cine trebuie sa se ocupe de sincronizarea

celor doua fire de executie : clasele Producator si Consumator sau resursa comuna Buffer.

Raspunsul este evident: resursa comuna Buffer, deoarece ea trebuie sa permita sau nu

accesul la continutul sau si nu firele de executie care o folosesc. In felul acesta efortul

sincronizarii este transferat de la producator/consumator la un nivel mai jos, cel al resursei

critice.

Activitatile producatorului si consumatorului trebuie sincronizate la nivelul resursei

comune in doua privinte:

Cele doua fire de executie nu trebuie sa acceseze simultan buffer-ul; acest lucru se

realizeaza prin blocarea obiectului Buffer atunci cand este accesat de un fir de executie,

astfel incat nici nu alt fir de executie sa nu-l mai poata accesa (vezi ”Monitoare”).

Cele doua fire de executie trebuie sa se coordoneze, adica producatorul trebuie sa

gaseasca o modalitate de a ”spune” consumatorului ca a plasat o valoare in buffer, iar

consumatorul trebuie sa comunice producatorului ca a preluat aceasta valoare, pentru

ca acesta sa poata genera o alta. Pentru a realiza aceasta comunicare, clasa Thread

pune la dispozitie metodele wait, notify, notifyAll. (vezi ”Semafoare”).

Folosind sincronizarea clasa Buffer va arata astfel:

class Buffer {

private int number = -1;

private boolean available = false;

public synchronized int get() {

while (!available) {

try {

wait ();

// Asteapta producatorul sa puna o valoare

} catch (InterruptedException e) {

e. printStackTrace ();

}

}

available = false;

notifyAll ();

return number;

}

public synchronized void put(int number) {

while (available) {

try {

wait ();

// Asteapta consumatorul sa preia valoarea

} catch (InterruptedException e) {

Page 157: Curs Practic de Java

157

e. printStackTrace ();

}

}

this.number = number;

available = true;

notifyAll ();

}

}

Rezultatul obtinut va fi cel scontat: Producatorul a pus: 0 Consumatorul a primit: 0

Producatorul a pus: 1 Consumatorul a primit: 1 ... Producatorul a pus: 9 Consumatorul a primit:

9

9.3.6 Monitoare

Definitie : Un segment de cod ce gestioneaza o resursa comuna mai multor de fire de

executie separate concurente se numeste sectiune critica. In Java, o sectiune critica poate fi

un bloc de instructiuni sau o metoda.

Controlul accesului intr-o sectiune critica se face prin cuvantul cheie synchronized.

Platforma Java asociaza un monitor (”lacat”) fiecarui obiect al unui program aflat in executie.

Acest monitor va indica daca resursa critica este accesata de vreun fir de executie sau este

libera, cu alte cuvinte ”monitorizeaza” resursa respectiva. In cazul in care este accesata, va

pune un lacat pe aceasta, astfel incat sa impiedice accesul altor fire de executie la ea. In

momentul cand resursa este eliberata ”lacatul” va fi eliminat, pentru a permite accesul altor fire

de executie.

In exemplul de tip producator/consumator de mai sus, sectiunile critice sunt metodele put

si get iar resursa critica comuna este obiectul buffer. Consumatorul nu trebuie sa acceseze

buffer-ul cand producatorul tocmai pune o valoare in el, iar producatorul nu trebuie sa modifice

valoarea din buffer in momentul cand aceasta este citita de catre consumator.

public synchronized int get() {

...

}

public synchronized void put(int number) {

...

}

Sa observam ca ambele metode au fost declarate cu modificatorul synchronized. Cu

toate acestea, sistemul asociaza un monitor unei instante a clasei Buffer si nu unei metode

anume. In momentul in care este apelata o metoda sincronizata, firul de executie care a facut

apelul va bloca obiectul a carei metoda o acceseaza, ceea ce inseamna ca celelalte fire de

executie nu vor mai putea accesa resursele critice ale acelui obiect. Acesta este un lucru logic,

deoarece mai multe sectiuni critice ale unui obiect gestioneaza de fapt o singura resursa

Page 158: Curs Practic de Java

158

critica.

In exemplul nostru, atunci cand producatorul apeleaza metoda put pentru a scrie un

numar, va bloca tot obiectul buffer, astfel ca firul de executie consumator nu va avea acces la

metoda get, si reciproc.

public synchronized void put(int number) {

// buffer blocat de producator

...

// buffer deblocat de producator

}

public synchronized int get() {

// buffer blocat de consumator

...

// buffer deblocat de consumator

}

Monitoare fine

Adeseori, folosirea unui monitor pentru intreg obiectul poate fi prea restrictiva. De ce sa

blocam toate resursele unui obiect daca un fir de executie nu doreste decat accesarea uneia

sau a catorva dintre ele. Deoarece orice obiect are un monitor, putem folosi obiecte fictive ca

lacate pentru fiecare din resursele obiectului nostru, ca in exemplul de mai jos:

class MonitoareFine {

//Cele doua resurse ale obiectului

Resursa x, y;

//Folosim monitoarele a doua obiecte fictive

Object xLacat = new Object(),

yLacat = new Object();

public void metoda() {

synchronized(xLacat) {

// Accesam resursa x

}

// Cod care nu foloseste resursele comune

...

synchronized(yLacat) {

// Accesam resursa y

}

...

synchronized(xLacat) {

synchronized(yLacat) {

// Accesam x si y

}

Page 159: Curs Practic de Java

159

}

...

synchronized(this) {

// Accesam x si y

}

}

}

Metoda de mai sus nu a fost declarata cu synchronized ceea ce ar fi determinat blocarea

tuturor resurselor comune la accesarea obiectului respectiv de un fir de executie, ci au fost

folosite monitoarele unor obiecte fictive pentru a controla folosirea fiecarei resursa in parte.

9.3.7 Semafoare

Obiectul de tip Buffer din exemplul anterior are o variabila membra privata numita

number, in care este memorat numarul pe care il comunica producatorul si pe care il preia

consumatorul. De asemenea, mai are o variabila privata logica available care ne da starea

buffer-ului: daca are valoarea true inseamna ca producatorul a pus o valoare in buffer si

consumatorul nu a preluat-o inca; daca este false, consumatorul a preluat valoarea din buffer

dar producatorul nu a pus deocamdata alta la loc. Deci, la prima vedere, metodele clasei

Buffer ar trebui sa arate astfel:

public synchronized int get() {

while (!available) {

// Nimic -asteptam ca variabila sa devina true

}

available = false;

return number;

}

public synchronized int put(int number) {

while (available) {

// Nimic -asteptam ca variabila sa devina false

}

available = true;

this.number = number;

}

Varianta de mai sus, desi pare corecta, nu este. Aceasta deoarece implementarea

metodelor este ”selfish”, cele doua metode isi asteapta in mod egoist conditia de terminare. Ca

urmare, corectitudinea functionarii va depinde de sistemul de operare pe care programul este

rulat, ceea ce reprezinta

Page 160: Curs Practic de Java

160

o greseala de programare.

Punerea corecta a unui fir de executie in asteptare se realizeaza cu metoda wait a clasei

Thread, care are urmatoarele forme:

void wait( )

void wait( long timeout )

void wait( long timeout, long nanos )

Dupa apelul metodei wait, firul de executie curent elibereaza monitorul asociat obiectului

respectiv si asteapta ca una din urmatoarele conditii sa fie indeplinita:

Un alt fir de executie informeaza pe cei care ”asteapta” la un anumit monitor sa se

”trezeasca” -acest lucru se realizeaza printr-un apel al metodei notifyAll sau notify.

Perioada de astepatare specificata a expirat.

Metoda wait poate produce exceptii de tipul InterruptedException, atunci cand firul de

executie care asteapta (este deciin starea ”Not Runnable”) esteintrerupt din asteptare si trecut

fortatin starea ”Runnable”, desi conditia asteptata nu era inca indeplinita.

Metoda notifyAll informeaza toate firele de executie care suntin asteptare la monitorul

obiectului curentindeplinirea conditiei pe care o asteptau. Metoda notify informeaza doar un

singur fir de executie, specificat ca argument.

Reamintim varianta corecta a clasei Buffer:

class Buffer {

private int number = -1;

private boolean available = false;

public synchronized int get() {

while (!available) {

try {

wait ();

// Asteapta producatorul sa puna o valoare

} catch (InterruptedException e) {

e. printStackTrace ();

}

}

available = false;

notifyAll ();

return number;

}

public synchronized void put(int number) {

while (available) {

try {

wait ();

// Asteapta consumatorul sa preia valoarea

} catch (InterruptedException e) {

Page 161: Curs Practic de Java

161

e. printStackTrace ();

}

}

this.number = number;

available = true; notifyAll ();

}

}

9.3.8 Probleme legate de sincronizare

Din pacate, folosirea monitoarelor ridica si unele probleme. Sa analizam cateva dintre ele

si posibilele lor solutii:

Deadlock Deadlock-ul este o problema clasica intr-un mediu in care ruleaza mai multe fire

de executie si consta in faptul ca, la un moment dat, intreg procesul se poate bloca deoarece

unele fire asteapta deblocarea unor monitoare care nu se vor debloca niciodata. Exista

numeroase exemple in acest sens, cea mai cunoscuta fiind ”Problema filozofilor”.

Reformulata, sa ne imaginam doua persoane ”A” si ”B” (fire de executie) care stau la aceeasi

masa si trebuie sa foloseasca in comun cutitul si furculita (resursele comune) pentru a manca.

Evident, cele doua persoane doresc obtinerea ambelor resurse. Sa presupunem ca ”A” a

otinut cutitul si ”B” furculita. Firul ”A” se va bloca in asteptarea eliberarii furculitei iar firul ”A” se

va bloca in astepatrea eliberarii cutitului, ceea ce conduce la starea de ”deadlock”. Desi acest

exemplu este desprins de realitate, exista numeroase situatii in care fenomenul de ”deadlock”

se poate manifesta, multe dintre acestea fiind dificil de detectat.

Exista cateva reguli ce pot fi aplicate pentru evitarea deadlock-ului:

Firele de executie sa solicite resursele in aceeasi ordine. Aceasta abordare elimina

situatiile de asteptare circulara.

Folosirea unor monitoare care sa controleze accesul la un grup de resurse. In cazul

nostru, putem folosi un monitor ”tacamuri” care trebuie blocat inainte de a cere furculita

sau cutitul.

Folosirea unor variabile care sa informeze disponibilitatea resurselor fara a bloca

monitoarele asociate acestora.

Cel mai importat, conceperea unei arhitecturi a sistemului care sa evite pe cat posibil

aparitia unor potentiale situatii de deaslock.

Variabile volatile Cuvantul cheie volatile a fost introdus pentru a controla unele aspecte

legate de optimizarile efectuate de unele compilatoare. Sa consideram urmatorul exemplu:

Page 162: Curs Practic de Java

162

class TestVolatile {

boolean test;

public void metoda() {

test = false;

if (test) {

// Aici se poate ajunge...

}

}

}

Un compilator care optimizeaza codul, poate decide ca variabila test fiind setata pe false,

corpul if-ului nu se va executa si sa excluda secventa respectiva din rezultatul compilarii. Daca

aceasta clasa ar fi insa accesata de mai multe fire de executie, variabile test ar putea fi setata

pe true de un alt fir, exact intre instructiunile de atribuire si if ale firului curent.

Declararea unei variabile cu modificatorul volatile informeaza compilatorul sa nu optimizeze

codul in care aceasta apare, previzionand valoarea pe care variabila o are la un moment dat.

Fire de executie inaccesibile

Uneori firele de executie sunt blocate din alte motive decat asteptarea la un monitor, cea

mai frecventa situatie de acest tip fiind operatiunile de in-trare/iesire (IO) blocante. Cand acest

lucru se intampla celelalte fire de executie trebuie sa poata accesa in continuare obiectul. Dar

daca operatiunea IO a fost facuta intr-o metoda sincronizata, acest lucru nu mai este posibil,

monitorul obiectului fiind blocat de firul care asteapta de fapt sa realizeze operatia de

intrare/iesire. Din acest motiv, operatiile IO nu trebuie facute in metode sincronizate.

9.4 Gruparea firelor de executie

Gruparea firelor de executie pune la dispozitie un mecanism pentru manipularea

acestora ca un tot si nu individual. De exemplu, putem sa pornim sau sa suspendam toate

firele dintr-un grup cu un singur apel de metoda. Gruparea firelor de executie se realizeaza prin

intermediul clasei ThreadGroup.

Fiecare fir de executie Java este membru al unui grup, indiferent daca specificam explicit

sau nu acest lucru. Afilierea unui fir la un anumit grup se realizeaza la crearea sa si devine

permanenta, in sensul ca nu vom putea muta un fir dintr-un grup in altul, dupa ce acesta a fost

creat. In cazul in care cream un fir folosind un constructor care nu specifica din ce grup face

parte, el va fi plasat automat in acelasi grup cu firul de executie care l-a creat. La pornirea unui

program Java se creeaza automat un obiect de tip ThreadGroup cu numele main, care va

reprezenta grupul tuturor firelor de executie create direct din program si care nu au fost atasate

Page 163: Curs Practic de Java

163

explicit altui grup. Cu alte cuvinte, putem sa ignoram complet plasarea firelor de executie in

grupuri si sa lasam sistemul sa se ocupe cu aceasta, adunandu-le pe toate in grupul main.

Exista situatiiinsa cand gruparea firelor de executie poate usura substantial manevrarea

lor. Crearea unui fir de executie si plasarea lui intr-un grup (altul decat cel implicit) se

realizeaza prin urmatorii constructori ai clasei Thread:

public Thread(ThreadGroup group, Runnable target)

public Thread(ThreadGroup group, String name)

public Thread(ThreadGroup group, Runnable target, String name)

Fiecare din acesti costructori creeaza un fir de executie, il initializeaza si il plaseaza

intr-un grup specificat ca argument. Pentru a afla carui grup apartine un anumit fir de executie

putem folosi metoda getThreadGroup a clasei Thread. In exemplul urmator vor fi create doua

grupuri, primul cu doua fire de executie iar al doilea cu trei:

ThreadGroup grup1 = new ThreadGroup("Producatori");

Thread p1 = new Thread(grup1, "Producator 1");

Thread p2 = new Thread(grup1, "Producator 2");

ThreadGroup grup2 = new ThreadGroup("Consumatori");

Thread c1 = new Thread(grup2, "Consumator 1");

Thread c2 = new Thread(grup2, "Consumator 2");

Thread c3 = new Thread(grup2, "Consumator 3");

Un grup poate avea ca parinte un alt grup, ceea ce inseamna ca firele de executie pot fi

plasate intr-o ierarhie de grupuri, in care radacina este grupul implicit main, ca in figura de

mai jos:

Page 164: Curs Practic de Java

164

Sa consideram un exemplu im care listam firele de executie active:

public class TestThreadGroup {

static class Dummy implements Runnable {

public void run() { while (true) Thread . yield (); }

}

public static void main(String args[]) {

// Cream o fereastra pentru a fi create // automat firele de executie din AWT

java.awt.Frame f = new java.awt.Frame("Test");

// Cream un fir propriu

new Thread(new Dummy(), "Fir de test").start();

// Obtinem o referinta la grupul curent

Thread firCurent = Thread.currentThread();

ThreadGroup grupCurent = firCurent.getThreadGroup();

// Aflam numarul firelor de executie active

int n = grupCurent.activeCount();

// Enumeram firele din grup

Thread[] lista = new Thread[n];

grupCurent . enumerate ( lista );

// Le afisam

for (int i= 0; i < n; i++)

System.out.println("Thread #" + i + "=" + lista[i]. getName ());

}

}

9.5 Comunicarea prin fluxuri de tip ”pipe”

O modalitate deosebit de utila prin care doua fire de executie pot comunica este realizata

prin intermediul canalelor de comunicatii (pipes). Acestea sunt implementate prin fluxuri

descrise de clasele:

PipedReader, PipedWriter -pentru caractere, respectiv

PipedOutputStream, PipedInputStream -pentru octeti.

Fluxurile ”pipe” de iesire si cele de intrare pot fi conectate pentru a efectua transmiterea

datelor. Acest lucru se realizeaza uzual prin intemediul constructorilor:

public PipedReader(PipedWriterpw) public PipedWriter(PipedReaderpr)

Page 165: Curs Practic de Java

165

In cazul in care este folosit un constructor fara argumente, conectarea unui flux de intrare

cu un flux de iesire se face prin metoda connect:

public void connect(PipedWriterpw) public void connect(PipedReaderpr)

Intrucat fluxurile care sunt conectate printr-un pipe trebuie sa execute simultan operatii de

scriere/citire, folosirea lor se va face din cadrul unor fire de executie.

Functionarea obicetelor care instantiaza PipedWriter si PipedReader este asemanatoare

cu a canalelor de comunicare UNIX (pipes). Fiecare capat al unui canal este utilizat dintr-un fir

de executie separat. La un capat se scriu caractere, la celalalt se citesc. La citire, daca nu sunt

date disponibile firul de executie se va bloca pana ce acestea vor deveni disponibile. Se

observa ca acesta este un comportament tipic producator-consumator asincron, firele de

executie comunicand printr-un canal.

Realizarea conexiunii se face astfel:

PipedWriter pw1 = new PipedWriter();

PipedReader pr1 = new PipedReader(pw1);

// sau

PipedReader pr2 = new PipedReader();

PipedWriter pw2 = new PipedWriter(pr2);

// sau

PipedReader pr = new PipedReader();

PipedWriter pw = new PipedWirter();

pr.connect(pw)

//echivalent cu

pw.connect(pr);

Scrierea si citirea pe/de pe canale se realizeaza prin metodele uzuale read si write, in

toate formele lor.

Sa reconsideram acum exemplul producator/consumator prezentat anterior, folosind

canale de comunicatie. Producatorul trimite datele printr-un flux de iesire de tip

DataOutputStream catre consumator, care le primeste printr-un flux de intrare de tip

DataInputStream. Aceste doua fluxuri vor fi interconectate prin intermediul unor fluxuri de tip

”pipe”.

import java.io.*;

class Producator extends Thread {

private DataOutputStream out;

public Producator(DataOutputStream out) { this.out = out; }

public void run() {

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

try { out. writeInt (i);

} catch (IOException e) { e. printStackTrace (); }

System.out.println("Producatorul a pus:\t" + i);

Page 166: Curs Practic de Java

166

try {

sleep((int)(Math.random() * 100));

} catch (InterruptedException e) { }

}

}

}

class Consumator extends Thread {

private DataInputStream in;

public Consumator(DataInputStream in) { this.in = in; }

public void run() {

int value = 0;

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

try { value = in.readInt(); } catch (IOException e) {

e. printStackTrace (); }

System.out.println("Consumatorul a primit:\t" + value);

}

}

}

public class TestPipes {

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

PipedOutputStream pipeOut = new PipedOutputStream();

PipedInputStream pipeIn = new PipedInputStream(pipeOut);

DataOutputStream out = new DataOutputStream(pipeOut);

DataInputStream in = new DataInputStream(pipeIn);

Producator p1 = new Producator(out);

Consumator c1 = new Consumator(in);

p1. start ();

c1. start ();

}

}

9.6 Clasele Timer si TimerTask

Clasa Timer ofera o facilitate de a planifica diverse actiuni pentru a fi realizate la un

anumit moment de catre un fir de executie ce ruleaza in fundal. Actiunile unui obiect de tip

Timer sunt implementate ca instante ale clasei TimerTask si pot fi programate pentru o singura

executie sau pentru executii repetate la intervale regulate.

Page 167: Curs Practic de Java

167

Pasii care trebuie facuti pentru folosirea unui timer sunt:

Crearea unei subclase Actiune a lui TimerTask si supreadefinirea metodei run ce va

contine actiunea pe care vrem sa o planificam. Dupa cum vom vedea, pot fi folosite si

clase anonime.

Crearea unui fir de executie prin instantierea clasei Timer;

Crearea unui obiect de tip Actiune;

Planificarea la executie a obiectuluii de tip Actiune, folosind metoda schedule din clasa

Timer;

Metodele de planificare pe care le avem la dispozitie au urmatoarele for-mate:

schedule(TimerTask task, Date time)

schedule(TimerTask task, long delay, long period)

schedule(TimerTask task, Date time, long period)

scheduleAtFixedRate(TimerTask task, long delay, long period)

scheduleAtFixedRate(TimerTask task, Date time, long period)

unde, task descrie actiunea ce se va executa, delay reprezinta intarzierea fata de

momentul curent dupa care va incepe executia, time momentul exact la care va incepe

executia iar period intervalul de timp intre doua executii.

Dupa cum se observa, metodele de planificare se impart in doua categorii:

schedule -planificare cu intarziere fixa: daca dintr-un anumit motiv actiunea este

intarziata, urmatoarele actiuni vor fi si ele intarziate in consecinta;

scheduleAtFixedRate -planificare cu numar fix de rate: daca dintrun anumit motiv

actiunea este intarziata, urmatoarele actiuni vor fi executata mai repede, astfel incat

numarul total de actiuni dintr-o perioada de timp sa fie tot timpul acelasi;

Un timer se va opri natural la terminarea metodei sale run sau poate fi oprit fortat folosind

metoda cancel. Dupa oprirea sa el nu va mai putea fi folosit pentru planificarea altor actiuni. De

asemenea, metoda System.exit va oprit fortat toate firele de executie si va termina aplicatia

curenta.

import java.util.*;

import java.awt.*;

class Atentie extends TimerTask {

public void run() {

Toolkit . getDefaultToolkit ().beep ();

System .out. print(".");

}

}

Page 168: Curs Practic de Java

168

class Alarma extends TimerTask {

public String mesaj;

public Alarma(String mesaj) {

this.mesaj = mesaj;

}

public void run() {

System .out. println ( mesaj );

}

}

public class TestTimer {

public static void main(String args[]) {

// Setam o actiune repetitiva , cu rata fixa

final Timer t1 = new Timer();

t1.scheduleAtFixedRate(new Atentie(), 0, 1*1000);

// Folosim o clasa anonima pentru o alta actiune

Timer t2 = new Timer();

t2.schedule(new TimerTask() {

public void run () {

System.out.println("S-au scurs 10 secunde.");

// Oprim primul timer

t1. cancel ();

}

}, 10*1000);

// Setam o actiune pentru ora 22:30

Calendar calendar = Calendar.getInstance();

calendar.set(Calendar.HOUR _OF _DAY , 22);

calendar.set(Calendar.MINUTE, 30);

calendar.set(Calendar.SECOND, 0);

Date ora = calendar.getTime();

Timer t3 = new Timer();

t3.schedule(new Alarma("Toti copiii la culcare!"), ora);

}

}

Page 169: Curs Practic de Java

169

10 Programarea in retea

10.1 Introducere

Programarea in retea implica trimiterea de mesaje si date intre aplicatii ce ruleaza pe

calculatoare aflate intr-o retea locala sau conectate la Internet. Pachetul care ofera suport

pentru scrierea aplicatiilor de retea este java.net. Clasele din acest pachet ofera o modalitate

facila de programare in retea, fara a fi nevoie de cunostinte prealabile referitoare la

comunicarea efectiva intre calculatoare. Cu toate acestea, sunt necesare cateva notiuni

fundamentale referitoare la retele cum ar fi: protocol, adresa IP, port, socket.

Ce este un protocol ? Un protocol reprezinta o conventie de reprezentare a datelor

folosita in comunicarea intre doua calculatoare. Avand in vedere faptul ca orice informatie care

trebuie trimisa prin retea trebuie serializata astfel incat sa poata fi transmisa secvential, octet

cu octet, catre destinatie, era nevoie de stabilirea unor conventii (protocoale) care sa fie

folosite atat de calculatorul care trimite datele cat si de cel care le primeste, pentru a se

”intelege” intre ele.

Doua dintre cele mai utilizate protocoale sunt TCP si UDP.

TCP (Transport Control Protocol) este un protocol ce furnizeaza un flux sigur de date

intre doua calculatoare aflate in retea. Acest protocol asigura stabilirea unei conexiuni

permanente intre cele doua calculatoare pe parcursul comunicatiei. UDP (User Datagram

Protocol) este un protocol bazat pe pachete independente de date, numite datagrame, trimise

de la un calculator catre altul fara a se garanta in vreun fel ajungerea acestora la destinatie sau

ordinea in care acestea ajung. Acest protocol nu stabileste o conexiuna permanta intre cele

doua calculatoare.

Cum este identificat un calculator in retea ?

Orice calculator conectat la Internet este identificat in mod unic de adresa sa IP (IP este

acronimul de la Internet Protocol). Aceasta reprezinta un numar reprezentat pe 32 de biti,

uzual sub forma a 4 octeti, cum ar fi de exemplu: 193.231.30.131 si este numit adresa IP

numerica. Corespunzatoare unei adrese numerice exista si o adresa IP simbolica, cum ar fi

thor.infoiasi.ro pentru adresa numerica anterioara.

De asemenea, fiecare calculator aflat intr-o retea locala are un nume unic ce poat fi folosit

la identificarea locala a acestuia. Clasa Java care reprezinta notiunea de adresa IP este

InetAddress.

Ce este un port ?

Un calculator are in general o singura legatura fizica la retea. Orice informatie destinata

unei anumite masini trebuie deci sa specifice obligatoriu adresa IP a acelei masini. Insa pe un

calculator pot exista concurent mai multe procese care au stabilite conexiuni in retea,

asteptand diverse informatii. Prin urmare, datele trimise catre o destinatie trebuie sa specifice

pe langa adresa IP a calculatorului si procesul catre care se indreapta informatiile respective.

Page 170: Curs Practic de Java

170

Identificarea proceselor se realizeaza prin intermdiul porturilor.

Un port este un numar pe 16 biti care identifica in mod unic procesele care ruleaza pe o

anumita masina. Orice aplicatie care realizeaza o conexiune in retea va trebui sa ataseze un

numar de port acelei conexiuni. Valorile pe care le poate lua un numar de port sunt cuprinse

intre 0 si 65535 (deoarece sunt numere reprezentate pe 16 biti), numerele cuprinse intre 0 si

1023 fiind insa rezervate unor servicii sistem si, din acest motiv, nu trebuie folosite in aplicatii.

10.2 Lucrul cu URL-uri

Termenul URL este acronimul pentru Uniform Resource Locator si reprezinta

o referinta (adresa) la o resursa aflata pe Internet. Aceasta este in general un fisier

reprezentand o pagina Web, un text, imagine, etc., insa un URL poate referi si interogari la

baze de date, rezultate ale unor comenzi executate la distanta, etc. Mai jost, sunt prezentate

cateva exemple de URL-uri sunt:

http://java.sun.com

http://google.com

Dupa cum se observa din exemplele de mai sus, un URL are doua componente

principale:

Identificatorul protocolului folosit (http, ftp, etc);

Numele resursei referite. Acesta are urmatoarele componente:

Numele calculatorului gazda (www.infoiasi.ro).

Calea completa spre resursa referita ( acf/java/curs/9/prog retea.html). Notatia user

semnifica uzual subdirectorul html al directorului rezervat pe un server Web

utilizatorului specificat (HOME). In cazul in care este specificat doar un director, fisierul

ce reprezinta resursa va fi considerat implicit index.html.

Optional, o referinta de tip anchorin cadrul fisierului referit (#url).

Optional, portul la care sa se realizeze conexiunea.

Clasa care permite lucrul cu URL-uri este java.net.URL. Aceasta are mai multi

constructori pentru crearea de obiecte ce reprezinta referinte catre resurse aflate in retea, cel

mai uzual fiind cel care primeste ca parametru un sir de caractere. In cazul in care sirul nu

reprezinta un URL valid va fi aruncata o exceptie de tipul MalformedURLException.

try {

URL adresa = new URL("http://xyz.abc");

} catch(MalformedURLException ex){ }

Page 171: Curs Practic de Java

171

Un obiect de tip URL poate fi folosit pentru:

Aflarea informatiilor despre resursa referita (numele calculatorului gazda, numele

fisierului, protocolul folosit. etc).

Citirea printr-un flux a continutului fisierului respectiv.

Conectarea la acel URL pentru citirea si scrierea de informatii.

Citirea continutului unui URL Orice obiect de tip URL poate returna un flux de intrare de

tip InputStream pentru citirea continutului sau. Secventa standard pentru aceasta operatiune

este prezentata in exemplul de mai jos, in care afisam continutul resursei specificata la linia de

comanda. Daca nu se specifica mici un argument, va fi afisat fisierul index.html de la adresa:

http://java.sun.com

import java.net.*; import java.io.*;

public class CitireURL {

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

String adresa = "http://www.java.sun.com";

if (args.length > 0)

adresa = args [0];

BufferedReader br = null;

try {

URL url = new URL(adresa);

InputStream in = url.openStream();

br = new BufferedReader(new InputStreamReader(in));

String linie;

while ((linie = br.readLine()) != null) {

// Afisam linia citita

System .out. println ( linie);

}

} catch(MalformedURLException e) {

System.err.println("URL invalid !\n" + e);

} finally { br. close (); }

}

}

Conectarea la un URL Se realizeaza prin metoda openConnection ce stabileste o

conexiune bidirectionala cu resursa specificata. Aceasta conexiune este reprezentata de un

obiect de tip URLConnection, ce permite crearea atat a unui flux de intrare pentru citirea

informatiilor de la URL-ul specificat, cat si a unui flux de iesire pentru scrierea de date catre

acel URL. Operatiunea de trimitere de date dintr-un program catre un URL este similara cu

trimiterea de date dintr-un formular de tip FORM aflat intr-o pagina HTML. Metoda folosita

pentru trimitere este POST.

In cazul trimiterii de date, obiectul URL este uzual un proces ce ruleaza pe serverul Web

Page 172: Curs Practic de Java

172

referit prin URL-ul respectiv (jsp, servlet, cgi-bin, php, etc).

10.3 Socket-uri

Definitie : Un socket (soclu) este o abstractiune software folosita pentru a reprezenta

fiecare din cele doua ”capete” ale unei conexiuni intre doua procese ce ruleaza intr-o retea.

Fiecare socket este atasat unui port astfel incat sa poata identifica unic programul caruia ii sunt

destinate datele.

Socket-urile sunt de doua tipuri:

TCP, implementate de clasele Socket si ServerSocket;

UDP, implementate de clasa DatagramSocket.

O aplicatie de retea ce foloseste socket-uri se incadreaza in modelul client/server de

concepere a unei aplicatii. In acest model aplicatia este formata din doua categorii distincte de

programe numite servere, respectiv clienti.

Programele de tip server sunt cele care ofera diverse servicii eventualilor clienti, fiind in

stare de asteptare atata vreme cat nici un client nu le solicita serviciile. Programele de tip client

sunt cele care initiaza conversatia cu un server, solicitand un anumit serviciu. Uzual, un server

trebuie sa fie capabil sa trateze mai multi clienti simultan si, din acest motiv, fiecare cerere

adresata serverului va fi tratata intr-un fir de executie separat.

Incepand cu versiunea 1.4 a platformei standard Java, exista o clasa utilitara care

implementeaza o pereche de tipul (adresa IP, numar port). Aceasta este InetSocketAddress

(derivata din SocketAddress), obiectele sale fiind utilizate de constructori si metode definite in

cadrul claselor ce descriu socketuri, pentru a specifica cei doi parametri necesari identificarii

unui proces care trimite sau receptioneaza date in retea.

10.4 Comunicarea prin conexiuni

In acest model se stabileste o conexiune TCP intre o aplicatie client si o aplicatie server

care furnizeaza un anumit serviciu. Avantajul protocolul TCP/IP este ca asigura realizarea unei

comunicari stabile, permanente in retea, existand siguranta ca informatiile trimise de un proces

vor fi receptionate corect si complet la destinatie sau va fi semnalata o exceptie in caz contrar.

Legatura intre un client si un server se realizeaza prin intermediul a doua obiecte de tip Socket,

cate unul pentru fiecare capat al ”canalului” de comunicatie dintre cei doi. La nivelul clientului

crearea socketului se realizeaza specificand adresa IP a serverului si portul la care ruleaza

acesta, constructorul uzual folosit fiind:

Page 173: Curs Practic de Java

173

Socket(InetAddress address, int port)

La nivelul serverului, acesta trebuie sa creeze intai un obiect de tip ServerSocket. Acest

tip de socket nu asigura comunicarea efectiva cu clientii ci este responsabil cu ”ascultarea”

retelei si crearea unor obiecte de tip Socket pentru fiecare cerere aparuta, prin intermediul

caruia va fi realizata legatura cu clientul. Crearea unui obiect de tip ServerSocket se face

specificand portul la care ruleaza serverul, constructorul folosit fiind:

ServerSocket(int port)

Metoda clasei ServerSocket care asteapta ”asculta” reteaua este accept. Aceasta

blocheaza procesul parinte pana la aparitia unui cereri si returneaza un nou obiect de tip

Socket ce va asigura comunicarea cu clientul. Blocarea poate sa nu fie permanenta ci doar

pentru o anumita perioada de timp aceasta va fi specificata prin metoda setSoTimeout, cu

argumentul dat in milisecunde.

Pentru fiecare din cele doua socketuri deschise pot fi create apoi doua fluxuri pe octeti

pentru citirea, respectiv scrierea datelor. Acest lucru se realizeaza prin intermediul metodelor

getInputStream, respectuv getOutputStream. Fluxurile obtinute vor fi folosite impreuna cu

fluxuri de procesare care sa asigure o comunicare facila intre cele doua procese. In functie de

specificul aplicatiei acestea pot fi perechile:

BufferedReader, BufferedWriter si PrintWriter -pentru comunicare prin intermediul

sirurilor de caractere;

DataInputStream, DataOutputStream -pentru comunicare prin date primitive;

ObjectInputStream, ObjectOutputStream -pentru cominicare prin intermediul obiectelor;

Structura generala a unui server bazat pe conexiuni este:

1 Creeaza un obiect de tip ServerSocket la un anumit port

2 Asteapta realizarea unei conexiuni cu un client, folosind metoda accept; (va fi creat un

obiect nou de tip Socket)

3 Trateaza cererea venita de la client:

3.1 Deschide un flux de intrare si primeste cererea

3.2 Deschide un flux de iesire si trimite raspunsul

Page 174: Curs Practic de Java

174

3.3 Inchide fluxurile si socketul nou creat

Este recomandat ca tratarea cererilor sa se realizeze in fire de executie separate, pentru

ca metoda accept sa poata fi reapelata cat mai repede in vederea stabilirii conexiunii cu un alt

client

.

Structura generala a unui client bazat pe conexiuni este:

1 Citeste sau declara adresa IP a serverului si portul la care acesta ruleaza;

2 Creeaza un obiect de tip Socket cu adresa si portul specificate;

3 Comunica cu serverul:

3.1 Deschide un flux de iesire si trimite cererea;

3.2 Deschide un flux de intrare si primeste raspunsul;

3.3 Inchide fluxurile si socketul creat;

In exemplul urmator vom implementa o aplicatie client-server folosind comunicarea prin

conexiuni. Clientul va trimite serverului un nume iar acesta va raspunde prin mesajul ”Hello

nume”. Tratarea cererilor se va face in fire de executie separate.

import java.net.*; import java.io.*;

class ClientThread extends Thread {

Socket socket = null;

public ClientThread(Socket socket) {

this.socket = socket;

}

public void run() {

// Executam solicitarea clientului

String cerere, raspuns;

try {

// in este fluxul de intrare de la client

BufferedReader in = new BufferedReader(new InputStreamReader(

socket.getInputStream() ));

// out este flux de iesire catre client

PrintWriter out = new PrintWriter( socket . getOutputStream ());

// Primim cerere de la client

cerere = in.readLine();

// Trimitem raspuns clientului

raspuns = "Hello " + cerere + "!";

out. println ( raspuns );

out.flush ();

} catch (IOException e) { System.err.println("Eroare IO \n" + e);

} finally {

// Inchidem socketul deschis pentru clientul curent

try { socket . close (); } catch (IOException e) {

Page 175: Curs Practic de Java

175

System.err.println("Socketul nu poate fi inchis \n" + e); }

}

}

}

public class SimpleServer {

// Definim portul pe care se gaseste serverul // (in afara intervalului 1 -1024)

public static final int PORT = 8100;

public SimpleServer() throws IOException {

ServerSocket serverSocket = null;

try {

serverSocket = new ServerSocket(PORT);

while (true) {

System.out.println("Asteptam un client...");

Socket socket = serverSocket.accept();

// Executam solicitarea clientului intr -un fir de executie

ClientThread t = new ClientThread(socket);

t. start ();

}

} catch (IOException e) { System.err.println("Eroare IO \n" + e);

} finally { serverSocket . close ();

}

}

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

SimpleServer server = new SimpleServer();

}

}

10.5 Comunicarea prin datagrame

In acest model nu exista o conexiune permanenta intre client si server prin intermediul

careia sa se realizeze comunicarea. Clientul trimite cererea catre server prin intermediul unuia

sau mai multor pachete de date independente, serverul le receptioneaza, extrage informatiile

continute si returneaza raspunsul tot prin intermediul pachetelor. Un astfel de pachet se

numeste datagrama si este reprezentat printr-un obiect din clasa DatagramPacket. Rutarea

datagramelor de la o masina la alta se face exclusiv pe baza informatiilor continute de acestea.

Primirea si trimiterea datagramelor se realizeaza prin intermediul unui socket, modelat prin

intermediul clasei DatagramSocket.

Page 176: Curs Practic de Java

176

Dupa cum am mentionat deja, dezavantajul acestei metode este ca nu garanteaza

ajungerea la destinatie a pachetelor trimise si nici ca vor fi primite in aceeasi ordinie in care au

fost expediate. Pe de alta parte, exista situatii in care aceste lucruri nu sunt importante si acest

model este de preferat celui bazat pe conexiuni care solicita mult mai mult atat serverul cat si

clientul. De fapt, protocolul TCP/IP foloseste tot pachete pentru trimiterea informatiilor dintr-un

nod in altul al retelei, cu deosebirea ca asigura respectarea ordinii de transmitere a mesajelor

si verifica ajungerea la destinatie a tuturor pachetelor -in cazul in care unul nu a ajuns, acesta

va fi retrimis automat.

Clasa DatagramPacket contine urmatorii constructori:

DatagramPacket(byte[] buf, int length, InetAddress address, int port)

DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)

DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)

DatagramPacket(byte[] buf, int length, SocketAddress address)

DatagramPacket(byte[] buf, int length) DatagramPacket(byte[] buf, int offset, int

length)

Primele doua perechi de constructori sunt pentru creare pachetelor ce vor fi expediate,

diferenta intre ele fiind utilizarea claselor InetAddress, respectiv SocketAddress pentru

specificarea adresei desinatie.

A trei pereche de constructori este folosita pentru crearea unui pachet in care vor fi

receptionate date, ei nespecificand vreo sursa sau destinatie.

Dupa crearea unui pachet procesul de trimitere si primire a acestuia implica apelul

metodelor send si receive ale clasei DatagramSocket. Deoarece toate informatii sunt incluse in

datagrama, acelasi socket poate fi folosit atat pentru trimiterea de pachete, eventual catre

destinatii diferite, cat si pentru receptionarea acestora de la diverse surse. In cazul in care

refolosim pachete, putem schimba continutul acestora cu metoda setData, precum si adresa la

care le trimitem prin setAddress, setPort si setSocketAddress.

Extragerea informatiilor contiunte de un pachet se realizeaza prin metoda getData din

clasa DatagramPacket. De asemenea, aceasta clasa ofera metode pentru aflarea adresei IP si

a portului procesului care a trimis datagrama, pentru a-i putea raspunde daca este necesar.

Acestea sunt: getAdress, getPort si getSocketAddress.

Page 177: Curs Practic de Java

177

import java.net.*; import java.io.*;

public class DatagramServer {

public static final int PORT = 8200;

private DatagramSocket socket = null; DatagramPacket cerere , raspuns = null;

public void start() throws IOException {

socket = new DatagramSocket(PORT);

try {

while (true) {

// Declaram pachetul in care va fi receptionata cererea

byte[] buf = new byte[256];

cerere = new DatagramPacket(buf, buf.length);

System.out.println("Asteptam un pachet...");

socket . receive ( cerere);

// Aflam adresa si portul de la care vine cererea

InetAddress adresa = cerere.getAddress();

int port = cerere.getPort();

// Construim raspunsul

String mesaj = "Hello " + new String(cerere.getData() );

buf = mesaj.getBytes();

// Trimitem un pachet cu raspunsul catre client

raspuns = new DatagramPacket(buf, buf.length, adresa, port);

socket.send( raspuns );

}

} finally { if (socket != null) socket. close (); }

}

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

new DatagramServer().start();

}

}

import java.net.*; import java.io.*;

public class DatagramClient {

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

// Adresa IP si portul la care ruleaza serverul

InetAddress adresa = InetAddress.getByName("127.0.0.1");

int port = 8200;

DatagramSocket socket = null; DatagramPacket packet = null; byte buf [];

try {

// Construim un socket pentru comunicare

socket = new DatagramSocket();

// Construim si trimitem pachetul cu cererea catre server

Page 178: Curs Practic de Java

178

buf = "Duke".getBytes();

packet = new DatagramPacket (buf , buf.length , adresa ,port ); socket . send ( packet ); // Asteaptam pachetul cu raspunsul de la server buf = new byte [256]; packet = new DatagramPacket (buf , buf. length ); socket . receive ( packet ); // Afisam raspunsul (" Hello Duke !") System . out. println ( new String ( packet . getData ()));

} finally { if ( socket != null ) socket . close ();

} }

}

10.6 Trimiterea de mesaje catre mai multi client

Diverse situatii impun gruparea mai multor clienti astfel incat un mesaj (pachet) trimis pe

adresa grupului sa fie receptionat de fiecare dintre acestia. Gruparea mai multor programe in

vederea trimiterii multiple de mesaje se realizeaza prin intermediul unui socket special, descris

de clasa Multicast-Socket, extensie a clasei DatagramSocket.

Un grup de clienti abonati pentru trimitere multipla este specificat printr-o adresa IP din

intervalul 224.0.0.1 -239.255.255.255 si un port UDP. Adresa 224.0.0.0 este rezervata si nu

trebuie folosita.

Page 179: Curs Practic de Java

179

import java.net.*; import java.io.*;

public class MulticastClient {

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

// Adresa IP si portul care reprezinta grupul de clienti

InetAddress group = InetAddress.getByName("230.0.0.1");

int port = 4444;

MulticastSocket socket = null;

byte buf [];

try {

// Ne alaturam grupului aflat la adresa si portul specificate

socket = new MulticastSocket(port);

socket . joinGroup ( group );

// Asteaptam un pachet venit pe adresa grupului

buf = new byte[256];

DatagramPacket packet = new DatagramPacket(buf, buf. length );

System.out.println("Asteptam un pachet...");

socket . receive ( packet);

System.out.println(new String(packet.getData()).trim()) ;

} finally {

if (socket != null) { socket . leaveGroup (group ); socket . close ();

}

}

}

}

import java.net.*; import java.io.*;

public class MulticastSend {

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

InetAddress grup = InetAddress.getByName("230.0.0.1");

int port = 4444; byte[] buf; DatagramPacket packet = null;

// Cream un socket cu un numar oarecare

DatagramSocket socket = new DatagramSocket (0);

try {

// Trimitem un pachet catre toti clientii din grup

buf = (new String("Salut grup!")).getBytes();

packet = new DatagramPacket(buf, buf.length, grup, port );

socket .send ( packet );

} finally { socket .close (); }

}

}