Documente Academic
Documente Profesional
Documente Cultură
Silviu DUMITRESCU
S
ALGORITMICA
I PROGRAMARE
Curs si probleme de seminar
JAVA
Brasov 2002
Cuprins
1 Instalarea mediului Java
1.1 Obtinerea mediului Java pentru platforma dumneavoastra
1.1.1 Medii de dezvoltare integrata . . . . . . . . . . . .
1.2 Instalarea mediului Java . . . . . . . . . . . . . . . . . . .
1.2.1 Instructiuni de instalare pentru Windows . . . . .
1.2.2 Instructiuni de instalare pentru Linux/Unix . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
6
6
6
7
7
7
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
9
9
9
10
10
10
10
10
11
12
12
12
13
14
14
14
15
15
15
16
18
18
19
20
21
22
23
23
CUPRINS
2.7
Probleme propuse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
3 Referinte
3.1 Ce este o referinta . . . . . . . . . . . . . . . . . .
3.2 Fundamente despre obiecte si referinte . . . . . . .
3.2.1 Operatorul punct (.) . . . . . . . . . . . . .
3.2.2 Declararea obiectelor . . . . . . . . . . . . .
3.2.3 Colectarea de gunoaie (garbage collection) .
3.2.4 Semnificatia lui = . . . . . . . . . . . . . .
3.2.5 Transmiterea de parametri . . . . . . . . .
3.2.6 Semnificatia lui == . . . . . . . . . . . . .
3.2.7 Suprancarcarea operatorilor pentru obiecte
3.3 Siruri de caractere (stringuri) . . . . . . . . . . . .
3.3.1 Fundamentele utilizarii stringurilor . . . . .
3.3.2 Concatenarea stringurilor . . . . . . . . . .
3.3.3 Comparatia stringurilor . . . . . . . . . . .
3.3.4 Alte metode pentru stringuri . . . . . . . .
3.3.5 Conversia de la string la tipurile primitive .
3.4 Siruri . . . . . . . . . . . . . . . . . . . . . . . . .
3.4.1 Declaratie, Atribuire si Metode . . . . . . .
3.4.2 Expansiunea dinamica a sirurilor . . . . . .
3.4.3 Siruri cu mai multe dimensiuni . . . . . . .
3.4.4 Argumente n linie de comanda . . . . . .
3.5 Tratarea exceptiilor . . . . . . . . . . . . . . . . . .
3.5.1 Procesarea exceptiilor . . . . . . . . . . . .
3.5.2 Exceptii uzuale . . . . . . . . . . . . . . . .
3.6 Intrare si iesire . . . . . . . . . . . . . . . . . . . .
3.6.1 Operatii de baza pe fluxuri (stream-uri) . .
3.6.2 Obiectul StringTokenizer . . . . . . . . . .
3.6.3 Fisiere secventiale . . . . . . . . . . . . . .
3.7 Probleme propuse . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
25
25
27
27
27
28
28
29
30
30
30
30
31
32
32
32
33
33
35
38
38
39
39
41
42
42
43
44
46
4 Obiecte si clase
4.1 Ce este programarea orientata pe obiecte?
4.2 Un exemplu simplu . . . . . . . . . . . . .
4.3 Metode uzuale . . . . . . . . . . . . . . .
4.3.1 Constructori . . . . . . . . . . . .
4.3.2 Modificatori si Accesori . . . . . .
4.3.3 Afisare si toString . . . . . . . . .
4.3.4 Metoda equals . . . . . . . . . . .
4.3.5 Variabile si metode statice . . . . .
4.3.6 Metoda main . . . . . . . . . . . .
4.4 Pachete . . . . . . . . . . . . . . . . . . .
4.4.1 Directiva import . . . . . . . . . .
4.4.2 Instructiunea package . . . . . . .
4.4.3 Variabila sistem CLASSPATH . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
47
47
48
50
50
52
52
53
53
53
53
54
55
55
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
CUPRINS
4.5
4.6
5 Mostenire
5.1 Ce este mostenirea? . . . . . . . . . . .
5.2 Sintaxa de baza Java . . . . . . . . . . .
5.2.1 Reguli de vizibilitate . . . . . . .
5.2.2 Constructor si super . . . . . . .
5.2.3 Metode si clase final . . . . . . .
5.2.4 Redefinirea unei metode . . . . .
5.2.5 Metode si clase abstracte . . . .
5.3 Exemplu: Extinderea clasei Shape . . .
5.4 Mostenire multipla . . . . . . . . . . . .
5.5 Interfete . . . . . . . . . . . . . . . . . .
5.5.1 Definirea unei interfete . . . . . .
5.5.2 Implementarea unei interfete . .
5.5.3 Interfete multiple . . . . . . . . .
5.6 Implementarea de componente generice
5.7 Anexa - clasa Reader . . . . . . . . . . .
5.8 Probleme propuse . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. . . . .
. . . . .
timpului
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
56
56
56
56
57
58
58
59
59
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
68
68
71
71
72
72
73
74
76
81
81
82
82
83
84
87
88
90
. . . . . . . . . . . . 90
. . . . . . . . . . . . 92
de executie al unui
. . . . . . . . . . . . 92
. . . . . . . . . . . . 94
. . . . . . . . . . . . 94
. . . . . . . . . . . . 95
. . . . . . . . . . . . 96
. . . . . . . . . . . . 97
. . . . . . . . . . . . 97
. . . . . . . . . . . . 97
. . . . . . . . . . . . 98
. . . . . . . . . . . . 100
. . . . . . . . . . . . 101
. . . . . . . . . . . . 103
CUPRINS
7 Structuri de date
7.1 De ce avem nevoie de structuri de date?
7.2 Stive . . . . . . . . . . . . . . . . . . . .
7.3 Cozi . . . . . . . . . . . . . . . . . . . .
7.4 Liste nlantuite . . . . . . . . . . . . . .
7.5 Arbori binari de cautare . . . . . . . . .
7.6 Tabele de repartizare . . . . . . . . . . .
7.7 Cozi de prioritate . . . . . . . . . . . . .
7.8 Aplicatie . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
8 Metoda Backtracking
8.1 Prezentare generala . . . . . . . . . . . . . . . . . . . . . . . . .
8.2 Prezentarea metodei . . . . . . . . . . . . . . . . . . . . . . . .
8.2.1 Atribuie si avanseaza . . . . . . . . . . . . . . . . . . . .
8.2.2 Incercare esuata . . . . . . . . . . . . . . . . . . . . . .
8.2.3 Revenire . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.2.4 Revenire dupa construirea unei solutii . . . . . . . . . .
8.3 Implementarea metodei backtracking . . . . . . . . . . . . . . .
8.4 Probleme clasice care admit rezolvare prin metoda backtracking
8.4.1 Problema generarii permutarilor . . . . . . . . . . . . .
8.4.2 Generarea aranjamentelor si a combinarilor . . . . . . .
8.4.3 Problema damelor . . . . . . . . . . . . . . . . . . . . .
8.4.4 Problema colorarii hartilor . . . . . . . . . . . . . . . .
8.5 Probleme propuse . . . . . . . . . . . . . . . . . . . . . . . . . .
9 Divide et Impera
9.1 Notiuni elementare referitoare la recursivitate
9.1.1 Functii recursive . . . . . . . . . . . .
9.1.2 Recursivitatea nu nseamna recurenta
9.2 Prezentarea metodei Divide et Impera . . . .
9.3 Cautare binara . . . . . . . . . . . . . . . . .
9.4 Sortarea prin interclasare (MergeSort) . . . .
9.5 Sortarea rapida (QuickSort) . . . . . . . . . .
9.6 Expresii aritmetice . . . . . . . . . . . . . . .
9.7 Probleme propuse . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
10 Algoritmi Greedy
10.1 Problema spectacolelor (selectarea activitatilor) .
10.1.1 Demonstrarea corectitudinii algoritmului .
10.2 Elemente ale strategiei Greedy . . . . . . . . . .
10.2.1 Proprietatea de alegere Greedy . . . . . .
10.2.2 Substructura optima . . . . . . . . . . . .
10.3 Minimizarea timpului mediu de asteptare . . . .
10.4 Interclasarea optima a mai multor siruri ordonate
10.5 Probleme propuse . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
107
107
109
111
113
117
121
123
124
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
129
. 129
. 130
. 133
. 133
. 134
. 134
. 135
. 137
. 137
. 138
. 140
. 141
. 143
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
153
. 153
. 153
. 157
. 159
. 160
. 161
. 162
. 164
. 167
.
.
.
.
.
.
.
. .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
172
172
173
174
176
177
177
178
181
Capitolul 1
1.1
Mediul de baza Java consta dintr-un browser web unde puteti vizualiza applet-urile Java,
un compilator Java ce transforma codul sursa Java n cod binar si un interpretor pentru
rularea programelor Java. Aveti nevoie de asemenea de un editor de texte ca emacs,
TextPad sau BBEdit. Alte unelte ca debugger, un mediu vizual de dezvoltare etc. nu sunt
absolut necesare.
Nu este necesar sa luati toate partile de la aceeasi sursa. De obicei, browser-ul vostru
de web va fi Internet Explorer sau Netscape. Celelalte le puteti obtine de la Suns Java
Developer Kit (JDK). Sun publica versiuni pentru toate platformele (Windows, Solaris,
X86 Linux).
JDK nu include un browser web dar contine un applet viewer pentru testarea applet-urilor.
JDK include de asemenea compilatorul javac, interpretorul java, profiler-ul javaprof, generatotul fisierelor C de tip header (pentru integrarea metodelor scrise n C ntr-o clasa Java)
javah precum si depanatorul Java si generatorul de documentatie. Mai multa documentatie
despre JDK puteti gasi n pagina de web a firmei Sun.
Sun furnizeaza masina virtuala Java pentru Solaris, X86 Linux si Wndows 95, 98, NT.
Pentru aceasta lucrare aveti nevoie de Java 2 Software Development Kit, versiunea 1.2
(JDK 1.2) sau urmatoarele. Versiunea JDK 1.3, utilizata si ea destul de des, nu este
disponibila pentru toate platformele. Totusi, diferentele ntre JDK1.2 si JDK1.3 nu sunt
foarte importante.
1.1.1
Posibilitatile de dezvoltare integrata ale aplicatiilor Java sunt nca primitive n comparatie
cu ceea ce este disponibil pentru C++. Se pare ca cel putin deocamdata mediile de
dezvoltare integrata (IDE-Integrated Development Environments) nu sunt foarte performante. Acestea includ Metrowerks Code Warrior, Borland JBuilder, WinGate Visual Cafe
6
1.2
1.2.1
Stergeti mai ntai toate variantele de JDK pe care le aveti deja instalate, mai ales dac
a
doriti sa puneti noul JDK ntr-un alt director. De asemenea, trebuie sa folositi regedit
pentru a sterge toate cheile anterior instalate.
Aveti nevoie de aproximativ 60 MB de spatiu de memorie liber pentru instalarea JDKului. Executati un dublu clic pe icoana din File Manager sau selectand Run... din meniul
Program Manager File editati calea catre fisier. Aceasta va poduce dezarhivarea incluzand
toate directoarele si subdirectoarele necesare. Vom presupune ca instalarea s-a facut n
C:\jdk.
Este necesar sa adaugati directotul C:\jdk\bin variabilei de mediu PATH. De exemplu:
C:\>set PATH="c:\jdk\bin;$PATH"
Acest lucru poate fi realizat permanent prin introducerea comenzii anterioare n fisierul
autoexec.bat.
Pentru a va asigura ca mediul vostru Java este corect configurat, deschideti o fereastr
a
DOS si editati javac nofile.java. Astfel:
C:\>javac nofile.java
Daca primiti raspunsul:
error: Cant read: nofile.java
atunci instalarea a fost facuta cu succes. Daca primiti raspunsul:
The name specified is not recognized as an
internal or external command, operable program or batch file.
sau ceva similar atunci mediul Java nu a fost bine instalat sau variabila PATH nu are o
valoare corecta. Trebuie rezolvate aceste probleme nainte de a continua.
1.2.2
Aveti nevoie de aproximativ 60 MB de spatiu de memorie liber pentru instalarea JDKului dar dublu ar fi de mare ajutor. Modul de dezarhivare dintr-un fisier gzipped tar este
urmatorul:
% gunzip jk1_2_2-linux-i386.tar.gz
% tar xvf jdk1_2_2-linux-i386.tar
Numele exact al fisierului poate fi un pic modificat daca folositi o platforma diferita ca si
Irix sau o versiune diferita.
Puteti face dezarhivarea n directorul curent sau daca aveti drepturi de root ntr-un alt
loc ca de exemplu /usr/local/java unde toti utilizatorii pot avea acces la fisiere. Oricum
drepturile de root nu sunt necesare pentru a instala Java.
Dezarhivarea creaza toate directoarele si subdirectoarele necesare. Calea exacta nu este
importanta, dar pentru simplitate vom presupune n continuare ca instalarea s-a facut n
/usr/local/java. Veti gasi fisierele n /usr/local/java/jdk1.2.2. Daca dezarhivati o versiune
diferita, atunci fisierele vor fi ntr-o cale usor modificata ca de exemplu /usr/local/java/jdk1.3.
Este posibil ca mai multe versiuni de JDK sa coexiste armonios n acelasi sistem. Daca
dezarhivati altundeva decat /usr/local/java trebuie sa nlocuiti /usr/local/java cu calea
completa pana la directorul java. Daca instalati n directorul curent puteti folosi /java
n loc de calea completa.
Acum trebuie sa adaugati directorul /usr/local/java/jdk1.2.2/bin variabilei de mediu PATH.
Acest lucru se poate face astfel dependent de shell-ul vostru:
csh,tcsh:
% set PATH=($PATH/usr/local/java/jdk1.2.2/bin)
sh:
% PATH=($PATH/usr/local/java/bin); export $PATH
Puteti sa ad
augati liniile anterioare la sfarsitul fisierelor .profile sau .cshrc pentru a nu le
mai scrie la fiecare login-are.
Pentru a va asigura ca mediul vostru Java este corect configurat, editati javac nofile.java
la prompt-ul vostru shell:
% javac nofile.java
Daca primiti raspunsul:
error: Cant read: nofile.java
atunci instalarea a fost facuta cu succes. Daca primiti raspunsul:
javac: Command not found
sau ceva similar atunci mediul Java nu a fost bine instalat sau variabila PATH nu are o
valoare corecta. Trebuie rezolvate aceste probleme nainte de a continua.
Capitolul 2
Notiuni fundamentale de
programare in Java
2.1
Codul sursa Java este continut n fisiere text care au extensia .java. Compilatorul local,
care este de obicei javac sau jikes1 , compileaza programul si genereaza fisiere .class care
contin byte-code. Byte-code este un limbaj intermediar portabil care este interpretat de
catre interpretorul de Java, numit java.
2.2
Sa ncepem prin a examina programul simplu din Figura 2.1. Acest program tipareste
un scurt mesaj pe ecran. Numerele din stanga fiecarei linii nu fac parte din program. Ele
sunt furnizate doar pentru o mai usoara referire a secventelor de cod.
Transpuneti programul ntr-un fisier cu numele FirstProgram.java2 dupa care compilati-l
si rulati-l. Java este case-sensitive, ceea ce nseamna ca face deosebirile ntre literele mari
si mici.
1.
2.
3.
4.
5.
6.
7.
8.
//Primul program
public class FirstProgram
{
public static void main(String[] args)
{
System.out.println("Primul meu program Java") ;
}
}
10
2.2.1
CAPITOLUL 2. NOT
IUNI FUNDAMENTALE DE PROGRAMARE IN JAVA
Comentarii
In Java exista trei tipuri de comentarii. Prima forma, care este mostenita de la C ncepe
cu /* si se termina cu */. Iata un exemplu:
1. /* Acesta este un comentariu
2. pe doua linii */
Comentariile nu pot fi imbricate, deci nu putem avea un comentariu n interiorul altui
comentariu.
Cea de-a doua forma de comentarii este mostenita de la limbajul C++ si ncepe cu //.
Nu exista simbol pentru ncheiere, deoarece un astfel de comentariu se extinde automat
pana la sfarsitul liniei curente. Acest comentariu este folosit n linia 1 din Figura 2.1.
Cea de-a treia forma este asemanatoare cu prima doar ca ncepe cu /** n loc de /*.
Acesta forma de comentariu este utilizata pentru a furniza informatii utilitarului javadoc.
Comentariile au fost introduse pentru a face codul mai lizibil pentru programatori. Un
program bine comentat reprezinta un semn al unui bun programator.
2.2.2
Functia main
Un program Java consta dintr-o colectie de clase care interactioneaza ntre ele prin intermediul metodelor. Echivalentul Java al unei proceduri sau functii din Pascal sau C este
metoda static
a, pe care o vom descrie mai pe larg n acest capitol. Atunci cand se executa
un program Java, va fi invocata automat metoda statica main. Linia 4 din Figura 2.1
arata ca metoda main poate fi eventual invocata cu anumiti parametri n linia de comanda.
Tipul parametrilor functiei main cat si tipul functiei, void, sunt obligatorii.
2.2.3
Scrierea pe ecran
Programul din Figura 2.1 consta dintr-o singura instructiune, aflata la linia 6. Functia
println reprezinta principalul mecanism de scriere n Java, fiind echivalent ntr-o anumita
masura cu functia writeln din Pascal sau printf din C. In aceasta situatie se scrie un sir
de caractere la fluxul de iesire standard System.out. Vom discuta despre citire/scriere mai
tarziu. Deocamdata ne multumim doar sa amintim ca aceeasi sintaxa este folosita pentru
a scrie orice fel de entitate, fie ca este vorba despre un ntreg, real, sir de caractere sau alt
tip.
2.3
Java defineste opt tipuri primitive de date, oferind de asemenea, o foarte mare flexibilitate n a defini noi tipuri de date, numite clase. Totusi n Java, exista cateva diferente
estentiale ntre tipurile de date primitive si cele definite de utilizator. In aceasta sectiune
vom examina tipurile primitive si operatiile fundamentale care pot fi realizate asupra lor.
2.3.1
Tipurile primitive
11
Tip de data
Ce retine
Valori
byte
ntreg pe 8 biti
-128 la 127
short
ntreg pe 16 biti
-32768 la 32767
int
ntreg pe 32 biti
-2.147.483.648 la 2.147.483.647
long
ntreg pe 64 biti
263 la 263 1
float
virgula mobila pe 32 biti
6 cifre semnificative, (1046 la 1038 )
double
virgula mobila pe 64 biti 15 cifre semnificative, (10324 la 10308 )
char
caracter unicode
boolean
variabila booleana
false si true
Figura 2.2 Cele opt tipuri de date primitive n Java
Cel mai des utilizat este tipul ntreg specificat prin cuvantul cheie int. Spre deosebire
de majoritatea altor limbaje de programare, marja de valori a tipurilor ntregi nu este
dependenta de masina. Java accepta si tipurile ntregi byte, short si long. Numerele reale
(virgul
a mobila) sunt reprezentate de tipurile float si double. Tipul double are mai multe
cifre semnficative, de aceea utilizarea lui este recomandata n locul tipului float. Tipul char
este folosit pentru a reprezenta caractere. Un char ocupa 16 biti pentru a putea reprezenta
standardul Unicode. Standardul Unicode contine peste 30.000 de caractere distincte care
acoper
a principalele limbi scrise (inclusiv Japoneza, Chineza etc.). Prima parte a tabelei
Unicode este identica cu tabela ASCII. Ultimul tip primitiv este boolean; o variabila de tip
boolean poate lua una din valorile true sau false.
2.3.2
Constante
Constantele ntregi pot fi reprezentate n bazele 10, 8 sau 16. Notatia octal
a este indicata printr-un 0 nesemnificativ la nceput; notatia hexa este indicata printr-un 0x sau 0X
la nceput. Iata cateva moduri echivalente de a reprezenta ntregul 37: 37, 045, 0x25.
Notatiile octale si hexazecimale nu vor fi utilizate n acest curs. Totusi trebuie sa fim
constienti de ele pentru a folosi 0-uri la nceput doar acolo unde chiar vrem aceasta.
O constant
a caracter este cuprinsa ntre apostrofuri, cum ar fi a. Intern, Java interpreteaza aceasta constanta ca pe un numar (codul Unicode). Ulterior, functiile de scriere
vor transforma acest numar n caracterul corespunzator. Constantele caracter mai pot fi
reprezentate si n forma:
\uxxxx.
unde xxxx este un numar n baza 16 reprezentand codul Unicode al caracterului.
Constantele de tip sir de caractere sunt cuprinse ntre ghilimele, ca n Primul meu program
Java. Exista anumite secvente speciale, numite secvente escape, care sunt folosite pentru
anumite caractere speciale. Noi vom folosi mai ales
\n, \\, \ si \",
care nseamna respectiv linie nou
a, backslash, apostrof si ghilimele.
12
CAPITOLUL 2. NOT
IUNI FUNDAMENTALE DE PROGRAMARE IN JAVA
2.3.3
Orice variabila Java, inclusiv cele primitive, sunt declarate prin descrierea numelui a tipului si, optional, a valorii initiale. Numele variabilei trebuie sa fie un identificator. Un
identificator poate sa contina orice combinatie de litere, cifre si caracterul underscore
(liniuta de subliniere). Identificatorii nu pot ncepe cu o cifra. Cuvintele rezervate, cum ar
fi int nu pot fi identificatori. Nu pot fi utilizati nici identificatorii care deja sunt declarati
si sunt vizibili.
Java este case-sensitive, ceea ce nseamna ca sum si Sum reprezinta identificatori diferiti.
In acest text vom folosi urmatoarea conventie pentru numele variabilelor:
toate numele de variabila ncep cu litera mica, iar cuvintele noi din cadrul numelui
ncep cu litera mare. De exemplu: sumaMaxima, nodVizitat etc.
numele claselor ncepe cu litera mare. De exemplu: FirstProgram, ArithmeticException, BinaryTree etc.
Alte conventii vor mai fi prezentate pe parcurs.
Iata cateva exemple de declaratii de variabile:
int numarElemente ;
double mediaGenerala ;
int produs = 1, suma = 0 ;
int produs1 = produs ;
O variabila este bine sa fie declarata imediat nainte de a fi folosita. Asa cum vom vedea
mai tarziu, locul unde este declarata determina domeniul de vizibilitate si semnificatia ei.
2.3.4
Citire/scriere de la terminal
Scrierea la terminal n Java se realizeaza cu functia println si nu pune probleme majore. Lucrurile nu stau deloc la fel cu citirea de la tastatura, care se realizeaza mult mai
anevoios. Acest lucru se datoreaza n primul rand faptului ca programele Java nu sunt
concepute pentru a citi de la tastatura. In imensa majoritate a cazurilor programele Java
si preiau datele dintr-o interfat
a grafic
a (Applet-urile), din forme HTML (Java Servlets,
Java Server Pages) sau din fisiere.
Citirea si scrierea de la consola sunt realizate prin readLine, respectiv println. Fluxul de
intrare standard este System.in iar fluxul de iesire standard este System.out.
Mecanismul de baza pentru citirea/scrierea formatata foloseste tipul String, care va fi descris n capitolul urmator. La afisare, operatorul + concateneaza doua String-uri. Pentru
tipurile primitive, daca parametrul scris nu este de tip String se face o conversie temporara
la String. Aceste conversii pot fi definite si pentru obiecte, asa cum vom arata mai tarziu.
Pentru citire se asociaza un obiect de tipul BufferedReader cu System.in. Apoi se citeste
un String care va fi ulterior prelucrat.
2.4
Operatori de baz
a
Aceasta sectiune descrie operatorii de baza care sunt disponibili n Java. Acesti operatori
sunt utilizati pentru a crea expresii. O constanta sau o variabila reprezinta o expresie,
13
2.4.1
Operatori de atribuire
Programul simplu din Figura 2.3 ilustreaza cativa operatori Java. Operatorul de atribuire
este semnul egal (=). De exemplu, n linia 16, variabilei a i se atribuie valoarea variabilei c
(care n acel moment are valoarea 6). Modificarile ulterioare ale variabilei c nu vor afecta
variabila a. Operatorii de atribuire pot fi nlantuiti ca n:
z=y=x=0.
Un alt operator de atribuire este += al carui mod de utilizare este ilustrat n linia 18.
Operatorul += adauga valoarea aflata la dreapta (operatorului) la variabila din stanga.
Astfel, valoarea lui c este incrementata de la 6 la 14. Java ofera si alti operatori de atribuire
cum ar fi -=, *= si /= care modifica variabila aflata n partea stanga prin scadere, nmultire
si respectiv mpartire.
1. public class OperatorTest
2. {
3. //program care ilustreaza operatorii de
4. //programul va afisa:
5. //12 8 6
6. //6 8 6
7. //6 8 14
8. //22 8 14
9. //24 10 33
10.
11. public static void main(String[] args)
12. {
13. int a = 12, b = 8, c = 6 ;
14.
15. System.out.println(a + " " + b + " " +
16. a = c ;
17. System.out.println(a + " " + b + " " +
18. c += b ;
19. System.out.println(a + " " + b + " " +
20. a = b + c ;
21. System.out.println(a + " " + b + " " +
22. a++ ;
23. ++b ;
24. c = a++ + ++b ;
25. System.out.println(a + " " + b + " " +
26. }
27.}
baza
c) ;
c) ;
c) ;
c) ;
c) ;
14
2.4.2
CAPITOLUL 2. NOT
IUNI FUNDAMENTALE DE PROGRAMARE IN JAVA
Linia 20 din Figura 2.3 ilustreaza unul dintre operatorii binari care sunt tipici pentru
limbajele de programare: operatorul de adunare (+). Operatorul + are ca efect adunarea
continutului variabilelor b si c; valorile lui b si c raman neschimbate. Valoarea rezultata
este atribuit
a lui a. Alti operatori aritmetici folositi n Java sunt: -, *, / si % utilizati
respectiv pentru sc
adere, nmultire, mp
artire si rest.
Impartirea a doua valori ntregi are ca valoare doar partea ntrega a rezultatului. De exemplu 3/2 are valoarea 1, dar 3.0/2 are valoarea 1.5.
Asa cum este si normal, adunarea si scaderea au aceeasi prioritate. Aceasta prioritate
este mai mica decat cea a grupului format din nmultire, mpartire si rest; astfel 1 + 2*3
are valoarea 7. Toti acesti operatori sunt evaluati de la stanga la dreapta (astfel 3-2-2 are
valoarea -1). Toti operatorii aritmetici au o anumita prioritate si o anumita asociere.
2.4.3
In plus fata de operatorii aritmetici binari care necesita doi operanzi, Java dispune si de
operatori unari care necesita doar un singur operand. Cel mai cunoscut operator unar
este operatorul minus (-) care returneaza operandul cu semn opus. Astfel, -x, este opusul
lui x.
Java ofera de asemenea operatorul de autoincrementare care adauga 1 la valoarea unei
variabile, notat prin ++, si operatorul de autodecrementare care scade 1 din valoarea
variabilei, notat cu - -. Un caz banal de utilizare a acestor operatori este exemplificat
n liniile 22 si 23 din Figura 2.3. In ambele cazuri operatorul ++ adauga 1 la valoarea
variabilei. In Java, ca si n C, orice expresie are o valoare. Astfel, un operator aplicat
unei variabile genereaza o expresie cu o anumita valoare. Desi faptul ca variabila este
incrementata nainte de executia urmatoarei instructiuni este garantat, se pune ntrebarea:
Care este valoarea expresiei de autoincrementare daca ea este utilizata n cadrul unei alte
expresii?
In acest caz, locul unde se plaseaza operatorul ++ este esential. Semnficatia lui ++x
este ca valoarea expresiei este egala cu noua valoare a lui x. Acest operator este numit
incrementare prefixat
a. In mod analog, x++ nseamna ca valoarea expresiei este egala cu
valoarea originala a lui x. Acesta este numit incrementare postfixat
a. Aceste trasaturi
sunt exemplificate n linia 24 din Figura 2.3. Atat a, cat si b sunt incrementate cu 1, iar
c este obtinut prin adunarea valorii initiale a lui a (care este 23) cu valoarea incrementata
a lui b (care este 10).
2.4.4
Conversii de tip
Operatorul conversie de tip, numit adeseori si operatorul de cast, este utilizat pentru a
genera o variabila temporara de un nou tip. Sa consideram, de exemplu, secventa de cod:
double rest ;
int x = 6 ;
int y = 10 ;
rest = x / y ; //mai mult ca sigur gresit!
2.5. INSTRUCT
IUNI CONDIT
IONALE
15
2.5
Instructiuni conditionale
Aceasta sectiune este dedicata instructiunilor care controleaza fluxul de executie al programului: instructiunile conditionale si iteratia.
2.5.1
Operatori relationali
Testul fundamental care poate fi realizat asupra tipurilor primitive este comparatia. Comparatia se realizeaza utilizand operatorii de egalitate/inegalitate si operatorii de comparatie
(<, > etc.). In Java, operatorii de egalitate/inegalitate sunt == respectiv !=. De exemplu,
exprStanga == exprDreapta
are valoarea true daca exprStanga si exprDreapta sunt egale; altfel are valoarea false.
Analog, expresia:
exprStanga != exprDreapta
are valoarea true daca exprStanga si exprDreapta sunt diferite; altfel are valoarea false.
Operatorii de comparatie sunt <, <=, >, >= iar semnficatia lor este cea naturala pentru
tipurile fundamentale. Operatorii de comparatie au prioritate mai mare decat operatorii
de egalitate. Totusi, ambele categorii au prioritate mai mica decat operatorii aritmetici,
dar mai mare decat operatorii de atribuire. Astfel, veti constata adeseori ca folosirea
parantezelor nu va fi necesara. Toti acesti operatori se asociaza de la stanga la dreapta,
dar cunoasterea acestui lucru nu ne foloseste prea mult. De exemplu, n expresia a < b < 6,
prima comparatie genereaza o valoare booleana, iar a doua expresie este gresita, deoarece
operatorul < nu este definit pentru valori booleene. Paragraful urmator descrie cum se
poate realiza acest test n mod corect.
2.5.2
Operatori logici
Java dispune de operatori logici care sunt utilizati pentru a simula operatorii and, or
si not din algebra Boolean
a. Acesti operatori mai sunt referiti uneori si sub numele de
conjunctie, disjunctie si, respectiv, negare, simbolurile corespunzatoare fiind &&, || si !.
Implementarea corecta a testului din paragraful anterior este:
16
CAPITOLUL 2. NOT
IUNI FUNDAMENTALE DE PROGRAMARE IN JAVA
OR pe biti
|
AND logic
&&
OR logic
||
Conditional
?:
Atribuire
= = /= %= += =
Asociere
Stanga la dreapta
Dreapta la stanga
Stanga la dreapta
Stanga la dreapta
Stanga la dreapta
Stanga la dreapta
Stanga la dreapta
Stanga la dreapta
Stanga la dreapta
Stanga la dreapta
Stanga la dreapta
Stanga la dreapta
Dreapta la stanga
Dreapta la stanga
2.5.3
Instructiunea if
2.5. INSTRUCT
IUNI CONDIT
IONALE
17
if( expresie )
instructiune1
else
instructiune2
urmatoarea instructiune
In acest caz, daca expresie are valoarea true, atunci se executa instructiune1; altfel se
executa instructiune2. In ambele cazuri controlul este apoi preluat de urmatoarea instructiune. Iata un exemplu:
System.out.println("1/x este") ;
if( x != 0 )
System.out.print( 1/x ) ;
else
System.out.print( "Nedefinit" ) ;
System.out.println() ;
De retinut ca doar o singura instructiune poate exista pe ramura de if sau de else indiferent
de cum indentati codul. Iata doua erori frecvente la ncepatori:
if( x == 0 ) ; //instructiune vida!!!
System.out.println( "x este 0" ) ;
else
System.out.print( "x este" ) ;
System.out.println(x) ; // a doua instructiune nu
// face parte din else
Prima gresala consta n a pune ; dupa if. Simbolul ; reprezinta n sine instructiunea vid
a;
ca o consecinta, acest fragment de cod nu va fi compilabil (else nu va fi asociat cu nici
un if). Dupa ce am corectat aceasta eroare, ramanem cu o eroare de logica: ultima linie
de cod nu face parte din if, desi acest lucru este sugerat de indentare. Pentru a rezolva
aceasta problema vom utiliza un bloc n care grupam o secventa de instructiuni printr-o
pereche de acolade:
if( x == 0 )
{
System.out.println( "x este 0" ) ;
}
else
{
System.out.print( "x este" ) ;
System.out.println( x ) ;
}
Observati ca am folosit acolade si pe ramura de if desi acestea nu sunt absolut necesare.
Aceasta practica mbunatateste foarte mult lizibilitatea codului si va invitam si pe dumneavoastra sa o adoptati.
Instructiunea if poate sa faca parte dintr-o alta instructiune if sau else, la fel ca si celelalte
instructiuni de control prezentate n continuare n aceasta sectiune.
18
2.5.4
CAPITOLUL 2. NOT
IUNI FUNDAMENTALE DE PROGRAMARE IN JAVA
Instructiunea while
2.5.5
Instructiunea for
Instructiunea while ar fi suficienta pentru a exprima orice fel de ciclare. Totusi, Java
mai ofera nca doua forme de a realiza ciclarea: instructiunea for si instructiunea do.
Instructiunea for este utilizata n primul rand pentru a realiza iteratia. Sintaxa ei este:
for( initializare; test; actualizare)
instructiune
urmatoarea instructiune
In aceasta situatie, initializare, test si actualizare sunt toate expresii, si toate trei sunt
optionale. Daca test lipseste, valoarea sa implicita este true. Dupa paranteza de nchidere
nu se pune ;.
Instructiunea for se executa realizand mai ntai initializare. Apoi, cat timp test este
true, au loc urmatoarele doua instructiuni: se executa instructiune, iar apoi se executa
actualizare. Daca initializare si actualizare sunt omise, instructiunea for se va comporta
exact ca si instructiunea while.
Avantajul instructiunii for consta n faptul ca se poate vedea clar marja pe care itereaza
variabilele contor.
Urmatoarea secventa de cod afiseaza primele 100 numere ntregi pozitive:
for( int i=1; i<100; ++i)
{
System.out.println( i );
}
Acest fragment ilustreaza si practica obisnuita pentru programatorii Java (si C++) de a
declara un contor ntreg n secventa de initializare a ciclului. Durata de viata a acestui
contor se extinde doar n interiorul ciclului.
Atat initializare cat si actualizare pot folosi operatorul virgul
a pentru a permite expresii
multiple. Urmatorul fragment ilustreaza aceasta tehnica frecvent folosita:
2.5. INSTRUCT
IUNI CONDIT
IONALE
19
2.5.6
Instructiunea do
Instructiunea while realizeaza un test repetat. Daca testul este true atunci se execut
a
instructiunea din cadrul ei. Totusi, daca testul initial este false, instructiunea din cadrul
ciclului nu este executata niciodata. In anumite situatii avem nevoie ca instructiunile din
ciclu sa se execute cel putin o data. Acest lucru se poate realiza utilizand instructiunea
do. Instructiunea do este asemanatoare cu instructiunea while, cu deosebirea ca testul este
realizat dupa ce instructiunile din corpul ciclului se executa. Sintaxa sa este:
do
instructiune
while( expresie ) ;
urmatoarea instructiune ;
Remarcati faptul ca instructiunea do se termina cu ;. Un exemplu tipic n care se utilizeaz
a
instructiunea do este dat de fragmentul de (pseudo-) cod de mai jos:
do
{
afiseaza mesaj ;
citeste data ;
}while( data nu este corecta ) ;
Instructiunea do este instructiunea de ciclare cel mai putin utilizata. Totusi, atunci cand
vrem sa executam ceva cel putin o data ciclul, si for nu poate fi utilizat, atunci do este
alegerea potrivita.
20
2.5.7
CAPITOLUL 2. NOT
IUNI FUNDAMENTALE DE PROGRAMARE IN JAVA
2.5. INSTRUCT
IUNI CONDIT
IONALE
21
{
continue ;
}
System.out.println( i ) ;
}
Desigur, ca exemplul de mai sus poate fi implementat si utilizand un if simplu. Totusi
instructiunea continue este adeseori folosita pentru a evita imbricari complicate de tip
if-else n cadrul ciclurilor.
2.5.8
Instructiunea switch
22
CAPITOLUL 2. NOT
IUNI FUNDAMENTALE DE PROGRAMARE IN JAVA
11.
switch(c)
12.
{
13.
case a:
14.
case e:
15.
case i:
16.
case o:
17.
case u:
18.
System.out.println("vocala");
19.
break;
20.
case y:
21.
case w:
22.
System.out.println(
23.
"Uneori vocale "); //doar in limba engleza!!!
24.
break;
25.
default:
26.
System.out.println("consoana");
27.
} //switch
28. } //for
29. } //main
30.} //class
Figura 2.5 Instructiunea switch
Functia Math.random() genereaza o valoare n intervalul [0,1). Prin nmultirea valorii
returnate de aceasta functie cu numarul de litere din alfabet (26 litere) se obtine un numar
n intervalul [0,26). Adunarea cu prima litera (a, care are de fapt valoarea 97) are ca
efect transpunerea n intervalul [97,123). In final se foloseste operatorul de conversie de
tip pentru a trunchia numarul la o valoare din multimea 97,98,...,122, adica un cod ASCII
al unui caracter din alfabetul englez .
2.5.9
Operatorul conditional
Operatorul coditional este folosit ca o prescurtare pentru instructiuni simple de tipul if-else.
Forma sa generala este:
exprTest ? expresieDa : expresieNu ;
Mai ntai se evalueaza exprTest urmata fie de expresieDa fie de expresieNu, rezultand
astfel valoarea ntregii expresii. expresieDa este evaluata daca exprTest are valoarea true;
n caz contrar se evalueaza expresieNu. Prioritatea operatorului conditional este chiar
deasupra operatorilor de atribuire. Acest lucru permite omiterea parantezelor atunci cand
asignam rezultatul operatorului conditional unei variabile. Ca un exemplu, minimul a
doua variabile poate fi calculat dupa cum urmeaza:
valMin =
x < y ? x : y ;
2.6. METODE
2.6
23
Metode
Ceea ce n alte limbaje de programare numeam procedura sau functie, n Java este numit
metod
a. O definitie mai exacta si completa a notiunii de metoda o vom da mai tarziu. In
acest paragraf prezentam doar cateva notiuni elementare pentru a putea scrie functii de
genul celor din C sau Pascal pe care sa le folosim n cateva programe simple.
Antetul unei metode consta dintr-un nume, o lista (eventual vida) de parametri si un tip
pentru valoarea returnata. Codul efectiv al metodei numit adeseori corpul metodei este
un bloc (o secventa de instructiuni cuprinsa ntre acolade). Definirea unei metode const
a
n antet si corp. Un exemplu de metoda si de o functie main care o utilizeaza este dat n
Figura 2.6.
Prin prefixarea metodelor cu ajutorul cuvintelor cheie public static putem mima ntro oarecare masura functiile din Pascal si C. Desi aceasta tehnica este utila n anumite
situatii, ea nu trebuie utilizata n mod abuziv. Numele metodei este un identificator.
Lista de parametri consta din 0 sau mai multi parametri formali, fiecare avand un tip
precizat. Cand o metoda este apelata, parametrii actuali sunt trecuti n parametrii formali utilizand atribuirea obisnuita. Aceasta nsemna ca tipurile primitive sunt transmise
utilizand exclusiv transmiterea prin valoare. Parametrii actuali nu vor putea fi modificati
de catre functie. Definirile metodelor pot aparea n orice ordine.
Instructiunea return este utilizata pentru a ntoarce o valoare catre codul apelant. Dac
a
tipul functiei este void atunci nu se ntoarce nici o valoare si se foloseste
return ;
fara nici un parametru.
1. public class Minim
2. {
3. public static void main( String[] args )
4. {
5.
int a = 3 ;
6.
int b = 7 ;
7.
System.out.println( min(a,b) ) ;
8. }
9. //declaratia metodei min
10. public static int min( int x, int y )
11. {
12. return x < y ? x : y ;
13. }
14.}
Figura 2.6 Declararea si apelul unei metode
2.6.1
Supranc
arcarea numelor la metode
Sa presupunem ca dorim sa scriem o metoda care calculeaza maximul a trei numere ntregi.
Un antet pentru aceasta metoda ar fi:
int max(int a, int b, int c)
24
CAPITOLUL 2. NOT
IUNI FUNDAMENTALE DE PROGRAMARE IN JAVA
In unele limbaje de programare (Pascal, C), acest lucru nu ar fi permis daca exista deja
o functie max cu doi parametri. De exemplu, se poate sa avem deja declarata o metoda
max cu antetul:
int max(int a, int b)
Java permite supranc
arcarea (engl. overloading) numelui metodelor. Aceasta nsemna ca
mai multe metode pot fi declarate n cadrul aceleiasi clase atata timp cat semn
aturile lor
(adica lista de parametri) difera. Atunci cand se face un apel al metodei max compilatorul
poate usor sa deduca despre care metoda este vorba examinand lista parametrilor de apel.
Se poate sa existe metode suprancarcate cu acelasi numar de parametri formali atata
timp cat cel putin unul din tipurile din lista de parametri este diferit.
De retinut faptul ca tipul functiei nu face parte din semnatura ei. Aceasta nseamna ca
nu putem avea doua metode n cadrul aceleiasi clase care sa difere doar prin tipul valorii
returnate. Metode din clase diferite pot avea acelasi nume, parametri si chiar tip returnat;
despre aceasta vom discuta pe larg mai tarziu.
2.7
Probleme propuse
Capitolul 3
Referinte
In capitolul 1 am prezentat tipurile primitive din Java. Toate tipurile care nu sunt ntre
cele opt primitive, inclusiv tipurile importante cum ar fi stringurile, sirurile si fisierele sunt
tipuri referint
a.
In acest capitol vom nvata:
Ce este un tip referint
a si ce este o variabil
a referint
a
Prin ce difer
a un tip referint
a de un tip primitiv
Exemple de tipuri referint
a incluz
and stringuri, siruri si fluxuri
Cum sunt utilizate exceptiile pentru a semnala comportamentul gresit al
unei secvente de cod
3.1
Ce este o referint
a
In capitolul 1 am examinat cele opt tipuri primitive, mpreuna cu cateva operatii care pot
fi realizate pe aceste tipuri. Toate celelalte tipuri de date din Java sunt referinte. Ce este
deci o referinta? O variabil
a referint
a n Java (numita adeseori simplu referint
a) este o
variabila care retine adresa de memorie la care se afla un anumit obiect.
point1
H
j
H
point3
*
point2
*
1000
1024
3200
3600
5124
(0,0)
(5,12)
point2 = 1024
point1 = 1000
point3 = 1000
Figura 3.1 Ilustrarea unei referinte: Obiectul de tip Point stocat la adresa de memorie
1000 este referit at
at de c
atre point1 c
at si de c
atre point3. Obiectul de tip Point stocat la
adresa 1024 este referit de c
atre point2. Locatiile de memorie unde sunt retinute variabilele
au fost alese arbitrar.
Ca un exemplu, n Figura 3.1 exista doua obiecte de tipul Point1 . Presupunem ca aceste
obiecte au fost stocate la adresele de memorie 1000 si respectiv 1024. Pentru aceste dou
a
1
25
26
CAPITOLUL 3. REFERINT
E
obiecte am definit trei referinte, point1, point2 si point3. Atat point1 cat si point3 refera
obiectul stocat la adresa 1000; point2 refera obiectul stocat la adresa 1024; aceasta nsemna
ca atat point1 cat si point3 au valoarea 1000, iar point2 va avea valoarea 1024. Retineti
ca locatiile efective, cum ar fi 1000 si 1024, sunt atribuite de compilator la discretia sa
(unde gaseste memorie libera). In consecinta, aceste valori nu sunt utile efectiv ca valori
numerice. Totusi, faptul ca point1 si point3 au aceeasi valoare este folositor: nseamna
ca ele refera acelasi obiect. O referinta stocheaza ntotdeauna adresa la care un anumit
obiect se afla, cu exceptia situatiei cand nu refera nici un obiect. In aceasta situatie va
stoca referinta nul
a, notata cu null. Java nu permite referinte catre tipurile primitive.
Exista doua mari tipuri de operatii care se pot aplica variabilelor referinta.
1. Prima categorie permite examinarea si manipularea valorii referinta. De exemplu,
daca modificam valoarea stocata n point1 (care este 1000), putem sa facem ca point1
sa refere un alt obiect. Putem de asemenea compara point1 si point3 pentru a vedea
daca refera acelasi obiect.
2. A doua categorie de operatii se aplica obiectului care este referit; am putea de
exemplu examina sau modifica starea unuia dintre obiectele de tipul Point (am
putea examina coordonatele x si y ale unui obiect de tipul Point).
Inainte de a descrie ce se poate face cu ajutorul referintelor, sa descriem ce nu se poate
face. Sa consideram expresia point1*point2. Deoarece valorile retinute de point1 si point2
sunt respectiv 1000 si 1024, produsul lor ar fi 1024000. Totusi acest calcul este lipsit de
sens si nu ar putea avea nici o valoarea practica. Variabilele referinta retin adrese, si nu
poate fi asociata nici o semnificatie logica nmultirii adreselor.
Analog, point1++ nu are nici un sens n Java; ar sugera ca point1 - care are valoarea
1000 - sa fie crescut la 1001, dar n acest caz nu ar mai referi un obiect valid. Multe alte
limbaje de programare definesc notiunea de pointer care are un comportament similar cu
cel al unei variabile referinta. Totusi, pointerii n C sunt mult mai periculosi, deoarece
este permisa aritmetica pe adresele stocate. In plus, deoarece C permite pointeri si catre
tipurile fundamentale trebuie avut grija pentru a distinge ntre aritmetica pe adrese si
aritmetica pe variabilele care sunt referite. Acest lucru se face prin dereferentierea explicita
a pointerului. In practica, pointerii limbajului C tind sa provoace numeroase erori greu
detectabile, care uneori i pot face si pe programatorii experimentati sa planga de ciuda!
In Java, singurele operatii care sunt permise asupra referintelor (cu o singura exceptie
pentru Stringuri) sunt atribuirea via = si comparatia via == sau !=. De exemplu, prin
atribuirea lui point3 a valorii lui point2, vom face ca point3 sa refere acelasi obiect pe care l
refera point2. Acum expresia point2 == point3 este adevarata, deoarece ambele referinte
stocheaza valoarea 1024 si refera deci acelasi obiect. point1 != point2 este de asemenea
adevarata, deoarece point1 si point2 refera acum obiecte distincte.
Cealalta categorie de operatii se refera la obiectul care este referit. Exista doar trei actiuni
fundamentale care pot fi realizate:
1. Aplicarea unei conversii de tip
2. Accesul la un camp al obiectului sau apelul unei metode prin operatorul punct (.)
3. Utilizarea operatorului instanceof pentru a verifica daca obiectul retinut are un anumit tip.
27
3.2
In Java un obiect este orice variabila de un tip ne-primitiv. Obiectele sunt tratate diferit
fata de tipurile primitive. Variabilele de tipuri primitive sunt manipulate prin valoare,
ceea ce nsemna ca valorile lor sunt retinute n acele variabile si sunt copiate dintr-o variabila primitiva n alta variabila primitiva n timpul instructiunii de atribuire. Dupa cum
am aratat n sectiunea anterioara, variabilele referinta stocheaza referinte catre obiecte.
Obiectul n sine este stocat undeva n memorie, iar variabila referinta stocheaza adresa
de memorie a obiectului. Astfel, variabila referinta nu este decat un nume pentru acea
zona de memorie. Aceasta nsemna ca variabilele primitive si cele referinta vor avea un
comportament diferit. Aceasta sectiune examineaza mai n detaliu aceste diferente si ilustreaza operatiile care sunt permise pe tipurile referinta.
3.2.1
Operatorul punct (.) este folosit pentru a selecta o metoda care se aplica unui obiect. De
exemplu, sa presupunem ca avem un obiect de tip Cerc care defineste metoda arie. Dac
a
variabila unCerc este o referinta catre un Cerc, atunci putem calcula aria (si salva aceast
a
arie ntr-o variabila de tip double) cercului referit astfel:
double arieCerc = unCerc.arie() ;
Este posibil ca variabila unCerc sa retina referinta null. In acest caz, aplicarea operatorului
punct va genera o exceptie de tipul NullPointerException la executia programului. De
obicei aceast
a exceptie va determina terminarea anormala a programului.
Operatorul punct poate fi folosit si pentru a accesa componentele individuale ale unui
obiect, daca cel care a proiectat obiectul permite acest lucru. Capitolul urmator descrie
cum se poate face acest lucru; tot acolo vom explica de ce n general este preferabil ca s
a
nu se permit
a accesul direct la componentele individuale ale unui obiect.
3.2.2
Declararea obiectelor
Am vazut deja care este sintaxa pentru declararea variabilelor primitive. Pentru obiecte
exista o diferenta importanta. Atunci cand declaram o referinta, nu facem decat s
a
furnizam un nume care poate fi utilizat pentru a referi un obiect stocat n prealabil n
memorie. Totusi, declaratia n sine nu furnizeaza si acel obiect. Sa presupunem, de exemplu, ca avem un obiect de tip Cerc caruia dorim sa i calculam aria folosind metoda arie().
Sa consideram secventa de instructiuni de mai jos:
Cerc unCerc ;
//unCerc poate referi un obiect de tip Cerc
double arieCerc = unCerc.arie() ; //calcul arie pentru cerc
Totul pare n regula cu aceste instructiuni, pana cand ne aducem aminte ca unCerc este
numele unui obiect oarecare de tip Cerc, dar nu am creat nici un cerc efectiv. In consecinta,
dupa ce se declara variabila unCerc, aceasta va contine valoarea null, ceea ce nseamna c
a
28
CAPITOLUL 3. REFERINT
E
unCerc nca nu refera un obiect Cerc valid. Aceasta nseamna ca a doua linie de program
este invalida, deoarece ncercam sa calculam aria unui cerc care nca nu exista. In exemplul
de fata chiar compilatorul va detecta eroarea, afirmand ca unCerc nu este initializat.
In alte situatii mai complexe compilatorul nu va putea detecta eroarea si se va genera o
NullPointerException n timpul executiei programului.
Singura posibilitate (normala) de a aloca memorie unui obiect este folosirea cuvantului
cheie new. new este folosit pentru a construi un nou obiect. O posibilitate de a face acest
lucru este:
Cerc unCerc ;
//unCerc poate referi un obiect de tip Cerc
unCerc = new Cerc() ; //acum unCerc refera un obiect alocat
double arieCerc = unCerc.arie() ; //calcul arie pentru cerc
Remarcati parantezele care se pun dupa numele obiectului.
Adeseori programatorii combina declararea si initializarea obiectului ca n exemplul de
mai jos:
Cerc unCerc = new Cerc() ; //acum unCerc refera un obiect alocat
double arieCerc = unCerc.arie() ; //calcul arie pentru cerc
Multe obiecte pot fi de asemenea construite cu anumite valori initiale. De exemplu, obiectul de tip Cerc ar putea fi construit cu trei parametri, doi pentru coordonatele centrului
si unul pentru lungimea razei.
Cerc unCerc = new Cerc(0,0,10) ; //cerc cu centru in origine si de raza 10
double arieCerc = unCerc.arie() ; //calcul arie pentru cerc
3.2.3
Deoarece toate obiectele trebuie construite, ne-am putea astepta ca atunci cand nu mai
este nevoie de ele sa trebuiasca sa le distrugem. Totusi, n Java, cand un obiect din
memorie nu mai este referit de nici o variabila, memoria pe care o consuma va fi eliberata
automat. Aceasta tehnica se numeste colectare de gunoaie.
3.2.4
Semnificatia lui =
29
Iata cateva exemple. Sa presupunem ca dorim sa cream doua obiecte de tip Cerc pentru
a calcula suma ariilor lor. Cream mai ntai obiectul cerc1, dupa care ncercam sa cream
obiectul cerc2 prin modificarea lui cerc1 dupa cum urmeaza (vezi si Figura 3.2):
Cerc cerc1 = new Cerc(0,0,10) ; //un cerc de raza 10
Cerc cerc2 = cerc1 ;
cerc2.setRaza(20) ; // modificam raza la 20 ;
double arieCercuri = cerc1.arie() + cerc2.arie() ; //calcul arie
cerc1
cerc2
H
j
H
cerc2 Cerc(0,0,10)
H
j
H
cerc1 Cerc(0,0,20)
Cerc(0,0,10)
cerc1
H
Y
H
*
3.2.5
Transmiterea de parametri
Din cauza ca apelul se face prin valoare, parametri actuali (de apel) se transpun n
parametri formali folosind atribuirea obisnuita. Daca parametrul trimis este un tip referint
a,
atunci stim deja ca prin atribuire atat parametrul formal, cat si parametrul de apel vor
referi acelasi obiect. Orice metoda aplicata parametrului formal este astfel implicit aplicat
a
si parametrului de apel. In alte limbaje de programare acest tip de apel se numeste apelare
prin referint
a. Utilizarea acestei notiuni n Java ar fi oarecum nepotrivita, deoarece ne-ar
putea face sa credem ca transmiterea referintelor s-ar face n mod diferit. In realitate,
transmiterea parametrilor nu s-a modificat; ceea ce s-a modificat sunt parametrii n sine,
care nu mai sunt tipuri primitive, ci tipuri referinta.
30
3.2.6
CAPITOLUL 3. REFERINT
E
Semnificatia lui ==
Pentru tipurile primitive operatia == are valoarea true daca au valori identice. Pentru
tipuri referinta semnificatia lui == este diferita, dar perfect consistenta cu discutia din
paragraful anterior. Doua variabile referinta sunt egale via == daca ele refera acelasi
obiect (s-au ambele sunt null). Sa consideram urmatorul exemplu:
Cerc cerc1 = new Cerc(0,0,10) ; //un cerc de raza 10
Cerc cerc2 = new Cerc(0,0,10) ; //un alt cerc tot de raza 10
Cerc cerc3 = cerc2 ;
In acest caz avem doua obiecte. Primul este cunoscut sub numele de cerc1, al doilea este
cunoscut sub doua nume: cerc2 si cerc3. Expresia cerc2 == cerc3 este adevarata. Totusi,
desi cerc1 si cerc2 par sa refere obiecte care au aceeasi valoare, expresia cerc1 == cerc2
este falsa, deoarece ele refera obiecte diferite. Aceleasi reguli se aplica si pentru operatorul
!=.
Cum facem nsa pentru a vedea daca obiectele referite sunt identice? De exemplu, cum
putem sa verificam faptul ca cerc1 si cerc2 refera obiecte Cerc care sunt egale? Obiectele
pot fi comparate folosind metoda equals. Vom vedea n curand un exemplu de folosire a
lui equals, n care vom discuta despre tipul String (paragraful 3.3). Fiecare obiect are o
metoda equals, care, n mod implicit, nu face altceva decat testul ==. Pentru ca equals sa
functioneze corect, programatorul trebuie sa redefineasca aceasta metoda pentru obiectele
pe care le creaza.
3.2.7
Supranc
arcarea operatorilor pentru obiecte
In afara unei singure exceptii pe care o vom discuta n paragraful urmator, operatorii nu
pot fi definiti pentru a lucra cu obiecte2 . Astfel, nu exista operatorul < pentru nici un
fel de obiect. Pentru acest scop, va trebui definita o metoda, cum ar fi lessThan, care va
realiza comparatia.
3.3
S
iruri de caractere (stringuri)
Sirurile de caractere n Java sunt definite folosind obiectul String. Limbajul Java face sa
para ca String este un tip primitiv, deoarece pentru el sunt definiti operatorii + si +=
pentru concatenare. Totusi, acesta este singurul tip referinta pentru care Java a permis
suprancarcarea operatorilor. In rest, String se comporta ca orice alt obiect.
3.3.1
Fundamentele utiliz
arii stringurilor
Exista doua reguli fundamentale referitoare la obiectele de tip String. Prima este aceea ca,
exceptand operatorul de concatenare, obiectele de tip String se comporta ca toate celelalte
obiecte. A doua regula este aceea ca stringurile sunt ne-modificabile. Aceasta nseamna
ca, odata construit, un obiect de tip String nu mai poate fi modificat. Deoarece obiectele
2
Aceasta este o diferent
a notabil
a ntre Java si C++, care permite supranc
arcarea operatorilor pentru
obiecte. Inginerii de la Sun au considerat c
a supranc
arcarea operatorilor pentru obiecte aduce mai multe
probleme dec
at beneficii si au decis ca Java s
a nu permit
a acest lucru
3.3. S
IRURI DE CARACTERE (STRINGURI)
31
de tip String nu se pot modifica, putem folosi linistiti operatorul = pentru ele. Iata un
exemplu:
String vid = "" ;
String mesaj = "Salutare!" ;
String repetat = mesaj ;
Dupa aceste declaratii, exista doua obiecte de tip String. Primul este un sir vid, si este
referit de variabila vid. Al doilea este sirul Salutare!, care este referit de variabilele mesaj
si repetat. Pentru majoritatea obiectelor, faptul ca obiectul este referit de doua variabile
ar putea genera probleme. Totusi, deoarece stringurile nu pot fi modificate, partajarea lor
nu pune nici un fel de probleme. Singura posibilitate de a modifica valoarea catre care
refera variabila repetat este aceea de a construi un nou obiect de tip String si a-l atribui
lui repetat. Aceasta operatie nu va avea nici un efect asupra valorii pe care o refera mesaj.
3.3.2
Concatenarea stringurilor
si atribuirea:
str=str+"hello"
32
3.3.3
CAPITOLUL 3. REFERINT
E
Comparatia stringurilor
3.3.4
Lungimea unui obiect de tip String (un sir vid are lungimea 0) poate fi obtinuta cu metoda
length(). Deoarece, length() este o metoda parantezele sunt necesare.
Exista doua metode pentru a accesa caracterele din interiorul unui String. Metoda charAt
returneaza caracterul aflat la pozitia specificata (primul caracter este pe pozitia 0). Metoda
substring returneaza o referinta catre un String nou construit. Metoda are ca parametri
pozitia de nceput si pozitia primului caracter neinclus.
Iata un exemplu de folosire al acestor metode:
String mesaj = "Hello" ;
int lungimeMesaj = mesaj.length() ;
char ch = mesaj.charAt(1) ;
String subSir = mesaj.substring( 2 , 4 ) ;
3.3.5
//lungimea este 5
//ch este e
//sub este "ll"
Metoda toString() poate fi utilizata pentru a converti orice tip primitiv la String. De
exemplu, toString(45) returneaza o referinta catre sirul nou construit 45. Majoritatea
obiectelor furnizeaza o implementare a metodei toString(). De fapt, atunci cand operatorul
+ are un operand de tip String, operandul care nu este de tip String este automat convertit
la String folosind metoda toString(). Pentru tipurile de date numerice, exista o varianta
a metodei toString() care permite precizarea unei anumite baze. Astfel, instructiunea:
System.out.println( Integer.toString( 55 , 2 ) ) ;
are ca efect tiparirea reprezentarii n baza 2 a numarului 55.
Pentru a converti un String la un int se poate folosi metoda Integer.parseInt(). Aceasta
metoda genereaza o exceptie daca String-ul convertit nu contine o valoare ntreaga. Despre
exceptii vom vorbi pe scurt n paragraful 3.5. Pentru a obtine un double dintr-un String
se poate utiliza metoda parseDouble(). Iata doua exemple:
int x = Integer.parseInt( "75" ) ;
double y = Double.parseDouble( "3.14" ) ;
3.4. S
IRURI
3.4
33
S
iruri
Sirurile sunt structura fundamentala prin care se pot retine mai multe elemente de acelasi
tip. In Java, sirurile nu sunt tipuri primitive; ele se comporta foarte asemanator cu un
obiect. Din acest motiv, multe dintre regulile care sunt valabile pentru obiecte se aplic
a
si la siruri.
Fiecare element dintr-un sir poate fi accesat prin mecanismul de indiciere oferit de operatorul [ ]. Spre deosebire de limbajele C sau C++, Java verifica validitatea indicilor3 .
In Java, ca si n C, sirurile sunt ntotdeauna indiciate de la 0. Astfel, un sir a cu 3
elemente este format din a[0], a[1], a[2]. Numarul de elemente care pot fi stocate n sirul a
este permanent retinut n variabila a.length. Observati ca aici (spre deosebire de String-uri)
nu se pun paranteze. O parcurgere tipica pentru un sir ar fi:
for( int i = 0 ; i < a.length ; i++ )
3.4.1
Acesta este un lucru foarte important care vine n ajutorul programatorilor, mai ales a celor ncep
atori.
Indicii ac
aror valoare dep
asesc num
arul de elemente alocat sunt adeseori cauza multor erori obscure n C
si C++. In Java, accesarea unui sir cu un indice n afara limitei este imediat semnalat
a prin exceptia
IndexOutOfBoundsException.
34
CAPITOLUL 3. REFERINT
E
//apelul metodei
//declaratia metodei
Conform conventiilor de transmitere a parametrilor n Java pentru tipurile referinta, variabilele sirActual si sirFormal refera acelasi obiect. Astfel, accesul la sirFormal[i] este de
fapt un acces la sirActual[i]. Aceasta nseamna ca variabilele continute n sir pot fi modificate de catre metoda. O observatie importanta este aceea ca linia de cod din cadrul lui
f():
sirFormal = new int [20] ;
nu are nici un efect asupra lui sirActual. Acest lucru se datoreaza faptului ca n Java
transmiterea parametrilor se face prin valoare, deci sirFormal este o noua referinta catre
sir. Instructiunea de mai sus nu face decat sa schimbe sirul catre care refera sirFormal
(vezi Figura 3.3).
35
3.4. S
IRURI
sirActual
H
j
H
sirActual sirFormal
H
j
H
sirActual
sirFormal
H
j
H
HH
j
3.4.2
Expansiunea dinamic
a a sirurilor
36
CAPITOLUL 3. REFERINT
E
sa alocam memorie pentru un numar fix de elemente care vor fi stocate. Daca nu stim de
la bun nceput cate elemente vor fi stocate n sir, va fi dificil sa alegem o valoare rezonabila
pentru dimensiunea sirului. Aceasta sectiune prezinta o metoda prin care putem extinde
dinamic sirul daca dimensiunea initiala se dovedeste a fi prea mica. Aceasta tehnica poarta
numele de expansiune dinamic
a a sirurilor si permite alocarea de siruri de dimensiune
arbitrara pe care le putem redimensiona pe masura ce programul ruleaza.
Alocarea obisnuita de memorie pentru siruri se realizeaza astfel:
int[] a = new int[10] ;
Sa presupunem ca dupa ce am facut aceasta declaratie, hotaram ca de fapt avem nevoie
de 12 elemente si nu de 10. In aceasta situatie putem folosi urmatoarea manevra:
int original = a ;
//salvam referinta lui a
a = new int[12] ;
//alocam din nou memorie
for(int i=0; i < 10; i++) //copiem elementele in a
{
a[i] = original[i] ;
}
Un moment de gandire este suficient pentru a ne convinge ca aceasta operatie este consumatoare de resurse, deoarece trebuie copiat originalul napoi n noul sir a. De exemplu,
daca extensia dinamica a numarului de elemente ar trebui facuta ca raspuns la citirea
de date, ar fi ineficient sa expansionam ori de cate ori citim cateva elemente. Din acest
motiv, de cate ori se realizeaza o extensie dinamica, numarul de elemente este crescut cu
un coeficient multiplicativ. Am putea de exemplu dubla numarul de elemente la fiecare
expansiune dinamica. Astfel, dintr-un sir cu N elemente, generam un sir cu 2N elemente,
iar costul expansiunii este mpartit ntre cele N elemente care pot fi inserate n sir fara a
realiza extensia.
Pentru a face ca lucrurile sa fie mai concrete, Figura 3.5 prezinta un program care citeste
un numar nelimitat de numere ntregi de la tastatura si le retine ntr-un sir a carui dimensiune este extinsa dinamic. Functia resize realizeaza expansiunea (sau contractia!) sirului
returnand o referinta catre un sir nou construit. Similar, metoda getInts returneaza o
referinta catre sirul n care sunt citite elementele.
La nceputul lui getInts(), nrElemente este initializat cu 0 si ncepem cu un sir cu 5 elemente. In linia 36 citim n mod repetat cate un element. Daca sirul este umplut,
lucru indicat de intrarea n testul de la linia 36, atunci sirul este expansionat prin apelul
metodei resize. Liniile 51-61 realizeaza expansiunea sirului folosind strategia prezentata
anterior. La linia 40, elementul citit este stocat n tablou, iar numarul de elemente citite
este incrementat. In final, n lina 48 contractam sirul la numarul de elemente citite efectiv.
1.
2.
3.
4.
5.
6.
7.
import java.io.* ;
//clasa pentru citirea unui numar nelimitat de valori intregi
public class ReadInts
{
public static void main( String[] args )
{
int [] array = getInts( ) ;
3.4. S
IRURI
37
8.
9.
System.out.println("Elementele citite sunt: " ) ;
10. for( int i = 0; i < array.length; i++ )
11. {
12.
System.out.println( array[i] ) ;
13. }
14. }
15.
16. /* citeste un numar nelimitat de valori intregi
17. * fara a trata erorile */
18. public static int[] getInts()
19. {
20. //BufferedReader este prezentata in sectiunile urmatoare
21. BufferedReader in = new BufferedReader(
22.
new InputStreamReader( System.in ) ) ;
23.
24. int[] elemente = new int[ 5] ; //se aloca 5 elemente
25. int nrElemente = 0 ; //numarul de elemente citite
26. String s ; //sir in care se citeste cate o linie
27.
28. System.out.println("Introduceti numere intregi cate unul pe linie:");
29.
30. try
31. {
32.
//cat timp linia e nevida
33.
while( (s = in.readLine()) != null )
34.
{
35.
int nr = Integer.parseInt( s ) ;
36.
if( nrElemente == elemente.length ) //sirul a fost "umplut"
37.
{
38.
elemente=resize(elemente,elemente.length*2);
//dubleaza dimensiunea sirului
39.
}
40.
elemente[ nrElemente++ ] = nr ;
41.
}
42. }
43. catch( Exception e )
44. {
45.
//nu se trateaza exceptia
46. }
47. System.out.println( "Citire incheiata." ) ;
48. return resize( elemente, nrElemente ) ;
//trunchiaza sirul la numarul de elemente citite
49. }
50.
51. public static int[] resize( int[] sir, int dimensiuneNoua )
38
CAPITOLUL 3. REFERINT
E
52. {
53. int[] original = sir ;
54. int elementeDeCopiat=Math.min(original.length,dimensiuneNoua);
55. sir = new int[ dimensiuneNoua ] ;
56. for( int i=0; i<elementeDeCopiat; ++i)
57. {
58.
sir[i] = original[i] ;
59. }
60. return sir ;
61. }
62.}
Figura 3.5 Program pentru citirea unui num
ar nelimitat de numere ntregi urmat
a de
afisarea lor
3.4.3
S
iruri cu mai multe dimensiuni
In anumite situatii trebuie sa stocam datele n siruri cu mai multe dimensiuni. Cel mai
des folosite sunt matricele, adica sirurile cu doua dimensiuni. Alocarea de memorie pentru
siruri cu mai multe dimensiuni se realizeaza precizand numarul de elemente pentru fiecare
indice, iar indicierea se face plasand fiecare indice ntre paranteze patrate. Ca un exemplu,
declaratia:
int[][] x = new int[2][3] ;
defineste matricea x, n care primul indice poate fi 0 sau 1, iar al doilea este de la 0 la 2.
3.4.4
9.
10.
11.
12.
13.
14.
15.
16.
3.5
39
return ;
}
for( int i=0 ; i < args.length; ++i)
{
System.out.print( args[i] ) ;
}
}
}
Tratarea exceptiilor
Exceptiile sunt obiecte care retin informatie si care sunt transmise n afara secventelor
return. Exceptiile sunt propagate napoi prin secventa de functii apelate pana cand o anumita metoda prinde exceptia. Exceptiile sunt folosite pentru a semnala situatii exceptie,
cum ar fi erorile.
De fiecare data cand o exceptie este ntalnita ntr-o metoda a unei clase, programul va
permite recuperarea pierderilor cauzate de exceptie si se va ncheia fara a cauza caderea
sistemului. Prin pregatirea tratarii conditiilor de exceptie n care va ajunge executia
programului, se va crea un program mult mai prietenos pentru utilizator. Un program Java
poate detecta erori si apoi indica sistemului de executie ce erori a ntalnit prin generarea
unor conditii de exceptie ce vor duce la oprirea executiei si afisarea unui cod de eroare,
sau daca doriti sa tratati unele exceptii ntr-un mod propriu, puteti folosi o clauza catch
pentru a obtine controlul ntr-o situatie de exceptie.
3.5.1
Procesarea exceptiilor
Secventa de cod din Figura 3.6 prezinta modul de folosire al exceptiilor. Secventa de
cod care ar putea genera o exceptie care sa fie propagata este inclusa ntr-un bloc try.
Blocul try se extinde de la linia 12 la linia 16. Imediat dupa blocul try trebuie sa apar
a
secventele de tratare a exceptiilor. Programul sare la secventa de tratare a exceptiilor
doar n situatia n care se genereaza o exceptie; n momentul generarii exceptiei, blocul
try din care exceptia provine se considera a fi ncheiat.
Fiecare dintre blocurile catch este ncercat pe rand pana cand se gaseste o secventa de
tratare adecvata. Deoarece Exception se potriveste cu toate exceptiile generate, ea este
adecvata pentru orice fel de exceptie generata n blocul try. Exceptiile generate de secventa
try din programul nostru pot fi IOException generate de readLine daca apare o eroare la
citire, si NumberFormatException generata de parseInt daca linia citita de la tastatura nu
poate fi convertita la int.
In cazul unei exceptii se executa codul din blocul catch -n situatia noastra linia 19. Dup
a
aceasta blocul catch si blocul continand cuplul try/catch se considera ncheiate. Un mesaj
referitor la eroarea generata este tiparit folosind obiectul de tip Exception numit e. Putem
alege sa facem si alte actiuni n caz de eroare, cum ar fi furnizarea de mesaje de eroare
mai detaliate etc.
1. import java.io.* ;
2.
40
CAPITOLUL 3. REFERINT
E
41
{
...//tratarea noii exceptii
}
catch(Exception e)
{
...//tratarea unor erori generale
}
...
Dupa cum am mai spus dupa tratarea unei exceptii este ignorata orice secventa de
instructiuni. Totusi, daca este neaparata nevoie sa se execute anumite operatii, veti invoca
clauza finally. Blocul finally este executat si n cazul n care nu are loc nici o exceptie n
blocul try.
try
{
...
}
finally
{
...
}
3.5.2
Exceptii uzuale
Exista mai multe tipuri de exceptii standard n Java. O prima categorie de exceptii o constituie exceptiile de executie standard (standard runtime exceptions) cum ar fi mpartirea
unui ntreg la 0 sau accesarea unui element de tablou cu indice ilegal. Avand n vedere
faptul ca aceste exceptii pot aparea practic n orice secventa de program, ar fi o munc
a
sisifica sa definim secvente de tratare a unor astfel de exceptii. Daca se furnizeaza un bloc
catch aceste exceptii se comporta exact la fel ca celelalte exceptii. Daca apare o exceptie
standard pentru care nu exista un bloc catch aceasta se propaga normal, trecand chiar si de
functia main. In aceasta situatie, exceptia produce o terminare anormala a programului,
nsotita de un mesaj de eroare. Figura 3.7 prezinta cateva dintre cele mai uzuale erori
standard de executie.
EXCEPT
IE STANDARD DE EXECUT
IE
SEMNIFICAT
IE
ArithmeticException
Dep
asire sau mp
artirea unui ntreg la 0
NumberFormatException
Conversie nepermis
a a unui String la un tip numeric
IndexOutOfBoundsException
NegativeArraySizeException
Tentativ
a de a crea un sir cu nr. negativ de elemente
NullPointerException
Tentativ
a de a folosi o referint
a care are valoarea null
Inc
alcare de securitate n timpul executiei
SecurityException
Figura 3.7 C
ateva exceptii de executie uzuale.
42
CAPITOLUL 3. REFERINT
E
Majoritatea exceptiilor sunt de tipul exceptii standard tratate (standard checked exceptions). Daca se apeleaza o metoda care poate genera direct sau indirect o astfel de
exceptie, atunci programatorul fie trebuie sa o trateze cu un bloc de tip catch sau sa
indice explicit faptul ca exceptia urmeaza sa fie propagata prin folosirea clauzei throws
n antetul metodei. Retineti faptul ca exceptia tot va trebui tratata la un moment dat,
deoarece metoda main (care este la ultimul nivel) nu poate avea o clauza throws.
EXCEPT
IE STANDARD TRATATA
SEMNIFICAT
IE
java.io.EOFException
java.io.FileNotFoundException
Fisierul nu a fost g
asit pentru a fi deschis
java.io.IOException
InterruptedException
Aruncat
a de metoda Thread.Sleep
3.6
Intrare si iesire
Intrarea si iesirea n Java se realizeaza cu ajutorul claselor din pachetul java.io. Din
acest motiv, orice program care foloseste rutinele de intrare/iesire trebuie sa cuprinda
instructiunea:
import java.io.* ;
Biblioteca Java de intrare/iesire este extrem de sofisticata si are un numar foarte mare
de optiuni. In aceasta faza vom examina doar elementele de baza referitoare la intrare si
iesire, concentrandu-ne n ntregime asupra operatiilor de intrare-iesire formatate.
3.6.1
Operatii de baz
a pe fluxuri (stream-uri)
Exista trei fluxuri predefinite pentru operatii I/O de la terminal: System.in, intrarea
standard, System.out, iesirea standard si System.err, fluxul de erori.
Asa cum am aratat deja, metodele print si println sunt folosite pentru afisare formatata.
Orice fel de tip poate fi convertit la o forma tiparibila, folosind metoda toString; n multe
cazuri, acest lucru este realizat automat. Spre deosebire de C si C++ care dispun de un
numar enorm de optiuni de formatare, afisarea n Java se face exclusiv prin concatenare
de String-uri, fara nici o formatare.
O modalitate simpla de a realiza citirea este aceea de a citi o singura linie ntr-un obiect
3.6. INTRARE S
I IES
IRE
43
de tip String folosind readLine. Metoda readLine preia caractere de la intrare pana cand
ntalneste un terminator de linie sau sfarsit de fisier. Metoda returneaza caracterele citite
(din care extrage terminatorul de linie) ca un String nou construit. Pentru a putea folosi
readLine trebuie sa construim un obiect BufferedReader dintr-un obiect InputStreamReader
care la randul sau este construit din System.in. Acest lucru a fost ilustrat n Figura 3.6,
liniile 8 si 9.
Daca primul caracter citit este EOF, atunci functia readLine returneaza null. Daca apare
o eroare la citire se genereaza o exceptie de tipul IOException. Daca sirul citit nu poate fi
convertit la o valoare ntreaga se genereaza o NumberFormatException.
3.6.2
Obiectul StringTokenizer
44
CAPITOLUL 3. REFERINT
E
3.6.3
Fisiere secventiale
Una dintre regulile fundamentale n Java este aceea ca ceea ce se poate face pentru operatii
I/O de la terminal se poate face si pentru operatii I/O din fisiere. Pentru a lucra cu fisiere
obiectul BufferedReader nu se construieste pe baza unui InputStreamReader. Vom folosi
n schimb un obiect de tip FileReader care va fi construit pe baza unui nume de fisier.
Un exemplu pentru ilustrarea acestor idei este prezentat n Figura 3.10. Acest program
listeaza continutul fisierelor text care sunt precizate ca parametri n linie de comanda.
Functia main parcurge pur si simplu argumentele din linia de comanda, apeland metoda
listFile pentru fiecare argument. In metoda listFile construim obiectul de tip FileReader
la linia 26 si apoi l folosim pe acesta pentru construirea obiectului de tip BufferedReader
n linia urmatoare. Din acest moment, citirea este identica cu cea de la tastatura.
Dupa ce am ncheiat citirea din fisier, trebuie sa nchidem fisierul, altfel s-ar putea sa nu
mai putem deschide alte stream-uri (sa atingem limita maxima de stream-uri care pot
fi deschise). Nu putem face acest lucru n primul bloc try, deoarece o exceptie ar putea
genera parasirea prematura a blocului si fisierul nu ar fi nchis. Din acest motiv, fisierul
este nchis dupa secventa try/catch.
Scrierea formatata n fisiere este similara cu citirea formatata.
1.
2.
3.
4.
5.
3.6. INTRARE S
I IES
IRE
6. {
7.
public static void main( String[] args )
8.
{
9.
if( args.length == 0 )
10. {
11.
System.out.println( "Nu ati precizat nici un fisier!") ;
12. }
13.
14. for(int i = 0 ; i < args.length; ++i )
15. {
16.
listFile( args[i] ) ;
17. }
18. }
19.
20. public static void listFile( String fileName )
21. {
22. System.out.println("NUME FISIER: " + fileName) ;
23.
24. try
25. {
26.
FileReader file = new FileReader( fileName ) ;
27.
BufferedReader in = new BufferedReader( file ) ;
28.
String s ;
29.
while( ( s = in.readLine() ) != null )
30.
{
31.
System.out.println( s ) ;
32.
}
33. }
34. catch( Exception e )
35. {
36.
System.out.println( e ) ;
37. }
38. try
39. {
40.
if( in != null )
41.
{
42.
in.close() ;
43.
}
44. }
45.
catch( IOException e )
46.
{ //ignora exceptia
47.
}
48. }
49.}
Figura 3.10 Program de listare a continutului unui fisier
45
46
CAPITOLUL 3. REFERINT
E
3.7
Probleme propuse
1. O sum
a de verificare este un ntreg pe 32 de biti (int) care este suma codurilor
Unicode ale caracterelor dintr-un fisier (se permite depasirea, desi aceasta este putin
probabila daca toate caracterele sunt ASCII). Doua fisiere identice au aceeasi suma
de verificare. Scrieti un program care calculeaza suma de verificare pentru un fisier
care este furnizat ca parametru n linie de comanda.
2. Modificati programul Echo.java din acest capitol astfel ncat daca nu se transmit
parametri de la linia de comanda sa foloseasca intrarea standard.
3. Scrieti o metoda care returneaza true daca stringul str1 este prefix pentru stringul
str2. Nu folositi nici o alta metoda generala de cautare pe stringuri n afara de
charAt.
Capitolul 4
Obiecte si clase
In acest capitol vom ncepe sa discutam despre programarea orientat
a pe obiecte (object
oriented programming - OOP). O componenta fundamentala a programarii orientate pe
obiecte este specificarea, implementarea si folosirea obiectelor. In capitolul anterior am
vazut deja cateva exemple de obiecte, cum ar fi string-urile si fisierele (stream-urile) care
fac parte din bibliotecile limbajului Java. Am putut observa si faptul ca fiecare obiect este
caracterizat de o anumita stare care poate fi modificata prin aplicarea operatorului punct
(.). In limbajul Java, starea si functionalitatea unui obiect se definesc prin intermediul
unei clase. Un obiect este de fapt o instant
a a unei clase.
4.1
Programarea orientata pe obiecte s-a impus ca modelul dominant al anilor 90. In aceast
a
sectiune vom prezenta modul n care Java suporta programarea orientata pe obiecte si
vom mentiona cateva dintre principiile ei fundamentale.
In centrul programarii orientate pe obiecte se afla notiunea de obiect. Obiectul este o
variabila care are o structur
a si o stare. Fiecare obiect dispune de operatii prin intermediul
carora i se poate manipula starea. Asa cum am vazut deja, n limbajul Java se face
distinctie ntre un obiect si o variabila de un tip primitiv, dar aceasta este o specificitate a
limbajului Java si nu a programarii orientate pe obiecte. Pe langa operatiile cu un caracter
general, asupra obiectelor mai putem sa realizam:
Crearea de noi obiecte, nsotit
a eventual de initializarea obiectelor
Copierea si testarea egalit
atii
Realizarea de operatii de intrare/iesire cu obiectele
Obiectul trebuie privit ca o unitate atomic
a pe care utilizatorul nu ar trebui sa o disece.
In mod normal, nu ne punem problema de a jongla cu bitii din care este format un numar
reprezentat n virgula mobila si ar fi de-a dreptul ridicol sa ncercam sa incrementam un
astfel de numar prin modificarea directa a reprezentarii sale interne.
Principiul atomicitatii este cunoscut sub numele de ascunderea informatiei. Utilizatorul
nu are acces direct la partile unui obiect sau la implementarea sa. Acestea vor putea
fi accesate doar prin intermediul metodelor care au fost furnizate mpreuna cu obiectul.
47
48
CAPITOLUL 4. OBIECTE S
I CLASE
Putem privi fiecare obiect ca fiind ambalat cu mesajul Nu deschideti! Nu contine componente reparabile de catre utilizator!. In viata de zi cu zi, majoritatea celor care ncearca sa
repare componente cu aceasta inscriptie sfarsesc prin a face mai mult rau decat bine. Din
acest punct de vedere, programarea imita lumea reala. Gruparea datelor si a operatiilor
asupra acestor date n acelasi ntreg (agregat), avand grija sa ascundem detaliile de implementare ale agregatului este cunoscuta sub numele de ncapsulare.
Unul dintre principalele scopuri ale programarii orientate pe obiecte este refolosirea codului. La fel cum inginerii refolosesc din nou si din nou aceleasi componente n proiectare,
programatorii ar trebui sa refoloseasca obiectele n loc sa le reimplementeze. Atunci cand
avem deja un obiect care implementeaza exact comportamentul pe care l dorim, refolosirea
nu pune nici un fel de probleme. Adevarata provocare apare atunci cand dorim sa folosim
un obiect care deja exista, dar care, desi are un comportament foarte similar cu ceea ce
vrem, nu corespunde exact cu necesitatile noastre.
Limbajele de programare orientate pe obiecte furnizeaza mai multe mecanisme n acest
scop. Unul dintre mecanisme este folosirea codului generic. Daca implementarea este
identica, si difera doar tipul de baza al obiectului, nu este necesar sa rescriem complet codul ei: vom scrie n schimb un cod generic care functioneaza pentru orice tip. De exemplu,
algoritmul de sortare al unui sir de obiecte nu depinde de obiectele care sunt sortate, deci
se poate implementa un algoritm generic de sortare.
Mostenirea este un alt mecanism care permite extinderea functionalitatii unui obiect. Cu
alte cuvinte, putem crea noi tipuri de date care sa extinda (sau sa restrictioneze) proprietatile tipului de date original.
Un alt principiu important al programarii orientate pe obiecte este polimorfismul. Un tip
referinta polimorfic poate sa refere obiecte de mai multe tipuri. Atunci cand se apeleaza o
metoda a tipului polimorfic, se va selecta automat metoda care corespunde tipului referit
n acel moment.
Un obiect n Java este o instanta a unei clase. O clasa este similara cu un tip record din
Pascal sau cu o structura din C, doar ca exista doua mbunatatiri majore. In primul rand,
membrii pot fi atat functii cat si date, numite n acest context metode respectiv atribute. In
al doilea rand, domeniul de vizibilitate al acestor membri poate fi restrictionat. Deoarece
metodele care manipuleaza starea obiectului sunt membri ai clasei, ele sunt accesate prin
intermediul operatorului punct, la fel ca si atributele. In terminologia programarii orientate pe obiecte, atunci cand apelam o metoda a obiectului spunem ca trimitem un mesaj
obiectului.
4.2
Un exemplu simplu
Sa ne amintim ca, atunci cand proiectam o clasa, este important sa ascundem detaliile
interne fata de utilizatorul clasei. Clasa poate sa si defineasca functionalitatea prin intermediul metodelor. Unele dintre aceste metode vor descrie cum se creeaza si initializeaza
o instanta a clasei, cum se realizeaza testele de egalitate si cum se scrie starea clasei.
Celelalte metode sunt specifice structurii particulare pe care o are clasa. Ideea este ca
utilizatorul nu trebuie sa aiba dreptul de a modifica direct starea obiectului, el va trebui
sa foloseasca metodele clasei pentru a realiza acest lucru. Aceasta idee poate fi impusa
prin ascunderea anumitor membri fata de utilizator. Pentru a realiza aceasta vom preciza
49
ca acesti membri sa fie stocati n sectiunea private. Compilatorul va avea grija ca membri
din sectiunea private sa fie inaccesibili utilizatorului acelui obiect. In general, toate datele
membru ar trebui sa fie declarate private.
Figura 4.1 prezinta modul de definire al unei clase care modeleaza un cerc. Definirea
clasei consta n doua parti: public si private. Sectiunea public reprezinta portiunea care
este vizibila pentru utilizatorul obiectului. Deoarece datele sunt ascunse fata de utilizator, sectiunea public va contine n mod normal numai metode si constante. In exemplul
nostru avem doua metode pentru a scrie si a citi raza obiectelor de tip Circle. Celelalte
doua metode calculeaza aria respectiv lungimea obiectului de tip Circle. Sectiunea private
contine datele: acestea sunt invizibile pentru utilizatorul obiectului. Atributul radius va
trebui accesat doar prin intermediul metodelor publice setRadius si getRadius.
1. //clasa simpla Java care modeleaza un Cerc
2. import java.util.* ;
3. public class Circle
4. {
5.
6.
//raza cercului
7.
//valoarea razei nu poate fi modificata
8.
//direct de catre utilizator
9.
private double radius ;
10.
11. public void setRadius(double r) //modifica raza cercului
12. {
13. radius = r ;
14. }
15.
16. public double getRadius() //metoda ptr a obt raza cercului
17. {
18. return radius ;
19. }
20.
21. public double area() //metoda ptr calculul ariei cercului
22. {
23. return Math.PI*radius*radius ;
24. }
25.
26. public double length() //metoda ptr calculul lungimii
27. {
28. return 2*Math.PI*radius ;
29. }
30.}
Figura 4.1 Definirea clasei Circle
Figura 4.2 prezinta modul de folosire al unui obiect de tip Circle. Deorece setRadius,
getRadius, area si length sunt membri ai clasei, ei sunt accesati folosind operatorul punct.
50
CAPITOLUL 4. OBIECTE S
I CLASE
4.3
Metode uzuale
Exista metode care sunt comune pentru toate clasele. Alte metode definesc comportamente
specifice unei anumite clase. In aceasta sectiune vom prezenta metodele care sunt comune
tuturor claselor: constructorii, modificatorii, accesorii, toString si equals.
4.3.1
Constructori
Asa cum am mentionat deja, una dintre proprietatile fundamentale ale obiectelor este ca
acestea pot fi definite si, eventual, initializate. In limbajul Java, metoda care controleaza
modul n care un obiect este creat si initializat este constructorul. Deoarece Java permite
suprancarcarea metodelor, o clasa poate sa defineasca mai multi constructori.
Daca la definirea clasei nu se furnizeaza nici un constructor, cum este cazul clasei Circle
din Figura 4.1, compilatorul creeaza un constructor implicit care initializeaza fiecare
data membru cu valorile implicite. Aceasta nseamna ca atributele de tipuri primitive
51
sunt initializate cu 0, iar atributele de tip referinta sunt initializate cu null. Astfel, n
cazul nostru, atributul radius va avea implicit valoarea 0.
Pentru a furniza un constructor, vom scrie o metoda care are acelasi nume cu clasa si
care nu returneaza nimic. In Figura 4.3 avem doi constructori: unul ncepe la linia 8,
iar celalalt la linia 16. Folosind acesti doi constructori vom putea construi obiecte de tip
Date n urmatoarele moduri:
Date d1 = new Date( ) ;
Date d2 = new Date( 15, 3, 2000) ;
De remarcat faptul ca odata ce ati definit un constructor pentru clasa, compilatorul nu
mai genereaza constructorul implicit fara parametri. Daca aveti nevoie de un astfel de
constructor, va trebui sa l scrieti. Astfel constructorul din linia 8 trebuie definit obligatoriu
pentru a putea construi un obiect de tipul celui referit de catre d1.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
52
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.}
CAPITOLUL 4. OBIECTE S
I CLASE
4.3.2
Modificatori si Accesori
Atributele sunt declarate de obicei ca fiind private. Aceasta nseamna ca ele nu vor putea
fi direct accesate de catre rutinele care nu apartin clasei. Exista totusi multe situatii n
care dorim sa examinam sau chiar sa modificam valoarea unui atribut.
O posibilitate este aceea de a declara atributele ca fiind public. Aceasta este nsa o alegere
proasta, deoarece ncalca principiul ascunderii informatiei. Putem nsa scrie metode care sa
examineze sau sa modifice valoarea fiecarui camp. O metoda care citeste, dar nu modifica
starea unui obiect este numita accesor. O metoda care modifica starea unui obiect este
numita modificator (engl. mutator).
Cazuri particulare de accesori si modificatori sunt cele care actioneaza asupra unui singur
camp. Accesorii de acest tip au un nume care ncepe de obicei cu get, cum ar fi getRadius,
iar modificatorii au un nume care ncepe de regula cu set, cum ar fi setRadius.
Avantajul folosirii unui modificator este ca acesta poate verifica daca starea obiectului este
corecta. Astfel un modificator care altereaza campul day al unui obiect de tip Date poate
verifica corectitudinea datei care rezulta.
4.3.3
Afisare si toString
In general, dorim sa afisam starea unui obiect folosind metoda print. Pentru a putea face
acest lucru trebuie definita o metoda cu numele de toString. Aceasta metoda ntoarce
un String care poate fi afisat. Ca un exemplu, n Figura 4.3 am prezentat o implementare rudimentara a unei metode toString pentru clasa Date n liniile 37-40. Definirea
acestei metode ne permite sa scriem un obiect d1 de tip Date cu instructiunea system.out.print(d1).
4.4. PACHETE
4.3.4
53
Metoda equals
Metoda equals este folosita pentru a testa daca doua referinte indica obiecte care au aceeasi
valoare (stare). Antetul acestei metode este ntodeauna
public boolean equals( Object rhs )
Ati remarcat probabil nedumeriti faptul ca parametrul trimis este de tip Object si nu de tip
Date, cum ar fi fost de asteptat. Ratiunea pentru acest lucru o sa o prezentam n capitolul
despre polimorfism. In general, metoda equals pentru o clasa X este implementata n asa
fel ncat sa returneze true doar daca rhs este o instanta a lui X si, n plus, dupa conversia
la X toate tipurile primitive sunt egale (via ==) si toate tipurile referinta sunt egale (prin
aplicarea lui equals pentru fiecare membru).
Un exemplu de implementare a lui equals este dat n Figura 4.3 pentru clasa Date n
liniile 26-34. Operatorul instanceof va fi discutat n paragraful 4.5.3.
4.3.5
Exista anumite cuvinte cheie ale limbajului Java care specifica proprietati speciale pentru unele variabile sau metode. Un astfel de cuvant cheie este cuvantul rezervat static.
Variabilele si metodele declarate static ntr-o clasa, sunt aceleasi pentru toate obiectele,
adica pentru toate variabilele de tipul acelei clase. Variabilele statice pot fi accesate far
a
sa fie nevoie de o instantiere a clasei respective (adica de o variabila de clasa respectiva).
Analog, nici metodele statice nu au nevoie de o instantiere a clasei pentru a fi folosite.
Metodele statice pot utiliza variabile statice declarate n interiorul clasei.
Cel mai cunoscut exemplu de metoda statica este main. Alte exemple de metode statice pot fi g
asite n clasele String, Integer si Math. Exemple de astfel de metode sunt
String.valueOf, Integer.parseInt, Math.sin si Math.max. Accesul la metodele statice respecta aceleasi reguli de vizibilitate ca si metodele normale.
4.3.6
Metoda main
Atunci cand este invocat, interpretorul java cauta metoda main din clasa care i se da ca
parametru. Astfel, putem scrie cate o metoda main pentru fiecare clasa. Acest lucru ne
permite sa testam functionalitatea de baza a claselor individuale. Trebuie sa avem totusi
n vedere faptul ca plasarea functiei main n cadrul clasei ne confera mai multa vizibilitate
decat ne-ar fi permis n general. Astfel, apeluri ale metodelor private pot fi facute n test,
dar ele vor esua ntr-un cadru mai general.
4.4
Pachete
Pachetele sunt folosite pentru a organiza clasele similare. Fiecare pachet consta dintr-o
multime de clase. Clasele care sunt n acelasi pachet au restrictii de vizibilitate mai slabe
ntre ele decat daca ar fi n pachete diferite.
Java furnizeaza o serie de pachete predefinite, printre care java.io, java.lang, java.util,
java.applet, java.awt etc. Pachetul java.lang include, printre altele, clasele Integer, Math,
String si System. Clase mai cunoscute din java.util sunt Date, Random, StringTokenizer.
54
CAPITOLUL 4. OBIECTE S
I CLASE
Pachetul java.io cuprinde diferitele clase pentru stream-uri pe care le-am prezentat n
capitolul anterior.
Clasa C din pachetul P este specificata ca P.C. De exemplu, putem declara un obiect de
tip Date care sa contina data si ora curenta astfel:
java.util.Date today = new java.util.Date() ;
Observati ca prin specificarea numelui pachetului din care face parte clasa evitam conflictele care pot fi generate de clase cu acelasi nume din pachete diferite1 .
4.4.1
Directiva import
Utilizarea permanenta a numelui pachetului din care fac parte clasele poate fi uneori
deosebit de anevoioasa. Pentru a evita acest lucru se foloseste directiva import:
import NumePachet.NumeClasa ;
sau
import NumePachet.* ;
Daca recurgem la prima forma a directivei import, vom putea folosi NumeClasa ca o
prescurtare pentru numele clasei cu calificare completa. Daca folosim cea de-a doua forma,
toate clasele din pachet vor putea fi abreviate cu numele lor.
De exemplu, realizand directivele import de mai jos:
import java.util.Date ;
import java.io.* ;
putem sa folosim:
Date today = new Date() ;
FileReader file = new FileReader( name ) ;
Folosirea directivelor import economiseste timpul de scriere. Avand n vedere ca cea de-a
doua forma este mai generala, ea este cel mai des folosita. Exista doua dezavantaje ale
folosirii directivei import. Primul dezavantaj este acela ca la citirea codului va fi mai greu
de stabilit pachetul din care o anumita clasa face parte. Al doilea este acela ca folosirea
celei de-a doua forme poate sa introduca prescurtari neintentionate pentru anumite clase,
ceea ce va genera conflicte de denumire care vor trebui rezolvate prin folosirea de nume
de clase calificate.
Sa presupunem ca avem urmatoarele directive:
import java.util.* ;
import myutil.* ;
//pachet predefinit
//pachet definit de catre noi
4.4. PACHETE
55
import java.util.Random ;
Directivele import trebuie sa apara nainte de orice declarare a unei clase. Pachetul
java.lang este automat inclus n ntregime. Acesta este motivul pentru care putem folosi
prescurtari de genul Math.max, System.out, Integer.parseInt etc.
4.4.2
Instructiunea package
Pentru a indica faptul ca o clasa face parte dintr-un anumit pachet trebuie sa realizam
doua lucruri. In primul rand trebuie sa scriem o instructiune package pe prima linie,
nainte de a declara clasa. Apoi, va trebui sa plasam clasa n directorul corespunzator.
4.4.3
Java cauta pachetele si clasele n directoarele care sunt precizate n variabila sistem
CLASSPATH. Ce nseamna aceasta? Iata doua posibile setari pentru CLASSPATH, mai
ntai pentru un sistem Windows 95/98/2000, iar apoi pentru un sistem Unix/Linux:
SET CLASSPATH = .;C:\JDK1.3\LIB\
export CLASSPATH=.:/usr/local/jdk1.3/lib:$HOME/java
In ambele cazuri variabila CLASSPATH contine directoarele (sau arhivele) care contin
fisierele .class din pachete. De exemplu, daca variabila CLASSPATH nu este setata corect,
nu veti putea compila nici macar cel mai banal program Java, deoarece pachetul java.lang
nu va fi gasit. O clasa care se afla n pachetul P va trebui sa fie pusa ntr-un director
cu numele P care sa se afle ntr-un director din CLASSPATH. Directorul curent (.) este
ntotdeauna n variabila CLASSPATH, deci daca lucrati ntr-un singur director principal,
puteti crea subdirectoarele chiar n el. Totusi, n majoritatea situatiilor, veti dori sa creati
un subdirector Java separat si sa creati directoarele pentru pachete chiar n el. In aceast
a
situatie va trebui sa adaugati la variabila CLASSPATH acest director. Acest lucru a fost
realizat n exemplul de mai sus prin adaugarea directorului $HOME/java la CLASSPATH.
In directorul Java veti putea acum crea subdirectorul io. In subdirectorul io vom plasa
codul pentru clasa Reader2 . O aplicatie va putea n aceasta situatie sa foloseasca metoda
readInt fie prin
int x = io.Reader.readInt() ;
sau pur si simplu prin
int x = Reader.readInt() ;
daca se furnizeaza directiva import corespunzatoare.
2
Reader este o clas
a ajut
atoare care contine metode pentru citirea de la tastatur
a a tipurilor uzuale.
Codul acestei clase este dat n paragraful 5.7.
56
4.4.4
CAPITOLUL 4. OBIECTE S
I CLASE
4.4.5
Compilarea separat
a
Atunci cand un program consta din mai multe fisiere .java, fiecare fisier trebuie compilat
separat. In mod normal, fiecare clasa este plasata ntr-un fisier .java propriu. Ca urmare
a compilarii vom obtine o colectie de fisiere .class. Clasele sursa pot fi compilate n orice
ordine.
4.5
Alte operatii
In aceasta sectiune vom prezenta nca trei cuvinte cheie importante: this, instanceof si
static. this are mai multe utilizari n Java. Doua dintre ele le vom prezenta n aceasta
sectiune. instanceof are si el mai multe utilizari; l vom folosi aici pentru a ne asigura ca o
conversie de tip se poate realiza. Si cuvantul cheie static are mai multe semnficatii. Vom
vorbi despre metode statice, atribute statice si initializatori statici.
4.5.1
Referinta this
Cea mai cunoscuta utilizare pentru this este ca o referinta la obiectul curent. Imaginati-va
ca this va indica n fiecare moment locul unde va aflati. O utilizare tipica pentru this este
calificarea atributelor unei clase n cadrul unei metode a clasei care primeste parametri cu
nume identic cu numele clasei. De exemplu, n clasa Circle, din Figura 4.1, putem defini
metoda setRadius astfel:
public void setRadius(double radius) //modifica raza cercului
{ //radius este parametrul iar this.radius este atributul clasei
this.radius = radius ;
}
57
In codul de mai sus, pentru a face distinctie ntre parametrul radius si atributul cu acelasi
nume (care este ascuns de catre parametru) se foloseste sintaxa this.radius pentru a
referi atributul clasei.
Un alt exemplu de folosire al lui this este pentru a testa ca parametrul pe care o metod
a
l primeste nu este chiar obiectul curent. Sa presupunem, de exemplu, ca avem o clas
a
Account care are o metoda finalTransfer pentru a transfera toata suma de bani dintr-un
cont n altul. Metoda ar putea fi scrisa astfel:
public void finalTransfer(Account account)
{
dollars += account.dollars ;
account.dollars = 0 ;
}
Sa consideram secventa de cod de mai jos:
Account account1 ;
Account account2 ;
....
account2 = account1 ;
account1.finalTransfer(account2) ;
Deoarece transferam bani n cadrul aceluiasi cont, nu ar trebui sa fie nici o modificare n
cadrul contului. Totusi, ultima linie din finalTransfer are ca efect golirea contului debitor.
O modalitate de a evita o astfel de situatie este folosirea unui test pentru pseudonime:
public void finalTransfer(Account account)
{
if( this = account)//se incearca un transfer in acelasi cont
{
return ;
}
dollars += account.dollars ;
account.dollars = 0 ;
}
4.5.2
Multe clase dispun de mai multi constructori care au un comportament similar. Putem
folosi this n cadrul unui constructor pentru a apela ceilalti constructori ai clasei. O alt
a
posibilitate de a scrie constructorul fara parametri pentru clasa Date este:
public Date()
{
this(1, 1, 2000) ;//apeleaza constructorul Date(int,int,int)
}
Se pot realiza si exemple mai complicate, dar ntotdeauna apelul lui this trebuie sa fie
prima instructiune din constructor, celelalte instructiuni fiind n continuarea acesteia.
58
4.5.3
CAPITOLUL 4. OBIECTE S
I CLASE
Operatorul instanceof
4.5.4
Atribute statice
Atributele statice sunt folosite n situatia n care avem variabile care trebuie partajate de
catre toate instantele unei clase. De obicei atributele statice sunt constante simbolice, dar
acest lucru nu este obligatoriu. Atunci cand un atribut al unei clase este declarat de tip
static, doar o singura instanta a acelei variabile va fi creata. Ea nu face parte din nici o
instanta a clasei. Ea se comporta ca un fel de variabila globala unica, vizibila n cadrul
clasei. Cu alte cuvinte, daca avem declaratia
public class Exemplu
{
private int x ;
private static int y ;
}
fiecare obiect de tip Exemplu va avea propriul atribut x, dar va fi doar un singur y partajat.
O folosire frecventa pentru campurile statice o reprezinta constantele. De exemplu, clasa
Integer defineste atributul MAX VALUE astfel
public final static int MAX_VALUE = 2147483647 ;
Analog se defineste si constanta PI din clasa Math pe care am folosit-o n clasa Circle.
Modificatorul final indica faptul ca identificatorul care urmeaza este o constanta. Daca
aceasta constanta nu ar fi fost un atribut static, atunci fiecare instanta a clasei Integer ar
fi avut un atribut cu numele de MAX VALUE, irosind astfel spatiu n memorie.
Astfel, vom avea o singura variabila cu numele de MAX VALUE. Ea poate fi accesata de
oricare dintre metodele clasei Integer prin identificatorul MAX VALUE. Ea va putea fi
folosita si de catre un obiect de tip Integer numit x prin sintaxa x.MAX VALUE, ca orice
alt camp. Acest lucru este permis doar pentru ca MAX VALUE este public. In sfarsit,
MAX VALUE poate fi folosit si prin intermediul numelui clasei ca Integer. MAX VALUE
(tot pentru ca este public). Aceasta ultima folosire nu ar fi fost permisa pentru un camp
care nu este static.
Chiar si fara modificatorul final, atributele statice sunt foarte folositoare. Sa presupunem
ca vrem sa retinem numarul de obiecte de tip Circle care au fost construite. Pentru aceasta
avem nevoie de o variabila statica. In clasa Circle vom face declaratia:
private static int numarInstante = 0 ;
59
Vom putea apoi incrementa numarul de instante n constructor. Daca acest camp nu ar fi
fost de tip static am avea un comportament incorect, deoarece fiecare obiect de tip Circle
ar fi avut propriul atribut numarInstante care ar fi fost incrementat de la 0 la 1.
Remarcati faptul ca, deorece un atribut de tip static nu necesita un obiect care sa l
controleze, fiind partajat de catre toate instantele clasei, el poate fi folosit de catre o
metod
a statica (daca regulile de vizibilitate permit acest lucru). Atributele nestatice ale
unei clase vor putea fi folosite de catre o metoda statica doar daca se furnizeaza si un
obiect care sa le controleze.
4.5.5
Initializatori statici
Atributele statice sunt initializate atunci cand clasa este ncarcata. Uneori este nsa nevoie
de o initializare mai complexa. Sa presupunem de exemplu ca avem nevoie de un sir static
care sa contina radacinile patrate ale primelor 100 numere naturale. O posibilitate ar fi
sa definim o metoda statica si sa cerem programatorului sa apeleze acea metoda nainte
de a folosi sirul.
O alternativa la aceasta solutie este folosirea initializatorului static. Un exemplu este
prezentat n Figura 4.4. Aici initializatorul static se extinde de la linia 5 la linia 11.
Initializatorul static trebuie sa urmeze imediat dupa membrul static.
1. public class Squares
2. {
3.
private static double squareRoots[] = new double[100] ;
4.
5.
static
6.
{
7.
for( int i =0; i < squareRoots.length; ++i)
8.
{
9.
squareRoots[i] = Math.sqrt( (double) i ) ;
10. }
11. }
12. //restul clasei
13.}
Figura 4.4 Exemplu de initializator static.
4.6
Probleme propuse
1. Scrieti o clasa care suporta numere rationale. Atributele clasei ar trebui sa fie dou
a
variabile de tip long una pentru numitor si una pentru numarator. Stocati numarul
sub forma de fractie ireductibila, cu numitorul pozitiv. Formati un numar rezonabil
de constructori; metodele add, subtract, multiply, divide; de asemenea, toString,
equals si compareTo (care se comporta ca cea din clasa String). Asigurati-va c
a
toString merge n cazul n care numitorul este zero.
2. Implementati o clasa pentru numere complexe, Complex. Adaugati aceleasi metode
ca la clasa Rational, daca au sens (de exemplu, compareTo nu are sens aici). Adaugati
accesori pentru partea reala si cea imaginara.
60
CAPITOLUL 4. OBIECTE S
I CLASE
3. Implementati o clasa completa IntType care sa suporte un numar rezonabil de constructori; metodele add, subtract, multiply, divide; de asemenea, toString, equals si
compareTo. Mentineti IntType sub forma unui sir de cifre suficient de mare.
4. Implementati o clasa simpla numita Date. Clasa va trebui sa reprezinte orice data
ntre 1 Ianuarie 1800 si 31 Decembrie 2500. Definiti metode care sa permita scaderera
a doua date, incrementarea unei date cu un numar de zile; compararea a doua date.
O data va fi reprezentata intern ca numarul de zile care au trecut de la o anumita
data,(de exemplu prima zi din 1800).
Solutie: Am creat dou
a siruri care sunt atribute statice. Primul numit pana1Lun
va contine num
arul de zile p
an
a la prima zi din fiecare lun
a a anilor care nu sunt
bisecti. Astfel, el va contine 0,31,59,90 etc. Cel de-al doilea sir, pana1Ian va contine
num
arul de zile p
an
a la nceputul fiec
arui an, ncep
and cu 1800. Astfel, el va contine
0, 365, 730, 1095, 1460, 1826 etc, deoarece 1800 nu este an bisect, dar 1804 este.
Programul va initializa acest sir o singur
a dat
a. Am utilizat acest sir pentru conversia din reprezentarea intern
a (num
ar de zile de la 01.01.1800) n reprezentarea
extern
a (zi/lun
a/an).
Clasa este prezentat
a n cele ce urmeaz
a:
import java.io.*;
import java.util.*;
import java.math.*;
public class Data
{
static int p,ziua,luna,an;
static int pana1Ian[];
static int pana1Lun[]={0,31,59,90,120,151,181,212,243,273,304,334};
public Data()
// prin acest constructor care se apeleaze la inceputul executari
// programului calculam numarul de zile trecute pana la 1 ianuarie
// al anului i+1800 de la 1 ian 1800
{
pana1Ian=new int[701];//aloc memorie pentru vector
pana1Ian[0]=0;
for(int i=1;i<701;i++)
// acest vector va contine numarul zilelor pana in 1 ianuarie a
// anului i+1800
{
pana1Ian[i]=pana1Ian[i-1]+365;
if ((((i+1800-1)%4==0) &&((i+1800-1)%100!=0))||((i+1800-1)%400==0))
pana1Ian[i]+=1;//verificam daca nu este an bisect
}
}
61
62
CAPITOLUL 4. OBIECTE S
I CLASE
case 11:
case 4:
case 6:
case 9:
do
{
System.out.print("Ziua");
ziua=readInt();
}//se citeste o zi din lunile cu 30 de zile
while((ziua>30)||(ziua<1));
break;
default :
if (bisect(an)==1)
do
{
System.out.print("Ziua");
ziua=readInt();
}//se citeste o zi din luna februarie a unui an bisect
while((ziua>29)||(ziua<1));
else
do
{
System.out.print("Ziua");
ziua=readInt();
}//se citeste o zi din luna februarie a unui an nebisect
while((ziua>28)||(ziua<1));
break;
}
}//readData
public static int conversie(int ziua,int luna,int an)
{
int s=0;
s+=pana1Ian[an-1800];
if ((bisect(an)==1)&&(luna>2))
s+=1;
s+=pana1Lun[luna-1];
s+=ziua;
return s;
}//conversia unei date in numar de zile trecute de la 1 ianuarie 1800
public static int bisect(int an)
//functie pentru verificarea unui an bisect
{
if ((((an)%4==0) &&((an)%100!=0))||((an)%400==0))
return 1;
63
else
return 0;
}
public static String toString(int a)
//conversia numarului de zile trecute de la 1 ian 1800 in data
{
int an=0,lu=0,zi=0;
int i=0,j=0;
while ((a>pana1Ian[j])&&(j<701))
{
j++;//cautam primul an care are trecute, pina la 1 ianuarie,
//un numar mai mare de zile decat numarul pe care dorim sa-l convertim
}
j--;//scadem unu din acel an si avem anul datei
an=j+1800;
if (an>1800)
a=a-pana1Ian[j];//scoatem din numarul initial numarul zilelor
//trecute pana la 1 ianuarie a anului gasit
if (bisect(an)==0)
//verificam daca anul e bisect si apoi cautam luna ce corespunde datei
{
i=0;
while ((pana1Lun[i]<a)&&(i<11))
i++;
if ((pana1Lun[i]<a)&&(i==11))
i++;
i--;
lu=i+1;
if (i >=0)
a=a-pana1Lun[i];
zi=a;
}
else //daca-i bisect problema are trei variante si anume
//daca-i mai mic de 60 se calculeaza ca mai sus, daca nu poate fi
//chiar 60 si atunci suntem pe 29 februarie
//altfel scadem 1 din numarul zilelor si obtinem o data dintr-un
//an obisnuit
{
if (a<60)
{
i=0;
while ((pana1Lun[i]<a)&&(i<12))
i++;
i--;
lu=i+1;
64
CAPITOLUL 4. OBIECTE S
I CLASE
if (i>=0)
a=a-pana1Lun[i];
zi=a;
}
if (a==60)
{
lu=2;
zi=29;
}
if (a>60)
{
a=a-1;
i=0;
while ((pana1Lun[i]<a)&&(i<12))
i++;
if ((pana1Lun[i]<a)&&(i==11))
i++;
i--;
lu=i+1;
if (i>=0)
a=a-pana1Lun[i];
zi=a;
}
}
return zi + "/" + lu + "/" + an ;
}
public static int incrementare(int a)
//incrementam numarul de zile cu un numar
{
int in;
do
{
System.out.print("Cu cate zile:");
in=readInt();
}
while ((in<0)||(in>a)||(in>256000));
return (a+in);
}
public static int decrementare(int a)
//decrementam numarul de zile cu un numar
{
int de;
do
{
65
66
CAPITOLUL 4. OBIECTE S
I CLASE
{
case 1:
readData();
System.out.println(ziua+"/"+luna+"/"+an);
a=0;
a=conversie(ziua,luna,an);
System.out.println("conversia datei este numarul:"+a);
break;
case 2:
readData();
System.out.println(ziua+"/"+luna+"/"+an);
a=0;
a=conversie(ziua,luna,an);
a=decrementare(a);
s="";
s=toString(a);
System.out.println("decrementarea datei este data:"+s);
break;
case 3:
readData();
System.out.println(ziua+"/"+luna+"/"+an);
a=0;
a=conversie(ziua,luna,an);
a=incrementare(a);
s="";
s=toString(a);
System.out.println("incrementarea datei este data:"+s);
break;
case 4:
readData();
a=0;
a=conversie(ziua,luna,an);
readData();
b=0;;
b=conversie(ziua,luna,an);
compareto(a,b);
break;
case 5:
readData();
a=0;
a=conversie(ziua,luna,an);
readData();
b=0;
b=conversie(ziua,luna,an);
boolean bun;
bun=equals(a,b);
if (bun)
System.out.println("Aceeasi data");
else
System.out.println("Data diferita");
break;
case 7 :
System.out.println("La revedere");
break;
case 6 :
readData();
a=0;
a=conversie(ziua,luna,an);
System.out.println("Scadem data.");
readData();
b=0;
b=conversie(ziua,luna,an);
System.out.println("Diferenta este de"+(a-b)+"zile");
break;
default :
System.out.println("Mai incercati");
}
}
while (v!=7);
}
}
67
Capitolul 5
Mostenire
Asa cum am mentionat n capitolul anterior, unul dintre principalele scopuri ale programarii orientate pe obiecte este reutilizarea codului. La fel cum inginerii folosesc aceleasi
componente din nou si din nou n proiectarea circuitelor, programatorii au posibilitatea sa
refoloseasca obiectele, n loc sa le reimplementeze. In limbajele de programare orientate pe
obiecte, mecanismul fundamental pentru refolosirea codului este mostenirea. Mostenirea
ne permite s
a extindem functionalitatea unui obiect. Cu alte cuvinte, putem crea noi
obiecte cu proprietati extinse (sau restr
anse) ale tipului original, formand astfel o ierarhie
de clase. De asemenea, mostenirea este mecanismul pe care Java l foloseste pentru a
implementa metode si clase generice. In acest capitol vom prezenta:
principiile generale ale mostenirii, inclusiv polimorfismul
cum este mostenirea implementat
a n Java
cum poate fi derivat
a o colectie de clase dintr-o singur
a clas
a abstract
a
interfata, care este un caz particular de clas
a abstract
a
cum se poate realiza programarea generic
a n Java folosind interfete
5.1
Ce este mostenirea?
69
Chiar si limbajul Java foloseste din plin mostenirea pentru a-si implementa propriile biblioteci de clase. Un exemplu relativ familiar l constituie exceptiile. Java defineste clasa
Exception. Asa cum am vazut deja, exista mai multe tipuri de exceptii, cum ar fi NullPointerException si ArrayIndexOutOfBoundsException. Fiecare constituie o clasa separata, ns
a
toate dispun de metoda toString pentru a furniza mesaje de eroare utile n depanare.
Mostenirea modeleaza aici o relatie de tip ESTE-UN. NullPointerException ESTE-O Exception. Datorita relatiei de tip ESTE-UN, proprietatea fundamentala a mostenirii garanteaza ca orice metoda care poate fi aplicata lui Exception poate fi aplicata si lui NullPointerException. Mai mult decat atat, un obiect de tip NullPointerException poate
sa fie referit de catre o referinta de tip Exception (reciproca nu este adevarata!). Astfel, deoarece metoda toString este o metoda disponibila n clasa Exception, vom putea
ntotdeauna scrie:
catch( Exception e )
{
System.out.println( e.toString() ) ;
}
Daca e refera un obiect de tip NullPointerException, atunci e.toString() este un apel corect.
Functie de modul de implementare al ierarhiei de clase, metoda toString ar putea fi invarianta sau ar putea fi specializat
a pentru fiecare clasa distincta. Atunci cand o metoda este
invarianta n cadrul unei ierarhii, adica are aceeasi functionalitate pentru toate clasele din
ierarhie, nu va trebui sa rescriem implementarea acelei metode pentru fiecare clasa.
Apelul lui toString mai ilustreaza un alt principiu important al programarii orientate pe
obiecte, cunoscut sub numele de polimorfism. O variabila referinta care este polimorfic
a
poate sa refere obiecte de tipuri diferite. Atunci cand se aplica o metoda referintei, se
selecteaza automat operatia adecvata pentru tipul obiectului care este referit n acel moment. In Java, toate tipurile referinta sunt polimorfice. In cazul unei referinte de tip
Exception se ia o decizie n timpul executiei: se va apela metoda toString pentru obiectul
pe care e l refera n acel moment n timpul executiei. Acest proces este cunoscut sub
numele de legare t
arzie sau legare dinamic
a.
In cazul mostenirii avem o clasa de baza din care sunt derivate alte clase. Clasa de baz
a
este clasa pe care se bazeaza mostenirea. O clas
a derivat
a mosteneste toate proprietatile
clasei de baza, nsemnand ca toti membri publici ai clasei de baza devin membri publici
ai clasei derivate. Clasa derivata poate sa adauge noi atribute si metode si poate modifica
semnificatia metodelor mostenite. Fiecare clasa derivata este o clasa complet noua. Clasa
de baza nu este n nici un fel afectata de modificarile aduse n clasele derivate. Astfel, la
crearea clasei derivate este imposibil sa se strice ceva n clasa de baza.
O clas
a derivata este compatibila ca tip cu clasa de baza, ceea ce nseamna ca o variabil
a
referinta de tipul clasei de baza poate referi un obiect al clasei derivate, dar nu si invers.
Clasele surori (cu alte cuvinte clasele derivate dintr-o clasa comuna) nu sunt compatibile
ca tip.
Asa cum am mentionat anterior, folosirea mostenirii genereaza de obicei o ierarhie de clase.
Figura 5.1 prezinta o mica parte din ierarhia de exceptii a limbajului Java. Remarcati
faptul ca NullPointerException este indirect derivata din Exception. Acest lucru nu constituie nici o problema, deoarece relatiile de tipul ESTE-UN sunt tranzitive. Cu alte cuvinte,
daca X ESTE-UN Y si Y ESTE-UN Z, atunci X ESTE-UN Z. Ierarhia Exception ilustreaz
a
70
CAPITOLUL 5. MOS
TENIRE
n acelasi timp designul clasic n care se extrag caracteristicile comune n clasa de baza,
urmate de specializari n clasele derivate. ntr-o astfel de ierarhie spunem ca clasa derivata
este o subclas
a a clasei de baza, iar clasa de baza este o superclas
a a clasei derivate. Aceste
relatii sunt tranzitive; mai mult, operatorul instanceof functioneaza pentru subclase. Astfel, daca obj este de tipul X (si nu e null), atunci expresia obj instanceof Z este adevarata.
In urmatoarele sectiuni vom examina urmatoarele probleme:
Care este sintaxa folosit
a pentru a deriva o clas
a nou
a dintr-o clas
a de
baz
a?
Cum afecteaz
a acest lucru statutul membrilor private sau public?
Cum preciz
am faptul c
a o metod
a este invariant
a pentru o ierarhie de
clase?
Cum specializ
am o metod
a?
Cum factoriz
am aspectele comune ntr-o clas
a abstract
a, pentru a crea
apoi o ierarhie?
Putem deriva o clas
a nou
a din mai mult de o clas
a (mostenire multipl
a)?
Cum se foloseste mostenirea pentru a implementa codul generic?
Cateva dintre aceste subiecte sunt ilustrate prin implementarea unei clase Shape din
care vom deriva clasele Circle, Square si Rectangle. Ne vom folosi de acest exemplu
pentru a vedea cum Java implementeaz
a polimorfismul cat si pentru a vedea cum poate
fi mostenirea folosita pentru a implementa metode generice.
ArrayIndexOutOfBoundsException
IndexOutOfBoundsException
J
J
^
J
RuntimeException H
StringIndexOutOfBoundsException
HH
j
NullPointerException
Exception
EOFException
J
6
^
J
IOException
J
J
J
^
J
FileNotFoundException
JAVA
5.2. SINTAXA DE BAZA
5.2
71
Sintaxa de baz
a Java
O clasa derivata mosteneste toate proprietatile clasei de baza. Clasa derivata poate apoi
sa adauge noi atribute, sa redefineasca metode sau sa adauge noi metode. Fiecare clas
a
derivat
a este o clasa complet noua. Aspectul general al unei clase derivate este prezentat
n Figura 5.2. Clauza extends declara faptul ca o clasa este derivata dintr-o alta clasa.
1. public class ClasaDerivata extends ClasaDeBaza
2. {
3. //orice membri (public sau protected) care nu sunt listati
4. //vor fi mosteniti nemodificati, cu exceptia constructorului
5.
6. //membri public
7. //constructor(i), daca cel implicit nu este suficient
8. //met din ClasaDeBaza a caror implementare este modificata
9. //metode publice aditionale
10.
11. //membri private
12. //atribute suplimentare (in general private)
13. //alte metode private
14.}
Figura 5.2 Aspectul general al unei clase derivate
Iata o descriere scurta a unei clase derivate:
In general, toate atributele sunt private, deci atributele suplimentare vor fi adaugate
clasei derivate prin precizarea lor n sectiunea private.
Orice metode din clasa de baza care nu sunt precizate n clasa derivata sunt mostenite
nemodificat, cu exceptia constructorului. Cazul particular al constructorului l vom
prezenta n paragraful 5.2.2.
Orice metoda din clasa de baza care este definita n sectiunea public a clasei derivate
este redefinita. Noua metoda va fi aplicata obiectelor din clasa derivata.
Metodele public din clasa de baza nu pot fi redefinite n sectiunea private a clasei
derivate.
Clasei derivate i putem adauga metode suplimentare.
5.2.1
Reguli de vizibilitate
S
tim deja ca orice membru care este declarat a fi private este accesibil doar n metodele
clasei. Rezulta deci ca nici un membru private din clasa de baza nu va fi accesibil n clasa
derivat
a.
Exista situatii n care clasa derivata trebuie sa aiba acces la membri clasei de baza. Exist
a
doua optiuni pentru a realiza acest lucru. Prima este aceea de a utiliza accesul de tip
public sau friendly. Totusi, accesul de tip public permite accesul si altor clase, pe lang
a
72
CAPITOLUL 5. MOS
TENIRE
clasele derivate. Accesul de tip friendly functioneaza doar daca ambele clase sunt n acelasi
pachet.
Daca dorim sa restrangem accesul unor membri, astfel ncat ei sa fi vizibili doar pentru
clasele derivate, putem sa declaram membri ca fiind de tip protected. Un membru de tip
protected este private pentru toate clasele cu exceptia claselor derivate (si a claselor din
acelasi pachet). Declararea atributelor ca fiind public sau protected ncalca principiile
ncapsularii si ascunderii informatiei si se recurge la ea doar din motive de comoditate. De
obicei, este preferabil sa se scrie modificatori si accesori sau sa se foloseasca accesul de tip
friendly. Totusi, daca o declaratie protected va scuteste de scrierea de cod stufos, atunci
se poate recurge la ea.
5.2.2
Constructor si super
Fiecare clasa derivata trebuie sa si defineasca proprii constructori. Daca nu se scrie nici un
constructor, Java va genera un constructor implicit fara parametri. Acest constructor va
apela constructorul fara parametri al clasei de baza pentru membrii care au fost mosteniti,
dupa care va aplica initializarea implicita pentru atributele adaugate (adica 0 pentru
tipurile primitive si null pentru tipurile referinta).
Construirea unei obiect al unei clase derivate are loc prin construirea prealabila a portiunii
mostenite. Acest lucru este natural, deoarece principiul ncapsularii afirma ca partea
mostenita este o entitate unica, iar constructorul clasei de baza ne spune cum sa initializam
aceasta entitate.
Constructorii clasei de baza pot fi apelati explicit prin metoda super. Astfel, constructorul
implicit pentru o clasa derivata are de fapt forma:
public Derived()
{
super() ;
}
Metoda super poate fi apelata si cu parametri care sa se potriveasca cu un constructor
din clasa de baza. De exemplu, daca clasa de baza are un constructor care accepta doi
parametri de tip int, atunci constructorul clasei derivate ar putea fi:
public Derived(int x, int y)
{
super(x, y) ;
// alte instructiuni
}
Metoda super poate sa apara doar n prima linie dintr-un constructor. Daca nu se face un
apel explicit, compilatorul va realiza automat un apel al metodei super fara parametri.
5.2.3
Asa cum am precizat deja mai devreme, clasa derivata poate sa redefineasca sau sa accepte
nemodificate metodele din clasa de baza. In multe cazuri este clar faptul ca o anumita
metoda trebuie sa fie invarianta de-a lungul ierarhiei, ceea ce nseamna ca clasele derivate
JAVA
5.2. SINTAXA DE BAZA
73
nu trebuie sa o redefineasca. In acest caz, putem declara metoda ca fiind de tip final iar
ea nu va putea fi redefinita.
Pe langa faptul ca declararea unei metode final este o practica buna de programare, ea
poate genera si cod mai rapid. Declararea unei metode final (atunci cand este cazul) constituie o practica buna de programare deoarece intentiile noastre devin astfel clare pentru
cititorul programului si al documentatiei si, pe de alta parte, putem preveni redefinirea
accidentala pentru o metoda care nu trebuie sa fie redefinita.
Pentru a vedea de ce folosirea lui final poate conduce la cod mai eficient, sa presupunem
ca avem o clasa de baza numita Base care defineste o metoda final f , iar Derived este o
clasa care extinde Base. Sa consideram functia:
void xxx(Base obj)
{
obj.f()
}
Deoarece f este o metoda final, nu are nici o importanta daca n momentul executiei obj
refera un obiect de tip Base sau un obiect de tip Derived; definirea lui f este invarianta, deci
stim de la nceput ceea ce f va face. O consecinta a acestui fapt este ca decizia pentru codul
care va fi executat se ia nca de la compilare, si nu la executie. Acest proces este cunoscut
sub numele de legare static
a. Deoarece legarea se face la compilare si nu la executie,
programul ar trebui sa ruleze mai repede. Daca acest fapt este sau nu perceptibil efectiv
(tinand cont de viteza de prelucrare a procesoarelor din generatia actuala) depinde de
numarul de ori n care evitam deciziile din timpul executiei n timpul rularii programului.
Un corolar al acestei observatii l constituie faptul ca daca f este o metoda banala, cum ar
fi un accesor pentru un atribut, compilatorul ar putea sa nlocuiasca apelul lui f, direct cu
corpul functiei. Astfel, apelul functiei va fi nlocuit cu o singura linie care acceseaza un
atribut, economisindu-se astfel timp. Daca f nu ar fi fost declarata final, acest lucru ar fi
fost imposibil, deoarece obj ar fi putut referi un obiect al unei clase derivate, pentru care
definirea lui f ar fi putut fi diferita.
Metodele statice nu au un obiect care sa le controleze, deci apelul lor este rezolvat nca de
la compilare folosind legarea statica.
Similare metodelor final sunt clasele final. O clasa final nu mai poate fi extinsa. In
consecinta, toate metodele unei astfel de clase sunt metode final. Ca un exemplu, clasa
Integer este o clasa final. De remarcat faptul ca daca o clasa are doar membri final, ea nu
este neaparat o clasa final.
5.2.4
Metodele din clasa de baza pot fi redefinite n clase derivate prin furnizarea unei metode
din clasa derivata care sa aiba aceeasi semnatura. Metoda din clasa derivata trebuie s
a
aiba aceeasi semnatura si nu are dreptul sa adauge exceptii la lista throws.
Adeseori, metoda din clasa derivata trebuie sa apeleze metoda din clasa de baza. Acest
proces este numit redefinire partial
a. Cu alte cuvinte vrem sa facem ceea ce face si metoda
din clasa de baza, plus nca ceva. Apeluri ale clasei de baza pot fi realizate folosind super.
Iata un exemplu:
public class Student extends Human
74
CAPITOLUL 5. MOS
TENIRE
{
public doWork()
{
takeBreak();
//pauzele lungi si dese
super.doWork();//cheia marilor
takeBreak();
//succese
}
}
5.2.5
Pana acum am vazut faptul ca unele metode sunt invariante de-a lungul ierarhiei de clase
(metodele final), iar alte metode si modifica semnificatia de-a lungul ierarhiei. O a treia
posibilitate este ca o metoda din clasa de baza sa aiba sens doar pentru clasele derivate,
si vrem ca ea sa fie obligatoriu definita n clasele derivate; totusi, implementarea metodei
nu are nici un sens pentru clasa de baza. In aceasta situatie, putem declara metoda ca
fiind abstracta.
O metod
a abstract
a este o metoda care declara functionalitati care vor trebui neaparat
implementate pana la urma n clasele derivate. Cu alte cuvinte o metoda abstracta spune
ceea ce obiectele derivate trebuie sa faca. Totusi, ea nu furnizeaza nici un fel de implementare; fiecare clasa derivata trebuie sa vina cu propria implementare.
O clasa care are cel putin o metoda abstracta este o clas
a abstract
a. Java pretinde ca toate
clasele abstracte sa fie definite explicit ca fiind abstracte. Atunci cand o clasa derivata nu
redefineste o metoda abstracta, metoda ramane abstracta si n clasa de baza. In consecinta
daca o clasa care nu intentionam sa fie abstracta nu redefineste toate metodele abstracte,
compilatorul va detecta inconsistenta si va genera un mesaj de eroare.
Un exemplu simplu de clasa abstracta este clasa Shape (shape nseamna forma sau curba),
pe care o vom folosi ntr-un exemplu n cadrul acestui capitol. Din Shape vom deriva forme
specifice cum ar fi Circle sau Rectangle. Putem deriva apoi Square ca un caz particular de
Rectangle. Figura 5.3 prezinta ierarhia de clase care rezulta.
.
Figura 5.3 Ierarhia de clase pentru forme
Clasa Shape poate sa aiba membri care sa fie comuni pentru toate clasele. Intr-un exemplu
mai extins, aceasta ar putea include coordonatele extremitatilor obiectului. Ea declara si
defineste metode cum ar fi positionOf, care sunt independente de tipul formei; positionOf
ar fi o metoda final. Ea defineste si metode care se aplica fiecarui obiect n parte. Unele
dintre aceste metode nu au nici un sens pentru clasa abstracta Shape. De exemplu, este
dificil de calculat aria unui obiect oarecare; metoda area va fi declarata abstract.
Asa cum am mentionat anterior, existenta cel putin a unei metode abstracte, face clasa
sa devina si ea abstracta, deci ea nu va putea fi instantiata. Astfel, nu vom putea crea un
obiect de tip Shape; vom putea crea doar obiecte derivate. Totusi, ca de obicei, o referinta
de tip Shape poate sa refere orice forma concreta derivata, cum ar fi Circle sau Rectangle.
Exemplu:
75
JAVA
5.2. SINTAXA DE BAZA
Shape a,b ;
a = new Circle( 3.0 ) ;
b = new Shape( "circle" ) ;
//corect
//incorect
Codul din Figura 5.4 prezinta clasa abstracta Shape. La linia 13, declaram o variabil
a
de tip String care retine tipul formei, folosita doar n clasele derivate. Atributul este de
tip private, deci clasele derivate nu au acces direct la el. Restul clasei cuprinde o lista de
metode.
Constructorul nu va fi apelat niciodata direct, deoarece Shape este o clasa abstracta. Avem
totusi nevoie de un constructor care sa fie apelat din clasele derivate pentru a initializa
atributele private. Constructorul clasei Shape stabileste valoarea atributului name.
Linia 15 declara metoda abstracta area. area este o metoda abstracta deoarece nu putem
furniza nici un calcul implicit al ariei pentru o clasa derivata care nu si defineste propria
metod
a de calcul a ariei.
Metoda de comparatie din liniile 22-25 nu este abstracta, deoarece ea poate fi aplicat
a
n acelasi mod pentru toate clasele derivate. De fapt, definirea ei este invarianta de-a
lungul ierarhiei, de aceea am declarat-o final. Parametrul rhs (de la right-hand-side)
reprezinta un alt obiect de tip Shape, a carui arie se compara cu cea a obiectului curent.
Este interesant de remarcat faptul ca variabila rhs poate sa refere orice instanta a unei
clase derivate din Shape (de exemplu o referinta a clasei Rectangle). Astfel este posibil ca
folosind aceasta metoda sa comparam aria obiectului curent (care poate fi, de exemplu, o
instant
a a clasei Circle) cu aria unui obiect de alt tip, derivat din Shape. Acesta este un
exemplu excelent de folosire a polimorfismului.
Metoda toString din liniile 27-30 afiseaza numele formei si aria ei. Ca si metoda de
comparatie, ea este invarianta de-a lungul ierarhiei, de aceea a fost declarata final.
Inainte de a trece mai departe, sa rezumam cele 4 tipuri de metode ale unei clase:
1. Metode finale. Apelul lor este rezolvat nca de la compilare. Folosim metode final
doar atunci cand metoda este invarianta de-a lungul ierarhiei (adica atunci cand
metoda nu este niciodata redefinita).
2. Metode abstracte. Apelul lor este rezolvat n timpul executiei. Clasa de baza nu
furnizeaza nici o implementare a lor si este abstracta. Clasele derivate trebuie fie s
a
implementeze metoda, fie devin ele nsele abstracte.
3. Metode statice. Apelul este rezolvat la compilare, deoarece nu exista obiect care s
a
le controleze.
4. Alte metode. Apelul este rezolvat la executie. Clasa de baza furnizeaza o implementare implicita care fie va fi redefinita n clasele derivate, fie acceptata nemodificata.
1.
2.
3.
4.
5.
6.
76
CAPITOLUL 5. MOS
TENIRE
7. //double area()
--> Intoarce aria (abstracta)
8. //boolean lessThan --> Compara doua forme dupa arie
9. //String toString
--> Metoda uzuala pentru scriere
10.
11.abstract class Shape
12.{
13. private String name ;
14.
15. abstract public double area() ;
16.
17. public Shape( String shapeName )
18. {
19. name = shapeName ;
20. }
21.
22. final public boolean lessThan( Shape rhs )
23. {
24. return area() < rhs.area() ;
25. }
26.
27. final public String toString()
28. {
29. return name + "avand aria " + area() ;
30. }
31.}
Figura 5.4 Clasa de baz
a abstract
a Shape
5.3
In aceasta sectiune vom implementa clasele derivate din clasa Shape si vom prezenta cum
sunt ele utilizate ntr-o maniera polimorfica. Iata enuntul problemei:
Sortare de forme. Se citesc N forme (cercuri, dreptunghiuri sau p
atrate). S
a se afiseze
ordonate dup
a arie.
Implementarea claselor, prezentata n Figura 5.5 este simpla si nu ilustreaza aproape
nici un concept pe care sa nu-l fi prezentat deja. Singura noutate este ca clasa Square este
derivata din clasa Rectangle care este, la randul ei, derivata din Shape. La implementarea
acestor clase trebuie sa:
1. Definim un nou constructor
2. Sa examinam fiecare metoda care nu este final sau abstract pentru a vedea daca dorim
sa o acceptam nemodificata. Pentru fiecare astfel de metoda care nu corespunde cu
necesitatile clasei trebuie sa furnizam o noua definire.
77
78
CAPITOLUL 5. MOS
TENIRE
44.
45.public class Square extends Shape
46.{
47. public Square( double side)
48. {
49. super(side, side) ;
50. }
51.}
Figura 5.5 Codul complet pentru clasele Circle, Rectangle si Square, care va fi salvat n
trei fisiere surs
a separate.
Pentru fiecare clasa am scris un constructor simplu care permite initializarea cu dimensiunile de baza (raza pentru cercuri, lungimea laturilor pentru dreptunghiuri si patrate). In
constructor, vom initializa mai ntai partea mostenita prin apelul lui super. Fiecare clasa
trebuie sa defineasca o metoda area, deoarece Shape a declarat-o ca fiind abstracta. Daca
uitam sa scriem o metoda area pentru una dintre clase, eroarea va fi detectata nca de la
compilare, deoarece - daca metoda area lipseste - atunci clasa derivata este si ea abstracta.
Observati ca clasa Square este dispusa sa accepte metoda area mostenita de la Rectangle,
de aceea nu o mai redefineste.
Dupa ce am implementat clasele, suntem gata sa rezolvam problema originala. Vom folosi
un sir de clase Shape. Retineti faptul ca prin aceasta nu alocam memorie pentru nici un
obiect de tip Shape (ceea ce ar fi ilegal); se aloca memorie doar pentru un sir de referinte
catre Shape. Aceste referinte vor putea referi obiecte de tip Circle, Rectangle sau Square1 .
In Figura 5.6 facem exact acest lucru. Mai ntai citim obiectele. La linia 21, apelul
lui readShape consta n citirea unui caracter, urmata de dimensiunile figurii si de crearea
unui nou obiect de tip Shape. Figura 5.7 prezinta o implementare primitiva a acestei
rutine. Observati ca n cazul unei erori la citire se creeaza un cerc de raza 0 si se ntoarce o
referinta la el. O solutie mai eleganta n aceasta situatie ar fi fost sa definim si sa aruncam
o exceptie.
Dupa aceasta, fiecare obiect create de catre readShape este referit de catre un element
al sirului shapes. Se apeleaza insertionSort pentru a sorta formele. In final afisam sirul
rezultat de forme, apeland astfel implicit metoda toString.
1. import java.io.* ;
2.
3. class TestShape
4. {
5. private static BufferedReader in ;
6.
7. public static void main( String[] args )
8. {
9. try
10. {
1
Nici nu se poate inventa o ilustrare mai bun
a pentru polimorfism (poli=mai multe, morphos=forme).
Intr-adev
a referinta de tip Shape (shape=form
a) poate referi clase de mai multe (poli) forme (morphos):
Circle, Rectangle si Square.
79
80
CAPITOLUL 5. MOS
TENIRE
6.
7. private static Shape readShape()
8. {
9.
double rad ;
10. double len ;
11. double wid ;
12. String s ;
13.
14. try
15. {
16.
System.out.println( "Introduceti tipul formei: ") ;
17.
do
18.
{
19.
s = in.readLine() ;
20.
}while( s.length() == 0 ) ;
21.
22.
switch( s.charAt(0) )
23.
{
24.
case c:
25.
System.out.println("Raza cercului: " ) ;
26.
rad = Integer.parseInt( in.readLine() ) ;
27.
return new Circle( rad ) ;
28.
case s:
29.
System.out.println("Latura patratului: " ) ;
30.
len = Integer.parseInt( in.readLine() ) ;
31.
return new Square( len ) ;
32.
case r :
33.
System.out.println("Lung. si latimea dreptunghiului "
34.
+ "pe linii separate: ") ;
35.
len = Integer.parseInt( in.readLine() ) ;
36.
wid = Integer.parseInt( in.readLine() ) ;
37.
return new Rectangle( len, wid ) ;
38.
default:
39.
System.err.println( "Introduceti c, r sau s") ;
40.
return new Circle( 0 ) ;
41.
}
42. }
43. catch( IOException e )
44. {
45.
System.err.println( e ) ;
46.
return new Circle( 0 ) ;
47. }
48.}
Figura 5.7 Rutin
a simpl
a pentru citirea si ntoarcerea unei noi forme.
49.//sortare porin insertie
5.4. MOS
TENIRE MULTIPLA
81
5.4
Mostenire multipl
a
Toate exemplele prezentate pana acum, derivau o clasa dintr-o singura clasa de baza. In
cazul mostenirii multiple o clasa poate fi derivata din mai mult de o clasa de baza. De
exemplu, putem avea clasele Student si Angajat. Din aceste clase ar putea fi derivata o
clasa AngajatStudent.
Desi mostenirea multipla pare destul de atragatoare, si unele limbaje (cum ar fi C++)
chiar o implementeaza, ea este mbibata de subtilitati care fac proiectarea claselor deosebit
de dificila. De exemplu, cele doua clase de baza ar putea contine metode care au aceeasi
semnatura, dar implementari diferite, sau ar putea avea atribute cu acelasi nume. Care
dintre ele ar trebui folosit?
Din aceste motive, Java nu permite mostenirea multipla. Java furnizeaza nsa o alternativa, numita interfat
a.
5.5
Interfete
Interfata n Java este cea mai abstracta clasa posibila. Ea consta doar din metode abstracte publice si din atribute statice si finale.
Spunem ca o clasa implementeaz
a o anumita interfata daca furnizeaza definitii pentru
toate metodele abstracte din cadrul interfetei. O clasa care implementeaza o interfata se
comporta ca si cand ar fi extins o clasa abstracta precizata de catre acea interfata.
In principiu, diferenta esentiala dintre o clasa abstracta si o interfata este ca desi amandou
a
furnizeaza o specificatie a ceea ce clasele derivate trebuie sa faca, interfata nu poate furniza
nici un fel de detaliu de implementare sub forma de atribute sau de metode implementate.
Consecinta practica a acestui lucru este ca interfetele nu sufera de problemele potentiale
pe care le are mostenirea multipla, deoarece nu putem avea implementari diferite pentru aceeasi metoda. Astfel, desi o clasa poate sa extinda o singura clasa, ea poate s
a
implementeze mai mult de o singura interfata.
82
CAPITOLUL 5. MOS
TENIRE
5.5.1
Din punct de vedere sintactic, nimic nu este mai simplu decat precizarea unei interfete.
Interfata arata ca o declaratie a unei clase, doar ca foloseste cuvantul cheie interface. Ea
consta dintr-o lista de metode care trebuie declarate. Un exemplu de interfata este Comparable, prezentata n Figura 5.9.
Interfata Comparable precizeaza doua metode pe care orice clasa derivata din ea trebuie
sa le implementeze: compareTo si lessThan. Metoda compareTo se va comporta similar
cu metoda compareTo a clasei String. Observati ca nu este necesar sa precizam faptul ca
aceste metode sunt public sau abstract, deoarece acest lucru este implicit pentru metodele
unei interfete.
5.5.2
5.5. INTERFET
E
83
5.5.3
Interfete multiple
Asa cum am mentionat mai devreme, o clasa poate sa implementeze mai mult de o singur
a
interfata. Sintaxa pentru a realiza acest lucru este simpla. O clasa poate implementa mai
84
CAPITOLUL 5. MOS
TENIRE
5.6
Sa ne reamintim ca unul dintre scopurile principale ale programarii orientate pe obiecte este
suportul pentru reutilizarea codului. Unul dintre mecanismele importante folosite pentru
ndeplinirea acestui scop este programarea generic
a: Daca implementarea unei metode este
identica pentru mai multe clase (cu exceptia tipului de baza al obiectului), se poate folosi
o implementare generic
a pentru a descrie functionalitatea de baza. De exemplu, putem
scrie o metoda care sa sorteze un sir de elemente; algoritmul pentru aceasta metoda este
independent de tipul de obiecte care sunt sortate, deci putem folosi un algoritm generic.
Spre deosebire de multe dintre limbajele de programare mai noi (cum ar fi C++) care
utilizeaza sabloane pentru a implementa programarea generica, Java nu ofera suport pentru
implementarea directa a programarii generice, deoarece programarea generica poate fi
implementat
a folosind conceptele de baza ale mostenirii. In aceasta sectiune vom prezenta
cum pot fi implementate metode si clase generice n Java folosind principiile de baza ale
mostenirii.
Ideea de baza n Java este ca putem implementa o clasa generica folosind o superclasa
adecvata, cum ar fi Object. In Java, daca o clasa nu extinde o alta clasa, atunci ea extinde
implicit clasa Object (definita n pachetul java.lang). Ca o consecinta, fiecare clasa este o
subclasa a lui Object. Sa consideram clasa MemoryCell din Figura 5.11. Aceasta clasa
poate sa retina un obiect de tip Object. Deoarece Object este clasa de baza pentru orice
clasa din Java, rezulta ca clasa noastra poate sa stocheze orice fel de obiecte.
1. //clasa MemoryCell
2. // Object read() --> Intoarce valoarea stocata
3. // void write( Object x )
--> x este stocat
4.
5. public class MemoryCell
6. {
7. //metode publice
8. public Object read()
9. {
10. return storedValue() ;
11. }
12.
13. public void write( Object x )
14. {
15. storedValue = x ;
16. }
85
17.
18. private object storedValue ;
19.}
Figura 5.11 Clasa generic
a MemoryCell
Exista doua detalii care trebuie luate n considerare atunci cand folosim aceasta strategie.
Ambele sunt ilustrate n Figura 5.12. Functia main scrie valoarea 5 ntr-un obiect MemoryCell, dupa care citeste din obiectul MemporyCell. In primul rand, tipurile primitive
nu sunt obiecte. Astfel, m.write(5) ar fi fost incorect. Totusi, aceasta nu este o problem
a,
deoarece Java dispune de clase wrapper (de mpachetare) pentru cele opt tipuri primitive. Astfel, obiectul de tip MemoryCell va retine un obiect de tip Integer.
Al doilea detaliu este ca rezultatul lui m.read() este un Object. Deoarece n clasa Object
este definita o metoda toString, nu este necesar sa facem conversia de la Object la Integer.
Referinta returnata de m.read() (de tip Object) este polimorfica si ea refera de fapt un
obiect de tip Integer. In consecinta, se va apela automat metoda toString a clasei Integer.
Daca am fi vrut totusi sa extragem valoarea retinuta n obiectul de tip MemoryCell, ar fi
trebuit sa scriem o linie de genul
int i = ( (Integer)m.read() ).intValue() ;
n care se converteste mai ntai valoarea returnata de read la Integer, dupa care se foloseste
metoda intValue() pentru a obtine un int.
Deoarece clasele wrapper sunt clase finale, constructorul si accesorul intValue pot fi expandate inline de catre compilator, generand astfel un cod la fel de eficient ca utilizarea
directa a unui int.
1. public class TestMemoryCell
2. {
3.
public static void main( String[] args )
4.
{
5.
MemoryCell m = new MemoryCell() ;
6.
7.
m.write( new MyInteger( 5 ) ) ;
8.
System.out.println( "Continutul este: " + m.read() ) ;
9.
}
10. }
Figura 5.12 Folosirea clasei generice MemoryCell
Un al doilea exemplu este problema sortarii. Am scris deja o metoda insertionSort care
lucreaz
a cu un sir de clase Shape. Ar fi interesant sa rescriem aceasta metoda pentru un
sir generic. Figura 5.13 prezinta o metoda de sortare generica insertionSort care este
practic identica cu metoda de sortare din Figura 5.8 doar ca foloseste Comparable n loc
de Shape. Putem pune aceasta metoda ntr-o clasa Sort. Observati ca nu sortam Object,
ci Comparable. Metoda insertionSort sorteaza un sir de elemente Comparable, deoarece
foloseste lessThan. Aceasta nseamna ca doar clasele care implementeaza interfata Comparable pot fi sortate astfel. De remarcat faptul ca insertionSort nu poate sa sorteze un
86
CAPITOLUL 5. MOS
TENIRE
sir de obiecte Shape, deoarece clasa Shape din Figura 5.4 nu implementeaza interfata
Comparable. Unul dintre exercitii propune modificarea clasei Shape n acest sens.
Pentru a vedea cum poate fi folosita metoda generica de sortare vom scrie un program,
prezentat n Figura 5.14, care citeste un numar nelimitat de valori ntregi, le sorteaza si
afiseaza rezultatul. Metoda readIntArray a clasei Reader (prezentata n anexa) este folosita
pentru a citi un sir de ntregi. Transformam apoi sirul citit ntr-un sir de elemente care
implementeaza interfata Comparable. In linia 15 cream sirul, iar n liniile 16-19 obiectele
care sunt stocate n sir. Sortarea este realizata n linia 22. In final, afisam rezultatele n
liniile 26-29. De retinut ca metoda toString este implicit apelata pentru clasa MyInteger.
1. //sortare prin insertie
2. public static void insertionSort( Comparable[] a )
3. {
4. for( int p=1; p < a.length; ++p )
5. {
6.
Comparable tmp = a[p] ;
7.
int j = p ;
8.
for( ; j>0 && tmp.lessThan( a[j-1] ) ; --j)
9.
{
10.
a[j] = a[j-1] ;
11. }
12. a[j] = tmp ;
13. }
14.}
Figura 5.13 Algoritm de sortare generic
1. import io.*;//pachet facut de noi pentru a citi de la tastatura
2. public class SortIns
3. {
4. //program de test care citeste valori intregi de la terminal
5. //(cate unul pe linie), le sorteaza si apoi le afiseaza
6.
7. public static void main( String[] args )
8. {
9.
10. //citeste un sir de intregi
11. System.out.print("Introduceti elementele sirului: ") ;
12. int [] sir = Reader.readIntArray() ;
13.
14. //conversie la un sir de MyInteger
15. MyInteger[] sirNou = new MyInteger[ sir.length ] ;
16. for( int i=0; i<sir.length; ++i)
17. {
18.
sirNou[i] = new MyInteger( sir[i] ) ;
19. }
20.
- CLASA READER
5.7. ANEXA
5.7
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
Anex
a - clasa Reader
package io ;
//clasa va trebui salvata intr-un director cu numele "io"
//directorul in care se afla "io" va trebui adaugat in CLASSPATH
import java.io.* ;
import java.util.StringTokenizer ;
public class Reader
{
public static String readString()
{
BufferedReader in = new BufferedReader(
new InputStreamReader( System.in ) ) ;
try
{
return in.readLine() ;
}
catch(IOException e)
{
//ignore
}
return null ;
}
public static int readInt()
{
return Integer.parseInt(readString()) ;
}
public static double readDouble()
{
87
88
CAPITOLUL 5. MOS
TENIRE
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.}
5.8
return Double.parseDouble(readString()) ;
}
public static char readChar()
{
BufferedReader in = new BufferedReader(
new InputStreamReader( System.in ) ) ;
try
{
return (char)in.read() ;
}
catch(IOException e)
{
//ignore
}
return \0 ;
}
public static int[] readIntArray()
{
String s = readString() ;
StringTokenizer st = new StringTokenizer( s ) ;
//aloca memorie pentru sir
int[] a = new int[ st.countTokens() ] ;
for(int i=0; i<a.length; ++i)
{
a[i] = Integer.parseInt( st.nextToken() ) ;
}
return a ;
}
Probleme propuse
1. Scrieti doua metode generice min si max, fiecare acceptand doi parametri de tip
Comparable. Folositi aceste metode ntr-o clasa numita MyInteger.
2. Scrieti doua metode generice min si max, fiecare acceptand un sir de Comparable.
Folositi apoi aceste metode pentru tipul MyInteger.
3. Pentru exemplul cu clasa Shape modificati metodele readShape si main astfel ncat
ele sa arunce si sa prinda exceptii (n loc sa creeze un cerc de raza 0) atunci cand se
observa o eroare la citire.
89
Capitolul 6
6.1
Cantitatea de timp pe care orice algoritm o cere pentru executie depinde aproape ntotdeauna
de cantitatea de date de intrare pe care o proceseaza. Este de asteptat ca sortarea a 10.000
de elemente sa necesite mai mult timp decat sortarea a 10 elemente. Timpul de executie
al unui algoritm este astfel o functie de dimensiunea datelor de intrare. Valoarea exacta a
acestei functii depinde de mai multi factori, cum ar fi viteza calculatorului pe care ruleaza
programul, calitatea compilatorului si, n anumite situatii, calitatea programului. Pentru
un program dat, care ruleaza pe un anumit calculator, putem reprezenta grafic timpul
90
6.1.
91
92
6.2
Notatia asimptotic
a
Notatia asimptotica are rolul de a estima timpul de calcul necesar unui algoritm pentru a
furniza rezultatul, functie de dimensiunea datelor de intrare.
6.2.1
Fie N multimea numerelor naturale, < multimea numerelor reale. Fie f : N [0, ) o
functie arbitrara. Definim multimea de functii:
O(f ) = {t : N [0, ) | c > 0, n0 N, astf el
c f (n)}
incit
n n0
avem
t(n)
Cu alte cuvinte, O(f ) (se citeste ordinul lui f) este multimea tuturor functiilor t marginite
superior de un multiplu real pozitiv al lui f, pentru valori suficient de mari ale argumentului
n. Vom conveni sa spunem ca t este n ordinul lui f (sau, echivalent, t este n O(f ) , sau
t O(f ) ) chiar si atunci cand t(n) este negativ sau nedefinit pentru anumite valori
n < n0 . In mod similar, vom vorbi despre ordinul lui f chiar si atunci cand valoarea f(n)
este negativa sau nedefinita pentru un numar finit de valori ale lui n; n acest caz, vom alege
n0 suficient de mare, astfel ncat pentru n n0 acest lucru sa nu mai apara. De exemplu,
vom vorbi despre ordinul lui n/log n , chiar daca pentru n=0 si n=1 functia nu este
definita. In loc de t O(f ), uneori este mai convenabil sa folosim notatia t(n) O(f (n))
, subntelegand aici ca t(n) si f(n) sunt functii.
Fie un algoritm dat si fie o functie t : N [0, ) astfel ncat o anumita implementare a
algoritmului sa necesite cel mult t(n) unitati de timp pentru a rezolva un caz de marime
n, n N . Principiul invariantei1 ne asigura atunci ca orice implementare a algoritmului
necesita un timp n ordinul lui t. Cu alte cuvinte, acest algoritm necesita un timp n
ordinul lui f pentru orice functie f : N [0, ) pentru care t O(f ). In particular avem
1
Acest principiu afirm
a c
a dou
a implement
ari diferite ale unui algoritm nu difer
a, ca eficient
a, dec
at
cel mult printr-o constant
a multiplicativ
a
93
6.2. NOTAT
IA ASIMPTOTICA
relatia: t O(t) . Vom cauta, n general, sa gasim cea mai simpla functie f , astfel nc
at
t O(f ).
Exemplul 6.1 Fie functia t(n) = 3n2 9n + 13 . Pentru n suficient de mare, vom avea
consecint
relatia t(n) 4n2 . In
a, lu
and c=4,
a t(n) O(n2 ). La fel de
putem spune c
2
bine puteam s
a spunem c
a t(n) O(13n 2n + 12.5), dar pe noi ne intereseaz
a s
a
4
g
asim o expresie c
at mai simpl
a. Este adev
arat
a si relatia t(n) O(n ), dar, asa cum
vom vedea mai t
arziu, suntem interesati de a m
argini ct mai str
ans ordinul de m
arime al
algoritmului, pentru a putea obiectiva c
at mai bine durata sa de executie.
Proprietatile de baza ale lui O(f ) sunt date ca exercitii (1 - 5) si ar fi recomandabil sa le
studiati nainte de a trece mai departe.
Notatia asimptotica defineste o relatie de ordine partiala ntre functii si deci ntre eficienta
relativa a diferitilor algoritmi care rezolva o anumita problema. Vom da n continuare o
interpretare algebrica a notatiei asimptotice. Pentru oricare doua functii f, g : N <
definim urmatoarea relatie binara: f g daca O(f ) O(g). Relatia este o relatie de
ordine partial
a (reflexiv
a, tranzitiv
a, antisimetric
a) n multimea functiilor definite pe N si
cu valori n [0, ) (exercitiul 4). Definim si o relatie de echivalent
a: f g daca O(f )=O(g).
Prin aceasta relatie obtinem clase de echivalenta, o clas
a de echivalent
a cuprinzand toate
functiile care difera ntre ele printr-o constanta multiplicativa. De exemplu, lg n ln n
si avem o clasa de echivalenta a functiilor logaritmice, pe care o notam generic cu O(log
n) . Notand cu O(1) clasa de echivalenta a algoritmilor cu timpul marginit superior de
o constanta (cum ar fi interschimbarea a doua numere, sau maximul a trei elemente),
ierarhia celor mai cunoscute clase de echivalenta este:
O(1) O(log n) O(n) O(nlog n) O(n2 ) O(n3 ) O(2n )
Aceasta ierarhie corespunde unei clasificari a algoritmilor dupa un criteriu al performantei.
Pentru o problema data, dorim mereu sa obtinem un algoritm corespunzator unei clase cat
mai de jos (cu timp de executie cat mai mic). Astfel, se considera a fi o mare realizare
daca n locul unui algoritm exponential gasim un algoritm polinomial. Exercitiul 5 ne d
a
o metoda de simplificare a calculelor n care apare notatia asimptotica. De exemplu:
n3 + 4n2 + 2n + 7 O(n3 + (4n2 + 2n + 7)) = O(max(n3 , 4n2 + 2n + 7)) = O(n3 )
Ultima egalitate este adevarata chiar daca max(n3 , 4n2 + 2n + 7) 6= n3 pentru 0 n 4
, deoarece notatia asimptotic
a se aplica doar pentru n suficient de mare. De asemenea,
3
incit
n n0
avem
t(n)
Exista o anumita dualitate ntre notatiile O(f ) si (f ): pentru doua functii oarecare
f, g : N [0, ), avem:
94
f O(g) dac
a si numai daca g (f ) .
O estimare foarte precisa a timpului de executie se obtine atunci cand timpul de executie
al unui algoritm este limitat atat inferior cat si superior de cate un multiplu real pozitiv
al aceleasi functii. In acest scop, introducem notatia:
(f ) = O(f ) (f )
numita ordinul exact al lui f. Pentru a compara ordinele a doua functii, notatia nu este
nsa mai puternica decat notatia O , n sensul ca O(f )=O(g) este echivalent cu (f ) =
(g).
Exista situatii n care timpul de executie al unui algoritm depinde simultan de mai multi
parametri. Aceste situatii sunt tipice pentru anumiti algoritmi care opereaza cu grafuri si
la care timpul depinde atat de numarul de varfuri cat si de numarul de muchii. Notatia
asimptotica se generalizeaza n mod natural si pentru functii cu mai multe variabile. Astfel,
pentru o functie arbitrara f : N N [0, ) definim
O(f ) = {t : N N [0, ) | c > 0, n0 , m0 N, astf el incit
n0 avem t(m, n) c f (m, n)}.
m m0 , n
6.3
Nu exista o metoda standard pentru analiza eficientei unui algoritm. Este mai curand o
chestiune de rationament, intuitie si experienta. Vom arata pe baza de exemple cum se
poate efectua o astfel de analiza.
6.3.1
95
n-i iteratii, acest ciclu necesita un timp de cel mult b + a(n i) unitati, unde b este o
constanta reprezentand timpul necesar pentru initializarea buclei. O singura executie a
buclei exterioare are loc n cel mult c + b + a(n i) unitati de timp, unde c este o alt
a
constanta. T
inand cont de faptul ca bucla dupa j se realizeaza de n-1 ori, timpul total de
executie al algoritmului este cel mult:
d+
Pn1
i=1
(c + b + a(n i))
unitati de timp, d fiind din nou o constanta. Simplificam aceasta expresie si obtinem
a
a 2
a algoritmul necesita un timp n O(n2 ).
2 n + (b + c 2 )n + (d c b), de unde deducem c
O analiza similara asupra limitei inferioare arata ca timpul este de fapt n (n2 ). Nu
este necesar sa consideram cazul cel mai nefavorabil sau cazul mediu deoarece timpul de
executie al sortarii prin selectie este independent de ordonarea prealabila a elementelor de
sortat.
In acest prim exemplu am analizat toate detaliile. De obicei nsa, detalii cum ar fi timpul
necesar initializarii ciclurilor nu se vor considera explicit, deoarece ele nu afecteaza ordinul
de complexitate al algoritmului. Pentru cele mai multe situatii, este suficient sa alegem
o anumita instructiune din algoritm ca barometru si sa numaram de cate ori se execut
a
aceast
a instructiune. In cazul nostru, putem alege ca barometru testul
a[i] < a[P ozM in]
din bucla interioara. Este usor de observat ca acest test se executa de
6.3.2
n(n1)
2
ori.
Timpul pentru algoritmul de sortare prin insertie (sectiunea 5.3, Figura 5.8) este dependent de ordonarea prealabila a elementelor de sortat. Vom folosi comparatia
tmp.lessThan( a[j-1] )
din ciclul for ca barometru.
Sa presupunem ca p este fixat si fie n = a.length lungimea sirului. Cel mai nefavorabil
caz apare atunci cand tmp < a[j 1] pentru fiecare j ntre p si 1, algoritmul facand n
aceasta situatie p 1 comparatii. Acest lucru se ntampla (pentru fiecare valoare a lui p
de la 1 la n 1) atunci cand tabloul a este initial ordonat descrescator. Numarul total de
comparatii pentru cazul cel mai nefavorabil este:
Pn
i=2 i
1=
n(n1)
2
(n2 )
Vom estima acum timpul mediu necesar pentru un caz oarecare. Presupunem ca elementele
tabloului a sunt distincte si ca orice permutare a lor are aceeasi probabilitate de aparitie.
Atunci, daca 1 k p , probabilitatea ca a[p] sa fie cel de-al k-lea cel mai mare element
dintre elementele a[1], a[2], . . . , a[p] este p1 . Pentru un p fixat, conditia a[p] < a[p 1] este
falsa cu probabilitatea p1 , deci probabilitatea ca sa se execute comparatia tmp < a[j 1]
o singura data nainte de iesirea din bucla while este p1 . Comparatia tmp < a[j 1] se
executa de exact doua ori tot cu probabilitatea p1 , etc. Probabilitatea ca sa se execute
comparatia de exact p 1 ori este p2 , deoarece aceasta se ntampla atat cand tmp <
b[0] cat si cand b[0] tmp < b[1]! Numarul mediu de comparatii, pentru un p fixat,
96
i+1
2
1
i
2
Pentru a sorta n elemente, avem nevoie de ni=2 ci comparatii, ceea ce este egal cu n +3n
4
Pn 1
2
Hn (n ). Prin Hn = i=1 i (log n) am notat al n-lea termen al seriei armonice.
Se observa ca algoritmul de sortare prin inserare efectueaza pentru cazul mediu de doua ori
mai putine comparatii decat pentru cazul cel mai nefavorabil. Totusi, n ambele situatii,
numarul comparatiilor este n (n2 ).
Cu toate ca algoritmul necesita un timp n (n2 ) atat pentru cazul mediu cat si pentru
cel mai nefavorabil caz, pentru cazul cel mai favorabil (cand initial tabloul este ordonat
crescator) timpul este n O(n). De fapt, pentru cazul cel mai favorabil, timpul este si n
(n) (deci n (n)).
P
6.3.3
Matematicianul francez Eduard Lucas a propus n 1883 o problema care a devenit apoi
celebra mai ales datorita faptului ca a prezentat-o sub forma unei legende. Se spune ca
Brahma (Zeul Creatiei la hindusi) a fixat pe Pamant trei tije de diamant si pe una din ele
a pus n ordine crescatoare 64 de discuri de aur de dimensiuni diferite, astfel ncat discul
cel mai mare era jos. Brahma a creat si o manastire, iar sarcina calugarilor era sa mute
toate discurile pe o alta tija. Singura operatiune permisa era mutarea cate unui singur disc
de pe o tija pe alta, astfel ncat niciodata sa nu se puna un disc mai mare peste un disc
mai mic. Legenda spune ca sfarsitul lumii se va petrece atunci cand calugarii vor savarsi
lucrarea. Aceasta se dovedeste a fi o previziune extrem de optimista asupra sfarsitului
lumii. Presupunand ca n fiecare secund
a se muta un disc si lucrand fara ntrerupere, cele
64 de discuri nu pot fi mutate nici n 500 de miliarde de ani de la nceputul actiunii!
Pentru a rezolva problema, vom numerota cele trei tije cu 1, 2 si respectiv 3. Se observa
ca pentru a muta cele n discuri de pe tija cu numarul i pe tija cu numarul j (i si j iau
valori ntre 1 si 3) este necesar sa transferam primele n 1 discuri de pe tija i pe tija
6 i j (adica pe tija ramasa libera), apoi sa transferam discul n de pe tija i pe tija j, iar
apoi retransferam cele n 1 discuri de pe tija 6 i j pe tija j. Cu alte cuvinte, reducem
problema mutarii a n discuri la problema mutarii a n 1 discuri. Urmatoarea metoda
Java descrie acest algoritm recursiv.
public static void hanoi(int n,int i,int j)
{
if( n > 0 )
{
hanoi(n-1, i, 6-i-j) ;
System.out.println(i + "-->" + j) ;
hanoi(n-1, 6-i-j, i) ;
}
}
Pentru rezolvarea problemei initiale, facem apelul
97
hanoi(64,1,2);
Consideram instructiunea println ca barometru. Timpul necesar algoritmului este exprimat prin urmatoare recurenta:
(
t(n) =
1 daca n = 1
2t(n 1) + 1 daca n > 1
6.4
Am vazut n exemplul precedent cat de puternica si n acelasi timp cat de eleganta este
recursivitatea n elaborarea unui algoritm. Cel mai important castig al exprimarii recursive este faptul ca ea este naturala si compacta, fara sa ascunda esenta algoritmului
prin detaliile de implementare. Pe de alta parte, apelurile recursive trebuie folosite cu
discernamant, deoarece solicita si ele resursele calculatorului (timp si memorie). Analiza
unui algoritm recursiv implica aproape ntotdeauna rezolvarea unui sistem de recurente.
Vom vedea n continuare cum pot fi rezolvate astfel de recurente. Incepem cu tehnica cea
mai simpla.
6.4.1
Metoda iteratiei
Cu putina experienta si intuitie putem rezolva de multe ori astfel de recurente prin metoda
iteratiei: se executa primii pasi, se intuieste forma generala, iar apoi se demonstreaza prin
inductie matematica ca forma este corecta. Sa consideram de exemplu recurenta problemei
turnurilor din Hanoi. Se observa ca pentru a muta n discuri este necesar sa mutam n 1
discuri, apoi sa mutam un disc si n final din nou n 1 discuri. In consecinta, pentru un
anumit n > 1 obtinem succesiv:
t(n) = 2t(n 1) + 1 = 22 t(n 2) + 2 + 1 = . . . = 2n1 t(1) +
Pn2
i=0
2i
6.4.2
Inductia constructiv
a
98
f (n) =
0 daca n = 1
f (n 1) + n altfel
Pn
i=0 i
Pn
i=0 n
n(n+1)
.
2
Avem:
= n2
si deci f (n) O(n2 ). Aceasta ne sugereaza sa formulam ipoteza inductiei specificate partial
IISP(n) conform careia f este de forma f (n) = an2 + bn + c. Aceasta ipoteza este partiala
n sensul ca a, b si c nu sunt nca cunoscute. Tehnica inductiei constructive consta n a
demonstra prin inductie matematica aceasta ipoteza incompleta si a determina n acelasi
timp valorile constantelor necunoscute a, b si c.
Presupunem ca IISP(n-1) este adevarata pentru un anumit n 1. Atunci, f (n 1) =
a(n 1)2 + b(n 1) + c = an2 + (1 + b 2a)n + (a b + c). Daca dorim sa aratam
ca IISP(n) este adevarata, trebuie sa aratam ca f (n) = an2 + bn + c. Prin identificarea
coeficientilor puterilor lui n, obtinem ecuatiile 1 + b 2a = b si a b + c = c, cu solutia
a = b = 21 , c putand fi oarecare. Avem acum o ipoteza mai completa, pe care o numim
2
tot IISP(n), f (n) = n2 + n2 + c. Am ar
atat ca daca IISP(n-1) este adevarata pentru un
anumit n 1, atunci este adevarata si IISP(n). Ramane sa aratam ca este adevarata si
IISP(0). Trebuie sa aratam ca f (0) = a02 + b0 + c. S
tim ca f (0) = 0, deci IISP(0) este
2
adevarata pentru c = 0. In concluzie am demonstrat ca f (n) = n2 + n2 pentru orice n.
6.4.3
Exista din fericire si tehnici care pot fi folosite aproape automat pentru a rezolva anumite
clase de recurente. Vom ncepe prin a considera ecuatii recurente liniare omogene, adica
ecuatii de forma:
a0 tn + a1 tn1 + . . . + ak tnk = 0 (*)
unde ti sunt valorile pe care le cautam, iar coeficientii ai sunt constante.
Conform intuitiei2 , vom cauta solutii de forma:
tn = xn
unde x este o constanta (deocamdata necunoscuta). Daca nlocuim aceasta solutie n (*),
obtinem
a0 xn + a1 xn1 + . . . + ak xnk = 0
Solutiile acestei ecuatii sunt fie solutia triviala x = 0, care nu ne intereseaza, fie solutiile
ecuatiei:
a0 xk + a1 xk1 + . . . + ak = 0
care se numeste ecuatia caracteristic
a a recurentei liniare si omogene(*). Presupunand
deocamdata ca cele k radacini r1 , r2 , . . . , rk ale acestei ecuatii caracteristice sunt distincte,
se verifica usor ca orice combinatie liniara
2
De fapt, adev
arul este c
a aici nu este vorba de intuitie, ci de experient
a
99
tn =
Pk
n
i=1 ci ri
cu radacinile r1,2 =
1 5
2 .
Impun
and conditiile initiale, t0 = 0, t1 = 1, obtinem
c1 + c2 = 0, n = 0
c1 r1n + c2 r2n = 1, n = 1
de unde determinam
c1.2 = 15
Deci, tn =
1 (r n
5 1
1 (n
5
()n )
care este cunoscuta relatie a lui Moivre, descoperita la nceputul secolului XVIII. Nu
prezinta nici o dificultate sa aratam acum ca timpul pentru calculul recursiv al sirului lui
Fibonacci este n (n ) .
Cum procedam nsa atunci cand radacinile ecuatiei caracteristice nu sunt distincte? Se
poate arata ca daca r este o radacina de multiplicitate m a ecuatiei caracteristice, atunci
tn = rn , tn = nrn , tn = n2 rn , . . . , tn = nm1 rn sunt solutii pentru (*). Solutia general
a
pentru o astfel de recurenta este atunci o combinatie liniara a acestor termeni si a termenilor proveniti de la celelalte radacini ale ecuatiei caracteristice. Din nou, sunt de
determinat exact k constante din conditiile initiale.
Vom da din nou un exemplu. Fie recurenta
tn = 5tn1 8tn2 + 4tn3
cu t0 = 0, t1 = 1, t2 = 2. Ecuatia caracteristica are radacinile 1 (de multiplicitate 1) si 2
(de multiplicitate 2). Solutia generala este:
tn = c1 1n + c2 2n + c3 n2n
Din conditiile initiale, obtinem c1 = 2, c2 = 2, c3 = 21 .
100
6.4.4
101
adica (x 2)(x 3)2 . Inca o data, observam ca factorul (x 2) provine din partea stang
a
a recurentei originale, n timp ce factorul (x 3)2 este rezultatul manipularii.
Generalizand acest procedeu, se poate arata ca pentru a rezolva (**) este suficient sa luam
urmatoarea ecuatie caracteristica:
(a0 xk + a1 xk1 + . . . + ak )(x b)d+1 = 0
Odata ce s-a obtinut aceasta ecuatie, se procedeaza ca n cazul omogen.
Vom rezolva acum recurenta corespunzatoare problemei turnurilor din Hanoi
tn = 2tn1 + 1, n 1
iar t0 = 0. Rescriem recurenta astfel
tn 2tn1 = 1
care este de forma (**) cu b = 1 si p(n) = 1, un polinom cu grad 0. Ecuatia caracteristic
a
este atunci (x 1)(x 2), cu solutiile 1 si 2. Solutia generala a recurentei este:
tn = c1 1n + c2 2n
Avem nevoie de doua conditii initiale. Stim ca t0 = 0; pentru a gasi cea de-a doua conditie
calculam
t1 = 2t0 + 1
Din conditiile initiale, obtinem tn = 2n 1.
Observatie: daca ne intereseaza doar ordinul lui tn , nu este necesar sa calculam efectiv
constantele n solutia generala. Daca stim ca tn = c1 1n + c2 2n , rezulta ca tn O(2n ). Din
faptul ca numarul de mutari a unor discuri nu poate fi negativ sau constant (deoarece avem
n mod evident tn n), deducem ca c2 > 0. Avem atunci tn (2n ) si deci tn (2n ).
Putem obtine chiar ceva mai mult.
Substituind solutia generala napoi n recurenta originara, gasim
1 = tn 2tn1 = c1 + c2 2n 2(c1 + c2 2n1 ) = c1
Indiferent de conditia initiala, c1 este -1.
6.4.5
Schimbarea variabilei
Uneori putem rezolva recurente mai complicate printr-o schimbare de variabila. In exemplele care urmeaza, vom nota cu T(n) termenul general al recurentei si cu tk termenul noii
recurente obtinute printr-o schimbare de variabila. Presupunem pentru nceput ca n este
o putere a lui 2.
Un prim exemplu este recurenta
T (n) = 4T ( n2 ) + n, n > 1
n care nlocuim pe n cu 2k , notam tk = T (2k ) = T (n) si obtinem:
tk = 4tk1 + 2k
102
103
6.5
n
n0
este o
(nk log n)
Probleme propuse
104
Pn
105
15. S
a se calculeze secventa de suma maxima, formata din termeni consecutivi, a unui
sir de numere.
Solutie: Problema are solutii de diferite ordine de complexitate timp. Puteti s
a
ncercati voi sa le gasiti. In acest moment vom da solutia optima din punct de vedere
al complexitatii. Este vorba de o singura parcurgere a sirului. Tehnica folosita este
cea a programarii dinamice.
public void subSirvalMax()
{
int max=0;//valoarea initiala a sumei subsirului de valoare maxima
int ci=0;//indicele de inceput al subsirului de valoare maxima
int i=0;//valoarea curenta a indicelui de inceput al subsirului actual
int cj=n;//indicele de sfarsit al subsirului de valoare maxima
int j=0;//valoarea curenta a indicelui de sfarsit al subsirului actual
int m=0;//valoarea sumei subsirului actual
for(int k=0;k<n;k++)//parcurgerea sirului dat
{
m+=a[k];
if (m>=max)
//daca suma actuala este mai mare de cea gasita pana acum atunci
//subsirul actual va fi cel de suma marxima
{
max=m;
j=k;
ci=i;
cj=j;
}//setam noile valori
if (m<0)
//daca subsirul actual are suma mai mica de 0 atunci daca
//am adauga un numar oricat de mare tot nu va fi un subsir
//cu suma maxima pentru ca suma astfel obtinuta este
//mai mica ca numarul pe care l-am adaugat
//si subsirul actual nu ne mai intereseaza
{
m=0;
i=k+1;
j=k+1;
}
//deci vom seta indicele de inceput si de sfarsit al subsirului actual
//cu indicele elementului urmator iar suma actuala o vom seta 0
} //am terminat verificarea
if ((max==0)&&(ci==0)&&(cj==n))
//daca nu s-au schimbat valorile initiale atunci inseamna
//ca nu avem nici un subsir cu valoare maxima
System.out.println("Suma maxima 0,secventa vida");
106
else
{//altfel afisam datele obtinute
System.out.println("Suma maxima este"+max);
System.out.println("Secventa este"+(ci+1)+" "+(cj+1));
}
}
Capitolul 7
Structuri de date
Multi algoritmi necesita o reprezentare adecvata a datelor pentru a fi cu adevarat eficienti.
Reprezentarea datelor, mpreuna cu operatiile care sunt permise asupra datelor formeaza o
structur
a de date. Fiecare structura de date permite inserarea de elemente. Structurile de
date difera n privinta modului n care permit accesul la membrii din grup. Unele permit
accesarea si stergerea arbitrara. Altele impun anumite restrictii, cum ar fi permiterea
accesului doar la ultimul sau la primul element inserat.
Acest capitol prezinta sapte dintre cele mai uzuale structuri de date: stive, cozi, liste
nl
antuite, arbori, arbori binari de c
autare, tabele de repartizare (hash-tables) si cozi de
prioritate. Vom defini fiecare structura de date si vom furniza o estimare intuitiva pentru
complexitatea n timp a operatiilor de inserare, stergere si accesare.
In acest capitol vom vedea:
Descrierea structurilor de date uzuale, operatiile permise pe ele si timpii
lor de executie
Pentru fiecare structur
a de date, vom defini o interfat
a Java contin
and
protocolul care trebuie implementat
Unele aplicatii ale structurilor de date
Vom urmari sa evidentiem faptul ca specificarea operatiilor suportate de o structura de
date, care i descrie functionalitatea, este independenta de modul de implementare.
7.1
107
108
7.2. STIVE
7.2
109
Stive
O stiva este o structura de date n care orice tip de acces este permis doar pe ultimul
element inserat. Comportamentul unei stive este foarte asemanator cu cel al unui mald
ar
de farfurii. Ultima farfurie adaugata va fi plasata n varf, fiind n consecinta usor de
accesat, n timp ce farfuriile puse cu mai mult timp n urma vor fi mai greu de accesat,
putand periclita stabilitatea ntregii gramezi. Astfel, stiva este adecvata n situatiile n care
avem nevoie sa accesam doar elementul din varf. Toate celelalte elemente sunt inaccesibile.
Cele trei operatii naturale, inserare, stergere si cautare, sunt denumite n cazul unei stive
push, pop si top. Cele trei operatii sunt ilustrate n Figura 7.4. O interfata Java pentru
o stiva abstracta este prezentata n Figura 7.5. Interfata declara si o metoda topAndPop
care combina doua operatii; tot aici apare si un element nou: clauza throws prin care se
declara ca o metoda poate sa arunce catre metoda apelanta o anumita exceptie. In cazul
nostru, metodele pop, top si topAndPop pot arunca o UnderflowException n cazul n care
se ncearca accesarea unui element cand stiva este goala. Aceasta exceptie va trebui s
a
fie prinsa pana la urma de o metoda apelanta. Clasa UnderflowException este definita n
Figura 7.3, si ea este practic identica cu clasa Exception. Important pentru noi este c
a
difera tipul, ceea ce ne permite sa prindem doar aceasta exceptie cu o secventa de tipul
catch(UnderflowException e):
1.
2.
3.
4.
5.
6.
7.
8.
package Exceptions ;
public class UnderflowException extends Exception
{
public UnderflowException(String thrower)
{
super( thrower ) ;
}
}
110
push
pop,top
J
J
^
J
Stiva
Figura 7.4 Modelul unei stive: Inserarea n stiv
a se face prin push, accesul prin top,
stergerea prin pop.
Stiva este deosebit de utila deoarece sunt multe aplicatii pentru care trebuie sa accesam
doar ultimul element inserat. Un exemplu ilustrativ este salvarea parametrilor si variabilelor locale n cazul apelului unei alte subrutine.
1. package DataStructures ;
2. import Exceptions.* ;//pachet care contine UnderflowException
3.
4. //Interfata pentru stiva
5. //
6. //********************OPERATII PUBLICE*********************
7. // void push( x )
--> insereaza x
8. // void pop()
--> Sterge ultimul element inserat
9. //Object top()
--> Intoarce ultimul element inserat
10.//Object topAndPop()-->Intoarce si sterge ultimul element
11.// boolean isEmpty( )
--> Intoarce true daca stiva e vida
12.// void makeEmpty( )
--> Elimina toate elementele din stiva
13.
14.public interface Stack
15.{
16. void push( Object x ) ;
17. void pop( )
throws UnderflowException ;
18. Object top( )
throws UnderflowException ;
19. Object topAndPop( )
throws UnderflowException ;
20. boolean isEmpty( ) ;
21. void makeEmpty( ) ;
22.}
Figura 7.5 Interfat
a pentru stiv
a
1.
2.
3.
4.
5.
6.
7.
8.
9.
import DataStructures.* ;
import Exceptions.* ;
public final class TestStack
{
public static void main( String args[])
{
Stack s = new StackAr() ;
111
7.3. COZI
7.3
Cozi
O alta structura simpla de date este coada. In multe situatii este important sa avem acces
si/sau sa stergem ultimul element inserat. Dar, ntr-un numar la fel de mare de situatii,
acest lucru nu numai ca nu mai este important, este chiar nedorit. De exemplu, ntr-o
retea de calculatoare care au acces la o singura imprimanta este normal ca daca n coada
de asteptare se afla mai multe documente spre a fi tiparite, prioritatea sa i fie acordat
a
documentului cel mai vechi. Acest lucru nu numai ca este corect, dar este si necesar pentru
a garanta ca procesul nu asteapta la infinit. Astfel, pe sistemele mari este normal sa se
foloseasca cozi de tiparire. Operatiile fundamentale suportate de cozi sunt:
enqueue - inserarea unui element la capatul cozii
dequeue - stergerea primului element din coada
getFront - accesul la primul element din coada
enqueue
dequeue,getFront
Queue
Figura 7.7 Modelul unei cozi: Intrarea se face prin enqueue, accesul prin getFront,
stergerea prin dequeue.
112
113
UITE
7.4. LISTE INLANT
19. {
20.
for( ; ; )
21.
{
22.
System.out.print( " " + q.dequeue() ) ;
23.
}
24. }
25. catch( UnderflowException e )
26. {
27. //aici se ajunge cand stiva se goleste
28. }
29.
30. System.out.println( ) ;
31. }
32.}
Figura 7.9 Exemplu de utilizare a cozii; programul va afisa:
Continut: 0 1 2 3 4
Figura 7.8 ilustreaza interfata pentru o coada, iar Figura 7.9 prezinta modul de utilizare
al cozii. Deoarece operatiile pe o coada sunt restrictionate ntr-un mod asemanator cu
operatiile pe o stiva, este de asteptat ca si aceste operatii sa fie implementate ntr-un timp
constant. Intr-adevar, toate operatiile pe o coada pot fi implementate n timp constant,
O(1).
7.4
Liste nl
antuite
Intr-o lista nlantuita elementele sunt retinute discontinuu, spre deosebire de siruri n care
elementele sunt retinute n locatii continue. Acest lucru este realizat prin stocarea fiecarui
obiect ntr-un nod care contine obiectul si o referinta catre urmatorul element n lista, ca
n Figura 7.10. In acest model se retin referinte atat catre primul cat si catre ultimul
element din lista. Concret vorbind, un nod al unei liste arata la modul urmator:
class ListNode
{
Object data ; //continutul listei
ListNode next ;
}
a1
first
last
J
J
J
a2
a3
a4
114
In cazul unei liste nlantuite un element oarecare nu mai poate fi gasit cu un singur acces.
Aceasta este oarecum similar cu diferenta ntre accesarea unei melodii pe CD (un singur
acces) si accesarea unei melodii pe caseta (acces secvential). Desi din acest motiv listele
pot sa para mai putin atractive decat sirurile, exista totusi cateva avantaje importante.
In primul rand, inserarea unui element n mijlocul listei nu implica deplasarea tuturor
elementelor de dupa punctul de inserare. Deplasarea datelor este foarte costisitoare (din
punct de vedere al timpului), iar listele nlantuite permit inserarea cu un numar constant
de instructiuni de atribuire.
Merita observat ca daca permitem accesul doar la first, atunci obtinem o stiva, iar daca
permitem inserari doar la last si accesari doar la first, obtinem o coada.
In general, atunci cand folosim o lista, avem nevoie de mai multe operatii, cum ar fi gasirea
sau stergerea unui element oarecare din lista. Trebuie sa permitem si inserarea unui nou
element n orice punct. Aceasta este deja mult mai mult decat ne permite o stiva sau o
coada.
Pentru a accesa un element n lista, trebuie sa obtinem o referinta catre nodul care i corespunde. Evident ca oferirea unei referinte catre un element ncalca principiul ascunderii
informatiei. Trebuie sa ne asiguram ca orice acces la lista prin intermediul unei referinte
nu pericliteaza structura listei. Pentru a realiza acest lucru, lista este definita n doua
parti: o clasa lista si o clasa iterator. Figura 7.11 furnizeaza interfata de baza pentru o
lista nlantuita, oferind si metodele care descriu doar starea listei.
Figura 7.12 defineste o clasa iterator care este folosita pentru toate operatiile de accesare a listei. Pentru a vedea cum functioneaza aceasta clasa, sa examinam secventa de cod
clasica pentru afisarea tuturor elementelor din cadrul unei structuri liniare. Daca lista ar
fi stocata ntr-un sir, secventa de cod ar arata astfel:
//parcurge sirul a, afisand fiecare element
for(int index = 0; index < a.length; ++index)
System.out.println( a[index] ) ;
In Java elementar, codul pentru a itera o lista este:
//parcurge lista theList de tip List, afisand fiecare element
for(ListNode p = theList.first; p != null; p = p.next)
System.out.println( p.data ) ;
1.
2.
3.
4.
5.
6.
7.
8.
package DataStructures;
//Interfata pentru lista
//
//Accesul se realizeaza prin clasa ListItr
//
//********OPERATII PUBLICE*************
//boolean isEmpty()-->Intoarce true daca lista e goala
UITE
7.4. LISTE INLANT
115
116
Initializarea dinaintea ciclului for creeaza un iterator al listei. Testul de terminare a ciclului
foloseste metoda isInList definita pentru clasa ListItr. Metoda advance trece la urmatorul
nod din cadrul listei. Putem accesa elementul curent prin apelul metodei retrieve definita
n ListItr. Principiul general este ca accesul fiind realizat prin intermediul clasei ListItr,
securitatea datelor este garantata. Putem avea mai multi iteratori care sa traverseze
simultan o singura lista.
Pentru ca sa functioneze corect, clasa ListItr trebuie sa mentina doua obiecte. In primul
rand, are nevoie de o referinta catre nodul curent. In al doilea rand are nevoie de o
referinta catre obiectul de tip List pe care l indica; aceasta referinta este initializata o
singura data n cadrul constructorului.
1. import DataStructures.* ;
2. import Exceptions.* ;
3.
4. //program simplu de testare a listelor
5.
6. public final class TestList
7. {
8. public static void main(String args[])
9. {
10. List theList = new LinkedList() ;
11. ListItr itr = new LinkedListItr( theList ) ;
12. //se insereaza noi elemente pe prima pozitie
13. for(int i=0; i<5; ++i)
14. {
15.
try
16.
{
17.
itr.insert( new Integer(i) ) ;
18.
}
19.
catch( ItemNotFoundException e )
20.
{
21.
}
22.
itr.zeroth() ;
23. }
24.
25. System.out.println("Continutul listei: ") ;
26. for( itr.first() ; itr.isInList() ; itr.advance() )
27. {
28.
System.out.println( " " + itr.retrieve() ) ;
29. }
30. }
31.}
Figura 7.13 Exemplu de utilizare al listei.
Programul va afisa: 4 3 2 1 0
Desi discutia s-a axat pe liste simplu nlantuite, interfetele din Figura 7.11 si Figura
7.12 pot fi folosite pentru oricare tip de lista, indiferent de implementarea pe care o are
117
7.5
Arbori binari de c
autare
In capitolul anterior am vazut ca cea mai eficienta cautare ntr-un sir este cautarea binar
a,
care se poate aplica atunci cand elementele sirului sunt ordonate, timpul de executie fiind
logaritmic.
Sa presupunem ca avem nevoie de o structura de date n care, pe langa cautare, dorim
sa putem adauga sau sterge eficient elemente. O astfel de structura este arborele binar de
c
autare. Figura 7.14 ilustreaza operatiile de baza permise asupra unui arbore binar de
cautare. O posibila interfata este prezentata n Figura 7.15.
insert
find,remove
J
J
^
J
package DataStructures ;
import Exceptions.* ;
//*********OPERATII PUBLICE*********
// void insert( x )-->insereaza x
// void remove( x )-->sterge x
// void removeMin()-->sterge cel mai mic element
118
7.5. ARBORI BINARI DE CAUTARE
1. import Datastructures.* ;
2.
3.public final class MyString implements Comparable, Hashable
4. {
5. private String value ;
6.
7. public MyString(String x)
8. {
9.
value = x ;
10. }
11.
12. public String toString()
13. {
14. return value ;
15. }
16.
17. public int compareTo(Comparable rhs)
18. {
19. return value.compareTo( ((MyString)rhs).value ) ;
20. }
21.
22. public boolean lessThan(Comparable rhs)
23. {
24. return compareTo(rhs) < 0 ;
25. }
26.
27. public boolean equals( Object rhs )
28. {
29. return value.equals( ( (MyString)rhs.value ) );
30. }
31.
32. public int hash(int tableSize)
33. {
34. return QuadraticProbingTable.hash(value, tableSize) ;
35. }
36.}
Figura 7.16 Clasa MyString
1. //program simplu pentru testarea arborilor de cautare
2.
3. public final class TestSearchTree
4. {
5. public static void main(String[] args)
6. {
7.
SearchTree t = new BinarySearchTree() ;
119
120
8.
MyString result = null ;
9.
10. try
11. {
12.
t.insert( new MyString( "Georgica" ) ) ;
13. }
14. catch(DupicateItemException e)
15. {
16. }
17.
18. try
19. {
20.
result = (MyString) t.find(new MyString("Georgica"));
21.
System.out.print("Gasit " + result + " " ) ;
22. }
23. catch(ItemNotFoundException e)
24. {
25.
System.out.print("Georgica nu a fost gasit") ;
26. }
27.
28. try
29. {
30.
result = (MyString) t.find( new MyString( "Ionel" ) ) ;
31.
System.out.print("Gasit " + result + " " ) ;
32. }
33. catch(ItemNotFoundException e)
34. {
35.
System.out.print("Ionel nu a fost gasit") ;
36. }
37.
38. System.out.println() ;
39. }
40.}
Ce putem spune despre operatiile findMin si findMax? In mod cert, aceste operatii necesita
un timp constant n cazul cautarii binare, deoarece implica doar accesarea unui element
indiciat. In cazul unui arbore binar de cautare aceste operatii iau acelasi timp ca o cautare
obisnuita, adica O(log n) n cazul mediu si O(n) n cazul cel mai nefavorabil. Dupa cum
i sugereaza si numele, arborele binar de cautare este implementat ca un arbore binar,
necesitand astfel cate doua referinte pentru fiecare element.
7.6
121
Tabele de repartizare
Exista foarte multe aplicatii care necesita o cautare dinamica bazata doar pe un nume.
O aplicatie clasica este tabela de simboluri a unui compilator. Pe masura ce compileaz
a
programul, compilatorul trebuie sa retina numele (mpreuna cu tipul, scopul, locatia de
memorie) tuturor identificatorilor care au fost declarati. Atunci cand vede un identificator
n afara unei instructiuni de declarare, compilatorul verifica sa vada daca acesta a fost
declarat. Daca a fost, compilatorul verifica informatia adecvata din tabela de simboluri.
Avand n vedere faptul ca arborele binar de cautare permite acces logaritmic la obiecte
cu denumiri oarecare, de ce am avea nevoie de o alta structura de date? Raspunsul este
ca arborele binar de cautare poate sa dea un timp de executie liniar pentru accesul unui
element, iar pentru a ne asigura de cost logaritmic este nevoie de algoritmi mult mai
sofisticati.
Tabela de repartizare este o structura de date care evita timpul de executie liniar, ba
mai mult, suporta aceste operatii n timp (mediu) constant. Astfel, timpul de acces la un
element din tabela nu depinde de numarul de elemente care sunt n tabela. In acelasi timp,
tabela de repartizare nu foloseste apeluri la rutinele de alocare a memoriei (ca arborele
binar). Aceasta face ca tabela de repartizare sa fie rapida n practica. Un alt avantaj fat
a
de arborele binar de cautare este ca elementele stocate n tabela de repartizare nu trebuie
sa implementeze interfata Comparable.
Operatiile permise sunt date n Figura 7.18 iar o interfata este prezentata n Figura
7.19. In acest caz, inserarea unui element care este duplicat ne genereaza o exceptie, ci
elementul va fi nlocuit cu noua valoare. Aceasta este o alternativa la metoda pe care
am aplicat-o n cazul arborilor binari de cautare. Tabela de repartizare functioneaz
a
doar pentru elemente care implementeaza interfata Hashable2 . Interfata Hashable cere o
functie de repartizare, care converteste obiectul de tip Hashable ntr-un ntreg. Metoda
are urmatorul antet:
//intoarce un intreg intre 0 si tableSize-1
int hash(int tableSize) ;
Elementele dintr-o tabela de repartizare trebuie sa redefineasca si metoda equals. Figura
7.16 arata cum clasa MyString implementeaza interfata Hashable prin implementarea
metodelor hash si equals. Un exemplu de utilizare a tabelelor de repartizare este dat n
Figura 7.20.
Una dintre utilizarile obisnuite ale tabelelor de repartizare sunt dictionarele. Un dictionar
retine obiecte care constau ntr-o cheie, care este cautata n dictionar, si definitia cheii,
care este returnata. Putem utiliza tabele de repartizare pentru a implementa dictionarul
astfel:
obiectul stocat este o clasa care retine atat cheia cat si definitia ei
egalitatea, comparatia si functia de repartizare se bazeaza doar pe cheia din obiectul
stocat
2
In limba englez
a, tabelele de repartizare se numesc Hashtable, de aceea un element care poate fi
repartizat se numeste Hashable.
122
cautarea se face prin construirea unui obiect e cu cheia dorita, urmata de apelarea
metodei find din tabela de repartizare
definitia este obtinuta prin folosirea unei referinte f careia i este atribuita valoarea
ntoarsa de find.
insert
find,remove
J
J
^
J
Tabela de repartizare
Figura 7.18 Modelul pentru tabela de repartizare: Oricare element etichetat poate fi
ad
augat sau sters n timp practic constant.
1. package DataStructures ;
2.
3. import Exceptions.* ;
4.
5. //Interfata Hashtable
6. //
7. //void insert( x )-->adauga x
8. //void remove(x)-->remove x
9. //Hashable find(x)-->intoarce elementul care se potriveste cu x
10.//void makeEmpty()-->sterge toate elementele
11.
12.//**************OPERATII PUBLIC******
13.
14.public interface HashTable
15.{
16. void insert( Hashable x ) ;
17. void remove( Hashable x) throws ItemNotFoundException ;
18. Hashable find( Hashable x) throws ItemNotFoundException ;
19. void makeEmpty() ;
20.}
Figura 7.19 Interfata pentru tabele de repartizare.
1.
2.
3.
4.
5.
6.
7.
import DataStructures.* ;
import Exceptions.* ;
//program simplu pentru testarea arborilor de cautare
public final class TestHashTable
{
123
7.7
Cozi de prioritate
Desi documentele trimise unei imprimante sunt asezate, de obicei, ntr-o coada, aceasta nu
este ntotdeauna cea mai buna varianta. De exemplu, un document poate sa fie deosebit de
important, deci el ar trebui executat imediat ce imprimanta este disponibila. De asemeni,
daca imprimanta a terminat de tiparit un document, iar n coada se afla cateva documente
avand 1-2 pagini si un document avand 100 de pagini, ar fi normal ca documentul lung s
a
fie tiparit ultimul, chiar daca nu este ultimul document trimis.
Analog, n cazul unui sistem multiutilizator, sistemul de operare trebuie sa decida la un
moment dat care dintre mai multe procese trebuie sa fie executat. In general, un proces
poate sa se execute doar o perioada de timp fixata. Si aici este normal ca procesele care
au nevoie de un timp foarte mic sa aiba prioritate.
Daca vom atribui fiecarei sarcini cate un numar, atunci numarul mai mic (pagini tiparite,
resurse folosite) va indica o prioritate mai mare. Astfel, vom dori sa accesam cel mai mic
element dintr-o colectie de elemente si sa l stergem din cadrul colectiei. Acestea sunt
operatiile findMin si deleteMin. Structura de date care ofera o implementare foarte eficienta a acestor operatii se numeste coad
a de priorit
ati. Figura 7.21 ilustreaza operatiile
fundamentale pentru coada de prioritati.
124
insert
findMin,deleteMin
J
J
^
J
Coad
a de prioritate
Figura 7.21 Modelul pentru coada de priorit
ati: Doar elementul minim este accesibil.
7.8
Aplicatie
Vom da n continuare o varianta a clasei SearchTree, prezentata anterior, numita Node care
contine metodele elementare, prezentate n comentariu, dar n plus va permite adaugarea
unui nod indiferent ca acesta mai exist
a n arbore precum si faptul ca rezultatul funtiei
de cautare este boolean deci nu necesita tratarea ca exceptie. Clasa BinaryTree este o
aplicatie a clasei Node iar clasa TestBinaryTree contine metoda main.
class Node
{
private Node left,right;//legaturile arborelui binar
private double data;//informatia utila din nod
Node()//constructorul fara parametrii
{
left=null;
right=null;
data=0;
}
Node(double data)
//constructorul cu parametru pentru initializare informatiei utile
{
left=null;
right=null;
this.data=data;
}
public static Node linkNode(Node root,double data)//inserarea unui nou nod
{
if (root==null)
{
root=new Node();//alocam informatie pentru noul nod
root.setData(data);//adaugam informatia utila
root.setLeftNode(null);//setam descendentii ca fiind nuli
root.setRightNode(null);
}
else//altfel cautam locul lui
{
7.8. APLICAT
IE
125
126
{
if (root.getData()==data)//daca informatia utila din nodul curent este cea
//cautata atunci atunci returnam adevarat altfel continuam cautarea
return true;
else
{
if(root.getdata()<data)//daca informatia utila decat cea cautata
search(root.getLeftNode(),data);//se merge in stanga
if(root.getdata()>data)//altfel
search(root.getRightNode(),data);//se merge in dreapta
}
}
return false;
}
public double getData()//returneaza informatia utila din nodul curent
{
return this.data;
}
7.8. APLICAT
IE
127
128
Capitolul 8
Metoda Backtracking
8.1
Prezentare general
a
In informatica apar frecvent situatii n care rezolvarea unei probleme conduce la determinarea unor vectori de forma:
x = (x1 , x2 , . . . , xn )
unde:
fiecare componenta xi apartine unei multimi finite Vi
componentele vectorului x respecta anumite relatii, numite conditii interne, astfel
ncat x este o solutie a problemei daca si numai daca aceste conditii sunt satisfacute
de componentele x1 , x2 , . . . , xn ale vectorului.
Produsul cartezian V1 V2 . . . Vn se numeste spatiul solutiilor posibile. Elementele
acestui produs cartezian care respecta conditiile interne se numesc solutii ale problemei.
Exemplul 8.1 Fie dou
a multimi de litere V1 = {A, B, C} si V2 = {M, N }. Se cere s
a
se determine acele perechi (x1 , x2 ) cu proprietatea c
a dac
a x1 este A sau B, atunci x2 nu
poate fi N.
Rezolvarea problemei de mai sus conduce la perechile:
(A, M ), (B, M ), (C, M ), (C, N )
deoarece din cele sase solutii posibile doar acestea ndeplinesc conditiile puse n enuntul
problemei.
Exemplul 8.2 Se d
a multimea cu elementele {A, B, C, D}. Se cere s
a se genereze toate
permut
arile elementelor acestei multimi.
Se cer deci multimile x = {x1 , x2 , x3 , x4 } care respect
a conditiile:
xi 6= xj pentru i 6= j
xi apartine multimii V = V1 = V2 = V3 = V4 = {A, B, C.D} .
129
130
Exist
a multi vectori care respect
a aceste conditii: {A, B, C, D}, {B, A, C, D}, {B, C, D, A},
{B, D, A, C} etc. Mai exact, num
arul de permut
ari ale elementelor unei multimi cu 4 elemente este 4! = 24.
O modalitate de rezolvare a problemei ar fi s
a se genereze toate cele 44 = 256 elemente
ale produsului cartezian V1 V2 V3 V4 (reprezent
and solutiile posibile) si s
a se aleag
a
dintre ele cele 24 care respect
a conditiile interne. S
a observ
am ns
a c
a dac
a n loc de 4
elemente multimea noastr
a ar avea 7 elemente, vor exista 77 = 823.543 variante posibile,
dintre care doar 7! = 5040 vor respecta conditiile interne.
Conform celor ar
atate mai sus, este indicat ca, pentru a rezolva o problem
a, s
a elabor
am
algoritmi al c
aror timp de lucru s
a nu fie at
at de mare, sau, dac
a este posibil, s
a nu fie
exponential. Metoda backtracking este o metod
a foarte important
a de elaborare a algoritmilor pentru problemele de genul celor descrise mai sus. Desi algoritmii de tip backtracking
au si ei, n general, complexitate exponential
a, ei sunt totusi net superiori unui algoritm
de genul celui descris mai sus, care genereaz
a toate solutiile posibile.
8.2
Prezentarea metodei
131
fie nu numai necesare, ci chiar suficiente pentru obtinerea unei solutii. In practica se
urmareste gasirea unor conditii de continuare care sa fie cat mai dure, adica sa elimine
din start cat mai multe solutii neviabile. De obicei, conditiile de continuare reprezint
a
restrictia conditiilor interne la primele k componente ale vectorului. Evident, conditiile de
continuare n cazul k=n sunt chiar conditiile interne.
De exemplu, o conditie de continuare n cazul celei de-a doua probleme luata ca exemplu
ar fi:
vk 6= vi , i = 1, k 1
Prin metoda backtracking, orice vector este construit progresiv, ncepand cu prima component
a si mergand catre ultima, cu eventuale reveniri asupra valorilor atribuite anterior.
Reamintim ca x1 , x2 , . . . , xn primesc valori n multimile V1 , V2 , . . . , Vn . Prin atribuiri sau
ncercari de atribuiri esuate din cauza nerespectarii conditiilor de continuare, anumite
valori sunt consumate. Pentru o componenta oarecare xk vom nota prin Ck multimea
valorilor consumate la momentul curent. Evident, Ck Vk .
O descriere completa a starii n care se afla algoritmul la un moment dat se poate face
prin precizarea urmatoarelor elemente:
1. numarul de componente ale vectorului x, carora li s-au atribuit valori, avand valoarea
k-1.
2. valorile curente v1 , v2 , . . . , vk1 ale primelor k-1 componente ale vectorului x: x1 , x2 ,
. . ., xk1 .
3. multimile de valori consumate C1 , C2 , . . . , Ck pentru fiecare din componentele x1 , x2 ,
. . ., xk .
Aceast
a descriere poate fi sintetizata ntr-un tabel numit configuratie, avand urmatoarea
forma:
v1 , . . . , vk1 xk , xk+1 , . . . , xn
Ck , , . . . ,
C1 , . . . , Ck1
132
133
v1 , . . . , vn
C1 , . . . , Cn
A
B
C
D
{A} {A, B} {A, B, C} {A, B, C, D}
8.2.1
Atribuie si avanseaz
a
Acest tip de modificare are loc atunci cand mai exista valori neconsumate pentru xk (deci
Ck Vk ), iar valoarea aleasa vk are proprietatea ca (v1 , . . . , vk ) respecta conditiile de
continuare. In acest caz valoarea vk se atribuie lui xk si se adauga multimii Ck , dupa care
se avanseaza la componenta urmatoare, xk+1 . Aceasta modificare a configuratiei poate fi
reprezentata n felul urmator:
. . . , vk1 xk , xk+1 , . . .
. . . , Ck1 Ck , , . . .
vk
. . . , vk1 ,
vk xk+1 , . . .
, . . .
. . . , Ck1 , Ck {Vk }
8.2.2
A
-
x2 x3 x4
A
{A}
Incercare esuat
a
Acest tip de modificare are loc atunci cand, ca si n cazul anterior, mai exista valori
neconsumate pentru xk , dar valoarea vk aleasa nu respecta conditiile de continuare. In
acest caz, vk este adaugata multimii Ck (deci este consumata), dar nu se avanseaz
a la
componenta urmatoare. Modificarea este notata prin:
. . . , vk1 xk , xk+1 , . . .
. . . , Ck1 Ck , , . . .
vk
===
z }| {
. . . , vk1 xk ,
xk+1 . . .
. . . , Ck1 Ck {Vk }, , . . .
A
===
z }| {
A
x2 x3 x4
{A} {A}
134
8.2.3
Revenire
Acest tip de transformare apare atunci cand toate valorile pentru componenta xk au fost
consumate (Ck = Vk ). In acest caz se revine la componenta precedenta, xk1 , ncercanduse atribuirea unei noi valori acestei componente. Este important de remarcat faptul ca
revenirea la xk1 implica faptul ca pentru xk se vor ncerca din nou toate variantele
posibile, deci multimea Ck trebuie din nou sa fie vida. Transformarea este notata prin:
. . . , vk1 xk , xk+1 , . . .
. . . , Ck1 Ck , , . . .
. . . , vk2 xk1 , xk , . . .
. . . , Ck2 Ck1 , , . . .
8.2.4
x4
{1, 2, 3, 4}
3
1
x3 x4
{1, 2, 3} {1} {1, 2}
Revenire dup
a construirea unei solutii
. . . , vn1 xn
. . . , Cn1 Cn
sol
In exemplul nostru cu generarea permutarilor, revenirea dupa gasirea primei solutii este
data de diagrama:
1
2
3
4
{1} {1, 2} {1, 2, 3} {1, 2, 3, 4}
!
sol
x4
1
2
3
{1} {1, 2} {1, 2, 3} {1, 2, 3, 4}
Revenirea dupa construirea unei solutii poate fi considerata ca fiind un caz particular al
revenirii daca adaugam vectorului solutie x o componenta suplimentara xn+1 , care nu
poate lua nici o valoare (Vn+1 = ).
O problema importanta este cea a ncheierii procesului de cautare a solutiilor, sau, cu alte
cuvinte ne putem pune ntrebarea: transform
arile succesive aplicate configuratiei initiale
se ncheie vreodat
a sau continu
a la nesf
arsit? Evident ca pentru ca metoda backtracking
sa constituie un algoritm trebuie sa respecte si ea proprietatile unui algoritm enuntate
nca din primul capitol, ntre care se afla si proprietatea de finitudine. Demonstrarea
finitudinii algoritmilor de tip backtracking se bazeaza pe urmatoarea observatie simpla:
prin transformarile succesive de configuratie nu este posibil ca o configuratie sa se repete,
iar numarul de elemente al produsului cartezian V1 V2 . . . Vn este finit; prin urmare,
la un moment dat se va ajunge la configuratia:
x1 x2 . . . xn
V1 . . .
135
! A
-
A
x2
{A} {M, N }
sol
A x2
{A}
!
C
x2
{A, B, C}
N
===
z }| {
C
N
{A, B, C} {M, N }
8.3
x1 x2
{A}
B
x2
{A, B} {M }
! M
-
A M
{A} {M }
! B
-
sol
sol
B
x2
{A, B}
B
x2
{A, B} {M, N }
C
M
{A, B, C} {M }
A
x2
{A} {M }
! M
-
sol
C
x2
{A, B, C} {M, N }
C
x2
{A, B, C} {M }
!
N
===
z }| {
B M
{A, B} {M }
x1 x2
{A, B}
C
-
! N
-
x1
x2
{A, B, C}
Procesul de obtinere a solutiilor prin metoda backtracking este usor de programat deoarece
la fiecare pas se modifica foarte putine componente (indicele k, reprezentand pozitia barei,
componenta xk si multimea Ck ).
Algoritmul corespunzator (n pseudocod) este urmatorul:
initializeaz
a (citeste) multimile de valori, V1 , . . . , Vn
k 1 //se construieste configuratia initial
a
pentru i = 1, n
Ci
//acum ncepe efectiv aplicarea celor 4 transform
ari, functie de caz
a terminarea c
aut
arii
c
at timp k > 0 //k = 0nseamn
dac
a k = n + 1 atunci //configuratia este tip solutie
retine solutia v1 , . . . , vn
k k 1 //revenire dup
a solutie
altfel
dac
a Ck 6= Vk atunci //mai exist
a valori neconsumate
136
Algoritmul de mai sus functioneaza pentru cazul cel mai general, dar este destul de dificil
de programat din cauza lucrului cu multimile Ck si Vk . Din fericire, adeseori n practica
multimile Vk au forma
Vk = {1, 2, . . . , sk }
deci fiecare multime Vk poate fi reprezentata foarte simplu, prin numarul sau de elemente,
sk . Pentru a simplifica si mai mult lucrurile, vom alege valorile pentru fiecare componenta
xk n ordine crescatoare, pornind de la 1, si pana la sk . In aceasta situatie, multimea de
valori consumate Ck va fi de forma {1, 2, . . . , vk } si, drept consecinta va putea fi reprezentata doar prin valoarea vk .
Consideratiile de mai sus permit nlocuirea algoritmului anterior, bazat pe multimi, cu un
algoritm simplificat, care lucreaza numai cu numere.
La Exemplul 8.1, vom conveni sa reprezentam pe A,B,C prin valorile 1,2,3, iar pe M si N
prin 1 si 2. In aceasta situatie, configuratiile succesive se vor reprezenta mai simplu astfel:
| x1 x2
1 | x2
2 |
sol
1 | x2
etc
Algoritmul n pseudocod pentru cazul particular prezentat mai sus se concretizeaza n
urmatoarea metoda Java:
public void backtracking()
{
int k =0 ;
while(k>=0)
{
if(k==n) //am gasit o solutie
{
retSol() ; //afisam solutia
k-- ;
//revenire dupa gasirea unei solutii
}
else
{
if(x[k]<s[k])
//mai sunt valori neconsumate
{
137
x[k]++ ;
//se ia urmatoarea valoare
if( continuare(k) ) //respecta cond. de cont?
{
k++ ;
//avanseaza
}
}
else
{
x[k--] = 0 ;
}
}
}
}
//revenire
8.4
8.4.1
Se d
a multimea A cu elementele {a1 , a2 , . . . , an }. S
a se genereze toate permut
arile elementelor acestei multimi.
Se observa ca aceasta problema este o simpla generalizare a Exemplului 8.2 din subcapitolul precedent. Mai mult decat atat, problema poate fi redusa la a genera permutarile
multimii de indici {1, 2, . . . , n}. In aceasta situatie vom avea V1 = V2 = . . . = Vn =
{1, 2, . . . , n}, deci putem aplica varianta simplificata a metodei bactracking.
Conditiile interne pe care trebuie sa le respecte un vector solutie sunt:
xi 6= xj pentru i, j = 1, n,
i 6= j.
138
{
if(x[k]==x[i])
{
return false ;
}
}
return true ;
}
Metoda retSol este si ea extrem de simpla n aceasta situatie: se scriu elementele multimii
A, ordonate dupa permutarea x.
public void retSol()
{
for(int i=0; i<n; ++i)
{
System.out.print(a[x[i]-1] + " ") ;
}
System.out.println() ;
}
Metoda backtracking pentru generarea permutarilor se obtine din metoda backtracking
pentru cazul general nlocuind numarul de elemente al multimilor Vk , sk cu valoarea n.
8.4.2
Vom prezenta acum modalitatea prin care se poate adapta foarte usor algoritmul de
generare a permutarilor unei multimi pentru a genera aranjamentele si combinarile acelei
multimi. Pentru a simplifica lucrurile, vom presupune ca multimea A este formata din
primele n numere naturale, adica A = {1, 2, . . . , n}.
Reamintim faptul ca prin aranjamente de n luate cate m (n m), notate Am
nteleg
n se
toate multimile ordonate cu m elemente formate din elemente ale multimii A, cu alte
cuvinte toti vectorii de forma:
x = (x1 , . . . , xn ), unde xi {1, 2, . . . , m},
xi 6= xj ,
i, j = 1, n
Se observa ca, din punct de vedere al reprezentarii formale, singura diferenta dintre aranjamente si permutari este ca aranjamentele au lungime m n loc de n. De altfel, pentru
m=n aranjamentele si permutarile coincid.
Exemplul 8.3 Aranjamentele de 3 luate c
ate 2 (A23 )sunt:
(1,2), (1,3), (2,1), (2,3), (3,1), (3,2).
Conditiile interne si, n consecinta, conditiile de continuare, sunt identice cu cele de la
generarea permutarilor. Prin urmare si functia de continuare este identica cu cea de la
permutari. Unde este totusi diferenta? Avand n vedere ca lungimea vectorului este m si
nu n, conditia de gasire a unei solutii trebuie nlocuita cu k=m. Prin urmare, n metoda
backtracking linia:
139
if(k==n)
va fi nlocuita cu:
if(k==m)
Desigur, aceeasi modificare este necesara si n metoda retSol, n care secventa
for(int i=0; i<n; ++i)
se va nlocui cu
for(int i=0; i<m; ++i)
Sa vedem acum modalitatea de generarea a combinarilor. Reamintim ca prin combinari
de n luate cate m (notat Cnm ) se noteaza toate submultimile cu m elemente ale multimii
A = {1, 2, . . . , n}.
Exemplul 8.4 Combin
arile de 3 luate c
ate 2 (C32 ) sunt:
(1,2), (1,3), (2,3).
Diferenta ntre combinari si aranjamente este data de faptul ca, n cazul combinarilor, ordinea n care apar componentele nu conteaza ( combinarea (1,2) este aceeasi cu combinarea
(2,1) etc.; tocmai din acest motiv noi am optat n exemplul de mai sus sa aranjam componentele unei combinari n ordine crescatoare). Prin urmare, combinarile unei multimi
cu n elemente luate cate m sunt definite de vectorii:
x = (x1 , . . . , xm ), unde x1 < x2 < . . . < xm .
Conditia de continuare n cazul combinarilor va fi pur si simplu:
xk > xk1 pentru k > 1.
Metodele backtracking si retSol sunt n cazul combinarilor identice cu cele de la aranjamente. Diferenta apare la functia de continuare, care are urmatoarea forma (forma din
dreapta este mai criptica, dar mai eleganta):
public boolean cont(int k)
public boolean cont(int k)
{
{
if (k > 0 && x[k] <= x[k 1])
return k == 0 || x[k] > x[k 1];
{
}
return false;
}
else
{
return true;
}
}
Una din problemele de la finalul capitolului propune o varianta mai eficienta de generare
a combinarilor n care functia de continuare este complet eliminata.
140
8.4.3
Problema damelor
S
a se aseze n dame pe o tabl
a de sah de dimensiune nn astfel nc
at damele s
a nu fie pe
aceeasi linie, aceeasi coloan
a sau aceeasi diagonal
a (damele s
a nu se atace ntre ele).
Reamintim ca n jocul de sah, o dama ataca pozitiile aflate pe aceeasi linie sau coloana
si pe diagonala. O posibila asezare a damelor pe o tabla de sah de dimensiuni 4x4 este
data n figura de mai jos:
Figura 8.1 O solutie pentru problema damelor n cazul unei table de dimensiuni 4x4
Sa vedem cum putem reformula problema damelor pentru a o aduce la o problema de tip
backtracking. Se observa cu usurinta ca pe o linie a tablei de sah se poate afla o singura
dama, prin urmare putem conveni ca prima dama se va aseza pe prima linie, a doua dama
pe a doua linie etc. Rezulta ca pentru a cunoaste pozitia damei numarul k este suficient sa
stim coloana pe care aceasta se gaseste. O solutie a problemei se poate astfel reprezenta
printr-un vector
x = (x1 , x2 , . . . , xn ),
xk {1, 2, . . . , n},
141
{
return false ;
}
}
return true ;
}
Modificarile care trebuie aduse metodelor retSol si backtracking sunt minime si le lasam
ca exercitiu.
Observatie: Problema damelor este primul exemplu de problema n care conditiile de
continuare sunt necesare, dar nu sunt suficiente. De exemplu (pentru n=4), la nceput,
algoritmul va aseza prima dama pe prima coloana, a doua dama pe a treia coloana, iar
cea de-a treia dama nu va putea fi asezata pe nici o pozitie, fiind necesara o revenire.
8.4.4
Problema color
arii h
artilor
Se d
a o hart
a ca cea din figura de mai jos, n care sunt reprezentate schematic 6 t
ari,
dintre care unele au granite comune. Presupun
and c
a dispunem doar de trei culori (rosu,
galben, verde), se cere s
a se determine toate variantele de colorare a h
artii astfel nc
at
oricare dou
a t
ari vecine (care au frontier
a comun
a) s
a fie colorate diferit.
T1
T2
T3 T4 T5
T6
0
1
1
0
0
1
1
0
1
1
1
0
1
1
0
1
0
1
0
1
1
0
1
1
0
1
0
1
0
1
1
0
1
1
1
0
vecin[i, j] =
Figura 8.2 reprezinta matricea de vecinatati pentru harta cu 6 tari n care s-a facut
conventia ca 1 reprezinta true si 0 reprezinta false.
Problema se poate generaliza usor si la o harta cu n tari care trebuie colorata cu m culori.
Vom utiliza pentru usurarea expunerii harta cu 6 tari de mai sus.
In aceasta problema, un vector solutie x = (x1 , x2 , . . . , xn ) reprezinta o varianta de colorare
a hartii, avand semnificatia ca tara numarul i va fi colorata cu culoarea xi . In exemplul
nostru, xi poate fi 1,2 sau 3, corespunzand respectiv culorilor rosu, galben, verde.
Conditia de continuare este ca tara careia urmarim sa i atribuim o culoare sa aiba o
culoare distincta de tarile cu care are granita. Cu alte cuvinte, trebuie sa avem:
xi 6= xk daca A[i, k] = 1,
Functia de continuare este n aceasta situatie:
i = 1, k 1
142
143
cu aceeasi culoare, fie langa cutie n caz contrar. Astfel, multimile Ci de valori consumate
la un moment dat sunt alcatuite din creioanele de langa cutia Vi si de pe tara Ti . Cu
aceste precizari, cele 4 modificari de configuratie au urmatoarele semnificatii concrete:
atribuie si avanseaz
a: se aseaza creionul ales pe tara corespunzatoare si se trece la
cutia urmatoare
ncercare esuat
a: creionul ales este asezat langa cutia din care a fost scos
revenire: creioanele corespunzatoare tarii curente sunt repuse n totalitate la loc si
se trece la cutia precedenta
revenire dup
a g
asirea unei solutii: semnul este adus la stanga ultimei cutii.
Procesul se ncheie n momentul n care toate creioanele ajung din nou n cutiile n care
se aflau initial.
8.5
Probleme propuse
1)!.
n
2. Idem problema 1, cu precizarea ca anumite persoane nu se agreeaza, deci nu pot fi
asezate una langa cealalta. La intrare se mai furnizeaza o matrice simetrica A, cu
urmatoarea semnificatie:
(
A(i, j) =
1 daca nu se ageaza
0
altfel
144
x[i] =
1 dac
a i apartine submultimii
0
altfel
Exist
a si o solutie ce genereaz
a vectorii caracteristici de lungime n prin adunarea n
baza 2. Initial vectorul este nul, corespunz
ator multimi vide, iar apoi prin adun
ari
repetate se genereaz
a toate submultimile. Atentie, num
arul total de submultimi este
2n !
7. O firm
a dispune de n angajati, dintre care p sunt femei. Firma trebuie sa formeze o
delegatie de m persoane dintre care k sunt femei. Sa se afiseze toate delegatiile care
se pot forma.
Indicatie: Pentru a forma o delegatie de k femei din p disponibile avem la dispozitie
Cpk variante. Delagatia de m persoane poate fi completat
a cu oricare din variantele
mk
arbatilor din delegatie. Asadar num
arul total de variante este
de Cnp de alegere a b
mk
k
Cp Cnp , ce urmeaz
a a fi calculat.
Generarea efectiv
a se bazeaz
a pe un vector caracteristic cu semnificatia:
(
x[i] =
1 dac
a persoana e femeie
0
altfel
145
A(i, j) =
1 dac
a exist
a leg
atur
a ntre orasul i si j
0
altfel
146
147
148
149
0
1
0
1
1
0
0
1
1
0
1
1
0
0
1
0
17. Un teren dreptunghiular este mpartit n m n parcele, reprezentate sub forma unei
matrice A, cu m linii si n coloane. Fiecare element al matricei este un numar real
care reprezinta naltimea parcelei respective. Pe una dintre parcele se afla plasat
a
o bila. Se cere sa se furnizeze toate posibilitatile prin care bila poate sa paraseasc
a
terenul, cunoscut fiind faptul ca bila se poate rostogoli numai pe parcele nvecinate
a caror naltime este strict inferioara naltimii parcelei pe care bila se afla.
Indicatie: Aceasta si problema anterioar
a sunt cazuri tipice de backtracking n plan.
Ideea rezolv
arii const
a n ncercarea de a ajunge la o pozitie vecin
a respect
and conditiile problemei. Modalitatea de miscare este dat
a de cele 8 directii cardinale N, NV,
V, SV, S, SE, E, NE. La fiecare pas avem grij
a s
a nu iesim din spatiul alocat dec
at
cu o solutie nou
a. Este posibil s
a d
am peste aceeasi solutie asa nc
at o vom retine pe
anterioara. Practic, stiva va nc
arca cordonatele curente si directia n care urmeaz
a
s
a ne deplas
am.
18. Pe o tabla de sah de dimensiune 8 8 se ncearca pozitionarea a n pioni dup
a
urmatoarele reguli:
(a) Pe fiecare linie se afla doi pioni.
(b) Pe fiecare coloana se afla cel mult doi pioni.
(c) Pe fiecare paralela la diagonala principala se afla cel mult doi pioni.
Solutie: Programul dat n continuare prezint
a n Figura 8.5 clasa de baz
a Backtracking care este usor modificat
a fat
a de cea dat
a n sectiunea 8.4, iar n Figura 8.6
clasa derivat
a Pions ce implementeaz
a metodele abstracte si contine functia main.
public abstract class BackTracking
{
150
int n;
//dimensiunea problemei
int[] x; //vectorul solutie
public BackTracking(int n, int m)//constructorul
{
this.n = n ;
x = new int[n] ;
for(int i=0; i< n ; ++i)
{
x[i] = -1 ;//initializarea cu -1 a elem vect solutie
}
}
public void back()
{
int k =0 ;
while(k>=0)
{
if(k==n) //am gasit o solutie si am afisat-o
k-- ; //pasul "back" dupa gasirea solutiei
else
{
if(x[k]<7)//in vectorul solutie x[k] este coloana
{
x[k]++ ;
if( cont(k) ) //verifica conditia ce continuare
{
k++;//cautam locul urmatorului pion
if (k!=n)//daca nu am ajuns la solutie
//verificam daca suntem la al doilea pion pe aceeasi linie.
//Pentru usurinta verificarilor de asezare
//(pentru primul pion deja verificasem) am presupus ca cei doi pioni
//de pe aceeasi linie se afla pe nivelele consecutive in stiva
//k si k+1 unde k este par, si initial pe aceeasi coloana.
{
if (((k-1)%2==0))
x[k]=x[k-1];
}
else
retSol();//afisarea rezultatului
}
}
else
x[k--] = -1 ;
//intoarcere cu o pozitie pentru ca nu se poate continua
}
151
}
}
public abstract boolean cont( int k ) ;
public abstract void retSol() ;
}
Figura 8.5 Clasa Backtracking
import java.io.* ;
public class Pions extends BackTracking
{
public boolean cont(int k)
{
int l=0,m=0;
for(int i=0; i<k; ++i)
{
if (x[i]==x[k]) //aflam citi pioni se mai afla pe aceeasi coloana
l++;
//cu ultimul pion introdus
}
if ((k%2==1)&&(x[k]==x[k-1]))
return false;
int e=k/2-1,f=x[k]-1;
while ((e>-1)&&(f>-1))
//aflam daca pe orice paralela la diagonala principala
//se afla mai mult de 2 pioni dupa introd pionului de pe nivelul k
{
if ((x[e*2]==f)||(x[2*e+1]==f))
//x[e*2]este primul element de pe linia e
//iar x[e*2+1]este al doilea element de pe linia e
m++;
e--;
f--;
}
if ((l<2) &&(m<2))
return true;//se poate continua daca pe aceeasi coloana sau
//paralela cu diagonala pricipala nu se afla mai mult de doi pioni
else
return false;
}
public void retSol()//afisarea rezultatului
{
System.out.println();
for(int l=0;l<8;l++)
152
{
for(int j=0; j<=7; ++j)
{
if((j==x[2*l])||(j==x[2*l+1]))
{
System.out.print("X ");//daca pe linia l unul dintre pioni este
//pe coloana j atunci il afisam (marcat cu X)
}
else
{
System.out.print("O ") ;
}
}
System.out.println() ;
}
}
public Pions(int n)
{
super(n,n) ;
//apelarea constructorului din clasa de baza backtracking;
}
public static void main(String[] args)
{
Pions pion = new Pions(16);
//constructorul este apelat pentru 16 pioni
pion.back();
//ce vor fi amplasati pe o tabla de sah obisnuita de dimensiune 8x8
}
}
Figura 8.6 Clasa Pions
Capitolul 9
Divide et Impera
In acest capitol vom studia o alta metoda fundamentala de elaborare a algoritmilor, numita Divide et Impera. Ca si Backtracking, Divide et Impera se bazeaza pe un principiu
extrem de simplu: descompunem problema n dou
a (sau mai multe) subprobleme de dimensiuni mai mici; rezolv
am subproblemele, iar solutia pentru problema initial
a se obtine
combin
and solutiile subproblemelor n care a fost descompus
a. Rezolvarea subproblemelor
se face n acelasi mod cu problema initiala. Procedeul se reia pana cand subproblemele
devin atat de simple ncat admit o rezolvare imediata.
Inca din descrierea globala a acestei tehnici s-au strecurat elemente de recursivitate. Pentru
a putea ntelege mai bine aceasta metoda de elaborare a algoritmilor care este eminamente
recursiva, vom prezenta n sectiunea 9.1 cateva elemente fundamentale referitoare la recursivitate. Continuam apoi n sectiunea 9.2 cu prezentarea generala a metodei, urmat
a
de rezolvarea anumitor probleme de Divide et Impera deosebit de importante: c
autare
binar
a, sortarea prin interclasare, sortarea rapid
a si evaluarea expresiilor aritmetice.
9.1
In acest paragraf vom reaminti cateva elemente esential referitoare la recursivitate. Cei
care st
apanesc deja acest mecanism, pot sa treaca direct la prezentarea metodei Divide et
Impera din paragraful 9.2.
Avand n vedere faptul ca recursivitatea este un mecanism de programare general, care nu
tine doar de limbajul Java, prezentarea facuta va folosi si limbajul pseudocod la descrierea
algoritmilor recursivi, pentru a nu ncarca prezentarea cu detalii de implementare.
9.1.1
Functii recursive
Recursivitatea este un concept care deriva n mod direct din notiunea de recurent
a matematic
a. Recursivitatea este un instrument elegant si puternic pe care programatorii l au la
dispozitie pentru a descrie algoritmii. Este interesant de retinut faptul ca programatorii
obisnuiau sa utilizeze recursivitatea pentru a descrie algoritmii cu mult nainte ca limbajele de programare sa suporte implementarea directa a acestui concept.
Din punct de vedere informatic, o subrutina (procedura sau functie) recursiva este o
subrutin
a care se autoapeleaz
a. Sa luam ca exemplu functia factorial, a carei definitie
matematica recurenta este:
153
154
F act(n) =
n F act(n 1) pentru n 1
1
pentru n = 0
F ib(n) =
9.1. NOT
IUNI ELEMENTARE REFERITOARE LA RECURSIVITATE
155
aceea ca n esent
a, un apel recursiv nu difer
a cu nimic de un apel de functie obisnuit.
Pentru a veni n sprijinul acestei afirmatii trebuie sa studiem mai n amanuntime ce se
petrece n cazul unui apel de functie.
Se cunoaste faptul ca n situatia n care compilatorul ntalneste un apel de functie, acesta
preda controlul executiei functiei respective, dupa care se revine la urmatoarea instructiune
de dupa apel. ntrebarea care apare n mod firesc este: de unde stie compilatorul unde s
a
se ntoarc
a la terminarea functiei? De unde stie care au fost valorile variabilelor nainte
de a se preda controlul functiei? Raspunsul este simplu: nainte de a realiza un apel
de functie compilatorul salveaza complet starea programului (linia de la care s-a realizat
apelul, valorile variabilelor locale, valorile parametrilor de apel) pe stiva, urmand ca la
revenirea din subrutina sa rencarce starea care a fost nainte de apel de pe stiva.
Pentru exemplificare sa consideram urmatoarea procedura (nerecursiva) care afiseaza o
linie a unei matrice. Atat linia care trebuie afisata, cat si matricea sunt transmise ca
parametru:
procedur
a AfisLin(a: tmatrice; n,lin: integer)
pentru i = 1,n
scrie a[lin,i]
return
Procedura AfisLin este apelata de procedura AfisMat descrisa mai jos, care afiseaza linie
cu linie o ntreaga matrice pe care o primeste ca parametru:
procedur
a AfisMat(a: tmatrice; n: integer)
pentru i = 1,n
AfisLin(a,n,i)
return
Sa presupunem ca procedura AfisMat este apelata ntr-un program astfel:
...
AfisMat(a,5)
...
pentru a afisa o matrice de dimensiuni 5 5.
In momentul n care compilatorul ntalneste acest apel, el salveaza pe stiva linia de la care
s-a facut apelul (sa spunem 2181), valoarea matricei a si alte variabile locale declarate n
program:
156
Controlul va fi apoi preluat de catre procedura AfisMat, care intra n ciclul pentru cu
apelul: AfisLin(a,n,i) aflat sa zicem la linia 2198.
In acest moment controlul va fi preluat de catre procedura AfisLin, dar nu nainte de a
adauga la varful stivei linia de la care s-a facut apelul, valorile parametrilor si a variabilei
locale i:
2145; F act(0);
2145; F act(1);
2145; F act(2);
2145; F act(3);
2145; F act(4);
xxxx; F act(5);
Figura 9.3 Continutul stivei programului c
and n devine 0, n cazul calcului lui Fact(5).
Se presupune c
a linia cu apelul recursiv este situat
a la adresa 2145.
157
9.1. NOT
IUNI ELEMENTARE REFERITOARE LA RECURSIVITATE
F act(1) fiind calculat se poate reveni la calculul nmultirii 2*f act(1) = 2, apoi, F act(2)
fiind calculat se revine la calculul nmultirii 3*f act(2)=6 s.a.m.d. pana se calculeaz
a
5*f act(4)=120 si se revine n programul apelant.
Sa vedem acum modul n care se realizeaza calculul recursiv al sirului lui Fibonacci. Vom
vedea ca timpul de calcul al acestei recurente este incomparabil mai mare fata de calculul factorialului. Sa presupunem ca functia fib se apeleaza cu parametrul n = 3.
In aceasta situatie, se depune pe stiva apelul f ib(3) mpreuna cu linia de unde s-a realizat apelul (de exemplu, 2160). In linia 2160 a procedurii are loc apelul recursiv:
f ib f ib(n 1) + f ib(n 2) care n cazul nostru, n fiind 3, presupune calcularea sumei
f ib(2) + f ib(1). Aceasta suma nu poate fi calculata nainte de a-l calcula pe f ib(2). Calculul lui f ib(2) presupune calcularea sumei f ib(1) + f ib(0). f ib(1) si f ib(0) se calculeaz
a
direct la urmatorul apel recursiv, dupa care se calculeaza suma lor, f ib(2) = 2. Abia
acum se revine la suma f ib(2) + f ib(1) si se calculeaza f ib(1), dupa care se revine si se
calculeaza f ib(3).
Modul de calcul al lui f ib(n) recursiv se poate reprezenta foarte sugestiv arborescent.
Radacina arborelui este f ib(n), iar cei doi fii sunt apelurile recursive pe care f ib(n) le
genereaza, si anume f ib(n 1) si f ib(n 2). Apoi se reprezinta apelurile recursive generate de f ib(n 2) s.a.m.d:
#
Fib(n)
"!
@
@ '$
'$
@
Fib(n-1)
Fib(n-2)
&%
'$
'$
@
&%
@
'$
'$
@
Fib(n-2)
Fib(n-3)
Fib(n-3)
&%
&%
Fib(n-4)
&%
&%
9.1.2
Recursivitatea nu nseamn
a recurent
a
158
functie inversare
public static void inversare( )
citeste a
{
0
0
daca a <> . atunci inversare
char a;
scrie inversare
a=Reader.readChar();
return
if(a!=.)
{
inversare();
}
System.out.print(a);
}
Este important de notat ca pentru ca functia sa functioneze corect, variabila a trebuie
declarata ca variabila locala; astfel, toate valorile citite vor fi salvate pe stiva, de unde vor
fi extrase succesiv (n ordinea inversa citirii) dupa ntalnirea caracterului ..
Exemplul 9.2 Transformarea unui num
ar din baza 10 ntr-o baz
a b, mai mic
a dec
at 10.
Sa ne reamintim algoritmul clasic de trecere din baza 10 n baza b. Numarul se mparte
la b si se retine restul. Catul se mparte din nou la b si se retine restul ... pana cand catul
devine mai mic decat b. Rezultatul se obtine prin scrierea n ordine inversa a resturilor
obtinute.
Formularea recursiva a acestei rezolvari pentru trecerea unui numar n n baza b este:
(
T ransf (n) =
pentru n < b
159
functie transform(n:integer)
public static void transform(int n)
rest=n mod b
{
daca n < b atunci transform(n div b)
int rest=n%b;
scrie rest
if(n < b)
return
{
transform(n/b);
}
System.out.print(rest);
}
De remarcat ca n aceasta functie variabila rest trebuie sa fie declarata local, pentru a fi
salvata pe stiva, n timp ce variabila b este bine sa fie declarata global, deoarece valoarea
ei nu se modifica, salvarea ei pe stiva ocupand spatiu inutil.
9.2
Divide et Impera este o metoda speciala prin care se pot aborda anumite categorii de
probleme. Ca si celelalte metode de elaborare a algoritmilor, Divide et Impera se bazeaz
a
pe un principiu extrem se simplu: se descompune problema initial
a n dou
a (sau mai multe)
subprobleme de dimensiune mai mic
a, care se rezolv
a, dup
a care solutia problemei initiale
se obtine combin
and solutiile subproblemelor n care a fost descompus
a. Se presupune c
a
fiecare dintre problemele n care a fost descompusa problema initiala se poate descompune
n alte subprobleme, la fel cum a fost descompusa si problema initiala. Procedeul de
descompunere se repeta pana cand, dupa descompuneri succesive, se ajunge la probleme
de dimensiune mica, pentru care exista rezolvare directa.
Evident, nu orice gen de problem
a se preteaza la a fi abordata cu Divide et Impera. Din
descrierea de mai sus reiese ca o problema abordabila cu aceasta metoda trebuie sa aib
a
doua proprietati:
1. Sa se poata descompune n subprobleme
2. Solutia problemei initiale sa se poata construi simplu pe baza solutiei subproblemelor
Modul n care metoda a fost descrisa, conduce n mod natural la o implementare recursiv
a,
avand n vedere faptul ca si subproblemele se rezolva n acelasi mod cu problema initiala.
Iata care este forma generala a unei functii Divide et Impera:
a)
functie DivImp(P: Problem
dac
a Simplu(P) atunci
Rezolv
aDirect(P) ;
altfel
Descompune(P, P1, P2) ;
DivImp(P1) ;
DivImp(P2) ;
Combin
a(P1, P2) ;
return
160
In consecinta, putem spune ca abordarea Divide et Impera implica trei pasi la fiecare nivel
de recursivitate:
1. Divide problema n doua subprobleme.
2. St
ap
aneste (Cucereste) cele doua subprobleme prin rezolvarea acestora n mod recursiv.
3. Combin
a solutiile celo doua subprobleme n solutia finala pentru problema initiala.
9.3
C
autare binar
a
C
autarea binar
a este o metoda eficienta de regasire a unor valori ntr-o secventa ordonata.
Desi este trecuta n acest capitol, cautarea binara nu este un exemplu tipic de problema
Divide et Impera, deoarece n cazul ei se rezolva doar una din cele doua subprobleme, deci
lipseste faza de recombinare a solutiilor. Enuntul problemei de cautare binara este:
Se d
a un vector cu n componente (ntregi), ordonate cresc
ator si un num
ar ntreg. S
a se
decid
a dac
a se g
aseste n vectorul dat, si, n caz afirmativ, s
a se furnizeze indicele pozitiei
pe care se g
aseste.
O rezolvare imediata a problemei presupune parcurgerea secventiala a vectorului dat, pana
cand p este gasit, sau am ajuns la sfarsitul vectorului. Aceasta rezolvare nsa nu foloseste
faptul ca vectorul este sortat.
Cautarea binara procedeaza n felul urmator: se compara p, cu elementul din mijlocul
vectorului; daca p este egal cu acel element, cautarea s-a ncheiat. Daca este mai mare, se
cauta doar n prima jumatate, iar daca este mai mic, se cauta doar n a doua jumatate.
Cititorul atent a observat cu siguranta ca n aceasta situatie problema nu se descompune
n doua subprobleme care se rezolva, dupa care se construieste solutia, ci se reduce la una
sau la alta din subprobleme. Cei trei pasi ai lui Divide et Impera sunt n aceasta situatie:
1. Divide: mparte sirul de n elemente n care se realizeaza cautarea n doua siruri cu
n/2 elemente.
2. St
ap
aneste: Cauta n una dintre cele doua jumatati, functie de valoarea elementului
din mijloc.
3. Combin
a: Nu exista.
Iata care este functia care realizeaza cautarea elementului x n sirul a, ntre indicii low si
high .
public static int binarySearch(int[] a, int x, int low, int high)
{
if(low <= high)
{
int middle = (low + high)/2 ;
if( x == a[middle] )
{
161
return middle ;
}
else
{
if( x < a[middle] )
{
return binarySearch(a, x, low, middle -1 ) ;
}
else
{
return binarySearch(a, x, middle+1, high) ;
}
}
}
return -1 ;
}
Pozitia pe care se gaseste elementul x n sirul a este data de apelul:
poz = binarySearch(a,x,0,a.length-1)
9.4
162
9.5
Sortarea rapid
a (QuickSort)
Sortarea rapid
a este, asa cum i spune si numele, cea mai rapida metoda de sortare cunoscuta n prezent . Exista foarte multe variante ale acestei metode, o parte dintre ele avand
doar rolul de a micsora timpul de executie n cazul cel mai nefavorabil. Vom prezenta
aici varianta clasica, despre care veti remarca cu surprindere ca este neasteptat de simpla.
Enuntul problemei este identic cu cel de la sortarea prin interclasare.
S
a se ordoneze cresc
ator un vector de numere ntregi.
Metoda de sortare rapida prezentata n acest paragraf este dintr-un anumit punct de
vedere complementara metodei Mergesort. Diferenta ntre cele doua metode este data de
faptul ca n timp ce la Mergesort mai nti vectorul se mpartea n doua parti dupa care se
sorta fiecare parte si apoi se interclasau cele doua jumatati, la Quicksort mpartirea se face
n asa fel ncat cele doua siruri sa nu mai trebuiasca a fi interclasate dupa sortare, adica
primul sir sa contina doar elemente mai mici decat al doilea sir. Cu alte cuvinte, n cazul
lui Quicksort etapa de recombinare este triviala, deoarece problema este astfel mpartita n
subprobleme astfel ncat sa nu mai fie necesara interclasarea sirurilor. Etapele lui Divide
et Impera pot fi descrise n aceasta situatie astfel:
1. Divide: Imparte sirul de n elemente care urmeaza a fi sortat n doua siruri, astfel
nc
at elementele din primul sir s
a fie mai mici dec
at elementele din al doilea sir
2. St
ap
aneste: Sorteaza recursiv cele doua subsiruri utilizand sortarea rapida
3. Combin
a: Sirul sortat este obtinut din concatenarea celor doua subsiruri sortate.
Functia care realizeaza mpartirea n subprobleme (astfel ncat elementele primului sir sa
fie mai mici decat elementele celui de-al doilea) se datoreaza lui C. A. Hoare, care a gasit
o metoda de a realiza aceasta mpartire (numita partitionare) n timp liniar.
Procedura de partitionare rearanjeaza elementele tabloului functie de primul element,
numit pivot, astfel ncat elementele mai mici decat primul element sunt trecute n stanga
163
(QUICKSORT)
9.5. SORTAREA RAPIDA
lui, iar elementele mai mari decat primul element sunt trecute n dreapta lui. De exemplu,
daca avem vectorul:
a = (7, 8, 5, 2, 3),
atunci procedura de partitionare va muta elementele 5,2 si 3 n stanga lui 7, iar 8 va fi
n dreapta lui. Cum se realizeaza acest lucru? Sirul este parcurs simultan de doi indici:
primul indice, low, pleaca de la primul element si este incrementat succesiv, al doilea indice,
high, porneste de la ultimul element si este decrementat succesiv. In situatia n care a[low]
este mai mare decat a[high], elementele se interschimba. Partitionarea este ncheiata n
momentul n care cei doi indici se ntalnesc (devin egali) undeva n interiorul sirului. La
un pas al algoritmului, fie se incrementeaza low, fie se decrementeaza high; ntotdeauna
unul dintre cei doi indici, low sau high, este pozitionat pe pivot. Atunci cand low indic
a
pivotul, se decrementeaza high, iar atunci cand high indica pivotul se incrementeaza low.
Iata cum functioneaza partitionarea pe exemplul de mai sus. La nceput, low indica primul
element, iar high indica ultimul element:
a = (7, 8, 5, 2, 3)
low
high
Deoarece a[low] > a[high] elementele 7 si 3 se vor interschimba. Dupa interschimbare,
pivotul va fi indicat de high, deci low va fi incrementat:
a = (3, 8, 5, 2, 7)
low high
Din nou avem a[low] > a[high], elementele 8 si 7 se vor interschimba. Dupa interschimbare,
pivotul va fi indicat de low, deci high va fi decrementat:
a = (3, 7, 5, 2, 8)
low high
Din nou avem a[low] > a[high], elementele 7 si 2 se vor interschimba. Dupa interschimbare,
pivotul va fi indicat de high, deci low va fi incrementat:
a = (3, 2, 5, 7, 8)
low high
De data aceasta avem a[low] <= a[high], deci low va fi incrementat din nou, fara a se
realiza interschimbari.
In acest moment low si high s-au suprapus (au devenit egale), deci partitionarea s-a
ncheiat. Pivotul este pe pozitia a 4-a, care este de fapt si pozitia lui final
a n sirul
sortat.
Functia de partitionare de mai jos primeste ca parametri limitele inferioara, respectiv superioar
a ale sirului care se partitioneaza si returneaza pozitia pe care se afla pivotul n
finalul partitionarii. Pozitia pivotului este importanta deoarece ne da locul n care sirul
va fi despartit n doua subsiruri.
164
9.6
Expresii aritmetice
Se d
a o expresie aritmetic
a n care operanzii sunt simbolizati prin litere mici (de la a la
z), iar operatorii sunt +, -, /, si * cu semnificatia cunoscut
a. Se cere s
a se scrie un
program care transform
a expresia n form
a polonez
a postfixat
a.
Reamintim faptul ca forma polonez
a postfixat
a (Lukasiewicz) este obtinuta prin scrierea
operatorului dupa cei doi operanzi, si nu ntre ei. Aceasta forma are avantajul ca nu
165
166
6. {
7.
switch(a.charAt(i))
8.
{
9.
case (:
10.
j=j+10;
11.
p[i]=0;
12.
break;
13.
case ):
14.
j=j-10;
15.
p[i]=0;
16.
break;
17.
case +:
18.
case -:
19.
p[i]=1+j;
20.
break;
21.
case *:
22.
case /:
23.
p[i]=10+j;
24.
break;
25.
default:
26.
p[i]=1000;
27. }
28. }
29. j=0;
30. for(i=0;i<a.length();i++)
31. {
32. if(p[i]!=0)
33. {
34.
eps=eps+a.charAt(i);
35.
epf[j]=p[i];
36.
j++;
37. }
38. }
39.}
Figura 9.5 Metoda ce transform
a expresia dat
a ntr-o expresie f
ar
a paranteze
In sirul p declarat n linia 3 vom trece valori ce corespund prioritatilor membrilor expresiei.
Calculul prioritatilor se face astfel:
prioritatea initiala a operatorilor + si - este 1;
prioritatea initiala a operatorilor * si / este 10;
la prioritatea unui operator se aduna 10 pentru fiecare pereche de paranteze ntre
care se gaseste;
prioritatea unui operand este 1000;
167
9.7
Probleme propuse
1. Reprezentati evolutia stivei pentru procedurile si functiile recursive din acest capitol.
2. Calculati de cate ori se recalculeaza valoarea Fk n cazul calcului recursiv al valorii
Fn a sirului lui Fibonacci.
3. Sa se calculeze recursiv si iterativ cel mai mare divizor comun a doua numere dup
a
formulele (Euclid):
(
cmmdc(a, b) =
168
cmmdc(a, b) =
Ack(m, n) =
n+1
pentru m = 0
Ack(m 1, 1)
pentru n = 0
n!
k!(nk)! .
F (x) =
x1
pentru x 12
F (F (x + 2)) altfel
7. Sa se scrie o functie care calculeaza recursiv suma cifrelor unui numar dupa formula:
(
S(n) =
an1 +bn1
2
si bn =
an1 bn1 , cu a0 = a si b0 = b.
169
10. S
a se scrie o functie care calculeaza maximul elementelor unui vector utilizand
tehnica Divide et Impera.
Indicatie: Se mparte vectorul n dou
a jum
at
ati egale, se calculeaz
a recursiv maximul
celor dou
a jum
at
ati si se alege num
arul mai mare.
11. (Turnurile din Hanoi) Se dau trei tije simbolizate prin literele A, B si C. Pe tija
A se afla n discuri de diametre diferite asezate descrescator n ordinea diametrelor,
cu diametrul maxim la baza. Se cere sa se mute discurile pe tija B respectand
urmatoarele reguli:
(a) la fiecare pas se muta un singur disc
(b) nu este permisa asezarea unui disc cu diametru mai mare peste un disc cu
diametrul mai mic
Indicatie: Formularea recursiv
a a solutiei este: se mut
a primele n-1 discuri de pe
tija A pe tija C folosind ca tij
a intermediar
a tija B; se mut
a discul r
amas pe A pe
tija B; se mut
a discurile de pe tija C pe tija B folosind ca tij
a intermediar
a tija A.
Parcurgerea celor trei etape permite definirea recursiv
a a sirului H(n,a,b,c) astfel:
(
H(n, a, b, c) =
ab
dac
a n=1
H(n 1, a, c, b), ab, H(n 1, c, b, a) dac
a n>1
170
taietura_max(xv[i],y,l+x-xv[i],h,max);
taietura_max(x,y,l,yv[i]-y,max);
taietura_max(x,yv[i],l,h+y-yv[i],max);
}
else
{
if((l*h)>(max[0]*max[1]))
{
max[2]=x;
max[3]=y;
max[0]=l;
max[1]=h;
}
}
}
}
171
Capitolul 10
Algoritmi Greedy
Algoritmii aplicati problemelor de optimizare sunt, n general, compusi dintr-o secventa
de pasi, la fiecare pas existand mai multe alegeri posibile. Un algoritm Greedy va alege
la fiecare moment de timp solutia care pare a fi cea mai buna. Deci este vorba despre o
alegere optim
a, f
acut
a local, cu speranta ca ea va conduce la un optim global. Acest capitol trateaza probleme de optimizare care pot fi rezolvate cu ajutorul algoritmilor Greedy.
Algoritmii Greedy conduc n multe cazuri la solutii optime, dar nu ntotdeauna... In
sectiunea 10.1 vom prezenta mai ntai o problema simpla dar netriviala, problema selectarii activitatilor, a carei solutie poate fi calculata n mod eficient cu ajutorul unei
metode de tip Greedy. In sectiunea 10.2 se recapituleaza cateva elemente de baza ale
metodei Greedy. Urmeaza apoi prezentarea catorva probleme specifice.
Metoda Greedy este destul de puternic
a si se aplica cu succes unui spectru larg de probleme. Cursurile de teoria grafurilor contin mai multi algoritmi care pot fi priviti ca aplicatii
ale metodei Greedy, cum ar fi algoritmii de determinare a arborelui partial de cost minim
(Kruskal, Prim) sau algoritmul lui Dijkstra pentru determinarea celor mai scurte drumuri
pornind dintr-un varf.
10.1
Primul exemplu pe care l vom considera este o problema de repartizare a unei resurse (o
sala de spectacol) mai multor activitati care concureaza pentru a obtine resursa respectiva
(diferite spectacole care vor sa ruleze n sala respectiva). Vom vedea ca un algoritm de tip
Greedy reprezinta o metoda simpla si eleganta pentru programarea unui numar maxim de
spectacole care nu se suprapun (numite activitati compatibile reciproc).
S
a presupunem c
a dispunem de o multime S = 1,2,...,n de n activit
ati (spectacole) care
doresc s
a foloseasc
a o aceeasi resurs
a (sala de spectacole). Aceast
a resurs
a poate fi folosit
a
de o singur
a activitate la un anumit moment de timp. Fiecare activitate i are un timp de
start si si un timp de terminare ti , unde si ti . Dac
a este selectat
a activitatea i, ea se
desf
asoar
a pe durata intervalului [si , ti ) . Dou
a activit
ati sunt compatibile dac
a duratele
lor de desf
asurare sunt disjuncte. Problema spectacolelor (select
arii activit
atilor) const
a
din selectarea unei multimi maximale de activit
ati compatibile ntre ele.
172
ILOR)
10.1. PROBLEMA SPECTACOLELOR (SELECTAREA ACTIVITAT
173
Un algoritm Greedy pentru aceasta problema este descris de urmatoarea functie, prezentata n pseudocod. Vom presupune ca spectacolele (adica datele de intrare) sunt ordonate
crescator dupa timpul de terminare:
t1 t2 , . . . , tn .
In cazul n care activitatile nu sunt ordonate astfel, ordonarea poate fi facuta n timpul
O(nlgn) (folosind Mergesort sau Quicksort). Algoritmul de mai jos presupune ca datele
de intrare s si t sunt reprezentate ca vectori.
functie SELECT-SPECTACOLE-GREEDY(s, t)
A {1} //A este multimea spectacolelor care sunt selectate
j 1 //j este indicele ultimului spectacol selectat
pentru i = 2, n
dac
a si tj atunci //spectacolul i ncepe dup
a ce j s-a terminat
A A {i} //se adaug
a i la spect. selectate
j i //ultimul spectacol este acum i
return A
In multimea A se introduc activitatile selectate. Variabila j identifica ultima activitate
introdusa n A. Deoarece activitatile sunt considerate n ordinea crescatoare a timpilor lor
de terminare, tj va reprezenta ntotdeauna timpul maxim de terminare a oricarei activitati
din A. Aceasta nseamna ca:
tj = max{tk |k A}
In liniile 2-3 din algoritm se selecteaza activitatea 1 (activitatea 1 trebuie planificata prima,
deoarece se termina cel mai repede), se initializeaza A astfel ncat sa nu contina dec
at
aceast
a activitate, iar variabila j ia ca valoare aceasta activitate. In continuare n ciclul
pentru se considera pe rand fiecare activitate i se adauga multimii A daca este compatibil
a
cu celelalte activitati deja selectate. Pentru a vedea daca activitatea i este compatibil
a
cu toate celelalte activitati existente la momentul curent n A, este suficient ca momentul
de start si sa nu fie mai devreme decat momentul de terminare tj al activitatii cel mai
recent adaugate multimii A. Daca activitatea i este compatibila, atunci ea este adaugat
a
multimii A, iar variabila j este actualizata. Procedura SELECT-SPECTACOLE-GREEDY
este foarte eficienta. Ea poate planifica o multime S de n activitati n O(n), presupunand
ca activitatile au fost deja ordonate dupa timpul lor de terminare. Activitatea aleas
a
de procedura SELECT-SPECTACOLE-GREEDY este ntotdeauna cea cu primul timp
de terminare care poate fi planificata legal. Activitatea astfel selectata este o alegere
Greedy (lacom
a) n sensul ca, intuitiv, ea lasa posibilitatea celorlalte activitati ramase
pentru a fi planificate. Cu alte cuvinte, alegerea Greedy maximizeaza cantitatea de timp
neplanificata ramasa.
10.1.1
Pana la acest moment, noi nu ne-am pus problema de a demonstra corectitudinea algoritmilor prezentati. Totusi, trebuie sa mentionam ca exista o ntreaga ramura a algoritmicii
174
care se ocupa exclusiv de demonstrarea corectitudinii algoritmilor. In general, demonstrarea corectitudinii unui algoritm nu este deloc simpla, de aceea am evitat sa prezentam
demonstratiile n acest curs introductiv. Am ales sa demonstram corectitudinea acestui
algoritm, deoarece pe de o parte ea este ilustrativa pentru o ntreaga clasa de probleme,
iar pe de alta parte, demonstratia nu este dificila.
Teorema 10.1 Algoritmul SELECT-SPECTACOLE-GREEDY furnizeaz
a solutia optim
a
(num
ar maxim de spectacole) pentru problema select
arii activit
atilor.
Demonstratie: Fie S = {1, 2, . . . , n} multimea activitatilor care trebuie planificate, ordonate crescator dupa timpul de terminare. In consecinta, activitatea 1 se termina cel mai
devreme. Vom arata ca exista o solutie optima care ncepe cu activitatea 1.
Sa presupunem ca avem o solutie A S optima pentru o instanta a problemei. Pentru
simplitate, presupunem ca activitatile din A sunt ordonate dupa timpul de terminare.
Daca primul spectacol din A este chiar 1, atunci demonstratia este ncheiata. Daca primul
spectacol din A nu este 1, atunci nlocuim primul spectacol cu spectacolul 1, obtinand
evident o solutie corecta, deoarece spectacolul 1 se va termina mai devreme decat primul
spectacol din A. Am aratat astfel ca exista o solutie optima pentru S care ncepe cu activitatea 1.
Mai mult, odata ce este facuta alegerea activitatii 1, problema se reduce la determinarea
solutiei optime pentru activitatile din S care sunt compatibile cu activitatea 1. Fie
S 0 = {i S|si t1 } multimea activitatilor care ncep dupa ce 1 se termina. Rezulta
ca daca A este o solutie optima pentru S, atunci A0 = A {1} este o solutie optima pentru
S. Daca nu ar fi asa, atunci ar exista o solutie optima B pentru S care sa aiba mai multe
activitati decat A. Adaugand activitatea 1 la B, vom obtine o solutie pentru S cu mai
multe activitati decat solutia A, ceea ce este absurd.
Astfel, prin inductie dupa numarul de alegeri facute se poate arata ca alegand primul
spectacol compatibil la fiecare pas, se obtine o solutie optima.
10.2
Un algoritm Greedy determina o solutie optima a unei probleme n urma unei succesiuni
de alegeri. La fiecare moment de decizie din algoritm este aleasa optiunea care pare a
fi cea mai potrivita. Aceasta strategie euristic
a nu produce ntotdeauna solutia optima,
dar exista si cazuri cand aceasta este obtinuta, cum ar fi n cazul problemei selectarii activitatilor. In acest paragraf vom prezenta cateva proprietati generale ale metodei Greedy.
Cum se poate decide daca un algoritm Greedy poate rezolva o problema particulara de
optimizare? In general nu exista o modalitate de a stabili acest lucru, dar exista anumite
caracteristici pe care le au majoritatea problemelor care se rezolva prin tehnici Greedy:
proprietatea de alegere Greedy si substructura optim
a.
In cazul general o problema de tip Greedy, are urmatoarele componente:
o multime de candidati (lucrari de planificat, varfuri ale grafului, etc);
o functie care verifica daca o anumita multime de candidati constituie o solutie
posibil
a (nu neap
arat optim
a) a problemei;
175
176
10.2.1
Prima caracteristica a unei probleme de tip Greedy este aceea de a avea proprietatea
alegerii Greedy, adica se poate ajunge la o solutie optima global, realizand alegeri (Greedy)
optime local . Intr-un algoritm Greedy se realizeaza orice alegere care pare a fi cea mai
buna la momentul respectiv, iar subproblema rezultata este rezolvata dupa ce alegerea
este facuta. Alegerea realizata de un algoritm Greedy poate depinde de alegerile facute
pana n momentul respectiv, dar nu poate depinde de alegerile ulterioare sau de solutiile
subproblemelor.
Desigur, trebuie sa demonstram ca o alegere Greedy la fiecare pas conduce la o solutie
optima global, si aceasta este o problema mai delicata. De obicei, demonstratia examineaza
o solutie optima global. Apoi se arata ca solutia poate fi modificata astfel ncat la fiecare
pas este realizata o alegere Greedy, iar aceasta alegere reduce problema la una similara dar
de dimensiuni mai reduse. Se aplica apoi principiul inductiei matematice pentru a arata
177
ca o alegere Greedy poate fi utilizata la fiecare pas. Faptul ca o alegere Greedy conduce
la o problema de dimensiuni mai mici reduce demonstratia corectitudinii la demonstrarea
faptului ca o solutie optima trebuie sa evidentieze o substructura optima.
10.2.2
Substructur
a optim
a
10.3
O singur
a statie de servire (procesor, pomp
a de benzin
a, etc) trebuie s
a satisfac
a cererile
a n clienti. Timpul de servire necesar fiec
arui client este cunoscut n prealabil: pentru
clientul i este necesar un timp ti , i = 1, n. Dorim s
a minimiz
am timpul total de asteptare:
T =
Pn
i=1 (timpul
Ceea ce este acelasi lucru cu a minimiza timpul mediu de asteptare, care este Tn .
De exemplu, daca avem trei clienti cu t1 = 5, t2 = 10, t3 = 3, sunt posibile sase ordini de
servire:
Ordine
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
T impul(T )
5 + (5 + 10) + (5 + 10 + 3)
5 + (5 + 3) + (5 + 3 + 10)
10 + (10 + 5) + (10 + 5 + 3)
10 + (10 + 3) + (10 + 3 + 5)
3 + (3 + 5) + (3 + 5 + 10) optim
3 + (3 + 10) + (3 + 10 + 5)
In primul caz, clientul 1 este servit primul, clientul 2 asteapta pana este servit clientul
1 si apoi este servit, clientul 3 asteapta pana sunt serviti clientii 1, 2 si apoi este servit.
Timpul total de asteptare a celor trei clienti este 38.
Algoritmul Greedy este foarte simplu - la fiecare pas se selecteaza clientul cu timpul minim
de servire din multimea de clienti ramasa. Vom demonstra ca acest algoritm este optim.
Fie I = (i1 i2 . . . in ) o permutare oarecare a ntregilor {1, 2, . . . , n}. Daca servirea are loc
n ordinea I, avem:
T (I) = ti1 + (ti1 + ti2 ) + (ti1 + ti2 + ti3 ) + . . . = nti1 + (n 1)ti2 + . . . =
Pn
Presupunem acum ca I este astfel ncat putem gasi doi ntregi a < b cu
tia > tib
k=1 (n
k + 1)tik
178
deci exista un client care necesita un timp mai lung de deservire, si care este servit nainte.
Interschimbam pe ia cu ib n I; cu alte cuvinte, clientul care a fost servit al b-lea va fi servit
acum al a-lea si invers. Obtinem o noua ordine de servire I, care este de preferat deoarece
T (I 0 ) = (n a + 1)tib + (n b + 1)tia +
Pn
k=1,k6=a,b (n
k + 1)tik
10.4
Interclasarea optim
a a mai multor siruri ordonate
179
A MAI MULTOR S
10.4. INTERCLASAREA OPTIMA
IRURI ORDONATE
150
150
@
@
140
@
@
10
130
40
@
20
@
80
30
30
10
@
@
50
30
@
@
30
@
@
20
60
@
@
10
110
90
20
@
@
10
50
11
10
6
10
2
8
4
2
@
@
@
@
@
@
@
@
@
@
@
@
@
180
L(A) =
Pn
i=1 ai qi
q1 + q2
@
@
@
q1
@
@
q2
10.5
181
Probleme propuse
2. (Problema discret
a a rucsacului) Acelasi enunt ca la problema precedenta, cu diferenta
ca dintr-un obiect nu se poate pune o fractiune (un obiect fie se pune ntreg, fie nu
se pune). Aratati ca algoritmul Greedy de la problema precedenta nu furnizeaz
a
ntotdeauna solutie optima n acest caz. Gasiti un algoritm care furnizeaza ntotdeauna
solutie optima! Ce complexitate are acest algoritm?
Indicatie: Aceast
a problem
a are solutia exact
a determinat
a doar printr-un algoritm
de tip backtracking. Exemplu n sprijinul celor afirmate anterior: fie c=(5,3,3) si
g=(3,2,2) iar M=4. Prin aplicarea algoritmului de la problema precedent
a am selecta
doar obiectul 1 (obiectul 2 nu ar nc
apea ntreg n rucsac deci nu ar putea fi selectat)
si am obtine costul 5 n timp ce dac
a am selecta obiectele 2 si 3 am obtine costul 6.
3. Scrieti algoritmul pentru problema interclasarii optimale din paragraful 10.4.
4. Gasiti o solutie Greedy pentru problema comis-voiajorului propusa la capitolul 8.
Este aceasta solutie optima?
182