Sunteți pe pagina 1din 61

CAPITOLUL 2.

PROIECTAREA CLASELOR 3

2.1 DESCOMPUNEREA PROBLEMELOR ŞI STABILIREA CLASELOR DE OBIECTE 4


2.1.1 UN MODEL ORIENTAT OBIECT AL CICLULUI DE VIAŢĂ SOFTWARE 4
2.1.2 DELIMITAREA ŞI PROIECTAREA CLASELOR 7
2.1.3 DATE, METODE ŞI ALGORITMI ÎN OBIECTE 11
2.2 CLASE ŞI TIPURI DE OBIECTE 13
2.2.1 CLASE ŞI TIPURI 13
2.2.2 CLASELE ŞI IMPLEMENTAREA LOR STRUCTURALĂ ŞI COMPORTAMENTALĂ 16
2.3 PRINCIPII DE PROIECTARE A STRUCTURILOR OBIECTUALE 19
2.3.1 ASCUNDEREA INFORMAŢIEI: ÎNCAPSULARE STRUCTURALĂ. VIZIBILITATEA ATRIBUTELOR
19
2.3.2 ÎNCAPSULAREA COMPORTAMENTELOR. VIZIBILITATEA OPERAŢIILOR 20
2.3.3 ÎMPACHETAREA ȘI VIZIBILITATEA CLASELOR 22
2.4 REUTILIZAREA OBIECTELOR ÎN SCOPUL CREĂRII SAU DERIVĂRII DE NOI STRUCTURI 26
2.4.1 REUTILIZARE PRIN REFERENŢIERE SIMPLĂ 27
2.4.2 REUTILIZARE PRIN MOŞTENIRE 29
2.4.3 MOŞTENIRE ŞI SUBTIPIZARE 33
2.5 OBIECTE ŞIRURI DE CARACTERE ŞI VALORILE FUNDAMENTALE PRIMITIVE 35
2.5.1 CLASA STRING ŞI INSTANŢIEREA ACESTEIA 36
2.5.2 SUB-ŞIRURI DE CARACTERE 38
2.5.3 COMPARAREA ŞIRURILOR ŞI PROCESAREA LOR. CĂUTAREA SUB-ŞIRURILOR ÎN ŞIRURI.
CONCATENAREA ŞIRURILOR 39
2.5.4 PRIMITIVELE ȘI CLASELE DE ACOPERIRE 42
2.6 OBIECTE-EXCEPŢII SAU TRATAREA EXCEPŢIILOR ÎNTR-UN MEDIU ORIENTAT OBIECT 50
2.6.1 RETURNAREA OBIECTELOR ŞI ÎNTRERUPEREA EXECUŢIEI METODELOR 50
2.6.2 CĂI DE EXECUŢIE EXCEPŢIONALE ANTICIPATE 51
2.6.3 CLASA EXCEPTION, SUBCLASELE ACESTEIA ŞI GESTIUNEA OBIECTELOR EXCEPŢII 52
2.7. OBIECTE PENTRU REPREZENTAREA DATEI CALENDARISTICE ȘI TIMPULUI 56
2 Dezvoltarea aplicaţiilor orientate obiect pe platforma Java
CAPITOLUL 2
Proiectarea claselor

După cum s-a putut constata din capitolul anterior, modelul obiectual
are la bază, drept element de construcţie formală, clasa din care pot fi
obţinute, prin instanţiere, obiectele concrete. În capitolul de faţă vom
încerca să conturăm, fără a avea pretenţia unei abordări exhaustive, câteva
principii-ghid în descoperirea şi formarea claselor. Cu alte cuvinte, vom
prezenta câteva elemente legate de proiectarea claselor de obiecte,
deocamdată la nivel general, fără a intra serios pe domeniul ingineriei
software (sau ingineriei aplicaţiilor software). Pentru a rămâne neutri faţă
de un anume specific tehnologic concret, modelul exemplificat va fi unul
din contextul afacerii, principiile enunţate şi dezbătute însă se pot aplica
în aceeaşi măsură şi interfeţelor grafice sau soluţiilor de integrare între
diferite contexte.
4 Capitolul 2

2.1 Descompunerea problemelor şi stabilirea claselor de


obiecte
Miza reală a acestui paragraf o constituie, de fapt, prezentarea unei
metode rapide de proiectare orientată obiect: orice proiect, chiar pentru o
aplicaţie fără o anvergură deosebită, presupune totuşi parcurgerea
anumitor etape sau faze pentru a se ajunge în cele din urmă la un produs
care să prezinte o anumită utilitate utilizatorilor cărora îi este destinat.

2.1.1 Un model orientat obiect al ciclului de viaţă software

Unul din cele mai simple şi mai liniare (deci mai simplu de prezentat)
modele pentru ciclul de dezvoltare al unei aplicaţii este cel, aşa-numit, în
cascadă. Denumirea acestui model provine de la modul de înlănţuire a
diferitelor etape. Mai important, la acest moment, decât această
modalitate de înlănţuire a etapelor, sunt activităţile în sine, aşa cum au
fost delimitate:
 culegerea sau enunţarea specificaţiilor;
 proiectarea elementelor constitutive;
 elaborarea codului de implementare;
 testarea componentelor şi proiectului în ansamblu;
 revizuirea elementelor proiectate sau codului de implementare
a componentelor.

Se observă că, deşi aparent liniar, de fapt, ultima etapă este mai
degrabă o etapă de tranziţie către o altă re-construcţie recursivă a
proiectului. Acesta este şi motivul pentru care se vorbeşte despre un ciclu
de dezvoltare. De altfel, diversitatea asamblării ciclice sau recursive pe
activităţi, etape şi faze s-a materializat într-o adevărată pleiadă de modele
ale ciclului de viaţă software. Interesant este însă faptul că, indiferent de
model, activităţile mai sus-menţionate nu au putut fi niciodată evitate sau
„escamotate”, ele fiind, de fapt, mai nuanţate sau re-asamblate în faze-
componente mai elaborate funcţie de complexitatea crescândă a
proiectelor software. Acest lucru reprezintă de fapt materia primă a unei
discipline de sine-stătătoare: ingineria software1.

1
Eng.: software engineering
Proiectarea claselor 5

Figura 2.1 Înlănţuirea activităţilor într-un ciclu de viaţă software

Adaptarea ciclului clasic, prezentat mai sus, la specificul unui proiect


software bazat pe obiecte presupune unele nuanţări cum ar fi:

 etapa specificaţiilor este, în general, neutră faţă de metodologia


practică de realizare, bazată pe obiecte sau nu;

 etapa proiectării poate fi descompusă mai departe în: proiectarea


claselor, proiectarea datelor şi metodelor plus proiectarea
algoritmilor de implementare;

 etapa implementării este cea mai specifică, fiind strict dependentă


de un limbaj şi de o platformă de servicii specifică (uneori chiar
mai multe limbaje şi platforme);

 etapele testării, depanării şi revizuirii urmează, în specificitate,


implementării.

Testarea poate fi uşor relativizată în sensul legării de specificitatea


unui anumit mediu, limbaj sau platformă, fiindcă anumite aspecte se
6 Capitolul 2

referă, de fapt, la utilitatea din punctul de vedere al utilizării externe,


punct de vedere destul de neutru faţă de platforma de implementare.

Figura 2.2 Ciclu de dezvoltare pentru un proiect software orientat obiect

În figura 2.2, se observă că etapa cea mai generică, și anume cea a


specificaţiilor, este o etapă mai degrabă a „clarificărilor” asupra a ce
urmează a fi realizat. Deşi redusă în aparenţă în privinţa spaţiului ocupat
în desfăşurarea ciclului de viaţă, elaborarea specificaţiilor este, de fapt,
esenţială ca importanţă, fiindcă este menită a demarca caracteristicile
finale ale produsului, adică ale aplicaţiei ce va rezulta prin finalizarea
proiectului software.

// Listing 2.1 [Specificaţii pentru o aplicaţie contabilă]


/* --------------------------------------------------------------
Realizaţi o aplicaţie cu interfaţă grafică prin care:
- să poată fi gestionat planul contabil general al organizaţiei,
cu preluarea şi actualizarea conturilor,
- să poată fi preluate jurnalizat şi să poată fi corelate
operaţiunile contabile, cu precădere cele referitoare la vânzări,
cumpărări, plăţi şi încăsări, dar şi operaţiuni diverse,
Proiectarea claselor 7

- să poată fi revizuită situaţia intrărilor în conturi, pe debit


şi pe credit, din operaţiunile contabile, prin fişa contabilă a
fiecărui cont,
- să poată fi revizuită situaţia contabilă centralizată a
organizaţiei printr-o balanţă organizată pe conturi şi jurnale.
-------------------------------------------------------------- */

Primul pas, după etapa specificaţiilor, are în vedere modul cum va


trebui procedat pentru a răspunde cerinţelor fixate prin specificaţii. Cu
alte cuvinte, se referă la o modalitate de descompunere, de descriere, a
problemei, folosind obiectele. Este, de fapt, o sinteză a terminologiei din
domeniul problemei formalizată pe baza obiectelor ce devin astfel mijloc
sau agent de comunicare a aspectelor domeniului problemei şi de
construire a soluţiei finale. Acestă primă descompunere a problemei este,
de fapt, un preambul pentru o etapă esenţială în faza de elaborare a
soluţiei, şi anume proiectarea claselor. Aceast pas am putea spune că
impune definitiv, de la acest moment în continuare, acel specific de
inovaţie din structura ciclului de dezvoltare, inovaţie care se datorează
paradigmei obiectelor. Este motivul pentru care îi vom acorda o atenţie
sporită în continuare.

2.1.2 Delimitarea şi proiectarea claselor

Aşa cum am sugerat deja, delimitarea claselor are în vedere transferul


conceptelor din domeniul real, concret, existenţial al problemei în
contextul computaţional al soluţiei. Există mai multe tehnici în acest sens
(de altfel acest punct constituie, în sine, material pentru o disciplină
distinctă).

Una dintre cele mai la îndemână tehnici constă, în esenţă, în


interpretarea semantică a substantivelor şi verbelor din declaraţiile
specificaţiilor problemei. În acest sens s-ar putea releva două tipuri de
informaţii de proiectare:

 actorii care participă sau structurează domeniul problemei;


 acţiunile şi activităţile care descriu funcţionalităţile specifice
problemei.
Actorii din domeniul problemei constituie, de fapt, materia primă
pentru modelarea obiectelor definite prin clase, acţiunile şi activităţile
definind comportamentele care vor fi asociate respectivelor obiecte.
8 Capitolul 2

// Listing 2.2 Terminologie orientată obiect specifică domeniului


// contabil
/* --------------------------------------------------------------
Conturi – care
 formează planul de conturi general,
 participă în operaţiuni,
 au o fişa contabilă specifică
 corespund unui rând sau linii din balanţa contabilă.
 Operaţiuni contabile
 formate din înregistrări contabile
 sistematizate pe jurnale contabile,
 de mai multe feluri: de vânzare, cumpărare, plată, încasare şi
diverse.

Înregistrări contabile - care


 participă la formarea operaţiunilor contabile,
 asociază sume conturilor pe debit sau pe credit,
 apar asociate operaţiunilor din jurnale
 apar în fişa contului: toate înregistrările asociate unui cont
formează fişa lui.

Balanţa Contabilă – care


 se constituie pentru un interval calendaristic
 prezintă câte un rând sintetic pentru fiecare cont, calculându-
i soldul final și rulajele.
-------------------------------------------------------------- */

Proiectarea generică a obiectelor, în fapt proiectarea claselor de


obiecte, are în vedere rezolvarea următoarelor categorii de probleme:

1. Ce roluri îşi vor asuma obiectele în contextul aplicaţiei?


2. Ce date sau informaţii sunt necesare pentru descrierea acestor
roluri?
3. Ce acţiuni vor fi capabile să execute obiectele din fiecare clasă
(din fiecare tip)?
4. Cum vor fi prezentate aceste obiecte altor obiecte? Adică:
 Ce informaţii vor fi disponibile sau accesibile altor
obiecte?
 Ce acţiuni sau funcţionalităţi vor fi disponibile sau
accesibile altor obiecte?
Proiectarea claselor 9

Figura 2.3 Diagrama de clase UML: Obiecte generale ale modelului contabil

Răspunsurile la întrebările de mai sus vor produce următoarele


definiţii ce vor fi formalizate prin intermediul structurilor orientate obiect
ce urmează a fi construite:

1. numele claselor sau, mai elaborat, numele interfeţelor pe care


le vor implementa;
2. variabilele de instanţă, dar şi parametrii din semnătura
operaţiilor;
3. operaţiile sau metodele ce urmează a fi implementate;
4. nivelul de vizibilitate al variabilelor de instanţă sau, mai
elaborat, proprietăţile convenţionale ale obiectelor;
5. nivelul de vizibilitate al operaţiilor sau metodelor ce urmează
a fi implementate.
10 Capitolul 2

Figura 2.4 Diagrama de clase UML: detalii ale obiectelor modelului contabil
Proiectarea claselor 11

2.1.3 Date, metode şi algoritmi în obiecte

Clarificarea aspectelor declarative structurale şi comportamentale,


legate de interfeţe şi de clasele de implementare ale acestora, va trebui
urmată de stabilirea detaliilor specificaţiilor ce vor permite, în final,
formalizarea lor completă (sau codificarea) într-un limbaj de
programare.

În legătură cu proiectarea datelor sau, mai exact, în legătură cu


proiectarea variabilelor de instanţă, trebuie definite în mod specific
următoarele:
 ce tip de date (care să acopere inclusiv multiplicitatea) este
potrivit,
 dar și ce specificator de vizibilitate este necesar.

De asemenea, cu privire la proiectarea metodelor trebuie clarificate


următoarele elemente:

1. Ce sarcină specifică va fi efectuată?

2. Ce informaţii sunt necesare? De unde vor fi accesate: de la


nivelul variabilelor de instaţă sau prin intermediul
parametrilor? Precum și care va fi tipul parametrilor?

3. Ce algoritm va fi utilizat?
a. Ce variabile locale vor fi necesare?
b. Ce structuri de control vor fi utilizate?

4. Cum se va materializa rezultatul execuţiei metodei?


a. Ce informaţii vor fi returnate?
b. Ce variabile de instanţă vor fi modificate?
12 Capitolul 2

Figura 2.5 Diagrama de secvenţe pentru implementarea operaţiei getSold()


aparținând clasei OperatiuneContabila

Mijloacele de implementare completă a acestor specificaţii de


proiectare într-un limbaj concret de programare, din care apoi să fie
compilată o formă executabilă a soluţiei, constituie obiectul disciplinei
Programare orientată obiect, şi vor fi detaliate, mai pe îndelete, în
paragrafele şi capitolele ce vor urma.
Proiectarea claselor 13

2.2 Clase şi tipuri de obiecte


Am menţionat în paragraful anterior că programarea orientată obiect
trebuie să pună la dispoziţie mijloacele metodologice necesare
implementării specificaţiilor de proiectare. Mai simplu spus, vor trebui
detaliate modalităţile concrete de obţinere a obiectelor. În acest context, şi
dincolo de specificul oricăror definiţii academice, trebuie să existe un
mecanism de definire a obiectelor în mediul runtime în care vor exista, am
putea spune, din punct de vedere concret sau fizic. În majoritatea
limbajelor mai mult sau mai puţin pur orientate obiect, acesta este rolul
claselor.

2.2.1 Clase şi tipuri

După cum am sugerat mai sus, în general, clasa este văzută din
perspectiva implementării: adică un mecanism concret de definire
(internă, în primul rând, dar şi externă) a obiectelor. Din acest punct de
vedere, trebuie să stabilim de la început că există o distincţie destul de
clară între clasă şi tip, pe care, într-o formă sau alta, mai explicit sau
implicit, o afirmă sau o „recunoaşte” şi o formalizează orice limbaj de
programare.

În Java (şi n-am putea spune că în C#.NET lucrurile ar sta altfel),


noţiunea de clasă pare implicit legată de implementare. Spre deosebire de
clasă, tipul are o semnificaţie eminamente declarativă. Deşi neavând un
construct clar circumscris, termenul de tip apare des invocat, în special în
contextul definirii valorilor legale care pot fi asociate variabilelor (de
instanţă sau locale), mai exact a naturii obiectelor ale căror referinţe pot fi
stocate în variabile sau, într-o altă interpretare, a naturii obiectelor care
pot fi manipulate prin intermediul numelor variabilelor.

Elaborarea unei clase se poate face fără impunerea declarativă a unui


tip, însă, în mod implicit, din elementele constitutive ale ei se poate
extrage definiţia unui tip, dacă ne limităm la declaraţiile membrilor
accesibili externi din structura clasei.

În multe medii de programare orientate obiect se recomandă, şi chiar


se favorizează, separarea intenţiilor declarative faţă de cele de
implementare, în mecanismul de construire a obiectelor, adică separarea
tipului față de clasă. În alte medii, cum este şi cazul limbajului Java,
separarea, deşi posibilă, nu este explicit obligatorie, prin urmare
construirea clasei include şi definiţia tipului, fiind mai degrabă un
principiu de proiectare argumentat prin construcţii suplimentare cum ar fi
14 Capitolul 2

interfeţele şi/sau clasele abstracte. Această separare are valenţe de


proiectare, pentru că separarea tipului față de clasă are drept consecinţe o
flexibilizare a modelelor de obiecte, adică:
 1 clasă poate implementa mai multe tipuri,
 1 tip poate fi implementat de mai multe clase.

De altfel, acest principiu este unul dintre izvoarele polimorfismului,


după cum vom vedea în capitolul următor.

În anumite medii de programare pur (sau parţial) orientate obiect


există destul de explicit definită noţiunea de tip abstract de date (TAD),
de fapt, o explicitare, mai specifică, a conceptului de tip. Acest concept
face trimitere, în primul rând, la reprezentarea datelor, lucru care se
realizează printr-un set de componente externe (atribute sau câmpuri), şi
pentru că ar trebui să fie accesibile, însă în mod read-only, convenţie rar
respectată. De asemenea, manipularea datelor reprezentate prin TAD ar
trebui realizată de un set de operatori care pot primi drept argumente
componentele externe declarate şi, deci, mai puţin (sau, dacă ar fi
respectată convenţia sugerată, deloc) prin modificarea directă a
componentelor de reprezentare. Prin declararea publică în cadrul unor
clase a unor variabile de instanţă reprezentative, limbajul Java ar părea că
se apropie de această definiţie. Însă, crearea în acest mod a unui TAD nu
este tocmai recomandabilă. Modalitatea-ghid în cazul acestui limbaj
constă în utilizarea convenţiei JavaBean ce presupune folosirea
proprietăţilor şi a principiului încapsulării.

În cazul limbajului Java, demarcarea explicită între aspectele


declarative şi cele legate de implementare se face prin mecanismul
interfeţelor ce vor fi implementate de către clase. Limitarea, oarecum, a
interfeţelor faţă de tipuri în sens clasic rezidă în faptul că acestea sunt
formate doar din declaraţii referitoare la operaţii (aspectele
comportamentale, dinamice), nu şi la atribute (aspectele structurale).
Există însă o convenţie, am putea spune universal recunoscută, în mediile
de dezvoltare bazate pe limbajul Java, cea a încapsulării variabilelor de
instanţă (mecanism prin care este implementat conceptul de atribut sau
câmp) în aşa-numitele proprietăţi, care sunt definite, de fapt, printr-un
cuplu de operaţii pentru accesul (citirea și scrierea) variabilelor de bază
(sau a câmpurilor).

Am precizat, mai sus, că cea mai generală definiţie a tipului acoperă o


reprezentare externă (adică una sau mai multe componente – atribute) şi
un un set de operatori. Asimilând componentele-atribute cu proprietăţile
(mai exact cu partea din convenţie care se referă la operaţiile de access) şi
Proiectarea claselor 15

operatorii cu operaţiile, aducem interfeţele cel mai aproape de definiţa


convenţională a tipului (TAD).

Figura 2.6 Interfeţe şi clase pentru TAD

În listingul de mai jos se găsește implementarea exemplului din figura 2.6


privitor la declararea tipurilor prin intermediul mecanismului interfețelor.

// Listing 2.3 Codificare interfețe și clase pentru TAD

public interface ICont {


// proprietatea [cod]
String getCod();
// proprietatea [denumire] cu operator de modificare
String getDenumire();
16 Capitolul 2

void setDenumire(String denumire);


}

public class ContImpl implements ICont{


// reprezentare proprietati [cod] si [denumire]
private String cod;
private String denumire;

// implementare conventie
// - incapsulare reprezentare interna
// - reprezentare externa prin proprietati JavaBean
public String getCod() {
return cod;
}
public String getDenumire() {
return denumire;
}
public void setDenumire(String denumire) {
this.denumire = denumire;
}

// constructor pentru obtinere valoare


public ContImpl(String cod, String denumire) {
this.cod = cod;
this.denumire = denumire;
}
}

2.2.2 Clasele şi implementarea lor structurală şi


comportamentală

Am arătat deja mai sus că în cazul multor sau poate chiar a majorităţii
limbajelor de programare care folosesc drept concept fundamental clasa,
aceasta pare mai relevantă preponderent prin latura ei legată de
implementare, adică de construire, mai mult decât latura declarativă, de
utilizare sau externă. De asemenea, am sugerat deja că cel mai relevant
din punctul de vedere al laturii declarative este, în cazul limbajului Java,
noţiunea de interfaţă.

În mediul Java, specificaţiile membrilor din cadrul unei clase


presupun următoarele aspecte:
Proiectarea claselor 17

 aspectele declarative: cine sunt aceşti membri (numele lor), ce fel


de membri sunt, adică:
 tipul, în cazul variabilelor de instanţă:
//sp.-vizibilitate : tip-variabilă : nume-variabilă
private String cod;

 tipul returnat şi tipurile argumentelor în cazul operaţiilor:


//sp.-vizibil. : tip-ret.2 : nume-op. : tip-param. : nume-param.
public void setCod (String cod) ;

 dar şi aspecte interne: în ce fel sau cum, sunt accesibili membrii


tocmai declaraţi (specificatorii de vizibilitate):
 cum sunt sau pot fi iniţializaţi, în cazul variabilelor de
instanţă (valorile implicite, dar şi caracterul static sau non-
static);
//sp.-vizibil. : tip-var. : nume-var. : expresie-init-var.
private Double sold = 0.0;

 cum vor acţiona concret atunci când sunt accesate sau


invocate (algoritmul de execuţie specific metodelor de
implementare), în cazul operaţiilor.

//sp.-vizibil. : tip-ret.3 : nume-op. : tip-param. : nume-param.


public void setSold (Double suma)
// metodă de implementare – bloc de instrucţiuni
{ this.sold = suma; }

Prin urmare, în Java clasa reprezintă, în primul rând, definiţia


completă a implementării unui tip. Structura expusă sau accesibilă extern,
cea care se suprapune cel mai bine conceptului de tip, este sau poate fi
destul de diferită faţă de structura internă, adică accesibilă din contextul
blocurilor de instrucţiuni aferente metodelor de implementare din clasa
curentă sau din subclasele ei. Apare astfel un efect colateral al utilizării
directe a numelui unei clase în declaraţia tipului unei variabile: dacă
respectiva variabilă este declarată accesibilă dintr-un context exterior
clasei, atunci ea va dezvălui aspectele publice, convenţionale legate de
noţiunea generică de tip, însă, dacă respectiva variabilă este declarată şi
accesibilă dintr-un context intern (direct într-o metodă a clasei sau într-o

2
În cazul operaţiilor, pe „locul” tipului returnat se poate găsi şi cuvântul-cheie void,
atunci când execuţia metodei de implementare nu returnează explicit o valoare prin
instrucţiunea return.
18 Capitolul 2

subclasă), tipul ei va dezvălui toate aspectele structurale şi


comportamentale, adică inclusiv cele declarate private sau încapsulate.

// Listing 2.4: Declaraţii şi impl. în def. unei clase


public class Cont implements ICont{
// reprezentare proprietati [cod] si [denumire]
private String cod, denumire;
private Cont contParinte;
private Double sold = 0.0;
// implementare conventie
// - incapsulare reprezentare interna
// - reprezentare externa prin proprietati JavaBean
public String getCod() {
return cod;
}
public String getDenumire() {
return denumire;
}
public void setDenumire(String denumire) {
this.denumire = denumire;
}
public Double getSold() {
return sold;
}
public void setSold(Double sold) {
this.sold = sold;
}
public Cont getContParinte() {
return contParinte;
}
public void setContParinte(Cont cont) {
this.contParinte = cont;
}
public String getCodContParinte(){
// desi cod este private
// el este accesebil in contextul clasei
return this.contParinte.cod;
}
// constructor pentru obtinere valoare
public Cont(String cod, String denumire) {
this.cod = cod;
this.denumire = denumire;
}
}
Proiectarea claselor 19

2.3 Principii de proiectare a structurilor obiectuale


După discuţia tip vs. clasă sau declaraţie vs. implementare din
paragraful anterior, în continuare vor fi prezentate câteva dintre principiile
şi mijloacele concrete legate de aceste distincţii.

2.3.1 Ascunderea informaţiei: încapsulare structurală.


Vizibilitatea atributelor

Ascunderea informaţiei este un principiu fundamental, specific


paradigmei orientării obiect, care se referă la faptul că numai acele detalii
structurale strict relevante vor fi accesibile în contextul în care sunt
folosite (instanţiate, referenţiate). Prin urmare, numai anumite variabile de
instanţă ar putea fi accesate în mod direct, astfel că, aşa cum deja am
discutat în paragraful precedent, pentru protejarea chiar şi a detaliilor
informaţionale accesibile din exteriorul obiectelor, a fost convenită
utilizarea proprietăţilor ca mecanism prin care să poată fi interceptat:
 atât accesul la citire, prin metode accesor, așa-numiţii getteri,
 cât şi accesul la modificare, prin metode modificator, așa-numiţii
setteri.

În mod concret, ascunderea informaţiei sau, mai exact, a membrilor


clasei, în special în scopul protejării, dar şi în scopul afirmării distincţiei
între aspectul declarativ (sau al reprezentării externe) şi cel al
implementării interne, este posibilă datorită specificatorilor de
vizibilitate. Aceștia pot fi ascociaţi (ca şi proprietăţi sau caracteristici)
variabilelor de instanţă şi operaţiilor.

În limbajele de programare orientate obiect, în speţă Java, vizibilitatea


comportă două aspecte: pe de o parte este vorba despre aplicarea
principiului ascunderii informaţiei la nivelul membrilor-variabile sau
metodelor şi, pe de altă parte, este vorba despre formarea „spaţiilor de
nume” la nivelul organizării claselor în module (pachete sau package-uri
în Java) funcţionale.

Specificatorii pentru acces (public, protected, private) au ca principal


rol separarea elementelor stabile, care pot fi accesate de „clienţii”
bibliotecilor de clase, faţă de elementele care se găsesc sub controlul
exclusiv al celor care se ocupă de implementare. Altfel spus, este vorba
despre separarea între elementele disponibile, ale căror specificaţii sunt
declarate utilizabile în contextul exterior, şi cele ascunse, care reprezintă
în cea mai mare măsură detalii de implementare.
20 Capitolul 2

Accesul la un membru-variabilă de instanţă (sau câmp) al unei clase,


este controlat în Java prin următorii specificatori:

 public, pentru a fi vizibil din orice context sau spaţiu de nume


din care face parte tipul respectivei clase;

 modul friendly, adică fără să fie însoţit de vreo indicaţie


explicită privind vizibilitatea, ceea ce îl face disponibil în
contextul pachetului în care este declarată clasa din definiţia
căreia face parte;

 protected, ceea ce-l va face vizibil numai pentru clasele


derivate (care moştenesc) clasa de bază în care a fost declarat;

 private, dacă se doreşte ca un membru să fie vizibil numai


intern, în cadrul specificaţiilor de implementare, şi invizibil
sau indisponibil în nici un fel în afară.

Un membru declarat public este accesibil din oricare clasă care


importă pachetul în care este declarat (mai multe detalii despre pachete
vezi în paragraful următor).

Atunci când un membru-intern, mai exact o variabilă de instanţă, este


declarată private în scopul încapsulării, controlul accesului se va
exercita:
 prin metodele de acces (get) şi modificare (set), în cazul
convenţiei proprietăţilor;
 prin constructorii care pot iniţializa membrii încapsulaţi,
iniţializare care poate fi influenţată prin argumentele transmise
metodelor-constructor.

Prin urmare, specificatorii de vizibilitate la nivelul membrilor-


variabile de instanţă pot face diferenţa între cei care vor fi expuşi sau vor
fi accesbili externi (care, în consecinţă, formează reprezentarea externă a
instanţelor claselor de structura cărora aparţin) şi structura internă,
specifică, de materializare concretă (sau implementare) a acestor
reprezentări.

2.3.2 Încapsularea comportamentelor. Vizibilitatea


operaţiilor

În linii mari, strategia specifică ascunderii informaţiilor sau strategia


încapsulării, aplicată variabilelor de instanţă, se poate utiliza şi la nivelul
operaţiilor, însă cu anumite nuanţe distinctive semnificative.
Proiectarea claselor 21

Specificatorii de vizibilitate sau de control al accesului, enumeraţi mai


sus, se aplică oricăror membri ai unei clase, adică deopotrivă variabilelor
de instanţă şi operaţiilor. Ascunderea operaţiilor, făcându-le inaccesibile
din exterior, constituie esenţa principiului încapsulării comportamentale
care afirmă că numai operaţiile strict relevante, imperios necesar a fi
invocate din exterior, să fie accesibile, restul operaţiilor fiind necesare
modularizării raţionale a comportamentelor complexe expuse prin
operaţiile accesebile extern, trebuind, prin urmare, să manifeste o
existenţă cât se poate de discretă.

De asemenea, trebuie spus că elementul implicit absolut „intern”,


adică prin definiţie împachetat în aşa fel încât să nu poată fi accesibil din
contextul extern sau al invocării operaţiei, este algoritmul concret de
implementare materializat în Java sub forma unui bloc de instrucţiuni,
deliminat prin caracterele speciale { … }. Acest bloc însoţeşte în mod
obligatoriu declaraţia operaţiei în clase, cu excepţia interfeţelor sau a
claselor declarate abstracte.

// Listing 2.5: Operaţii publice şi încapsulate


// în def. unei clase, vezi şi Figura 2.5
public class OperatiuneContabila{
... ... ...
public Double getSold(){
return getDebit() - getCredit();
}
private Double getDebit(){
Double debit = 0.0;
for (InregistrareContabila i: this.getInregistrari()){
if (i instanceof InregistrareDebit)
debit += i.getSuma();
}
return debit;
}
private Double getCredit(){
Double credit = 0.0;
for (InregistrareContabila i: this.getInregistrari()){
if (i instanceof InregistrareCredit)
credit += i.getSuma();
}
return credit;
}
}
22 Capitolul 2

2.3.3 Împachetarea și vizibilitatea claselor

Relativizând discuţia despre încapsulare, ascunderea informaţiei,


„împachetare” sau, mai concret, despre vizibilitate, trebuie spus că aceste
distincţii au sens raportate la contextul sau spaţiul (adesea numit şi spaţiu
de nume) de care aparţin sau în care rezidă definiţiile ori specificaţiile la
care se referă.

Orice definiţie declarativă a unui element structural, cum este


declaraţia unei variabile, operaţie sau chiar a unui tip-respectiv clasă,
reprezintă o componentă a unei supra-structuri formată, am putea spune,
pe principiul containerelor. Astfel:
 variabilele locale fac parte din blocurile de instrucţiuni care
compun metodele de implementare ale operaţiilor;
 membrii, variabile de instanţă şi operaţii, fac parte şi compun
spaţiul intern al unei clase;
 declaraţiile claselor fac parte şi formează pachete.

Figura 2.7 Spații de nume și vizibilitate


Proiectarea claselor 23

Pachetele de clase reprezintă, cel puțin pentru limbajul de programare


Java, spaţiile sau formele de organizare cele mai elaborate şi complexe.
În speţă, „deasupra” lor nu mai există alte „supra-structuri”, deşi, uneori,
componentele ori modulele funcţionale (aplicaţii executabile de sine-
stătătoare ori biblioteci de funcţionalităţi) sunt considerate a fi structurate
pe baza pachetelor, ceea ce comportă totuşi o altă ... discuţie. Deşi numele
pachetelor poate fi format pornind de la numele altor pachete, folosind
sintaxa „cu punct”, totuşi, formal, între pachete nu există nici o formă de
subordonare, adică nu există cazul în care vizibilitatea asupra
conţinutului unui pachet înglobează ierarhic şi vizibilitatea
subpachetelor sale: pur şi simplu nu există noţiunea de subpachet. De
exemplu a.b.c nu este un „subpachet” al pachetului a.b, care, la rândul lui,
nu este un subpachet al lui a. Prin urmare, din punct de vedere logic,
pachetele a.b.c, a.b au o existență independentă.

Un spaţiu de nume presupune că fiecare element intern are un nume


unic în interiorul acestuia, iar, din exterior, numele elementelor vor putea
fi invocate (dacă, bineînţeles, sunt publice) specificând (mai ales pentru a
evita conflictele de nume) numele intern calificat prin prefixare cu
numele spaţiului de nume.

Spaţiile de nume cele mai relevante în Java sunt clasele şi pachetele.


Prin urmare, numele membrilor (fie variabile de instanţă, operaţii sau
clase interne) trebuie să fie unice în interiorul claselor, iar numele claselor
trebuie să fie unice în cadrul package-urilor în care sunt definite, în
exterior fiind vizibile prin prefixarea numelui lor cu numele spaţiului
(pachetului) „părinte”. Numele pachetului ar putea fi omis prin
importarea explicită a claselor sau a întregului package, obligativitatea
prefixării rămânând obligatorie doar în cazul unor conflicte potenţiale cu
numele celorlalte clase existente.

Cuvântul-cheie import declară includerea întregului spaţiu de nume


(*) sau doar a unor elemente (clase) specificate distinct din spaţiul de
nume al pachetului (bibliotecii) indicat(e) astfel. De exemplu pentru
importul elementelor bibliotecii util din distribuţia Java:
import java.util.*;

Altfel, clasele vor fi utilizate (sau invocate) prin numele lor întreg,
adică inclusiv numele complet al bibliotecii (pachetului) acesteia.

// Listing 2.6: Clase invocate din alte spaţii de nume


// specificate prin declaraţii de import
// modul de subliniere al pachetelor este corelat
// cu elementele acestora, invocate ulterior
24 Capitolul 2

import app.model.validare.Validatable;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public class OperatiuneContabila implements


Comparable, Serializable, Validatable{
private Integer idOperatiune;
private Date dataContabilizare;

public OperatiuneContabila() {
}

public OperatiuneContabila(Integer idOperatiune,


Date dataContabilizare) {
this.idOperatiune = idOperatiune;
this.dataContabilizare = dataContabilizare;
}
private Map<Integer, InregistrareContabila> inregistrari =
new TreeMap<Integer, InregistrareContabila>();

public Date getDataContabilizare() {


return dataContabilizare;
}
public void setDataContabilizare(Date dataContabilizare) {
this.dataContabilizare = dataContabilizare;
}
public List<InregistrareContabila> getInregistrari() {
List<InregistrareContabila> result =
new ArrayList<InregistrareContabila>();
result.addAll(inregistrari.values());
return result;
}
public Integer getIdOperatiune() {
return idOperatiune;
}
public void setIdOperatiune(Integer idOperatiune) {
this.idOperatiune = idOperatiune;
}
}
Proiectarea claselor 25

Un caz particular al definirii claselor îl constituie găzduirea lor în


interiorul altor clase – clasele interne. Ca urmare, clasele pot fi
declarate:

 în interiorul pachetelor,
 dacă sunt declarate public, atunci, în momentul în care se
declară accesul la un pachet, înseamnă că numai clasele
publice pot fi invocate sau referenţiate;
 dacă nu sunt însoţite de specificatorul de vizibilitate public
(altele nu sunt acceptate) atunci vor fi considerate cu
vizibilitate la nivel (sau în interiorul) package;

 în interiorul altor clase: în acest fel se creează clase interne, care


pot fi specificate
 la fel ca membrii variabile de instanţă sau operaţie, situaţie
în care pot fi asociate cu un specificator de vizibilitate:
 dacă sunt declarate public, numele lor fi accesibile
prin specificatorul clasei de context din exteriorul
pachetului, numele acestora va fi obligatoriu
prefixat cu numele clasei de context;
 dacă sunt declarate private, vor putea fi instanţiate
sau referenţiate numai în interiorul clasei gazdă;
 dacă sunt declarate protected, vor fi accesibile, la
fel ca şi în cazul altor membri și la nivelul
subclaselor;
 în interiorul metodelor de implementare, la fel ca
variabilele de instanţă, situaţie în care specificatorii de
vizibilitate nu au sens.

// Listing 2.7: Clasă internă: comparator


// pentru ordonarea operaţiunilor într-un registru
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
// clasa „externă” RegistruOperatiuni
public class RegistruOperatiuni {
private OperatiuneContabila[] operatiuni =
new OperatiuneContabila[0];
// membru intern de tipul clasei interne
private ComparatorOperatiuneContabila comparator =
new ComparatorOperatiuneContabila();
// proprietatea operatiuni
26 Capitolul 2

public OperatiuneContabila[] getOperatiuni() {


return operatiuni;
}
public void addOperatiuneContabila(
OperatiuneContabila operatiuneContabila){
List<OperatiuneContabila> opList =
new ArrayList<OperatiuneContabila>();
opList = Arrays.asList(operatiuni);
opList.add(operatiuneContabila);
//operatiunea de sortare
// cu sprijinul clasei utilitare Collections
Collections.sort(opList, comparator);
}
// clasa internă – ComparatorOperatiuneContabilă –
// după data calendaristică
private class ComparatorOperatiuneContabila implements
Comparator<OperatiuneContabila> {
public int compare(OperatiuneContabila o1,
OperatiuneContabila o2) {
if (o1.getDataContabilizare()
.before(o2.getDataContabilizare()))
return -1;
else if (o1.getDataContabilizare()
.after(o2.getDataContabilizare()))
return 1;
else
return 1;
}
}
}

2.4 Reutilizarea obiectelor în scopul creării sau derivării


de noi structuri
Pe lângă principiul (sau principiile, după anumiţi autori) încapsulării
și/sau ascunderii (sau împachetării) informaţiei, un alt principiu, cel
puţin la fel de important, este cel al reutilizării.

Reutilizarea obiectelor este, de fapt, ingredientul fundamental în


construirea unor noi structuri prin compunere (reutilizare prin
referenţiere) sau derivare (reutilizare prin moştenire).
Proiectarea claselor 27

2.4.1 Reutilizare prin referenţiere simplă

Scopul creării obiectelor constă în invocarea funcţionalităţii asociate


acestora, adică de fiecare dată când un obiect este instanţiat,
funcţionalitatea generică a acestuia urmează a fi reutilizată. Prin urmare,
instanţierea obiectelor este factorul esenţial în reutilizare. De regulă
instanţierea obiectelor se face:
 pentru iniţializarea variabilelor locale din interiorul unei metode –
reutilizare simplă;
 pentru iniţializarea variabilelor de instanţă din structura unui alt
obiect – reutilizare structurală sau compunere.

Metodele constructor

Constructorul reprezintă o metodă asociată oricărei clase şi care este


apelată în momentul creării instanţelor. Numele constructorului
desemnează o funcţie-membru cu numele clasei şi nu prezintă nici un tip
returnat (nici măcar void).
Funcţie de modul de definire şi parametrizare se pot deosebi mai
multe tipuri de constructori:
 constructor default sau implicit;
 constructor definiţi explicit;
 constructor fără parametri;
 constructor parametrizat.

De altfel, pentru o clasă pot fi definite mai multe metode-constructor


care vor fi diferenţiate prin modul de parametrizare, situaţie în care apare
şi fenomenul supraîncărcării constructorilor, deoarece toţi constructorii
au acelaşi nume care corespunde, de altfel, cu numele clasei.

// Listing 2.8: Metode constructor pentru clasa


// OperatiuneContabila. Supraîncărcare constructori

public class OperatiuneContabila implements Comparable,


Serializable, Validatable{
... ... ...
// constructor fără parametri
public OperatiuneContabila() { }

// constructor cu parametri
public OperatiuneContabila(Integer idOperatiune,
Date dataContabilizare) {
this.idOperatiune = idOperatiune;
28 Capitolul 2

this.dataContabilizare = dataContabilizare;
}
... ... ...
}

Diferenţierea metodelor în cazul supraîncărcării, indiferent dacă este


sau nu este vorba de constructori, se face prin:
(1) tipul argumentelor,
(2) ordinea argumentelor.

Atenţie! nu se iau în considerare numele argumentelor şi nici tipul


returnat.

Compunerea claselor

Compunerea şi/sau agregarea au şi o accepţiune semantică mai


deosebită, însă, din punctul de vedere strict al programării orientate
obiect, se referă la plasarea de referinţe pentru obiecte în structura noilor
clase: obiectele compuse vor include structural obiectele componente.

Din punctul de vedere strict al implementării, obiectele compuse


conţin variabile de instanţe unde vor fi stocate referinţe către obiectele
componente.

// Listing 2.9: Compuneri între clasele „contabile”


public class RegistruOperatiuni {
... ... ...
// variabilă de instaţă – impl. compunere:
// stocare referinţe către instanţe operaţiuni
private OperatiuneContabila[] operatiuni =
new OperatiuneContabila[0];

// proprietate expunere variabilă internă


// pentru implementare compunere
public OperatiuneContabila[] getOperatiuni() {
return operatiuni;
}
... ... ...
}
public class OperatiuneContabila implements
Comparable, Serializable, Validatable{
... ... ...
// variabilă de instaţă – impl. compunere:
// stocare referinţe către instanţe înregistrări
Proiectarea claselor 29

private Map<Integer, InregistrareContabila> inregistrari =


new TreeMap<Integer, InregistrareContabila>();
// proprietate expunere variabilă internă
// implementare compunere, se observă diferenţierea
// dintre tipul membrului de implementare şi
// tipul proprietăţii de expunere
public List<InregistrareContabila> getInregistrari() {
List<InregistrareContabila> result =
new ArrayList<InregistrareContabila>();
result.addAll(inregistrari.values());
return result;
}
... ... ...
}

Din punct de vedere semnatic însă compunerea şi agregarea pot avea


şi valenţe suplimentare:

 se vorbește de compunere dacă obiectele componente sunt inlcluse


exclusiv într-un singur întreg,

 se vorbește de agregare dacă obiectele componente sunt partajate


de mai multe obiecte compuse.

2.4.2 Reutilizare prin moştenire

Reutilizarea prin moştenire este posibilă datorită relaţiilor de


generalizare-specializare între clase, şi nu a relaţiilor de asociere-
referenţiere, ca în cazul compunerii.

Din punct de vedere semantic, generalizarea are legătură cu efortul de


abstractizare. A abstractiza înseamnă, în esenţă, eliminarea diferenţelor,
principiu care, aplicat claselor, se traduce prin „sintetizarea” unei clase
mai generale ce va cumula caracteristicile comune, ce pot apărea
recurent, în structura unor clase iniţiale. Legătura sau relaţia ce va apărea
astfel între clasa generică şi subclasele sale se numeşte generalizare (vezi
figura 2.8).
Invers, extinderea setului de caracteristici ale unei clase de bază prin
includerea unor caracteristici specifice, adică nu eliminarea, ci adăugarea
diferenţelor, ar putea produce noi clase diferenţiate pornind de la o bază
comună într-un efort de specializare (vezi figura 2.9).
30 Capitolul 2

Figura 2.8 Specializări contabile, mai detaliate sau mai sintetice


Proiectarea claselor 31

Figura 2.9 Derivări ale operațiunilor rezultate din specializarea naturii contabile

În acest context, moştenirea se referă la preluarea, deci reutilizarea, în


subclasele mai specializate, a tuturor aspectelor structurale şi
32 Capitolul 2

comportamentale (a operaţiilor şi metodelor de implementare) din clasele


generale. Subclasele au obligaţia de „respecta” întocmai caracteristicile
din declaraţiile membrilor (variabile de instanţe şi operaţii); numai în
cazul operaţiilor subclasele pot modifica sau rescrie metodele de
implementare.

// Listing 2.10: Moştenire într-o „ierarhie” de specializare


// Superclasa InregistrareContabilă ------------------------------
public class InregistrareContabila {
// variabile de instanţă moştenite
private Integer id;
private Integer nrOrdine;
private String tip; // discriminator debit, credit
private Double suma;
// referenţiere (compunere) instanţă Cont
private Cont cont;
// referenţiere (compunere) instanţă operaţiune
private OperatiuneContabila operatiune;

// proprietate Id – care va fi moştenită


public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
... ... ...
// constructori (nu se moştenesc)
public InregistrareContabila(Integer id, Cont cont,
Double suma){
this.id = id;
this.tip = this.getClass().getSimpleName();
this.suma = suma;
this.cont = cont;
}
public InregistrareContabila() {
}
}

// Subclase specializate InregistrareDebit şi InregistrareCredit--


public class InregistrareDebit extends InregistrareContabila{
private Integer nrOrdineDebit;

// proprietate specifică: nrOrdineDebit


Proiectarea claselor 33

public int getNrOrdineDebit() {


return nrOrdineDebit;
}
... ... ...
}
//------------------------------------
public class InregistrareCredit extends InregistrareContabila{
private Integer nrOrdineCredit;

// proprietate specifică: nrOrdineCredit


public Integer getNrOrdineCredit() {
return nrOrdineCredit;
}
... ... ...
}

2.4.3 Moştenire şi subtipizare

Vom încheia acest paragraf cu o nouă discuție referitoare la tipuri,


având în vedere un alt efect al moştenirii – subtipizarea: adică preluarea
de către tipurile subclaselor (claselor obţinute prin specializare) a
tuturor caracteristicilor (declarative – adică a operaţiilor) provenite din
tipurile de bază.
Ca urmare, din moment ce o subclasă are aceeaşi natură ca şi
superclasa sa, referinţa la subclasa respectivă este, de asemenea, şi o
referinţă la superclasa sa. În limbajul Java, mecanismul prin care se poate
verifica natura reală a tipului unei variabile se bazează pe operatorul
instanceof.

De asemenea, mecanismul prin care tipul declarat al unei variabile


poate fi reconsiderat astfel:
 în sensul unui sub-tip al tipului declarat, operație care se numeşte
down-casting;
 în sensul unui super-tip al tipului declarat, operație care se
numeşte up-casting.

În acest sens, se poate afirma că subtpizarea reprezintă mecanismul


prin care o instanţă a unei clase poate fi utilizată în orice context în care
este specificată superclasa sa.
În limbajul Java, pot fi produse tipuri prin intermediul următoarelor
blocuri constructive: clase simple, interfeţe şi clase abstracte. În acest sens
subtipizarea se poate manifesta astfel:
34 Capitolul 2

 o clasă (abstractă sau nu) care extinde o altă clasă devine sub-tip al
acesteia;
 o clasă care implementează o interfaţă devine sub-tip al acesteia;
 o interfaţă care extinde o altă interfaţă devine sub-tip al acesteia.

// Listing 2.11: Utilizarea operatorului instanceof


public class OperatiuneContabila{
... ... ...
public Double getSold(){
return getDebit() - getCredit();
}

private Double getDebit(){


Double debit = 0.0;
for (InregistrareContabila i: this.getInregistrari()){
if (i instanceof InregistrareDebit)
debit += i.getSuma();
}
return debit;
}

private Double getCredit(){


Double credit = 0.0;
for (InregistrareContabila i: this.getInregistrari()){
if (i instanceof InregistrareCredit)
credit += i.getSuma();
}
return credit;
}
}

După cum am arătat deja, interfeţele, fiind prin natura lor formate
numai din specificaţii declarative, pot fi asimilate foarte simplu tipurilor,
adică pot fi folosite doar pentru declararea variabilelor, nu şi pentru
construirea instanţelor ale căror referinţe vor fi reţinute în aceste
variabile.
Clasele abstracte constituie un caz mai aparte: nu pot fi utilizate
nemijlocit în crearea instanţelor, fiindcă reprezintă de obicei specificaţii
de implementare incomplete, parţiale, dar nici nu pot fi considerate
absolut declarative, deoarece ar putea prezenta elemente de implementare
care ar putea fi reutilizate în clasele lor concrete. Prin urmare, clasele
abstracte:
Proiectarea claselor 35

 nu pot fi instanţiate în mod direct;


 instanţe ale acestor clase se obţin prin intermediul claselor
concrete care le moştenesc;
 metodele abstracte sunt specificate în clasele abstracte şi sunt
implementate în clasele concrete.

// Listing 2.12: Reconsiderarea unei superclase


// drept clasă abstractă

// Superclasa InregistrareContabilă –
// abstractă deci neinstanţiabilă direct
public abstract class InregistrareContabila {
... ... ...
}

// Subclase concrete InregistrareDebit şi InregistrareCredit -----


public class InregistrareDebit extends InregistrareContabila{
... ... ...
}
//------------------------------------
public class InregistrareCredit extends InregistrareContabila{
... ... ...
}

2.5 Obiecte şiruri de caractere şi valorile fundamentale


primitive
Şirurile de caractere reprezintă un tip de date fundamental în orice
limbaj de programare. Dacă nu pot fi asimilate ca numere sau valori
booleene, datele iniţial nestructurate dintr-un proiect sunt, cel mai la
îndemână, reprezentate ca şiruri de caractere. Acest lucru este favorizat
de faptul că, indiferent de natura lor internă sau reală, orice tip de date
poate fi reprezentat prin şiruri de caractere: în limbajul Java clasa Object,
care este moştenită de orice clasă nou creată, prezintă metoda toString()
destinată a fi supra-scrisă tocmai în acest sens. De asemenea, prin
interpretarea inteligentă a formatului unor şiruri de caractere pot fi
desprinse componentele necesare iniţializării corecte a instanţelor oricăror
tipuri de date specifice.
Prin urmare, înţelegerea şi manipularea şirurilor de caractere, instanţe
ale clasei String în limbajul Java, reprezintă un factor esenţial în aspectele
legate de programarea orientată obiect privind compunerea de noi clase
36 Capitolul 2

sau formarea de noi tipuri de caractere ale căror variabile de instanţă sau
componente (interne şi externe) se bazează în foarte multe cazuri pe şiruri
de caractere ca cea mai flexibilă cale de reprezentare.

2.5.1 Clasa String şi instanţierea acesteia

Reprezentarea externă a unui şir de caractere este, într-un anume sens,


trivială. Specifici limbajului Java sunt separatorii ”...” pentru delimitarea
literalilor-valori şiruri de caractere faţă de celelalte nume sau cuvinte-
cheie din cadrul secvenţei de instrucţiuni ale unui bloc de implementare
specific unei metode.
În ceea ce priveşte implementarea structurală, clasa String are ca
fundament obiectele de tip Array (ale căror instanţe sunt tablourile de
obiecte) şi valorile char (valori reprezentând un singur caracter din
tabloul ASCII). Un şir de caractere în Java reprezintă, de fapt, o secvenţă
de caractere reprezentată ca un vector, adică un tablou cu o singură
dimensiune.

// Listing 2.13 Array-uri de caractere

void testCharArrays() {
char[] char_seq = {'P', 'R', 'O'};
for (char c : char_seq) {
System.out.print(c + ",");
}
System.out.println();
String char_str = "PRO";
for (int i = 0; i < char_str.length(); i++) {
System.out.print(char_str.charAt(i) + "+");
}
System.out.println();
char[] char_str_array = char_str.toCharArray();
for (int i = 0; i < char_seq.length; i++) {
System.out.println("char_seq[" + i + "] == "
+ "char_str_array["
+ char_str.indexOf(char_seq[i]) + "] : "
+ char_str.charAt(i) + " : "
+ (char_seq[i] == char_str_array[i]));
}
}

Prin urmare, tipul fundamental pentru reprezentarea caracterelor


(singulare) este tipul primitiv char în corepondenţă cu clasa de acoperire
Character.
Proiectarea claselor 37

// Listing 2.14 Obţinere coduri ascii

void testCharAsciiCode() {
char c = 'a';
int asciiCode = (int) c;
System.out.println("char 'a' - ascii cod = " + asciiCode);
}

Clasa Character reprezintă o clasă de acoperire pentru valorile char


(aşa cum este Integer pentru valorile int), oferind un set interesant de
operaţii legate de caractere (conversie, ordonare etc.)

// Listing 2.15 Clasa Character

void testCharacterInstance() {
Character c_a = new Character('a');
Character c_A = 'a'; // auto-boxing
System.out.println("character a - ascii cod = "
+ Character.getNumericValue('a'));
System.out.println("character a - majuscula = "
+ Character.toUpperCase('a'));
System.out.println("character a este numeric: "
+ Character.isDigit('a'));
System.out.println("character a este literal: "
+ Character.isLetter('a'));
System.out.println("character a este spatiu: "
+ Character.isSpaceChar('a'));
System.out.println("character a este minuscula:"
+ Character.isLowerCase('a'));
}

Instanţierea şirurilor de caractere reprezintă o operaţiune destul de


specială pentru că se poate efectua în mai multe moduri:

 prima apariţie a unui literal delimitat prin caracterele ”...” produce


instanţierea corespunzătoare a obiectului-şir de caractere, instanţă
String, corespunzător;

 invocarea unui constructor al clasei String prin cuvântul-cheie


new; un astfel de constructor primeşte, de regulă, sub o anumită
formă o secvenţă de caracterere;
38 Capitolul 2

 invocarea metodei statice valueOf() a clasei String pentru


conversia în şir de caractere a unei valori de alt tip (boolean, char,
int, float, double sau chiar o instanţă Object oarecare, situaţie în
care capătă importanţă metoda toString() a clasei reale din care
provine obiectul respectiv).

// Listing 2.16 Instanţiere şiruri de caractere

void testStringInstance(){
String str_1 = "pro";
String str_2 = "pro";
System.out.println("str_1 == str_2 :" + (str_1 == str_2));
// true
String str_3 = new String("pro");
System.out.println("str_1 == str_3 :" + (str_1 == str_3));
// false
String str_4 = String.valueOf(true);
System.out.println("true = " + str_4.equals("true"));
// true
}

2.5.2 Sub-şiruri de caractere

Am arătat mai sus faptul că instanţele String sunt, de fapt, secvenţe


organizate de caractere: fiecare caracter are stabilită o poziţie explicită, iar
numărul total de caractere desemnează dimensiunea sau lungimea şirului.
În acest context putem vorbi şi de sub-şiruri de caractere, formate dintr-o
sub-secvenţă de caractere dintr-un şir iniţial. Trebuie spus că formarea
unui sub-şir înseamnă crearea unui şir nou, diferit de şirul iniţial, fiindcă
şirurile de caractere sunt imutabile, adică nu-şi pot schimba starea, prin
urmare secvenţa de caracterere iniţială nu poate fi modificată.

// Listing 2.17 Imutabilitatea instanţelor clasei String

void testStringImmutable() {
char[] char_seq = {'i', 'm', 'm', 'u', 't',
'a', 'b', 'l', 'e'};
String str_1 = new String(char_seq);

String str_2 = new String("immutable");

String str_3 = "immutable";


Proiectarea claselor 39

String str_4 = str_3.replace("immu", "permu");


System.out.println("Initial string state : " + str_3);
System.out.println("Replace result : " + str_4);
System.out.println("str_3 == str_4 : "
+ (str_3 == str_4));
System.out.println("str_3 == str_4 : "
+ (str_3.equals(str_4)));
}

Pentru a extrage (ceea ce înseamnă şi a crea) sub-şiruri de caractere,


există un set de operaţii specifice ale clasei String, după cum se
exemplifică și în listingul următor.

// Listing 2.18 Creare sub-şiruri de caractere


void testSubStrings_1() {
String str_1 = "SELECT o FROM Client c WHERE c.cod = 111";
String s_str_1 = str_1.substring(0, 8);
System.out.println("s_str_1: " + s_str_1);
System.out.println("s_str_1 in str_1: "
+ str_1.indexOf(s_str_1));
String s_str_2 = str_1.substring(s_str_1.length() + 1,
(s_str_1.length() + 1) + 13);
System.out.println("s_str_2: " + s_str_2);
System.out.println("s_str_2 in str_1: "
+ str_1.indexOf(s_str_2));
String s_str_3 = str_1.substring(
s_str_1.length() + 1 + s_str_2.length() + 1);
System.out.println("s_str_3: " + s_str_3);
System.out.println("s_str_3 in str_1: "
+ str_1.indexOf(s_str_3));

String sir_2 = s_str_1 + ' ' + s_str_2 + ' ' + s_str_3;


System.out.println("sir_2 : " + sir_2);
}

2.5.3 Compararea şirurilor şi procesarea lor. Căutarea sub-


şirurilor în şiruri. Concatenarea şirurilor

Compararea şirurilor comportă o discuţie aparte datorită unei


semantici destul de diverse în acest sens:

 compararea identităţii valorii a două variabile ce conţin referinţe


către instanţe String, lucru care se face prin operatorul ==;
40 Capitolul 2

 compararea conţinutului a două instanţe String, adică, mai exact, a


secvenţelor de caractere prin: equals(), adică în întregime sau
startWith(), contains(), endWith(), adică parţială;

 compararea în scopul ordonării prin compareTo().

// Listing 2.19 Compararea şirurilor de caractere

void testSubStrings_2() {
String str_1 = "SELECT o FROM Client c WHERE c.cod = 111";
String s_str_1 = str_1.substring(0, 8);
System.out.println("s_str_1: " + s_str_1);
String s_str_2 = str_1.substring(
str_1.indexOf(s_str_1) + s_str_1.length() + 1,
(str_1.indexOf(s_str_1) + s_str_1.length()+1)+ 13);
System.out.println("s_str_2: " + s_str_2);

String s_str_3 = str_1.substring(


str_1.indexOf(s_str_2) + s_str_2.length() + 1);
System.out.println("s_str_3: " + s_str_3);

String str_2 = s_str_1.concat("")


.concat(s_str_2)
.concat("")
.concat(s_str_3);
System.out.println("str_2 : " + str_2);

System.out.println("str_1.startsWith(s_str_1) :: "
+ str_1.startsWith(s_str_1));
System.out.println("str_1.contains(s_str_2) :: "
+ str_1.contains(s_str_1));
System.out.println("str_1.endsWith(s_str_3) :: "
+ str_1.endsWith(s_str_3));

System.out.println("str_1 == str_2 :" + (str_1 == str_2));


System.out.println("str_1.equals(str_2) :"
+ (str_1.equals(str_2)));

String str_3 = "SELECT o FROM Client c WHERE c.cod = 111";


System.out.println("str_1 == str_3 :" + (str_1 == str_3));

String str_4 = new String(


"SELECT o FROM Client c WHERE c.cod = 111");
Proiectarea claselor 41

System.out.println("str_1 == str_4 :" + (str_1 == str_4));

String x1 = "abcd";
String x2 = "bdce";

System.out.println("x1.compareTo(x2) = "
+ x1.compareTo(x2));
// return -1, adica x1 se gaseste inainte de x2
}

Am discutat mai sus modul de creare a subşirurilor, însă din şirurile


existente pot fi create noi instanţe şi prin concatenare. Concatenarea
şirurilor de caractere reprezintă însă o operaţie destul de costisitoare:
instanţele String fiind imutabile, fiecare aplicare a operatorului de
concatenare va produce o nouă instanţă. În acest sens, de foarte mare
folos este clasa StringBuffer recomandată atunci când aplicarea multiplă
(de un număr considerabil de ori) a operatorului de concatenare va fi
înlocuită cu invocarea operaţiei append() a unei instanţe StringBuffer
pregătite înainte. Obţinerea şirului final ca rezultat al concatenării se
realizează prin invocarea operaţiei toString() asupra instanţei
StringBuffer... O altă posibilitate de eficientizare a concatenării o
reprezintă şi folosirea operaţiei concat din clasa String în locul
operatorului clasic de concatenare +.

// Listing 2.20 Modalităţi de concatenare a şirurilor de caractere

String multiply_StringBuffer(String str, Integer level) {


Date startTime = new Date();
StringBuffer strBuff = new StringBuffer();
for (int i=0; i <= level; i++){
strBuff.append(str);
}
String strFinal = strBuff.toString();
System.out.println("Time multiply_StringBuffer: "
+ (new Date().getTime() – startTime.getTime()));
return strFinal;
}

String multiply_OperatorConcatenare(String str,


Integer level) {
Date startTime = new Date();
String strBuff = ""; //şirul null
for (int i=0; i <= level; i++){
42 Capitolul 2

strBuff += str;
}
System.out.println("Time multiply_OperatorConcatenare: "
+ (new Date().getTime() – startTime.getTime()));
return strBuff;
}

String multiply_OperatieConcat(String str, Integer level) {


Date startTime = new Date();
String strBuff = "";
for (int i=0; i <= level; i++){
strBuff = strBuff.concat(str);
}
System.out.println("Time multiply_OperatieConcat: "
+ (new Date().getTime() – startTime.getTime()));

return strBuff.toString();
}

2.5.4 Primitivele și clasele de acoperire

Deși limbajul Java a avut și are în continuare un rol determinant în


proliferarea abordării orientate obiect în programarea aplicațiilor, există o
categorie de teoreticieni și practicieni din acest domeniu care îi contestă
caracterul „pur” orientat obiect. Gâlceava pornește de la tipurile de date
fundamentale sau primitive, pe baza cărora sunt construite toate celelalte
tipuri de date furnizate de platforma standard sau construite de utilizatorii-
programatori.

Tipuri de date primitive

Toate limbajele de programare procedurală (și, să nu uităm,


programarea orientată obiect s-a „ridicat” tocmai pe fundația acestor
limbaje) folosesc în mod convențional un set de tipuri de bază pentru a
reprezenta în memoria internă valorile boolene, numerele și caracterele.
Nici limbajul Java nu poate face excepție de la regula că orice noi tipuri
de date trebuie să fie fondate pe o bază de tipuri elementare; la urma
urmei pentru orice nouă construcție, fie ea și abstractă, trebuie să existe o
fundație. În cazul tipurilor de date din Java, această fundație este formată
din boolean, char, byte, int, short, long, float, și double. Aceste tipuri de
date sunt considerate primitive și pentru că sunt ne-decompozabile sau
atomice.
Proiectarea claselor 43

Variabilele de tip int vor fi utilizate pentru a stoca valori întregi. Prin
urmare int provine, de fapt, de la integer. Însă, la fel ca şi int, valori
întregi pot stoca și tipurile primitive byte, short și long. Ele se diferenţiază
prin dimensiunea memoriei alocate și, ca urmare, dimensiunea
intervalului matematic acoperit. Dacă este nevoie de stocarea unor valori
reale, atunci limbajul Java pune la dispoziție tipurile float și double. Tipul
boolean este cel mai simplu şi acoperă doar două valori: true și false, pe
când un char va reprezenta un cod (ca urmare tot un număr întreg), însă
un cod dintr-o tabelă în care fiecare element reprezintă un caracter
aparţinând unui set oficial agreat și folosit într-o anumită zonă geografică
(după cum se știe, și limba română are un set de caractere specific).

Variabilele care stochează valori provenind din tipuri de date


primitive sunt un gen mai special de variabile, în sensul că, din punct de
vedere logic, ele nu pot stoca „obiecte”. Prin urmare, sunt inițializate
printr-un sistem specific de literali și nu apelând la cuvântul-cheie new ce
introduce constructorii corespunzători. Acest fapt are la bază o restricție
de ordin fizic: variabilele primitive vor stoca valori efective și nu un șir de
octeți reprezentând adresa sau referința unui obiect.

Tabelul 2.1 Tipuri primitive în Java


Tip Domeniu valori Exemplu inițializare
primitiv
boolean adevărat, fals boolean b = true;
boolean b = false;
char Setul/tabela de caractere unicode char c = ‘a’;
byte -128...+127 byte b = 7;
short -32768...+ -32767 short s = 7;
int -2147483648...+2147483647 int n = 7;
long -263...+ -263-1 long n = 7l;
float -3.40292347E + 38… float n = 7.0f;
+3.40292347E + 38
double -1.79769313486231570E + 308 … double n = 7.0;
+1.79769313486231570E + 308

Urmărind exemplele de iniţializare din tabelul de mai sus, se observă


că există două tipuri de date strict numerice care necesită şi specificarea
unui caracter special, l pentru long şi f pentru float, pe lângă caracterele
numerice folosite în instrucţiunea de atribuire.
Interesant este faptul că limbajul Java permite specificarea operaţiilor
între valori provenind din tipuri primitive diferite. Însă, întotdeauna,
rezultatul unei astfel de expresii de calcul va fi corespunzător tipului celui
mai acoperitor sau cu precizia cea mai mare, ca urmare valorile
44 Capitolul 2

considerate „inferioare” din acest punct de vedere vor fi „promovate” prin


conversie la tipul „superior”. De exemplu, dacă este vorba de o expresie
în care adunăm un întreg, reprezentat ca un int, şi un număr real doar cu
parte întreagă, deci cu partea zecimală 0, reprezentat ca un double,
rezultatul final va fi de tip double.
Acest fenomen de promovare nu are loc dacă toate valorile sunt de
acelaşi tip chiar în condiţiile în care ar exista o operaţie care ar părea că
„cere” acest lucru. De exemplu, dacă se împart două valori întregi (valori
int), se va obţine tot o valoare întreagă (int), deşi rezultatul matematic
corect ar părea să fie o valoare reală, deci ar trebui reprezentat ca un float
sau double (vezi un exemplu în acest sens în listingul 2.22).

Împachetarea valorilor primitive în obiecte. Auto-boxing

Într-unul din paragrafele de mai sus, s-a menţionat caracterul


neorientat obiect al manipulării variabilelor conţinând valori ale tipurilor
de date primitive. Şi totuşi, fără a inhiba acest mod de lucru, limbajul
Java, încă de la primele versiuni, a introdus un set de clase de acoperire
(wrapping classes) ale căror instanţe împachetează valorile primitive,
manipulabile astfel în mod orientat obiect.

Tabelul 2.2 Clase de acoperire pentru tipuri de date primitive


Tip Clasă de Exemplu inițializare Exemplu autoboxing
primitiv acoperire orientată obiect
boolean Boolean Boolean b = Boolean b = true;
new Boolean(true); Boolean b = false;

Boolean b =
new Boolean(false);
char Character Character c = Character c = ‘a’;
new Character(‘a’);
byte Byte Byte b = Byte b = 7;
new Byte(7);
short Short Short s = Short s = 7;
new Short(7);
int Integer Integer n = Integer n = 7;
new Integer(7);
long Long Long n = Long n = 7L;
new Long(7);
float Float Float n = Float n = 7.0f;
new Float(7.07);
double Double Double n = Double n = 7.0;
new Double(7.07);
Proiectarea claselor 45

Toate clasele de acoperire care împachetează valori numerice sunt


subclase ale clasei abstracte Number, de la care moştenesc un set de
operaţii (cu obligaţia de a le implementa) referitoare la conversie,
comparare şi egalitate mai ales între valori primitive provenite din tipuri
diferite. De asemenea, fiecare subclasă Number prezintă două constante
MIN_VALUE şi MAX_VALUE care conţin informaţii cu privire la limitele
intervalului acoperit de fiecare dintre tipurile primitive.

// Listing 2.21 Operaţiile clasei Number


// operaţii de conversie
byte byteValue()
short shortValue()
int intValue()
long longValue()
float floatValue()
double doubleValue()
// operaţii de comparabilitate
int compareTo(Byte anotherByte)
int compareTo(Double anotherDouble)
int compareTo(Float anotherFloat)
int compareTo(Integer anotherInteger)
int compareTo(Long anotherLong)
int compareTo(Short anotherShort)
// operaţia de egalitate implementată de fiecare subclasă de
// acoperire comparând valoarea primitivă împachetată
boolean equals(Object obj)

Avantajul acestor clase de acoperire pare acum destul de evident, însă


există şi „reversul medaliei”: s-a observat introducerea unei anumite
confuzii în rândul programatorilor, mai ales în rândul începătorilor, cu
privire la construirea expresiilor care implică variabile numerice: când şi
cum să se folosească tipurile primitive iniţiale şi când să se folosească
instanţe ale claselor de acoperire? În cele din urmă, a fost introdusă o
facilitate expresă în limbaj, facilitate care încearcă să rezolve majoritatea
acestor situaţii „tulburătoare”. Această opţiune de lucru se numeşte auto-
boxing şi se referă la conversia automată de la valori primitive la obiecte
numerice şi invers, responsabilitate care a fost lăsată pe seama
compilatorului. În consecinţă, putem folosi variabile-referinţă pentru
manipularea valorilor primitive, variabile care să fie iniţializate direct cu
aceste primitive (adică fără invocare explicită a constructorilor, vezi
ultima coloană din tabelul de mai sus). De asemenea, obiectele claselor de
acoperire numerice pot fi utilizate direct în expresii care implică operaţii
matematice (+, -, /, *, Math.pow()..., >, <, >=, <= etc.), în afară de
46 Capitolul 2

comparatorul standard de egalitate „==”, în locul căruia trebuie folosită


operaţia equals (vezi un exemplu în acest sens în listingul 2.22).

Conversia datelor bazate pe primitive în și din șiruri de


caractere

Indiferent de tipurile de date folosite, există un numitor comun pentru


toate obiectele ce se „perindă” în interiorul aplicaţiilor Java: posibilitatea
sau, după caz, cerinţa reprezentării lor ca şiruri de caractere.

Pentru conversia unei valori primitive într-un şir de caractere există


două posibilităţi, facilitate prin clasele de acoperire:

 conversia implicită prin invocarea operaţiei toString() şi care,


bineînţeles, nu ţine seama de anumite caracteristici ale
convenţiilor regionale (de exemplu, privind delimitatorii părţii
fracţionare pentru valorile reale americanii folosesc punctul,
iar europenii folosesc virgula);

 folosind un obiect de tip Format (de exemplu DecimalFormat)


iniţializat printr-o convenţie de formatare exprimată ca un şir
de caractere sau printr-un grup de parametri de formatare, de
exemplu instanţe ale DecimalFormatSymbols, şi invocat prin
operaţia format() (vezi un exemplu în listingul următor).

Conversia unui şir de caractere într-o valoare primitivă se poate


realiza tot prin două modalităţi:
 apelând metoda valueOf a clasei de acoperire corespunzătoare
tipului ţintă primitiv, cu condiţia ca şirul iniţial să respecte
convenţia de exprimare implicită a literarilor specifici
respectivelor valori primitive;

 folosind, la fel ca şi în cazul conversiei din primitiv în şir, un


obiect Format, dar apelându-i operaţia parse() (vezi listingul
următor).

// Listing 2.22 Manipulare valori primitive


// prin clase de acoperire
//--------------------------------------------------------------
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
Proiectarea claselor 47

import java.text.ParseException;

public class TestPrimitive {


public static void main(String[] args){
//1) Autoboxing initializare
Integer a = 7;
Double b = 7.0;
Integer c = 2;
System.out.println("1) a = " + a + ", "
+ "b = " + b + ", "
+ "c = " + c);
//2) Autoboxing expresie matematica
Number r1 = a/c;
Number r2 = b/c;
Number r3 = a + c;
Number r4 = b + c;
System.out.println("2) r1 = a/c = " + r1 + ", \n"
+ "r2 = b/c = " + r2 + ", \n"
+ "r3 = a+c = " + r3 + ", \n"
+ "r4 = b+c = " + r4);
//3) Pastrarea tipului rezultatului dupa operatii
// cu primitive numerice de acelasi tip
System.out.println("3.1) r1 este de tip: "
+ r1.getClass().getName());
System.out.println("3.2) r3 este de tip: "
+ r1.getClass().getName());

//4) Promovarea tipului rezultatului dupa operatii


// cu primitive numerice de tipuri diferite
System.out.println("4.1) r2 este de tip: "
+ r2.getClass().getName());
System.out.println("4.2) r4 este de tip: "
+ r2.getClass().getName());

//5) Comparare primitive numerice de tipuri diferite


System.out.println("5.1) a > b? " + (a>b));
System.out.println("5.2) a [egal] b? " + (a.equals(b)) );
System.out.println("5.3) a [compareTo] c?"
+ (a.compareTo(c)) );

//6) Conversie intre valori numerice


Integer b_parteIntreaga = new Integer(b.intValue());
System.out.println("6.1) a == b_parteIntreaga? "
+ (a == b_parteIntreaga) );
48 Capitolul 2

System.out.println("6.2) a [egal] b_parteIntreaga? "


+ (a.equals(b_parteIntreaga)) );

//7) Conversie cu format default


// din siruri de caractere in valori numerice
String d = "7.5";
String e = "2";
Object r5 = Double.valueOf(d)/Integer.valueOf(e);
System.out.println("7.1) 7.5/2 = " + r5 + "\n"
+ "tip rezultat conversie default: "
+ r5.getClass().getName());
String f = "725256.856";
// format default, ne-explicitat
DecimalFormat fn1 = new DecimalFormat();
try {
Object r6 = fn1.parseObject(f);
System.out.println("7.2) string -> double: \n"
+ f + " -> " + r6 + "\n"
+ "tip rez conversie default: "
+ r6.getClass().getName());
} catch (ParseException e1) {
e1.printStackTrace();
}

//8) Conversie cu format explicit


// din siruri de caractere in valori numerice
String g = "725.256,856";
// format pe un grup de setari zecimale
DecimalFormat fn2 = new DecimalFormat();
DecimalFormatSymbols dfs1 = new DecimalFormatSymbols();
dfs1.setGroupingSeparator('.');
dfs1.setDecimalSeparator(',');
fn2.setMaximumIntegerDigits(6);
fn2.setMaximumFractionDigits(3);
fn2.setDecimalFormatSymbols(dfs1);
try {
Object r7 = fn2.parseObject(g);
System.out.println("8) string -> double: \n"
+ g + " -> " + r7 + "\n"
+ "tip rez conversie: "
+ r7.getClass().getName());
} catch (ParseException e1) {
e1.printStackTrace();
}
Proiectarea claselor 49

//9) Conversie cu format default


// din valori numerice in siruri de caractere
Double h = 877411.99;
System.out.println("9.1) double -> string: "
+ h.toString());
// format pe un grup de setari zecimale
DecimalFormat fn3 = new DecimalFormat();
String r8 = fn3.format(h);
System.out.println("9.2) double -> string: " + r8);

//10) Conversie cu format explicit


// din valori numerice in siruri de caractere
Double i = 877411.99;
// format pe un grup de setari zecimale
DecimalFormat fn4 = new DecimalFormat();
DecimalFormatSymbols dfs2 = new DecimalFormatSymbols();
dfs2.setGroupingSeparator('.');
dfs2.setDecimalSeparator(',');
fn4.setMaximumIntegerDigits(6);
fn4.setMaximumFractionDigits(3);
fn4.setDecimalFormatSymbols(dfs2);
String r9 = fn4.format(i);
System.out.println("10) double -> string: " + r9);
}
}
//--------------------------------------------------------------
// Rezultatul execuţiei
1) a = 7, b = 7.0, c = 2
2) r1 = a/c = 3,
r2 = b/c = 3.5,
r3 = a+c = 9,
r4 = b+c = 9.0
3.1) r1 este de tip: java.lang.Integer
3.2) r3 este de tip: java.lang.Integer
4.1) r2 este de tip: java.lang.Double
4.2) r4 este de tip: java.lang.Double
5.1) a > b? false
5.2) a [egal] b? false
5.3) a [compareTo] c? 1
6.1) a == b_parteIntreaga? false
6.2) a [egal] b_parteIntreaga? true
7.1) 7.5/2 = 3.75
tip rezultat conversie default: java.lang.Double
50 Capitolul 2

7.2) string -> double:


725256.856 -> 725256.856
tip rez conversie default: java.lang.Double
8) string -> double:
725.256,856 -> 725256.856
tip rez conversie: java.lang.Double
9.1) double -> string: 877411.99
9.2) double -> string: 877,411.99
10) double -> string: 877.411,99

2.6 Obiecte-excepţii sau tratarea excepţiilor într-un


mediu orientat obiect
Un alt tip de obiecte specifice, fundamentale în programarea orientată
obiect, cel puţin pentru limbajul Java, sunt excepţiile.

2.6.1 Returnarea obiectelor şi întreruperea execuţiei


metodelor

În general, în contextul unei aplicaţii Java, execuţia unei metode


poate produce un obiect rezultat, trimis în contextul din care a fost
invocată prin următoarele căi alternative:
 folosind instrucţiunea return;
 folosind instrucţiunea throw.
Atenţie, ambele instrucţiuni produc întreruperea execuţiei operaţiei
invocate, pe lângă returnarea unei valori sau unui obiect în contextul din
care au fost invocate operaţiile respective.

Prin return poate fi returnat oricare obiect, cu menţiunea că tipul


acestuia trebuie menţionat în semnătura (sau declaraţia) metodei. Prin
instrucţiunea throw pot fi returnate numai obiecte ce pot fi interpretate ca
Throwable (prin urmare din subclasele acestui tip).

// Listing 2.23 Modalităţi de returnare de obiecte


// din contextul execuţiei metodelor
String returnObjectMethod(){
return "obiect-returnat";
}
void throwObjectMethod() throws Throwable{
throw new Throwable("object-returned");
}
Proiectarea claselor 51

Preluarea obiectelor astfel returnate se face diferit: dacă în cazul


return valoarea returnată poate fi folosită direct într-o instrucţiune de
atribuire, în cazul throw este necesară o structură de control specifică try
{…} catch(){...} finally{...}.

// Listing 2.24 Structura de control try-catch


public static void main(String[] args) {
TestThrowable test = new TestThrowable();

String str = test.returnObjectMethod();


System.out.println("str - " + str);

try{
test.throwObjectMethod();
}catch(Throwable t){
System.out.println("t - " + t.getMessage());
}

2.6.2 Căi de execuţie excepţionale anticipate

Excepţiile sunt obiecte care, formal, provin din subclasa Exception a


superclasei Throwable. Scopul lor este însă de semnalare a căilor de
execuţie speciale, în sensul abaterii de la un curs normal al execuţiei. Prin
urmare, semnalându-se astfel, aceste căi sunt anticipate şi pot fi gestionate
într-un mod care să nu compromită aplicaţia în întregime. Se vor putea
strecura bineînţeles şi alte posibilităţi de execuţie anormale, care s-ar
putea materializa în erori de execuţie a aplicaţiei cu grade de severitate
diferite.

java.lang.Object
|____ java.lang.Throwable
|____ java.lang.Exception

O excepţie provenind dintr-o subclasă a clasei Throwable înseamnă că


semnalizarea ei se va face tot prin cuvântul-cheie throw, iar gestiunea ei
se va face în final tot prin try...catch.

// Listing 2.25 Structura de control try-catch-finally

void testFinally(){
52 Capitolul 2

try{
if (1==1){
throw new Throwable("Trebuie să-l prinzi!");
}
if (1==1){
throw new RuntimeException("Runtime Exception");
}
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("Tratament pentru "
+ e.getMessage());
}catch(Throwable e){
System.out.println("Tratament pentru "
+ e.getMessage());
}finally{
System.out.println(
"Anyway ... with or without exception ...");
}
}

În structura try...catch exemplificată mai sus, își face apariția și un alt


treilea cuvânt-cheie – finally. Blocul de instrucțiuni introdus prin acest
cuvânt-cheie al limbajului Java are rolul de a fi executat indiferent dacă se
produce sau nu o excepție, indiferent dacă, atunci când se produce o
excepție, aceasta este fie prinsă sau capturată de o clauză catch, fie, în
cazul în care acest fapt nu se produce, execuția blocului din blocul try va
fi în mod sigur întreruptă. De exemplu, o aplicație ar putea accesa și bloca
o serie de resurse (fișiere, conexiuni la baze de date etc.), înainte de
întreruperea bruscă a acesteia ca urmare a unei excepții-runtime ne-
tratate. În acest caz, blocul finally ar putea face „curățenie”, adică ar putea
elibera respectivele resurse pentru a nu afecta disponibilitatea și
scalabilitatea sistemelor de calcul.

2.6.3 Clasa Exception, subclasele acesteia şi gestiunea


obiectelor excepţii

Există în mediul Java un set de excepţii gata-create pentru a semnaliza


cele mai comune astfel de căi de execuţie, formându-se o ierarhie de clase
specifică.
Proiectarea claselor 53

Figura 2.9 Clase de excepţii sistem

Structura clasei Exception este relativ simplă:

 getMessage() returnează mesajul de excepţie explicativ pentru


incidentul care a provocat excepţia;

 getCause() dacă incidentul are la bază un alt obiect Throwable,


atunci acesta este returnat;

 getStackTrace() returnează, sub forma unui array, stiva de


apeluri (ca instanţe StackTraceElement) care a produs
excepţia;

 printStackTrace() returnează, sub forma unui şir de caractere


afişabil la consolă, stiva de apeluri.

Gestiunea excepţiilor provenite dintr-un set de instrucţiuni poate fi


făcută, de fapt, în două moduri:
54 Capitolul 2

 tratare locală într-un bloc specific catch ce urmează blocului


try care încadrează obligatoriu setul de instrucţiuni prezumtibil
a genera excepţia;

// Listing 2.26 Tratare excepţii locale

void testLocalExceptions(String str) {


try {
if (!str.matches("SELECT.*")) {
throw new Exception(
"Not SELECT command : " + str);
}
} catch (Exception ex) {
System.out.println("Catched: "
+ Listing getMessage());
}
}

 delegarea tratării excepţiei prin „trimirea” ei către contextul


din care a fost invocată metoda care conţine setul de
instrucţiuni prezumtibil a genera excepţia, caz în care tipul real
al excepţiei trebuie marcat în semnătura operaţiei.

// Listing 2.27 Delegare tratare excepţii locale


void testDeclaredExceptions(String str) throws Exception{
processSelectComands(str);
}

void processSelectComands(String str) throws Exception {


if (!str.matches("SELECT.*")) {
throw new Exception("Not SELECT command : " + str);
}
}

Obligativitatea tratării excepţiilor reprezintă o problemă intens


dezbătută şi astăzi în comunitatea programatorilor Java. După părerea
noastră este cumva o falsă problemă, iată de ce: în general, excepţiile sunt
împărţite în checked şi unchecked, adică obligatoriu a fi gestionate
explicit şi opţional. Excepţiile checked sunt, de fapt, în mod obligatoriu
declarate, iar compilatorul ţine seama de aceste declaraţii şi, dacă nu
găseşte o rezoluţie finală a acestora, generează o eroare de compilare.
Există însă două clase, Error şi RuntimeException, care nu au un astfel de
tratament din partea compilatorului:
Proiectarea claselor 55

 instanţe ale clasei Error, folosite pentru a semnala erori grave


care nu ar trebui „prinse” de vreun handler catch;

 instanţe RuntimeException – pentru excepţii ce semnalează


căi anormale de execuţie, dar care ar trebui prinse de un anume
handler catch, gravitatea lor nejustificând întreruperea
execuţiei aplicaţiei şi oprirea mediului runtime.

// Listing 2.28 Erori și excepţii checked şi unchecked

void testErrors(String str) {


processUpdateComands(str);

void testUncheckedExceptions(String str) {


try {
processDeleteComands(str);
} catch (Exception ex) {
System.out.println("Catched: "
+ Listing getMessage());
}

void processUpdateComands(String str) {


if (!str.matches("UPDATE.*")) {
throw new Error("Not UPDATE command : " + str);
}
}

void processDeleteComands(String str) {


if (!str.matches("DELETE.*")) {
throw new RuntimeException("Not DELETE command : "
+ str);
}
}

Pornind de la aceste clase, adică Exception, Error şi


RuntimeException, sau chiar de la ierarhia extinsă de excepţii a mediului
Java, din figura 2.9, poate fi construit un întreg sistem de excepţii adaptat
la specificul unei anumite aplicaţii, pentru controlul pre sau post
56 Capitolul 2

condiţiilor de execuţie a unor servicii sau integritatea proprietăţilor


(metodele get/set) unor entităţi din domeniul afacerii.

2.7. Obiecte pentru reprezentarea datei calendaristice și


timpului
Deşi nu face parte din tipurile de date primitive sau fundamentale, pe
lângă valorile şir de caractere, instanţe ale clasei String, şi valorile ce ar
trebui să reprezinte data calendaristică şi timpul sunt considerate printre
tipurile de bază indispensabile pentru majoritatea aplicaţiilor scrise (sau
nu) în limbajul Java.

Din păcate, platforma Java nu are un istoric favorabil referitor la


reprezentarea şi manipularea datei calendaristice. Din acest motiv, de-a
lungul timpului, au apărut mai multe clase dedicate în acest sens:
 java.util.Date,
 java.util.Calendar,
 java.sql.Date,
 java.sql.Time,
 java.sql.Timestamp.

Clasa java.util.Date a constituit prima formă de gestiune a valorilor de


acest tip în aplicaţiile Java şi încă a rămas modalitatea de bază de stocare
a unor astfel de valori. Manipularea sau exprimarea datei calendaristice
pornind de la componentele externe rămâne însă unul din punctele
nevralgice ale acesteia. Clasa java.util.Calendar încearcă să rezolve acest
fel de probleme, adăugând în plus câteva funcţionalităţi legate de
conversia între diferitele convenţii regionale de exprimare a timpului.
Cadrul de lucru JDBC, baza comunicării cu bazele de date SQL, a
adăugat propriile modalităţi de tratare a datei şi timpului, din necesităţi
legate uneori chiar de acurateţea şi precizia conversiei către formatele de
stocare specifice bazelor de date ţintă.

La acest moment, valorile java.util.Date au rolul principal de a stoca


informaţiile referitoare la dată şi timpul calendaristic, cu precizie de
milisecunde. Însă, iniţializarea pornind de la componentele obişnuite,
convenţionale an, lună, zi, oră, minut, secundă, milisecundă şi extragerea
sau manipularea acestor componente se recomandă a se realiza cu ajutorul
unor obiecte format, de exemplu instanţe java.text.SimpleDateFormat, ce
asigură conversia din şi reconversia în şiruri de caractere. La fel ca şi în
cazul claselor de acoperire a tipurilor primitive, au rămas la nivelul clasei
java.util.Date responsabilităţile de comparare a două date calendaristice
prin operaţii de genul:
Proiectarea claselor 57

 int compareTo(Date),
 boolean before(Date),
 boolean after(Date),
 boolean equals(Date).

Baza acestor operaţii de comparare, precum şi a conversiei valorilor


de tip dată calendaristică din java.util.Date în instanţe ale celorlalte clase
alternative o constituie faptul că mecanismul de reprezentare internă se
bazează pe un număr real însemnând numărul de milisecunde parcurse de
la dată de referinţă 1 ianuarie 1970 00:00:00 GMT.

O clasă utilă destul de interesantă este java.util.Calendar, introdusă


ulterior clasei java.util.Date. Aceasta, pe lângă un suport formal mult mai
solid pentru regionalizare, prezintă şi o serie de operaţini pentru:
 manipularea directă a componentelor datei calendaristice prin
valori sau constante numerice (prin operaţii get/set, vezi
exemplul 3 din listingul următor);
 susţinerea operaţiilor cu intervale de date (add/roll) pornind de
la componentele convenţionale (vezi exemplul 4 din listingul
următor).
// Listing 2.29 Manipulare valori de tip dată calendaristică
//--------------------------------------------------------------
import java.sql.Date;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
public class TestDateCalendaristice {
public static void main(String[] args) {
// 1. initializare data curenta
// 1.1 cu java.util.Date
java.util.Date dataC = new java.util.Date();
java.util.Date altaDataC = new java.util.Date();
// 1.2 cu java.util.Calendar
Calendar calendar = Calendar.getInstance();
Calendar altCalendar = Calendar.getInstance();
System.out.println("1.1 init Date: \n dataC -> " + dataC
+ ", \n altaDataC -> " + altaDataC);
System.out.println("1.2 init Calendare: \n calendar -> "
+ calendar.getTime() + ", \n altCalendar -> "
+ altCalendar.getTime());
System.out.println("1.3 dataC==altaDataC : "
+ (dataC==altaDataC));
System.out.println("1.4 calendar==altCalendar : "
+ (calendar==altCalendar));
58 Capitolul 2

System.out.println("1.5 dataC.equals(altaDataC) : "


+ (dataC.equals(altaDataC)) );
System.out.println("1.6 calendar.equals(altCalendar) : "
+ (calendar.equals(altCalendar)) );
System.out.println("1.7 dataC.equals(calendar.getTime()) : "
+ (dataC.equals(calendar.getTime())) );

Date dataC_minus_1000ms = new Date(dataC.getTime() - 1000);


System.out.println("1.8 dataC.after(dataC_minus_1000ms) : "
+ (dataC.after(dataC_minus_1000ms)) );
Date dataC_plus_5000ms = new Date(dataC.getTime() + 5000);
System.out.println("1.9 dataC.before(dataC_plus_5000ms) : "
+ (dataC.before(dataC_plus_5000ms)) );

// 2. initializare data predefinita


// 2.1 cu java.util.Date si SimpleDateFormat
SimpleDateFormat f1 = new SimpleDateFormat("dd/MM/yyyy");
SimpleDateFormat f2 = new SimpleDateFormat(
"dd-MMM-yyyy hh24:mm:ss");
try {
java.util.Date d1 = f1.parse("01/09/2010");
System.out.println("2.1 d1 : " + f2.format(d1) + "/"
+ d1.getTime() + " milisecunde");
} catch (ParseException e) {
e.printStackTrace();
}
// 2.2 cu java.util.Calendar si SimpleDateFormat
Calendar c1 = f1.getCalendar();
System.out.println("2.2 c1 : " + f2.format(c1.getTime())
+ "/" + c1.getTimeInMillis() + " milisecunde");
// 2.3 prin modificare java.util.Calendar initial
Calendar c2 = Calendar.getInstance();
c2.set(Calendar.DAY_OF_MONTH, 1);
c2.set(Calendar.MONTH, Calendar.SEPTEMBER);
c2.set(Calendar.YEAR, 2010);
c2.set(Calendar.HOUR_OF_DAY, 0);
c2.set(Calendar.MINUTE, 0);
c2.set(Calendar.SECOND, 0);
c2.set(Calendar.MILLISECOND, 0);
System.out.println("2.3 c2 : " + f2.format(c2.getTime())
+ "/" + c2.getTimeInMillis() + " milisecunde");

// 3 Conversii
// 3.1 java.util.Date -> java.util.Calendar
Proiectarea claselor 59

java.util.Date d2 = new java.util.Date();


Calendar c3 = Calendar.getInstance();
c3.setTime(d2);
System.out.println("3.1 d2.getTime(): " + d2.getTime()
+ "-> c3.getTimeInMillis(): "
+ c3.getTimeInMillis());
// 3.2 java.util.Calendar -> java.util.Date
java.util.Date d3 = c3.getTime();
System.out.println("3.2 c3.getTimeInMillis(): "
+ c3.getTimeInMillis()
+ " -> d2.getTime(): " + d2.getTime());
// 3.3 java.util.Date -> java.sql.Date
java.sql.Date dSQL1 = new java.sql.Date(d2.getTime());
System.out.println("3.3 d2.getTime(): " + d2.getTime()
+ "-> dSQL1.getTime(): " + dSQL1.getTime());
// 3.4 java.util.Calendar -> java.sql.Date
java.sql.Date dSQL2 =
new java.sql.Date(c3.getTimeInMillis());
System.out.println("3.4 c3.getTimeInMillis(): "
+ c3.getTimeInMillis()
+ "-> dSQL2.getTime(): " + dSQL2.getTime());

// 4.Operatii cu date calendaristice


// 4.1 Adunare 2 luni interval calendaristic
SimpleDateFormat f3 = new SimpleDateFormat("dd/MM/yyyy");
Calendar c4 = Calendar.getInstance();
System.out.println("4.1 Data start: "
+ f3.format(c4.getTime()));
c4.add(Calendar.MONTH, 2);
System.out.println("Data finala = data start + 2luni : "
+ f3.format(c4.getTime()));
// 4.2 Scadere 15 zile interval calendaristic
Calendar c5 = Calendar.getInstance();
System.out.println("4.2 Data start: "
+ f3.format(c5.getTime()));
c5.add(Calendar.WEEK_OF_MONTH, -2);
System.out.println(
"Data finala = data start - 2saptamini : "
+ f3.format(c5.getTime()));
// 4.3 Adunare interval 14 ore
// fara trecere peste ordin (nr zilelor nu se schimba)
SimpleDateFormat f4 = new SimpleDateFormat("HH:mm:ss");
Calendar c6 = Calendar.getInstance();
System.out.println("4.3 Timp start: "
60 Capitolul 2

+ f4.format(c6.getTime()));
c6.roll(Calendar.HOUR_OF_DAY, 3);
System.out.println("Timp final = timp start + 3ore : "
+ f4.format(c6.getTime()));
// 4.4 Scadere interval 25 secunde
// fara trecere peste ordin (nr minutelor nu se schimba)
Calendar c7 = Calendar.getInstance();
System.out.println("4.4 Timp start: "
+ f4.format(c7.getTime()));
c7.roll(Calendar.SECOND, -25);
System.out.println("Timp final = timp start - 25secunde : "
+ f4.format(c7.getTime()));
}
}

Rezultatele exemplelor legate de iniţializarea implicită(1) şi


predefinită(2), şi a celor legate de conversie(3) şi de operaţiile cu date
calendaristice(4) ar trebui să fie următoarele:
//--------------------------------------------------------------
// Rezultatul execuţiei
//--------------------------------------------------------------
1.1 init Date:
dataC -> Sat Sep 25 11:00:17 EEST 2010,
altaDataC -> Sat Sep 25 11:00:17 EEST 2010
1.2 init Calendare:
calendar -> Sat Sep 25 11:00:17 EEST 2010,
altCalendar -> Sat Sep 25 11:00:17 EEST 2010
1.3 dataC==altaDataC : false
1.4 calendar==altCalendar : false
1.5 dataC.equals(altaDataC) : true
1.6 calendar.equals(altCalendar) : true
1.7 dataC.equals(calendar.getTime()) : false
1.8 dataC.after(dataC_minus_1000ms) : true
1.9 dataC.before(dataC_plus_5000ms) : true
2.1 d1 : 01-Sep-2010 1224:00:00/1283288400000 milisecunde
2.2 c1 : 01-Sep-2010 1224:00:00/1283288400000 milisecunde
2.3 c2 : 01-Sep-2010 1224:00:00/1283288400000 milisecunde
3.1 d2.getTime(): 1285401617645-> c3.getTimeInMillis():
1285401617645
3.2 c3.getTimeInMillis(): 1285401617645 -> d2.getTime():
1285401617645
3.3 d2.getTime(): 1285401617645-> dSQL1.getTime(): 1285401617645
Proiectarea claselor 61

3.4 c3.getTimeInMillis(): 1285401617645-> dSQL2.getTime():


1285401617645
4.1 Data start: 25/09/2010
Data finala = data start + 2luni : 25/11/2010
4.2 Data start: 25/09/2010
Data finala = data start - 2saptamini : 11/09/2010
4.3 Timp start: 11:00:17
Timp final = timp start + 3ore : 14:00:17
4.4 Timp start: 11:00:17
Timp final = timp start - 25secunde : 11:00:52

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