Limbajul Java-Carte in Limba Romana
Limbajul Java-Carte in Limba Romana
Cuprins
Cuvânt înainte
Introducere
Calculatorul este agenda, telefonul ºi televizorul secolului urmãtor. ªtiþi sã-l folosiþi?
Despre un om care ºtie douã limbi se spune cã este de douã ori mai deºtept. Dar ce
spuneþi despre un programator care ºtie douã limbaje?
În mod normal, sursele unui program complex sunt împãrþite în mai multe
fiºiere pentru o administrare mai uºoarã. În Java, existã în mod normal câte
un fiºier sursã pentru fiecare clasã de obiecte în parte. Limbajul Java
defineºte o serie de structuri sintactice care permit conectarea codului
rezultat în urma compilãrii diferitelor clase precum ºi organizarea acestor
clase într-o structurã ierarhicã de pachete.
Bibliografie
JavaRo
Cuvânt înainte
Deºi trãim într-o societate în care rata de schimb a tehnologiei a ajuns sã ne depãºeascã, existã domenii
care se schimbã mult prea lent faþã de aºteptãrile noastre. Sã luãm de exemplu calculatoarele. Nu existã
zi în care sã nu auzim de noutãþi în ceea ce priveºte viteza, numãrul de culori sau miniaturizarea. Nu
existã zi în care sã nu auzim de noi aplicaþii ºi de noi domenii în care a fost introdusã informatica. ªi
totuºi, nimic esenþial nu s-a schimbat în partea de fundamente. Aceeaºi arhitecturã numericã guverneazã
întreg spectrul de calculatoare aflate azi pe piaþã ca ºi acum jumãtate de secol.
În ceea ce priveºte comunicaþia om-calculator, lucrurile nu stau cu mult mai bine. Deºi nu mai
comunicãm folosindu-ne de cifre binare ºi nici în limbaje apropriate de maºinã, comunicãm în continuare
folosindu-ne de câteva primitive structurale de tip bucle sau condiþii plus directive de calcul ºi transfer al
informaþiilor. Abstracþii precum programarea logicã, funcþionalã sau orientatã obiect nu extind
blocurile de bazã cu care acþionãm asupra maºinii, le fac doar mai accesibile pentru modul nostru de a
gândi.
Într-un fel, programarea face exact aceeaºi greºealã pe care a fãcut-o ºi logica clasicã statuând cã orice
enunþ nu poate fi decât adevãrat sau fals, fãrã nici o altã nuanþare. În pasul imediat urmãtor s-au stabilit
câteva enunþuri adevãrate fãrã demonstraþie ºi s-a considerat cã toate celelalte enunþuri trebuie deduse
din ele. Programarea a fãcut aceleaºi presupuneri în ceea ce priveºte comunicaþia om-calculator: existã
câteva primitive funcþionale de bazã ºi toatã comunicaþia trebuie sã poatã fi exprimatã cu ajutorul
acestora.
În aceste condiþii, este normal ca apariþia fiecãrui nou limbaj de programare sã trezeascã un interes
major în lumea informaticã. De fiecare datã, sperãm cã noul limbaj ne va permite o exprimare mai
uºoarã, mai flexibilã, mai bogatã. De aceea, la apariþia fiecãrui limbaj de programare care promite sã
iasã din anonimat, ne grãbim sã aflãm care sunt principalele facilitãþi care ni se oferã.
Apariþia limbajului Java a fost însoþitã de multã publicitate ºi de mult scepticism. Pãrerile au variat de la
a spune cã Java este o revoluþie în programarea calculatoarelor ºi, mai ales, a reþelelor de calculatoare
pânã la afirmaþii care neagã orice caracter novator al noului limbaj ºi care pun succesul Java în
exclusivitate pe seama renumelui de care se bucurã firma Sun ºi a campaniei publicitare inteligent
condusã de marketingul acestei firme.
Indiferent însã de motivul pentru care limbajul Java a ajuns la cota extraordinarã de popularitate pe care
o simþim cu toþii, sentimentul general este acela cã Java a devenit deja o realitate ºi va rãmâne aºa pentru
suficient de mult timp. Suficient de mult pentru a fi interesaþi sã apreciem în cunoºtinþã de cauzã care
este adevãrul despre acest nou limbaj. Oricum, de la apariþia limbajului C++, acum mai bine de un
deceniu în urmã, nici un alt limbaj nu a înnegrit atâta hârtie ºi nu a adus atâtea beneficii vânzãtorilor de
servicii Internet.
Sper ca aceastã carte sã ofere suportul de care aveþi nevoie pentru a vã putea forma propria pãrere: avem
[cuprins]
Introducere
Scurt istoric
Ce este Java?
Despre aceastã carte
Convenþii utilizate în aceastã carte
Sugestii ºi reclamaþii
Alte surse de informaþii
Mulþumiri
Scurt istoric
Limbajul Java împreunã cu mediul sãu de dezvoltare ºi execuþie au fost proiectate pentru a rezolva o
parte dintre problemele actuale ale programãrii. Proiectul Java a pornit cu scopul declarat de a dezvolta
un software performant pentru aparatele electronice de larg consum. Aceste echipamente se definesc ca:
mici, portabile, distribuite ºi lucrând în timp real. De la aceste aparate, ne-am obiºnuit sã cerem fiabilitate
ºi uºurinþã în exploatare.
Limbajul luat iniþial în considerare a fost C++. Din pãcate, atunci când s-a încercat crearea unui mediu
de execuþie care sã respecte toate aceste condiþii s-a observat cã o serie de trãsãturi ale C++ sunt
incompatibile cu necesitãþile declarate. În principal, problema vine din faptul cã C++ este prea
complicat, foloseºte mult prea multe convenþii ºi are încã prea multe elemente de definiþie lãsate la
latitudinea celor care scriu compilatoare pentru o platformã sau alta.
În aceste condiþii, firma Sun a pornit proiectarea unui nou limbaj de programare asemãnãtor cu C++ dar
mult mai flexibil, mai simplu ºi mai portabil. Aºa s-a nãscut Java. Pãrintele noului limbaj a fost James
Gostling care vã este poate cunoscut ca autor al editorului emacs ºi al sistemului de ferestre grafice
NeWS. Proiectul a început încã din 1990 dar Sun a fãcut publicã specificaþia noului limbaj abia în 1995
la SunWorld în San Francisco.
Numele iniþial al limbajului a fost Oak, numele unui copac care creºte în faþa biroului lui James
Gostling. Ulterior, s-a descoperit cã numele fusese deja folosit în trecut pentru un alt limbaj de
programare aºa cã a fost abandonat ºi înlocuit cu Java, spre deliciul programatorilor care iubesc
cafenelele ºi aromele exotice.
Ce este Java?
În primul rând, Java încearcã sã rãmânã un limbaj simplu de folosit chiar ºi de cãtre programatorii
neprofesioniºti, programatori care doresc sã se concentreze asupra aplicaþiilor în principal ºi abia apoi
asupra tehnicilor de implementare a acestora. Aceastã trãsãturã poate fi consideratã ca o reacþie directã
Au fost îndepãrtate din Java aspectele cele mai derutante din C++ precum supraîncãrcarea operatorilor ºi
moºtenirea multiplã. A fost introdus un colector automat de gunoaie care sã rezolve problema dealocãrii
memoriei în mod uniform, fãrã intervenþia programatorului. Colectorul de gunoaie nu este o trãsãturã
nouã, dar implementarea acestuia în Java este fãcutã inteligent ºi eficient folosind un fir separat de
execuþie, pentru cã Java are încorporate facilitãþi de execuþie pe mai multe fire de execuþie. Astfel,
colectarea gunoaielor se face de obicei în timp ce un alt fir aºteaptã o operaþie de intrare-ieºire sau pe un
semafor.
Limbajul Java este independent de arhitectura calculatorului pe care lucreazã ºi foarte portabil. În loc sã
genereze cod nativ pentru o platformã sau alta, compilatorul Java genereazã o secvenþã de instrucþiuni
ale unei maºini virtuale Java. Execuþia aplicaþiilor Java este interpretatã. Singura parte din mediul de
execuþie Java care trebuie portatã de pe o arhitecturã pe alta este mediul de execuþie cuprinzând
interpretorul ºi o parte din bibliotecile standard care depind de sistem. În acest fel, aplicaþii Java
compilate pe o arhitecturã SPARC de exemplu, pot fi rulate fãrã recompilare pe un sistem bazat pe
procesoare Intel.
Una dintre principalele probleme ale limbajelor interpretate este viteza de execuþie, considerabil scãzutã
faþã de cea a limbajelor compilate. Dacã nu vã mulþumeºte viteza de execuþie a unei astfel de aplicaþii,
puteþi cere mediului de execuþie Java sã genereze automat, plecând de la codul maºinii virtuale, codul
specific maºinii pe care lucraþi, obþinându-se astfel un executabil nativ care poate rula la vitezã maximã.
De obicei însã, în Java se compileazã doar acele pãrþi ale programului mari consumatoare de timp, restul
rãmânând interpretate pentru a nu se pierde flexibilitatea. Mediul de execuþie însuºi este scris în C
respectând standardele POSIX, ceea ce îl face extrem de portabil.
Interpretorul Java este gândit sã lucreze pe maºini mici, precum ar fi procesoarele cu care sunt dotate
aparatele casnice. Interpretorul plus bibliotecile standard cu legare dinamicã nu depãºesc 300 Kocteþi.
Chiar împreunã cu interfaþa graficã totul rãmâne mult sub 1 Moctet, exact ca-n vremurile bune.
Limbajul Java este orientat obiect. Cu el se pot crea clase de obiecte ºi instanþe ale acestora, se pot
încapsula informaþiile, se pot moºteni variabilele ºi metodele de la o clasã la alta, etc. Singura trãsãturã
specificã limbajelor orientate obiect care lipseºte este moºtenirea multiplã, dar pentru a suplini aceastã
lipsã, Java oferã o facilitate mai simplã, numitã interfaþã, care permite definirea unui anumit
comportament pentru o clasã de obiecte, altul decât cel definit de clasa de bazã. În Java orice element
este un obiect, în afarã de datele primare. Din Java lipsesc funcþiile ºi variabilele globale. Ne rãmân
desigur metodele ºi variabilele statice ale claselor.
Java este distribuit, având implementate biblioteci pentru lucrul în reþea care ne oferã TCP/IP, URL ºi
încãrcarea resurselor din reþea. Aplicaþiile Java pot accesa foarte uºor reþeaua, folosindu-se de apelurile
cãtre un set standard de clase.
Java este robust. În Java legarea funcþiilor se face în timpul execuþiei ºi informaþiile de compilare sunt
disponibile pânã în momentul rulãrii aplicaþiei. Acest mod de lucru face ca sistemul sã poatã determina
în orice moment neconcordanþa dintre tipul referit la compilare ºi cel referit în timpul execuþiei evitându-
se astfel posibile intruziuni rãuvoitoare în sistem prin intermediul unor referinþe falsificate. În acelaºi
timp, Java detecteazã referinþele nule dacã acestea sunt folosite în operaþii de acces. Indicii în tablourile
Java sunt verificaþi permanent în timpul execuþiei ºi tablourile nu se pot parcurge prin intermediul unor
pointeri aºa cum se întâmplã în C/C++. De altfel, pointerii lipsesc complet din limbajul Java, împreunã
cu întreaga lor aritmeticã, eliminându-se astfel una din principalele surse de erori. În plus, eliberarea
memoriei ocupate de obiecte ºi tablouri se face automat, prin mecanismul de colectare de gunoaie,
evitându-se astfel încercãrile de eliberare multiplã a unei zone de memorie.
Java este un limbaj cu securitate ridicatã. El verificã la fiecare încãrcare codul prin mecanisme de CRC
ºi prin verificarea operaþiilor disponibile pentru fiecare set de obiecte. Robusteþea este ºi ea o trãsãturã
de securitate. La un al doilea nivel, Java are incorporate facilitãþi de protecþie a obiectelor din sistem la
scriere ºi/sau citire. Variabilele protejate într-un obiect Java nu pot fi accesate fãrã a avea drepturile
necesare, verificarea fiind fãcutã în timpul execuþiei. În plus, mediul de execuþie Java poate fi
configurat pentru a proteja reþeaua localã, fiºierele ºi celelalte resurse ale calculatorului pe care ruleazã o
aplicaþie Java.
Limbajul Java are inclus suportul nativ pentru aplicaþii care lucreazã cu mai multe fire de execuþie,
inclusiv primitive de sincronizare între firele de execuþie. Acest suport este independent de sistemul de
operare, dar poate fi conectat, pentru o performanþã mai bunã, la facilitãþile sistemului dacã acestea
existã.
Java este dinamic. Bibliotecile de clase în Java pot fi reutilizate cu foarte mare uºurinþã. Cunoscuta
problemã a fragilitãþii superclasei este rezolvatã mai bine decât în C++. Acolo, dacã o superclasã este
modificatã, trebuie recompilate toate subclasele acesteia pentru cã obiectele au o altã structurã în
memorie. În Java aceastã problemã este rezolvatã prin legarea târzie variabilelor, doar la execuþie.
Regãsirea variabilelor se face prin nume ºi nu printr-un deplasament fix. Dacã superclasa nu a ºters o
parte dintre vechile variabile ºi metode, ea va putea fi refolositã fãrã sã fie necesarã recompilarea
subclaselor acesteia. Se eliminã astfel necesitatea actualizãrii aplicaþiilor, generatã de apariþia unei noi
versiuni de bibliotecã aºa cum se întâmplã, de exemplu, cu MFC-ul Microsoft (ºi toate celelalte ierarhii C
++).
Cartea se adreseazã în acelaºi timp începãtorilor ºi programatorilor profesioniºti. Începãtorii vor gãsi în
prima parte noþiuni fundamentale necesare oricãrui programator. Profesioniºtii, în schimb, vor gãsi o
referinþã completã a limbajului Java.
Prima parte a cãrþii îºi introduce cititorul pas cu pas în fundamentele funcþionãrii ºi programãrii
calculatoarelor tratând la un nivel accesibil noþiuni precum memoria ºi procesorul, datele ºi
instrucþiunile, clasele de obiecte împreunã cu trãsãturile fundamentale ale programãrii orientate obiect.
Partea a doua a cãrþii prezintã limbajul Java în detaliu împreunã cu o serie de exemple simple. Aceastã
parte este conceputã pentru a fi în acelaºi timp o introducere în sintaxa Java pentru cei care au programat
deja ºi o introducere în constrângerile sintactice ºi semantica unui limbaj de programare pentru începãtori.
Partea a treia a cãrþii prezintã câteva dintre aspectele fundamentale ale programãrii în Java precum
aplicaþiile, apleturile, pachetele de clase, firele de execuþie ºi tratarea excepþiilor. Aceastã parte este
conceputã de sine stãtãtoare ºi poate servi ca exemplu de programare în Java. Fiecare secþiune din
aceastã parte conþine exemple extinse de aplicaþii scrise în Java, comentate în sursã ºi în afara acesteia.
Cititorul începãtor în ale programãrii trebuie sã parcurgã cartea de la început pânã la sfârºit pentru a
putea intra treptat în tainele programãrii în general ºi ale limbajului Java.
Pentru cititorii avansaþi, prima parte nu constituie un interes major ºi poate fi sãritã fãrã implicaþii
majore în înþelegerea materialului din pãrþile urmãtoare. Aceºti cititori pot sã treacã direct la partea a
treia pentru a urmãri exemplele ºi abia dupã aceea sã revinã la partea a doua pentru informaþii detaliate
asupra sintaxei ºi facilitãþilor Java. Partea a doua poate fi folositã ºi pe post de referinþã.
O singurã excepþie: ultima secþiune din prima parte introduce noþiunea de interfaþã, puþin cunoscutã
programatorilor în alte limbaje decât Java. Este util ca aceastã secþiune sã fie consultatã de cãtre toþi
cititorii, indiferent de nivelul de pregãtire în care se aflã.
● Cuvintele rezervate ale limbajului Java sunt scrise în caractere îngroºate, ca: while, do, final.
● Cuvintele care þin locul construcþiilor reale ale programului sunt scrise cu caractere oblice, ca în:
if( i < j )
min = i;
else
min = j;
● O listã de termeni separaþi prin caractere |, se poate înlocui cu oricare dintre termenii listei.
De exemplu:
înseamnã cã în locul acestui termen poate sã aparã oricare dintre cuvintele rezervate specificate în
listã, public, private sau protected.
● Dacã, dupã un termen apare caracterul *, acesta reprezintã faptul cã termenul se poate repeta de
un numãr arbitrar de ori, eventual niciodatã. De exemplu, în:
Semnul din stânga reprezintã faptul cã în paragraful marcat introduce definiþia unui nou termen.
Termenul nou introdus este reprezentat în interiorul semnului ºi este scris cu caractere oblice în textul
paragrafului. În cazul de faþã cuvântul nou introdus este cuvântul definiþia. Atunci când gãsiþi o
trimitere la un paragraf pentru explicaþii, cãutaþi acest semn pe margine pentru a gãsi mai repede
definiþia noþiunii dorite.
Semnul din stânga reprezintã o trimitere înapoi cãtre o noþiune deja prezentatã în partea de fundamente.
Aceastã trimitere conþine deasupra simbolului grafic numele noþiunii pe care trebuie sã îl cãutaþi într-un
simbol de definiþie, ºi sub simbolul grafic numãrul paragrafului unde se aflã definiþia. Cititorul
începãtor ca ºi cel avansat poate urma aceste trimiteri pentru a-ºi împrospãta cunoºtinþele sau pentru a se
familiariza cu cuvintele sau expresiile folosite pentru a desemna noþiuni greu traductibile.
Semnul din stânga reprezintã o referire în avans a unei noþiuni care va fi introdusã mai târziu. Acest
semn este destul de rar în interiorul cãrþii, dar este totuºi nevoie de el pentru a face referire la anumite
facilitãþi care îºi gãsesc cel mai bine locul în paragraful în care apare semnul. Puteþi sãri fãrã probleme
aceste precizãri la prima citire a capitolului ºi sã le luaþi în seamã doar atunci când folosiþi cartea pe post
de referinþã pentru limbajul Java.
Semnul din stânga reprezintã referirea unei noþiuni care nu este descrisã în carte. Acest semn apare
atunci când se face o referinþã la una din clasele standard ale mediului Java. Puteþi gãsi informaþii
exacte despre clasa respectivã la adresa www.javasoft.com, sau puteþi încãrca din Internet o
documentaþie completã prin ftp de la adresa ftp.javasoft.com, directorul /docs.
Semnul din stânga reprezintã o trimitere cãtre altã carte, în care noþiunile din paragraf sunt prezentate
mai pe larg. Am folosit acest semn doar de douã ori, pentru a trimite cãtre o carte disponibilã la aceeaºi
editurã cu cartea prezentã, despre HTML, scrisã de Dumitru Rãdoiu. O puteþi gãsi pe prima poziþie în
bibliografie.
Sugestii ºi reclamaþii
Posibilele erori care s-au strecurat în aceastã carte cad în sarcina exclusivã a autorului ei care îºi cere pe
aceastã cale scuze în avans. Orice astfel de eroare sau neclaritate cu privire la conþinutul cãrþii poate fi
comunicatã direct autorului pe adresa [email protected] sau:
Eugen Rotariu
Computer Press Agora
Str. Tudor Vladimirescu, Nr. 63/1, Cod. 4300, Târgu Mureº
România
În Internet, puteþi sã contactaþi editura Computer Press Agora la adresa www.agora.ro. Pe server existã
o paginã separatã dedicatã acestei cãrþi. Puteþi verifica periodic aceastã paginã pentru eventuale
corecturi, exemple suplimentare sau adãugiri la conþinutul cãrþii.
Pânã atunci însã, puteþi consulta cãrþile listate în secþiunea de bibliografie de la sfârºitul acestei cãrþi.
Numai cãrþi în limba englezã în acest domeniu deocamdatã, dar mã aºtept ca situaþia sã se schimbe
dramatic în perioada imediat urmãtoare. Noi cãrþi de Java dedicate cititorilor români nu vor întârzia sã
aparã.
Internet-ul este de asemenea o sursã interminabilã de informaþii, situri Java existã ºi vor continua sã
aparã în întreaga lume. Adresa de bazã este probabil www.javasoft.com, adresã care vã pune în legãturã
directã cu firma care a creat ºi întreþine în continuare dezvoltarea Java. În plus, puteþi consulta revista
electronicã JavaWorld de la adresa www.javaworld.com care conþine întotdeauna informaþii fierbinþi,
cursuri, apleturi ºi legãturi cãtre alte adrese unde vã puteþi îmbogãþi cunoºtinþele despre Java. Pentru
documentaþii, exemple ºi noutãþi în lumea Java puteþi consulta ºi www.gamelan.com sau www.
blackdown.org.
Pentru cei care doresc sã dezvolte apleturi pe care sã le insereze în propriile pagini HTML, recomand în
plus citirea cãrþii lui Dumitru Rãdoiu, HTML - Publicaþii Web, editatã de asemenea la Computer Press
Agora.
În fine, nu încetaþi sã cumpãraþi revistele PC Report ºi Byte România ale aceleiaºi edituri, pentru cã ele
vor conþine ca de obicei informaþii de ultimã orã, cursuri ºi reportaje de la cele mai noi evenimente din
lumea calculatoarelor.
Mulþumiri
Mulþumesc tuturor celor care, voit sau nu, au fãcut posibilã aceastã carte. Mulþumesc celor de la editura
Computer Press Agora ºi managerului ei Romulus Maier pentru cã mi-au facilitat publicarea acestei
cãrþi. Fãrã munca lor, cartea s-ar fi aflat în continuare în vitrina proprie cu visuri nerealizate. Mulþumesc
celor care mi-au dãruit o parte din timpul lor preþios pentru a citi ºi comenta primele versiuni ale cãrþii:
Iosif Fettich, Mircea ºi Monica Cioatã, Alexandru Horvath. Mulþumesc celor cu care am discutat de
atâtea ori despre soarta calculatoarelor, programelor, României ºi lumii în general. Ei au fãcut din mine
omul care sunt acum: Mircea Sârbu, Dumitru Rãdoiu, Szabo Laszlo, Mircea Pantea. Mulþumesc celor
care s-au ocupat de designul ºi tehnoredactarea acestei cãrþi în frunte cu Adrian Pop ºi Octav Lipovan.
Cel dintâi lucru care v-a atras la aceastã carte este munca lor. Carmen, îþi mulþumesc cã nu te-ai dat
bãtutã. Va veni ºi vremea în care sãrbãtorim cu calculatoarele oprite.
[cuprins]
Capitolul I
Arhitectura calculatoarelor
1.1 Modelul Von Neumann de arhitecturã a calculatoarelor
1.2 Organizarea memoriei interne
1.3 Reprezentarea informaþiilor în memoria internã
1.4 Modelul funcþional al calculatoarelor
Modelul Von Neumann defineºte calculatorul ca pe un ansamblu format dintr-o unitate centralã ºi o
memorie internã. Unitatea centralã sau procesorul este responsabilã cu administrarea ºi prelucrarea
informaþiilor în timp ce memoria internã serveºte la depozitarea acestora. În terminologia calculatoarelor,
depozitarea informaþiilor în memoria internã a calculatorului se numeºte memorare.
Acest ansamblu unitate centralã plus memorie internã comunicã cu exteriorul prin intermediul unor
dispozitive periferice. Dispozitivele periferice pot fi de intrare sau de ieºire în funcþie de direcþia în care
se miºcã datele. Dacã datele sunt furnizate de dispozitivul periferic ºi transferate spre unitatea centralã,
atunci dispozitivul este de intrare precum sunt tastatura sau mausul. Dacã datele sunt generate de unitatea
centralã ºi transmise spre dispozitivul periferic atunci dispozitivul este de ieºire precum sunt ecranul sau
imprimanta. Existã ºi dispozitive mixte de intrare/ieºire precum sunt discurile pentru memorarea externã a
informaþiilor.
Tastatura calculatorului reprezintã un set de butoane (taste) inscripþionate care ne permite transmiterea
cãtre unitatea centralã a unor litere, cifre, semne de punctuaþie, simboluri grafice sau comenzi
funcþionale. Mausul reprezintã un dispozitiv simplu, mobil, care ne permite indicarea unor regiuni ale
ecranului cu ajutorul unui cursor.
În plus, mausul ne permite activarea regiunilor respective cu ajutorul celor 1-2-3 butoane ale sale.
Ecranul este un dispozitiv cu ajutorul cãruia calculatorul comunicã informaþii spre exterior. Aceste
informaþii apar sub formã de litere, cifre, semne de punctuaþie, simboluri grafice sau desene oarecare într-
o varietate mai mare sau mai micã de culori. Informaþia de pe ecran se pierde odatã cu redesenarea
acestuia. Pentru transferul acestor informaþii pe hârtie ºi îndosarierea lor s-au creat alte dispozitive
periferice, numite imprimante.
Memoria internã pierde informaþiile odatã cu oprirea alimentãrii calculatorului. Pentru a salva
informaþiile utile, precum ºi programele de prelucrare ale acestora este nevoie de dispozitive de memorare
permanente. Din aceastã categorie fac parte discurile calculatorului. Existã mai multe modele de discuri
precum discurile fixe, discurile flexibile sau compact-discurile, fiecare dintre acestea având caracteristici,
viteze de acces ºi capacitãþi de memorare diferite. Informaþiile salvate pe discuri pot fi încãrcate din nou
în memoria internã la o pornire ulterioarã a calculatorului. Vã veþi întreba desigur de ce este nevoie de
douã tipuri distincte de memorie: pentru cã discurile au viteze de acces mult prea mici pentru a putea fi
folosite direct de cãtre unitatea centralã.
Deºi modelul constructiv de bazã al calculatoarelor nu a evoluat prea mult, componenta tehnologicã a
acestora s-a aflat într-o permanentã evoluþie. Transformarea vizeazã la ora actualã viteza de lucru ºi setul
de instrucþiuni ale unitãþii centrale, capacitatea ºi viteza de stocare a memoriei interne precum ºi tipurile
ºi calitatea dispozitivelor periferice.
sã reprezinþi un principiu binar ca absenþa/prezenþa sau plus/minus decât unul nuanþat. O cifrã binarã
este numitã, în termeni de calculatoare, bit.
Biþii sunt grupaþi, opt câte opt, în unitãþi de memorare numite octeþi.
Iarãºi, alegerea cifrei opt are raþiuni istorice: era nevoie de o putere a lui doi care sã fie cât mai mare,
pentru a putea permite transferuri rapide între diversele componente ale calculatorului (unitate centralã,
memorie, dispozitive periferice), dar totodatã suficient de micã pentru ca realizarea dispozitivelor
implicate, cu tehnologia existentã, sã fie posibilã. Cifra opt avea în plus avantajul cã permitea
reprezentarea tuturor caracterelor tipãribile necesare la ora respectivã precum: literele, cifrele sau semnele
de punctuaþie. Într-un octet se pot reprezenta pânã la 256 (28) astfel de caractere. În prezent octetul este
depãºit datoritã necesitãþii de reprezentare a caracterelor tuturor limbilor scrise din lume.
Pentru a accesa o informaþie în memorie este nevoie de un mod de a referi poziþia acesteia. Din acest
motiv, octeþii memoriei au fost numerotaþi unul câte unul începând de la 0 pânã la numãrul maxim de
octeþi în memorie. Numãrul de ordine al unui octet îl vom numi pentru moment adresã. Noþiunea de
adresã ºi-a extins semnificaþia în ultimul timp dar, pentru înþelegerea acestui capitol, explicaþia de mai
sus este suficientã.
Zona fizicã de memorie rezervatã unei anumite informaþii se numeºte locaþia informaþiei respective în
memorie. În unele dintre locaþiile din memorie putem pãstra chiar adresa unor alte locaþii din memorie.
Informaþia memoratã în aceste locaþii se numeºte referinþã. Cu alte cuvinte, o referinþã este o informaþie
memoratã într-o locaþie de memorie care ne trimite spre (se referã la) o altã locaþie de memorie. O
locaþie de memorie se poate întinde pe mai mult decât un octet.
cu numãrul 1, culoarea albastrã cu numãrul 2 ºi aºa mai departe. Ori de câte ori vom memora 2 vom
memora albastru ºi ori de câte ori vom dori sã memorãm roºu, vom memora 1.
O cale similarã de rezolvare vom întâlni ºi la reprezentarea caracterelor. Caracterele sunt denumirea într-
un singur cuvânt a literelor, cifrelor, semnelor de punctuaþie sau simbolurilor grafice reprezentate în
memorie. Este nevoie de o convenþie prin care ataºãm fiecãrui caracter câte un numãr natural memorabil
într-un octet.
În cazul reprezentãrii caracterelor, existã chiar un standard internaþional care defineºte numerele,
reprezentabile pe un octet, corespunzãtoare fiecãrui caracter în parte, numit standardul ASCII. Alte
standarde, cum ar fi standardul Unicode, reprezintã caracterele pe doi octeþi, ceea ce le dã posibilitatea sã
ia în considerare o gamã mult mai largã de caractere.
Reprezentare pe un octet
Caracter
(ASCII)
A-Z 65-90
a-z 97-122
0-9 48-57
Ã,ã 195,227
Î,î 206,238
Â,â 194,226
ª,º 170,186
Þ,þ 222,254
Tabelul 1.1 Un fragment din codurile ASCII ºi Unicode de reprezentare a caracterelor grafice în
memoria calculatoarelor.
Desigur, este greu sã þinem minte codul numeric asociat fiecãrui caracter sau fiecãrei culori. Este nevoie
de paºi suplimentari de codificare, care sã punã informaþia în legãturã cu simboluri mai uºor de þinut
minte decât numerele. De exemplu, este mult mai uºor pentru noi sã þinem minte cuvinte sau imagini. Dar
sã nu uitãm niciodatã cã, pentru calculator, cel mai uºor este sã memoreze ºi sã lucreze cu numere.
Fiecare calculator defineºte un numãr de operaþii care pot fi executate de unitatea sa centralã. Aceste
operaþii sunt în principal destinate memorãrii sau recuperãrii informaþiilor din memoria internã,
calculelor aritmetice sau logice ºi controlului dispozitivelor periferice. În plus, existã un numãr de
instrucþiuni pentru controlul ordinii în care sunt executate operaþiile.
O instrucþiune este o operaþie elementarã executabilã de cãtre unitatea centralã a unui calculator. O
secvenþã de mai multe instrucþiuni executate una dupã cealaltã o vom numi program.
Instrucþiunile care compun un program trebuiesc ºi ele reprezentate în memorie, la fel ca orice altã
informaþie, din cauza faptului cã unitatea centralã nu are posibilitatea sã-ºi pãstreze programele în
interior. Pentru memorarea acestor instrucþiuni este nevoie de o nouã convenþie de reprezentare care sã
asocieze un numãr sau o secvenþã de numere naturale fiecãrei instrucþiuni a unitãþii centrale.
Execuþia unui program de cãtre calculator presupune încãrcarea instrucþiunilor în memoria internã ºi
execuþia acestora una câte una în unitatea centralã. Unitatea centralã citeºte din memorie câte o
instrucþiune, o executã, dupã care trece la urmãtoarea instrucþiune. Pentru pãstrarea secvenþei, unitatea
centralã memoreazã în permanenþã o referinþã cãtre urmãtoarea instrucþiune într-o locaþie internã numitã
indicator de instrucþiuni.
Modelul de execuþie liniarã a instrucþiunilor, în ordinea în care acestea sunt aºezate în memorie, este
departe de a fi acceptabil. Pentru a fi util, un program trebuie sã poatã sã ia decizii de schimbare a
instrucþiunii urmãtoare în funcþie de informaþiile pe care le prelucreazã. Aceste decizii pot însemna
uneori comutarea execuþiei de la o secvenþã de instrucþiuni la alta. Alteori, este necesar sã putem executa
o secvenþã de instrucþiuni în mod repetat pânã când este îndeplinitã o anumitã condiþie exprimabilã cu
ajutorul informaþiilor din memorie. Numãrul de repetãri ale secvenþei de instrucþiuni nu poate fi hotãrât
decât în momentul execuþiei. Aceste ramificãri ale execuþiei se pot simula destul de uºor prin schimbarea
valorii referinþei memorate în indicatorul de instrucþiuni.
[cuprins]
Capitolul II
Limbaje de programare
2.1 Comunicaþia om-maºinã
2.2 Tipuri de numere reprezentabile în calculator
2.3 Valori de adevãr
2.4 ªiruri de caractere
2.5 Tipuri primitive de valori ale unui limbaj de programare
2.6 Tablouri de elemente
2.7 Expresii de calcul
2.8 Variabile
2.9 Instrucþiuni
O alternativã mai bunã este folosirea unui limbaj de comunicaþie format dintr-un numãr foarte mic de
cuvinte ºi caractere speciale împreunã cu un set de convenþii care sã ajute la descrierea numerelor ºi a
operaþiilor care trebuiesc executate cu aceste numere. Limbajul trebuie sã fie atât de simplu încât
calculatorul sã poatã traduce singur, prin intermediul unui program numit compilator, frazele acestui
limbaj în instrucþiuni ale unitãþii centrale. Ori de câte ori vom imagina un nou limbaj de comunicaþie cu
calculatorul va trebui sã creãm un nou program compilator care sã traducã acest limbaj în instrucþiuni
ale unitãþii centrale, numite uneori ºi instrucþiuni maºinã.
În realitate folosirea termenului de limbaj de comunicaþie nu este extrem de fericitã pentru cã de obicei
noi doar instruim calculatorul ce are de fãcut ºi cum trebuie sã facã acel lucru, fãrã sã-i dãm vreo ºansã
acestuia sã comenteze sarcinile primite. În continuare vom numi aceste limbaje simplificate limbaje de
programare pentru a le deosebi de limbajele pe care le folosim pentru a comunica cu alþi oameni ºi pe
care le vom numi limbaje naturale.
execuþiei unei comenzi dintr-un limbaj de programare trebuie sã fie complet determinat. Asta înseamnã
cã în limbajele de programare nu este permisã nici o formã de ambiguitate a exprimãrii.
Informaþiile reprezentate în memoria calculatorului ºi prelucrate de cãtre un program scris într-un limbaj
de programare se numesc date. Tipurile de date care se pot descrie direct cu un anumit limbaj de
programare se numesc tipurile de date elementare ale limbajului. Operaþiile elementare posibil de
exprimat într-un limbaj de programare se numesc instrucþiuni ale limbajului.
Un alt aspect în care se reflectã evoluþia limbajelor de programare este reprezentat de creºterea
profunzimii ºi calitãþii controlului pe care compilatoarele îl fac în momentul traducerii din limbaj de
programare în limbaj maºinã, astfel încât ºansa de a greºi la descrierea programului sã fie cât mai micã.
Cu toate cã limbajele de programare au fãcut paºi esenþiali în ultimul timp, ele au rãmas totuºi foarte
departe de limbajele naturale. Pentru a programa un calculator, trebuie sã poþi gândi ºi sã poþi descrie
problemele într-unul dintre limbajele pe care acesta le înþelege. Din acest motiv, scrierea programelor
continuã sã rãmânã o activitate rezervatã unui grup de iniþiaþi, chiar dacã acest grup este în continuã
creºtere.
Numerele naturale sunt întotdeauna pozitive. Pentru reprezentarea unui numãr întreg cu semn pot fi
folosite douã numere naturale. Primul dintre acestea, având doar douã valori posibile, reprezintã semnul
numãrului ºi se poate reprezenta folosind o singurã cifrã binarã. Dacã valoarea acestei cifre binare este 0,
numãrul final este pozitiv, iar dacã valoarea cifrei este 1, numãrul final este negativ. Al doilea numãr
natural folosit în reprezentarea numerelor întregi cu semn conþine valoarea absolutã a numãrului final.
Aceastã convenþie, deºi are dezavantajul cã oferã douã reprezentãri pentru numãrul zero, un zero pozitiv
ºi altul negativ, este foarte aproape de reprezentarea numerelor cu semn folositã de calculatoarele
moderne.
În realitate, convenþia care se foloseºte pentru reprezentarea numerelor întregi cu semn este aºa numita
reprezentare în complement faþã de doi. Aceasta reprezintã numerele negative prin complementarea
valorii lor absolute bit cu bit ºi apoi adunarea valorii 1 la numãrul rezultat. Complementarea valorii unui
bit se face înlocuind valoarea 1 cu 0 ºi valoarea 0 cu 1. Dacã avem de exemplu numãrul 1, reprezentat pe
un octet ca un ºir de opt cifre binare 00000001, complementarea bitcu bit a acestui numãr este numãrul
reprezentat pe un octet prin11111110.
Pentru a reprezenta valoarea -1 nu ne mai rãmâne altceva de fãcut decât sã adunãm la numãrul rezultat în
urma complementãrii un 1 ºi reprezentarea finalã a numãrului întreg negativ -1 pe un octet este
11111111.
În aceastã reprezentare, numãrul 00000000 binar reprezintã numãrul 0 iar numãrul 10000000, care mai
înainte reprezenta numãrul 0 negativ, acum reprezintã numãrul -128. Într-adevãr, numãrul 128 se poate
reprezenta în binar prin 10000000. Complementat, acest numãr devine 01111111 ºi dupã adunarea cu 1,
10000000. Observaþi cã numerele 128 ºi -128 au aceeaºi reprezentare. Convenþia este aceea cã se
pãstreazã reprezentarea pentru -128 ºi se eliminã cea pentru 128. Alegerea este datoratã faptului cã toate
numerele pozitive au în primul bit valoarea 0 ºi toate cele negative valoarea -1. Prin transformarea lui
10000000 în -128 se pãstreazã aceastã regulã.
Folosind reprezentarea în complement faþã de doi, numerele întregi reprezentabile pe un octet sunt în
intervalul -128 pânã la 127, adicã -27 pânã la 27 -1. În general, dacã avem o configuraþie de n cifre
binare, folosind reprezentarea în complement faþã de doi putem reprezenta numerele întregi din
intervalul închis -2n-1 pânã la 2n-1-1. În practicã, se folosesc numere întregi cu semn reprezentate pe 1
octet, 2 octeþi, 4 octeþi ºi 8 octeþi, respectiv 8, 16, 32 ºi 64 de biþi.
Dacã dorim sã reprezentãm un numãr real în memoria calculatorului, o putem face memorând câteva
cifre semnificative ale acestuia plus o informaþie legatã de ordinul sãu de mãrime. În acest fel, deºi
pierdem precizia numãrului, putem reprezenta valori foarte aproape de zero sau foarte departe de aceastã
valoare.
Soluþia de reprezentare este aceea de a pãstra douã numere cu semn care reprezintã cifrele semnificative
ale numãrului real respectiv un exponent care dã ordinul de mãrime. Cifrele reprezentative ale numãrului
se numesc împreunã mantisã. Numãrul reprezentat în final este 0.mantisaEexponent. E are valoarea 256
spre deosebire de exponentul 10 pe care îl folosim în uzual. Dacã valoarea exponentului estefoarte mare
ºi pozitivã, numãrul real reprezentat este foarte departe de 0, înspre plus sau înspre minus. Dacã
exponentul este foarte mare în valoare absolutãºi negativ, numãrul real reprezentat este foarte aproape de
zero.
Aceastã mutare a virgulei imediat în faþa cifrelor semnificative poate sã presupunã modificarea
exponentului care pãstreazã ordinul de mãrime al numãrului. Numerele fracþionare se numesc în
limbajul calculatoarelor numere în virgulã mobilã sau numere flotante tocmai din cauza acestei
eventuale ajustãri a poziþiei virgulei.
Cu aceastã convenþie nu se poate reprezenta orice numãr real, dar se poate obþine o acoperire destul de
bunã a unui interval al axei numerelor reale cu valori. Atunci când încercãm sã reprezentãm în memoria
calculatorului un numãr real, cãutãm de fapt cel mai apropiat numãr real reprezentabil în calculator ºi
aproximãm numãrul iniþial cu acesta din urmã. Ca rezultat, putem efectua calcule complexe cu o
precizie rezonabilã.
Descrierea convenþiei exacte de reprezentare a numerelor reale în calculator depãºeºte cadrul acestei
prezentãri. Unele dintre detalii pot fi diferite de cele prezentate aici, dar principiul este exact acesta.
Convenþia de reprezentare a numerelor reale este standardizatã de IEEE în specificaþia 754.
Desigur, în unele probleme, lipsa de precizie poate sã altereze rezultatul final, mai ales cã, uneori, erorile
se cumuleazã. Existã de altfel o teorie complexã, analiza numericã, conceputã pentru a studia cãile prin
care putem þine sub control aceste erori de precizie. Putem creºte precizia de reprezentare a numerelor
reale prin mãrirea spaþiului rezervat mantisei. În acest fel mãrim numãrul de cifre semnificative pe care
îl pãstrãm. În general, unitãþile centrale actuale lucreazã cu douã precizii: numerele flotante simple,
reprezentate pe 4 octeþi ºi numerele flotante duble, reprezentate pe 8 octeþi.
În ceea ce priveºte cuvintele, ele pot fi reprezentate în memoria calculatorului prin caracterele care le
formeazã. Înºiruirea acestor caractere în memorie duce la noþiunea de ºir de caractere. Pe lângã
caracterele propriu-zise care construiesc cuvântul, un ºir de caractere trebuie sã poatã memora ºi numãrul
total de caractere din ºir, cu alte cuvinte lungimea sa.
În realitate, un ºir de caractere nu conþine doar un singur cuvânt ci este o înºiruire oarecare de caractere
printre care pot exista ºi caractere spaþiu. De exemplu, urmãtoarele secvenþe sunt ºiruri de caractere:
Convenþia de reprezentare în memorie diferã de la un limbaj de programare la altul prin modul în care
este memoratã lungimea ºirului, precum ºi prin convenþia de reprezentare în memorie a caracterelor:
ASCII, Unicode sau alta.
Împreunã cu fiecare tip de datã, limbajele de programare trebuie sã defineascã ºi operaþiile ce se pot
executa cu datele de tipul respectiv. Pentru ºirurile de caractere, principala operaþie este concatenarea.
Prin concatenarea a douã ºiruri de caractere se obþine un ºir de caractere care conþine caracterele celor
douã ºiruri puse în prelungire. De exemplu, prin concatenarea ºirurilor de caractere "unu" ºi ", doi",
rezultã ºirul de caractere: "unu, doi".
Pentru fiecare dintre aceste tipuri primitive de valori, limbajul trebuie sã defineascã dimensiunea locaþiei
de memorie ocupate, convenþia de reprezentare ºi mulþimea valorilor care pot fi reprezentate în aceastã
locaþie. În plus, limbajul trebuie sã defineascã operaþiile care se pot executa cu aceste tipuri ºi
comportarea acestor operaþii pe seturi de valori diferite.
Tipurile primitive ale unui limbaj sunt de obicei: numere de diverse tipuri, caractere, ºiruri de caractere,
valori de adevãr ºi valori de tip referinþã. Totuºi, acest set de tipuri primitive, denumirea exactã a
tipurilor ºi operaþiile care se pot executa cu ele variazã mult de la un limbaj de programare la altul.
În cazul în care dorim sã reprezentãm o structurã conþinând date de acelaºi tip, va trebui sã folosim o
structurã clasicã de reprezentare a datelor numitã tablou de elemente. Practic, un tablou de elemente este
o alãturare de locaþii de memorie de acelaºi fel. Alãturarea este continuã în sensul cã zona de memorie
alocatã tabloului nu are gãuri. De exemplu, putem gândi întreaga memorie internã a calculatorului ca
fiind un tablou de cifre binare sau ca un tablou de octeþi.
Informaþiile de acelaºi fel reprezentate într-un tablou se pot prelucra în mod unitar. Referirea la un
element din tablou se face prin precizarea locaþiei de început a tabloului în memorie ºi a numãrului de
ordine al elementului pe care dorim sã-l referim. Numãrul de ordine al unui element se numeºte indexul
elementului. De aceea, tablourile se numesc uneori ºi structuri de date indexate.
Sã presupunem cã, la adresa 1234 în memorie, deci începând cu octetul numãrul 1234, avem un tablou
de 200 de elemente de tip întregi cu semn reprezentaþi pe 4 octeþi. Pentru a accesa elementul numãrul
123 din tablou, trebuie sã îi aflãm adresa în memorie. Pentru aceasta, trebuie sã calculãm deplasamentul
acestui element faþã de începutul tabloului, cu alte cuvinte, la câþi octeþi distanþã faþã de începutul
tabloului se gãseºte elementul numãrul 123.
Pentru calcului deplasamentului, este nevoie sã ºtim cu ce index începe numerotarea elementelor din
tablou. De obicei aceasta începe cu 0 sau cu 1, primul element fiind elementul 0 sau elementul 1. Sã
presupunem, în cazul nostru, cã numerotarea ar începe cu 0. În acest caz, elementul cu indexul 123 este
de fapt al 124-lea element din tablou, deci mai are încã 123 de elemente înaintea lui. În aceste condiþii,
pentru a afla adresa elementului dat, este suficient sã adãugãm la adresa de început a tabloului,
deplasamentul calculat ca numãrul de elemente din faþã înmulþit cu dimensiunea unui element din
tablou. Adresa finalã este 1234 + 123 * 4 = 1726.
Pentru a putea lucra cu elementele unui tablou este deci suficient sã memorãm adresa de început a
tabloului ºi indexul elementelor pe care dorim sã le accesãm. Alternativa ar fi fost sã memorãm adresa
locaþiei fiecãrui element din tablou.
Unul dintre marile avantaje ale utilizãrii tablourilor este acela cã elementele dintr-un tablou se pot
prelucra în mod repetitiv, apelându-se aceeaºi operaþie pentru un subset al elementelor din tablou. Astfel,
într-un program putem formula instrucþiuni de forma: pentru elementele tabloului T începând de la al
treilea pânã la al N-lea, sã se execute operaþia O. Numãrul N poate fi calculat dinamic în timpul
execuþiei programului, în funcþie de necesitãþi.
Elementele unui tablou pot fi de tip primitiv, referinþã sau pot fi tipuri compuse, inclusiv alte tablouri.
Expresiile sunt formate dintr-o serie de valori care intrã în calcul, numite operanzi ºi din simboluri care
specificã operaþiile care trebuiesc efectuate cu aceste valori, numite operatori. Operatorii reprezintã
operaþii de adunare, înmulþire, împãrþire, concatenare a ºirurilor de caractere, etc.
Operanzii unor expresii pot fi valori elementare precum numerele, ºirurile de caractere sau pot fi referiri
cãtre locaþii de memorie în care sunt memorate aceste valori. Tot operanzi pot fi ºi valorile unor funcþii
predefinite precum sinus, cosinus sau valoarea absolutã a unui numãr. Calculul complex al valorii
acestor funcþii pentru argumentele de intrare este astfel ascuns sub un nume uºor de recunoscut.
2.8 Variabile
Uneori, atunci când calculele sunt complexe, avem nevoie sã pãstrãm rezultate parþiale ale acestor
calcule în locaþii temporare de memorie. Alteori, chiar datele iniþiale ale problemei trebuie memorate
pentru o folosire ulterioarã. Aceste locaþii de memorie folosite ca depozit de valori le vom numi
variabile. Variabilele pot juca rol de operanzi în expresii.
Pentru a regãsi valoarea memoratã într-o anumitã variabilã, este suficient sã memorãm poziþia locaþiei
variabilei în memorie ºi tipul de datã memoratã la aceastã locaþie, numit ºi tipul variabilei. Cunoaºterea
tipului variabilei este esenþialã la memorarea ºi regãsirea datelor. La locaþia variabilei gãsim întotdeauna
o configuraþie de cifre binare. Interpretarea acestor cifre binare se poate face numai cunoscând
convenþia de reprezentare care s-a folosit la memorarea acelor cifre binare. Mai mult, tipul de variabilã
ne ºi spune câþi octeþi de memorie ocupã locaþia respectivã.
Pentru a putea referi variabilele în interiorul unui program, trebuie sã atribuim câte un nume pentru
fiecare dintre acestea ºi sã rezervãm locaþiile de memorie destinate lor. Aceastã rezervare a locaþiilor se
poate face fie la pornirea programului fie pe parcurs.
Putem împãrþi variabilele în funcþie de perioada lor de existenþã în variabile statice, care existã pe tot
parcursul programului ºi variabile locale care se creeazã doar în secþiunea de program în care este
nevoie de ele pentru a fi distruse imediat ce se pãrãseºte secþiunea respectivã. Variabilele statice
pãstreazã de obicei informaþii esenþiale pentru execuþia programului precum numele celui care a pornit
programul, data de pornire, ora de pornire sau numãrul p . Variabilele locale pãstreazã valori care au sens
doar în contextul unei anumite secþiuni din program. De exemplu, variabilele care sunt utilizate la
calculul sinusului dintr-un numãr, sunt inutile ºi trebuiesc eliberate imediat ce calculul a fost terminat. În
continuare, doar valoarea finalã a sinusului este importantã.
În plus, existã variabile care se creeazã doar la cererea explicitã a programului ºi nu sunt eliberate decât
atunci când programul nu mai are nevoie de ele. Aceste variabile se numesc variabile dinamice. De
exemplu, se creeazã o variabilã dinamicã atunci când utilizatorul programului introduce un nou nume
într-o listã de persoane. Crearea ºi ºtergerea acestui nume nu are legãturã cu faza în care se aflã rularea
programului ci are legãturã cu dorinþa celui care utilizeazã programul de a mai adãuga sau ºterge un
nume în lista persoanelor cu care lucreazã, pentru a le trimite, de exemplu, felicitãri de anul nou.
2.9 Instrucþiuni
Operaþiile care trebuiesc executate de un anumit program sunt exprimate în limbajele de programare cu
ajutorul unor instrucþiuni. Spre deosebire de limbajele naturale, existã doar câteva tipuri standard de
instrucþiuni care pot fi combinate între ele pentru a obþine funcþionalitatea programelor. Sintaxa de
scriere a acestor tipuri de instrucþiuni este foarte rigidã. Motivul acestei rigiditãþi este faptul cã un
compilator recunoaºte un anumit tip de instrucþiune dupã sintaxa ei ºi nu dupã sensul pe care îl are ca în
cazul limbajelor naturale.
Tipurile elementare de instrucþiuni nu s-au schimbat de-a lungul timpului pentru cã ele sunt conþinute în
însuºi modelul de funcþionare al calculatoarelor Von Neumann.
În primul rând, avem instrucþiuni de atribuire. Aceste instrucþiuni presupun existenþa unei locaþii de
memorie cãreia dorim sã-i atribuim o anumitã valoare. Atribuirea poate sã parã o operaþie foarte simplã
dar realitatea este cu totul alta. În primul rând, valoarea respectivã poate fi rezultatul evaluãrii
(calculului) unei expresii complicate care trebuie executatã înainte de a fi memoratã în locaþia de
memorie doritã.
Apoi, însãºi poziþia locaþiei în memorie ar putea fi rezultatul unui calcul în urma cãruia sã rezulte o
anumitã adresã. De exemplu, am putea dori sã atribuim o valoare unui element din interiorul unui tablou.
Într-o astfel de situaþie, locaþia de memorie este calculatã prin adunarea unui deplasament la adresa de
început a tabloului.
În fine, locaþia de memorie ºi valoarea pe care dorim sã i-o atribuim ar putea avea tipuri diferite. În acest
caz, operaþia de atribuire presupune ºi transformarea, dacã este posibilã, a valorii într-o nouã valoare de
acelaºi tip cu locaþia de memorie. În plus, aceastã transformare trebuie efectuatã în aºa fel încât sã nu se
piardã semnificaþia valorii originale. De exemplu, dacã valoarea originalã era -1 întreg iar locaþia de
memorie este de tip flotant, valoarea obþinutã dupã transformare trebuie sã fie -1.0.
Transformarea valorilor de la un tip la altul se numeºte în termeni informatici conversie. Conversiile pot
apãrea ºi în alte situaþii decât instrucþiunile de atribuire, de exemplu la transmiterea parametrilor unei
funcþii, în calculul unei expresii.
Locaþie = Valoare
Locaþia poate fi o adresã cu tip, un nume de variabilã sau o expresie în urma cãreia sã rezulte o adresã cu
tip. Printr-o adresã cu tip înþelegem o adresã de memorie pentru care a fost specificat tipul valorilor care
pot fi memorate în interior. O adresã simplã, fãrã tip, nu poate intra într-o atribuire pentru cã nu putem ºti
care este convenþia de reprezentare care trebuie folositã la memorarea valorii.
Al doilea tip de instrucþiune elementarã este instrucþiunea condiþionalã. Aceastã instrucþiune permite o
primã formã de ramificare a ordinii în care se executã instrucþiunile. Ramificarea depinde de o condiþie
care poate fi testatã pe setul de date cu care lucreazã aplicaþia. În funcþie de valoarea de adevãr care
rezultã în urma evaluãrii condiþiei se poate executa o instrucþiune sau alta. Modelul de exprimare a unei
expresii condiþionale este urmãtorul:
executã instrucþiunea 1
altfel
executã instrucþiunea 2.
Cele douã ramuri de execuþie sunt disjuncte în sensul cã dacã este executatã instrucþiunea de pe o
ramurã atunci cu siguranþã instrucþiunea de pe cealaltã ramurã nu va fi executatã. Dupã execuþia uneia
sau a celeilalte instrucþiuni ramurile se reunificã ºi execuþia îºi continuã drumul cu instrucþiunea care
urmeazã dupã instrucþiunea condiþionalã.
Condiþia care hotãrãºte care din cele douã instrucþiuni va fi executatã este de obicei un test de egalitate
între douã valori sau un test de ordonare în care o valoare este testatã dacã este mai micã sau nu decât o
altã valoare. Condiþiile pot fi compuse prin conectori logici de tipul “ºi”, “sau” sau “non”, rezultând în
final condiþii de forma: dacã variabila numitã Temperaturã este mai micã decât 0 ºi mai mare decât -10,
atunci…
executã instrucþiunea 1
executã instrucþiunea 2
altfel
Un al treilea tip de instrucþiuni elementare sunt instrucþiunile de ciclare sau repetitive sau buclele.
Acestea specificã faptul cã o instrucþiune trebuie executatã în mod repetat. Controlul ciclurilor de
instrucþiuni se poate face pe diverse criterii. De exemplu se poate executa o instrucþiune de un numãr fix
de ori sau se poate executa instrucþiunea pânã când o condiþie devine adevãratã sau falsã.
Condiþia de terminare a buclei poate fi testatã la începutul buclei sau la sfârºitul acesteia. Dacã condiþia
este testatã de fiecare datã înainte de execuþia instrucþiunii, funcþionarea ciclului se poate descrie prin:
executã Instrucþiunea
Acest tip de instrucþiuni de ciclare poatã numele de cicluri while, cuvântul while însemnând în limba
englezã "atâta timp cât”. În cazul ciclurilor while, existã posibilitatea ca instrucþiunea din interiorul
ciclului sã nu se execute niciodatã, dacã condiþia este de la început falsã.
Dacã condiþia este testatã de fiecare datã dupã execuþia instrucþiunii, funcþionarea ciclului se poate
descrie prin:
Executã Instrucþiunea
Acest tip de instrucþiuni de ciclare poartã numele de cicluri do-while, cuvântul do însemnând în limba
englezã "executã”. În cazul ciclurilor do-while instrucþiunea din interiorul ciclului este garantat cã se
executã cel puþin o datã, chiar dacã valoare condiþiei de terminare a buclei este de la început fals.
Dupã cum aþi observat, probabil, dacã instrucþiunea din interiorul buclei nu afecteazã în nici un fel
valoarea de adevãr a condiþiei de terminare a buclei, existã ºansa ca bucla sã nu se termine niciodatã.
Al patrulea tip de instrucþiuni sunt apelurile de proceduri. O procedurã este o secvenþã de instrucþiuni
de sine stãtãtoare ºi parametrizatã. Un apel de procedurã este o instrucþiune prin care forþãm programul
sã execute pe loc instrucþiunile unei anumite proceduri. Dupã execuþia procedurii, se continuã cu
execuþia instrucþiunii de dupã apelul de procedurã.
Procedurile sunt utile atunci când, în zone diferite ale programului, dorim sã executãm aceeaºi secvenþã
de instrucþiuni. În aceste situaþii, putem sã scriem secvenþa de instrucþiuni o singurã datã ºi s-o apelãm
apoi oriunde din interiorul programului.
De exemplu, ori de câte ori vom dori sã ºtergem toate semnele de pe ecranul calculatorului, putem apela
la o procedurã, pentru a nu fi nevoiþi sã scriem de fiecare datã secvenþa de instrucþiuni care duce la
ºtergerea ecranului. Procedura în sine, o vom scrie o singurã datã ºi îi vom da un nume prin care o vom
putea apela ulterior ori de câte ori avem nevoie.
Uneori secvenþa de instrucþiuni dintr-o procedurã depinde de niºte parametri adicã de un set de valori
care sunt precizate doar în momentul apelului procedurii. În aceste cazuri, procedura se poate comporta
în mod diferit în funcþie de valorile de apel ale acestor parametri. În funcþie de tipul declarat al unor
parametri ºi de tipul real al valorilor care sunt trimise ca parametri într-un apel de procedurã, se poate
întâmpla ca înainte de apelul propriu-zis sã fie necesarã o conversie de tip.
De exemplu, în cazul procedurii care calculeazã valoarea sinusului unui numãr, parametrul de intrare
este însuºi numãrul pentru care trebuie calculat sinusul. Rezultatul acestui calcul va fi diferit, în funcþie
de valoarea la apel a parametrului. În mod normal, parametrul va fi un numãr flotant iar dacã la apel vom
transmite ca parametru o valoare întreagã, ea va fi convertitã spre o valoare flotantã.
În urma apelului unei proceduri poate rezulta o valoare pe care o vom numi valoare de retur a
procedurii. De exemplu, în urma apelului procedurii de calcul a sinusului unui numãr, rezultã o valoare
flotantã care este valoarea calculatã a sinusului. Acest tip de proceduri, care întorc valori de retur se mai
numesc ºi funcþii. Funcþiile definesc întotdeauna un tip al valorii de retur pe care o întorc. În acest fel,
apelurile de funcþii pot fi implicate ºi în formarea unor expresii de calcul sub formã de operanzi,
expresia cunoscând tipul de valoare care trebuie aºteptatã ca valoare a apelului unei anumite funcþii.
Analogia cu funcþiile pe care le-aþi studiat la matematicã este evidentã dar nu totalã. În cazul funcþiilor
scrise într-un limbaj de programare, douã apeluri consecutive ale aceleiaºi funcþii cu aceleaºi valori ca
parametri se pot comporta diferit datoritã dependenþei de condiþii exterioare sistemului format din
funcþie ºi parametrii de apel. În plus, se poate întâmpla ca modul de comportare al funcþiei ºi valoarea de
retur sã nu fie definite pentru anumite valori de apel ale parametrilor. Deºi scrierea acestui tip de funcþii
este nerecomandabilã, în practicã întâlnim astfel de funcþii la tot pasul.
[cuprins]
Capitolul III
Reprezentarea informaþiilor cu obiecte
3.1 Obiecte
3.2 Încapsularea informaþiilor în interiorul obiectelor
3.3 Clase de obiecte
3.4 Derivarea claselor de obiecte
3.5 Interfeþe spre obiecte
3.1 Obiecte
Informaþiile pe care le reprezentãm în memoria calculatorului sunt rareori atât de simple precum culorile
sau literele. În general, dorim sã reprezentãm informaþii complexe, care sã descrie obiectele fizice care
ne înconjoarã sau noþiunile cu care operãm zilnic, în interiorul cãrora culoarea sau o secvenþã de litere
reprezintã doar o micã parte. Aceste obiecte fizice sau noþiuni din lumea realã trebuiesc reprezentate în
memoria calculatorului în aºa fel încât informaþiile specifice lor sã fie pãstrate la un loc ºi sã se poatã
prelucra ca un tot unitar. Sã nu uitãm însã cã, la nivelul cel mai de jos, informaþia ataºatã acestor obiecte
continuã sã fie tratatã de cãtre compilator ca un ºir de numere naturale, singurele informaþii
reprezentabile direct în memoria calculatoarelor actuale.
Pentru a reprezenta în memoria internã obiecte fizice sau noþiuni, este nevoie sã izolãm întregul set de
proprietãþi specifice acestora ºi sã îl reprezentãm prin numere. Aceste numere vor ocupa în memorie o
zonã compactã pe care, printr-un abuz de limbaj, o vom numi, într-o primã aproximare, obiect. Va trebui
însã sã aveþi întotdeauna o imagine clarã a deosebirii fundamentale dintre un obiect fizic sau o noþiune
ºi reprezentarea acestora în memoria calculatorului.
De exemplu, în memoria calculatorului este foarte simplu sã creãm un nou obiect, identic cu altul deja
existent, prin simpla duplicare a zonei de memorie folosite de obiectul pe care dorim sã-l dedublãm. În
realitate însã, este mult mai greu sã obþinem o copie identicã a unui obiect fizic, fie el o simplã foaie de
hârtie sau o bancnotã de 10000 de lei.
Sã revenim însã la numerele naturale. Din moment ce ele sunt singurele entitãþi reprezentabile în
memoria calculatorului, este firesc ca acesta sã fie echipat cu un set bogat de operaþii predefinite de
prelucrare a numerelor. Din pãcate, atunci când facem corespondenþa dintre numere ºi litere de exemplu,
nu ne putem aºtepta la un set la fel de bogat de operaþii predefinite care sã lucreze cu litere. Dar, þinând
cont de faptul cã în cele din urmã lucrãm tot cu numere, putem construi propriile noastre operaþii
specifice literelor, combinând în mod corespunzãtor operaþiile numerice predefinite.
De exemplu, pentru a obþine dintr-o literã majusculã, sã spunem ‘A’, corespondenta ei minusculã ‘a’,
este suficient sã adunãm un deplasament numeric corespunzãtor, presupunând cã literele mari ºi cele
mici sunt numerotate în ordine alfabeticã ºi imediat una dupã cealaltã în convenþia de reprezentare
folositã. În setul ASCII deplasamentul este 32, reprezentarea lui ‘A’ fiind 65 iar reprezentarea lui ‘a’
fiind 97. Acest deplasament se pãstreazã pentru toate literele din alfabetul englez ºi român, la cel din
urmã existând totuºi o excepþie în cazul literei ‘ª’.
Putem sã extindem cerinþele noastre mai departe, spunând cã, atunci când analizãm un obiect fizic sau o
noþiune pentru a le reprezenta în calculator, trebuie sã analizãm nu numai proprietãþile acestora dar ºi
modul în care acestea pot fi utilizate ºi care sunt operaþiile care pot fi executate asupra lor sau cu ajutorul
lor. Acest set de operaþii va trebui ulterior sã-l redefinim în contextul mulþimii de numere care formeazã
proprietãþile obiectului din memoria calculatorului, ºi sã îl descompunem în operaþii numerice
preexistente. În plus, trebuie sã analizãm modul în care reacþioneazã obiectul atunci când este supus
efectului unor acþiuni exterioare. Uneori, setul de operaþii specifice unui obiect împreunã cu modul în
care acesta reacþioneazã la stimuli exteriori se numeºte comportamentul obiectului.
De exemplu, dacã dorim sã construim un obiect care reprezintã o minge de formã sfericã în spaþiu, este
necesar sã definim trei numere care sã reprezinte coordonatele x, y ºi z relativ la un sistem de axe dat,
precum ºi o valoare pentru raza sferei. Aceste valori numerice vor face parte din setul de proprietãþi ale
obiectului minge. Dacã mai târziu vom dori sã construim o operaþie care sã reprezinte mutarea în spaþiu
a obiectului minge, este suficient sã ne folosim de operaþiile cu numere pentru a modifica valorile
coordonatelor x, y ºi z.
Desigur, obiectul minge este insuficient descris prin aceste coordonate ºi, pentru a simula în calculator
obiectul real este nevoie de multe proprietãþi suplimentare precum ºi de multe operaþii în plus. Dar, dacã
problema pe care o avem de rezolvat nu necesitã aceste proprietãþi ºi operaþii, este preferabil sã nu le
definim în obiectul folosit pentru reprezentare. Rezultatul direct al acestui mod de abordare este acela cã
vom putea defini acelaºi obiect real în mai multe feluri pentru a-l reprezenta în memoria internã. Modul
de definire depinde de problema de rezolvat ºi de programatorul care a gândit reprezentarea. De altfel,
aceste diferenþe de percepþie ale unui obiect real existã ºi între diverºi observatori umani.
Figura 3.1 Modelul de reprezentare al unui obiect în memorie. Stratul exterior reprezintã doar
operaþiile care oferã calea de a interacþiona cu proprietãþile obiectului ºi nu are corespondent
direct în zona de memorie ocupatã de obiect.
Din acest punct de vedere, putem privi obiectul ca pe un set de valori care formeazã miezul obiectului ºi
un set de operaþii care îmbracã aceste valori, protejându-le. Vom spune cã proprietãþile obiectului sunt
încapsulate în interiorul acestora. Mai mult, obiectul încapsuleazã ºi modul de funcþionare a operaþiilor
lui specifice, din exterior neputându-se observa decât modul de apelare a acestor operaþii ºi rezultatele
apelurilor. Cu alte cuvinte, procesul de încapsulare este procesul de ascundere a detaliilor neimportante
sau sensibile de construcþie a obiectului.
Dar nu numai proprietãþile unui obiect trebuiesc protejate ci ºi operaþiile definite de cãtre acesta. Unele
dintre operaþiile definite pentru un obiect, cum ar fi de exemplu procesul respirator al omului, sunt
periculos de lãsat la dispoziþia oricui. Este preferabil sã putem controla foarte exact cine ce operaþii
poate apela pentru un anumit obiect. În acest mod, din punctul de vedere al unui observator strãin, omul
este perceput ca un obiect mult mai simplu decât este în realitate, pentru cã acel observator nu poate
vedea decât acele valori ºi nu poate apela decât acele operaþii care sunt fãcute publice.
Desigur, în practicã este nevoie de o oarecare rafinare a gradului de protejare a fiecãrei operaþii sau
proprietãþi în aºa fel încât însuºi accesul observatorilor exteriori sã poatã fi nuanþat în funcþie de gradul
de similitudine ºi apropiere al observatorului faþã de obiectul accesat. Rafinarea trebuie sã meargã pânã
la a putea specifica pentru fiecare proprietate ºi operaþie în parte care sunt observatorii care au acces ºi
care nu.
Aceastã protejare ºi încapsulare a proprietãþilor ºi operaþiilor ce se pot executa cu ajutorul unui obiect
are ºi o altã consecinþã ºi anume aceea cã utilizatorul obiectului respectiv este independent de detaliile
constructive ale obiectului respectiv. Structura internã a obiectului poate fi astfel schimbatã ºi
perfecþionatã în timp fãrã ca funcþionalitatea de bazã sã fie afectatã.
Cu alte cuvinte, fiecare minge din lume are acelaºi set de proprietãþi, dar valorile acestora pot sã difere
de la o minge la alta. Modelul de reprezentare în memorie a unui obiect este întotdeauna acelaºi, dar
valorile memorate în locaþiile corespunzãtoare proprietãþilor sunt în general diferite.
În ceea ce priveºte operaþiile, acestea sunt întotdeauna aceleaºi dar rezultatul aplicãrii lor poate sã difere
în funcþie de valorile proprietãþilor obiectului asupra cãruia au fost aplicate. De exemplu, atunci când
aruncãm o minge spre pãmânt ea va ricoºa din acesta ridicându-se din nou în aer. Înãlþimea la care se va
ridica însã, este dependentã de dimensiunile ºi materialul din care a fost confecþionatã mingea. Cu alte
cuvinte, noua poziþie în spaþiu se va calcula printr-o operaþie care va þine cont de valorile memorate în
interiorul obiectului. Se poate întâmpla chiar ca operaþia sã hotãrascã faptul cã mingea va strãpunge
podeaua în loc sã fie respinsã de cãtre aceasta.
obiect pentru cã acestea sunt întotdeauna disponibile operaþiei. Nici o operaþie nu se poate aplica asupra
unui obiect fãrã sã ºtim exact care este obiectul respectiv ºi ce proprietãþi are acesta. Este absurd sã ne
gândim la ce înãlþime se va ridica o minge în general, fãrã sã facem presupuneri asupra valorilor
proprietãþilor acesteia. Sã mai observãm însã cã, dacã toate mingile ar avea aceleaºi valori pentru
proprietãþile implicate în operaþia descrisã mai sus, am putea sã calculãm înãlþimea de ricoºeu în
general, fãrã sã fim dependenþi de o anumitã minge.
În concluzie, putem spune cã obiectele cu care lucrãm fac parte întotdeauna dintr-o familie mai mare de
obiecte cu proprietãþi ºi comportament similar. Aceste familii de obiecte le vom numi în continuare
clase de obiecte sau concepte în timp ce obiectele aparþinând unei anumite clase le vom numi instanþe
ale clasei de obiecte respective. Putem vorbi despre clasa de obiecte minge ºi despre instanþele acesteia,
mulþimea tuturor obiectelor minge care existã în lume. Fiecare instanþã a clasei minge are un loc bine
precizat în spaþiu ºi în timp, un material ºi o culoare. Aceste proprietãþi diferã de la o instanþã la alta,
dar fiecare instanþã a aceleiaºi clase va avea întotdeauna aceleaºi proprietãþi ºi aceleaºi operaþii vor
putea fi aplicate asupra ei. În continuare vom numi variabile aceste proprietãþi ale unei clase de obiecte
ºi vom numi metode operaþiile definite pentru o anumitã clasã de obiecte.
Pentru a clarifica, sã mai reluãm încã o datã: O clasã de obiecte este o descriere a proprietãþilor ºi
operaþiilor specifice unui nou tip de obiecte reprezentabile în memorie. O instanþã a unei clase de
obiecte este un obiect de memorie care respectã descrierea clasei. O variabilã a unei clase de obiecte este
o proprietate a clasei respective care poate lua valori diferite în instanþe diferite ale clasei. O metodã a
unei clase este descrierea unei operaþii specifice clasei respective.
Sã mai precizãm faptul cã, spre deosebire de variabilele unei clase, metodele acesteia sunt memorate o
singurã datã pentru toate obiectele. Comportarea diferitã a acestora este datã de faptul cã ele depind de
valorile variabilelor.
O categorie aparte a claselor de obiecte este categoria acelor clase care reprezintã concepte care nu se pot
instanþia în mod direct, adicã nu putem construi instanþe ale clasei respective, de obicei pentru cã nu
avem destule informaþii pentru a le putea construi. De exemplu, conceptul de om nu se poate instanþia
în mod direct pentru cã nu putem construi un om despre care nu ºtim exact dacã este bãrbat sau femeie.
Putem în schimb instanþia conceptul de bãrbat ºi conceptul de femeie care sunt niºte subconcepte ale
conceptului om.
Clasele abstracte, neinstanþiabile, servesc în general pentru definirea unor proprietãþi sau operaþii
comune ale mai multor clase ºi pentru a putea generaliza operaþiile referitoare la acestea. Putem, de
exemplu sã definim în cadrul clasei de obiecte om modul în care acesta se alimenteazã ca fiind
independent de apartenenþa la conceptul de bãrbat sau femeie. Aceastã definiþie va fi valabilã la
amândouã subconceptele definite mai sus. În schimb, nu putem decât cel mult sã precizãm faptul cã un
om trebuie sã aibã un comportament social. Descrierea exactã a acestui comportament trebuie fãcutã în
cadrul conceptului de bãrbat ºi a celui de femeie. Oricum, este interesant faptul cã, indiferent care ar fi
clasa acestuia, putem sã ne bazãm pe faptul cã acesta va avea definit un comportament social, specific
clasei lui.
Cele douã metode despre care am vorbit mai sus, definite la nivelul unui superconcept, sunt profund
diferite din punctul de vedere al subconceptelor acestuia. În timp ce metoda de alimentaþie este definitã
exact ºi amândouã subconceptele pot sã o foloseascã fãrã probleme, metoda de comportament social este
doar o metodã abstractã, care trebuie sã existe, dar despre care nu se ºtie exact cum trebuie definitã.
Fiecare dintre subconcepte trebuie sã-ºi defineascã propriul sãu comportament social pentru a putea
deveni instanþiabil. Dacã o clasã de obiecte are cel puþin o metodã abstractã, ea devine în întregime o
clasã abstractã ºi nu poate fi instanþiatã, adicã nu putem crea instanþe ale unei clase de obiecte abstracte.
Altfel spus, o clasã abstractã de obiecte este o clasã pentru care nu s-au precizat suficient de clar toate
metodele astfel încât sã poatã fi folositã în mod direct.
Uneori, în loc de derivare se foloseºte termenul de extindere. Termenul vine de la faptul cã o subclasã îºi
extinde superclasa cu noi variabile ºi metode.
În spiritul acestei ierarhizãri, putem presupune cã toate clasele de obiecte sunt derivate dintr-o clasã
iniþialã, sã-i spunem clasa de obiecte generice, în care putem defini proprietãþile ºi operaþiile comune
tuturor obiectelor precum ar fi testul de egalitate dintre douã instanþe, duplicarea instanþelor sau aflarea
clasei de care aparþine o anumitã instanþã.
Ierarhizarea se poate extinde pe mai multe nivele, sub formã arborescentã, în fiecare punct nodal al
structurii arborescente rezultate aflându-se clase de obiecte. Desigur, clasele de obiecte de pe orice nivel
pot avea instanþe proprii, cu condiþia sã nu fie clase abstracte, imposibil de instanþiat.
Figura 3.2 O ierarhie de clase de obiecte în care clasele sunt reprezentate în câmpuri eliptice iar
instanþele acestora în câmpuri dreptunghiulare. Clasele abstracte de obiecte au elipsa dublatã.
Desigur, este foarte dificil sã construim o ierarhie de clase de obiecte completã, care sã conþinã clase de
obiecte corespunzãtoare fiecãrui concept cunoscut. Din fericire, pentru o problemã datã, conceptele
implicate în rezolvarea ei sunt relativ puþine ºi pot fi uºor izolate, simplificate ºi definite. Restrângerea la
minimum a arborelui de concepte necesar rezolvãrii unei anumite probleme fãrã a se afecta generalitatea
soluþiei este un talent pe care fiecare programator trebuie sã ºi-l descopere ºi sã ºi-l cultive cu atenþie. De
alegerea acestor concepte depinde eficienþa ºi flexibilitatea aplicaþiei.
O clasã de obiecte derivatã dintr-o altã clasã pãstreazã toate proprietãþile ºi operaþiile acesteia din urmã
aducând în plus proprietãþi ºi operaþii noi. De exemplu, dacã la nivelul clasei de obiecte om am definit
forma bipedã a acestuia ºi capacitatea de a vorbi ºi de a înþelege, toate acestea vor fi moºtenite ºi de cãtre
clasele derivate din clasa om, ºi anume clasa bãrbaþilor ºi cea a femeilor. Fiecare dintre aceste clase de
obiecte derivate îºi vor defini propriile lor proprietãþi ºi operaþii pentru a descrie diferenþa dintre ele ºi
clasa originalã.
Unele dintre proprietãþile ºi operaþiile definite în superclasã pot fi redefinite în subclasele de obiecte
derivate. Vechile proprietãþi ºi operaþii sunt disponibile în continuare, doar cã pentru a le putea accesa
va trebui sã fie specificatã explicit superclasa care deþine copia redefinitã. Operaþia de redefinire a unor
operaþii sau variabile din interiorul unei clase în timpul procesului de derivare o vom numi rescriere.
Aceastã redefinire ne dã de fapt o mare flexibilitate în construcþia ierarhiei unei probleme date pentru cã
nici o proprietate sau operaþie definitã într-un punct al ierarhiei nu este impusã definitiv pentru
conceptele derivate din acest punct direct sau nu.
Revenind pentru un moment la protejarea informaþiilor interne ale unui obiect sã precizãm faptul cã
gradul de similitudine de care vorbeam mai sus este mãrit în cazul în care vorbim de douã clase derivate
una din cealaltã. Cu alte cuvinte, o subclasã a unei clase are acces de obicei la mult mai multe informaþii
memorate în superclasa sa decât o altã clasã de obiecte oarecare. Acest lucru este firesc þinând cont de
faptul cã, uneori, o subclasã este nevoitã sã redefineascã o parte din funcþionalitatea superclasei sale.
Aceastã observaþie ne spune cã trebuie sã dãm definiþii despre ce înseamnã cu adevãrat faptul cã un
obiect poate fi privit ca un mamifer sau ca o fiinþa gânditoare sau ca un obiect spaþio-temporal. Aceste
definiþii, pe care le vom numi în continuare interfeþe, sunt aplicabile nu numai clasei de obiecte om dar
ºi la alte clase de obiecte derivate sau nu din acesta, superclase sau nu ale acesteia. Putem sã gãsim o
mulþime de clase de obiecte ale cãror instanþe pot fi privite ca obiecte spaþio-temporale dar care sã nu
aibã mare lucru în comun cu omul. Practic, atunci când construim o interfaþã, definim un set minim de
operaþii care trebuie sã aparþinã obiectelor care respectã aceastã interfaþã. Orice clasã de obiecte care
declarã cã respectã aceastã interfaþã va trebui sã defineascã toate operaþiile.
Operaþiile însã, sunt definite pe cãi specifice fiecãrei clase de obiecte în parte. De exemplu, orice obiect
spaþial trebuie sã defineascã o operaþie de modificare a poziþiei în care se aflã. Dar aceastã operaþie
este diferitã la un om, care poate sã-ºi schimbe singur poziþia, faþã de o minge care trebuie ajutatã din
exterior pentru a putea fi mutatã. Totuºi, dacã ºtim cu siguranþã cã un obiect este o instanþã a unui clase
de obiecte care respectã interfaþa spatio-temporalã, putem liniºtiþi sã executãm asupra acestuia o
operaþie de schimbare a poziþiei, fãrã sã trebuiascã sã cunoaºtem amãnunte despre modul în care va fi
executatã aceastã operaþie. Tot ceea ce trebuie sã ºtim este faptul cã operaþia este definitã pentru
obiectul respectiv.
În concluzie, o interfaþã este un set de operaþii care trebuiesc definite de o clasã de obiecte pentru a se
înscrie într-o anumitã categorie. Vom spune despre o clasã care defineºte toate operaþiile unei interfeþe
cã implementeazã interfaþa respectivã.
Cu alte cuvinte, putem privi interfeþele ca pe niºte reguli de comportament impuse claselor de obiecte. În
clipa în care o clasã implementeazã o anumitã interfaþã, obiectele din clasa respectivã pot fi privite în
exclusivitate din acest punct de vedere. Interfeþele pot fi privite ca niºte filtre prin care putem privi un
anumit obiect, filtre care nu lasã la vedere decât proprietãþile specifice interfeþei, chiar dacã obiectul în
vizor este mult mai complicat în realitate.
Interfeþele creazã o altã împãrþire a obiectelor cu care lucrãm. În afarã de împãrþirea normalã pe clase,
putem sã împãrþim obiectele ºi dupã interfeþele pe care le implementeazã. ªi, la fel cu situaþia în care
definim o operaþie doar pentru obiectele unei anumite clase, putem defini ºi operaþii care lucreazã doar
cu obiecte care implementeazã o anumitã interfaþã, indiferent de clasa din care acestea fac parte.
[cuprins]
Capitolul IV
Structura lexicalã Java
4.1 Setul de caractere
4.2 Unitãþi lexicale
4.2.4 Separatori
4.2.5 Operatori
4.3 Comentarii
Vechiul standard ASCII este însã un subset al setului Unicode, ceea ce înseamnã cã vom regãsi
caracterele ASCII cu exact aceleaºi coduri ca ºi mai înainte în noul standard.
Java foloseºte setul Unicode în timpul rulãrii aplicaþiilor ca ºi în timpul compilãrii acestora. Folosirea
Unicode în timpul execuþiei nu înseamnã nimic altceva decât faptul cã o variabilã Java de tip caracter
este reprezentatã pe 16 biþi iar un ºir de caractere va ocupa fizic în memorie de doua ori mai mulþi
octeþi decât numãrul caracterelor care formeazã ºirul.
În ceea ce priveºte folosirea Unicode în timpul compilãrii, compilatorul Java acceptã la intrare fiºiere
sursã care pot conþine orice caractere Unicode. Se poate lucra ºi cu fiºiere ASCII obiºnuite în care putem
introduce caractere Unicode folosind secvenþe escape. Fiºierele sursã sunt fiºiere care conþin declaraþii
ºi instrucþiuni Java. Aceste fiºiere trec prin trei paºi distincþi la citirea lor de cãtre compilator:
1. ªirul de caractere Unicode sau ASCII, memorat în fiºierul sursã, este transformat într-un ºir de
caractere Unicode. Caracterele Unicode pot fi introduse ºi ca secvenþe escape folosind doar
caractere ASCII.
2. ªirul de caractere Unicode este transformat într-un ºir de caractere în care sunt evidenþiate separat
caracterele de intrare faþã de caracterele de sfârºit de linie.
3. ªirul de caractere de intrare ºi de sfârºit de linie este transformat într-un ºir de cuvinte ale
limbajului Java.
În primul pas al citirii fiºierului sursã, sunt generate secvenþe escape. Secvenþele escape sunt secvenþe
de caractere ASCII care încep cu caracterul backslash \. Pentru secvenþele escape Unicode, al doilea
caracter din secvenþã trebuie sã fie u sau U. Orice alt caracter care urmeazã dupã backslash va fi
considerat ca fiind caracter nativ Unicode ºi lãsat nealterat. Dacã al doilea caracter din secvenþa escape
este u, urmãtoarele patru caractere ASCII sunt tratate ca ºi cifre hexazecimale (în baza 16) care formeazã
împreunã doi octeþi de memorie care reprezintã un caracter Unicode.
Se pot folosi la intrare ºi fiºiere ASCII normale, pentru cã ASCII este un subset al Unicode. De exemplu,
putem scrie:
int f\u0660 = 3;
Numele variabilei are douã caractere ºi al doilea caracter este o cifrã arabic-indic.
În al doilea pas al citirii fiºierului sursã, sunt recunoscute ca ºi caractere de sfârºit de linie caracterele
ASCII CR ºi ASCII LF. În acelaºi timp, secvenþa de caractere ASCII CR-ASCII LF este tratatã ca un
singur sfârºit de linie ºi nu douã. În acest mod, Java suportã în comun standardele de terminare a liniilor
folosite de diferite sisteme de operare: MacOS, Unix ºi DOS.
Este important sã separãm caracterele de sfârºit de linie de restul caracterelor de intrare pentru a ºti unde
se terminã comentariile de o singurã linie (care încep cu secvenþa //) precum ºi pentru a raporta odatã
cu erorile de compilare ºi linia din fiºierul sursã în care au apãrut acestea.
În pasul al treilea al citirii fiºierului sursã, sunt izolate elementele de intrare ale limbajului Java, ºi
anume: spaþii, comentarii ºi unitãþi lexicale.
Spaþiile pot fi caracterele ASCII SP (spaþiu), FF (avans de paginã) sau HT (tab orizontal) precum ºi
orice caracter terminator de linie. Comentariile le vom discuta în paragraful 4.3.
● Cuvinte cheie
● Identificatori
● Literali
● Separatori
● Operatori
Cuvintele cheie sunt secvenþe de caractere ASCII rezervate de limbaj pentru uzul propriu. Cu ajutorul
lor, Java îºi defineºte unitãþile sintactice de bazã. Nici un program nu poate sã utilizeze aceste secvenþe
altfel decât în modul în care sunt definite de limbaj. Singura excepþie este aceea cã nu existã nici o
restricþionare a apariþiei cuvintelor cheie în ºiruri de caractere sau comentarii.
Dintre acestea, cele îngroºate sunt efectiv folosite, iar restul sunt rezervate pentru viitoare extensii ale
limbajului.
4.2.2 Identificatori
Identificatorii Java sunt secvenþe nelimitate de litere ºi cifre Unicode, începând cu o literã.
Identificatorii nu au voie sã fie identici cu cuvintele rezervate.
Reprezentare Caracter
Explicaþie
Unicode ASCII
Un caracter Unicode este o literã dacã este în urmãtoarele intervale ºi nu este cifrã:
Reprezentare Caracter
Explicaþie
Unicode ASCII
4.2.3 Literali
Un literal este modalitatea de bazã de exprimare în fiºierul sursã a valorilor pe care le pot lua tipurile
primitive ºi tipul ºir de caractere. Cu ajutorul literalilor putem introduce valori constante în variabilele de
tip primitiv sau în variabilele de tip ºir de caractere.
● literali întregi
● literali flotanþi
● literali booleeni
● literali caracter
● literali ºir de caractere
Literalii întregi pot fi reprezentaþi în baza 10, 16 sau 8. Toate caracterele care se folosesc pentru scrierea
literalilor întregi fac parte din subsetul ASCII al setului Unicode.
Literalii întregi pot fi întregi normali sau lungi. Literalii lungi se recunosc prin faptul cã se terminã cu
sufixul l sau L. Un literal întreg este reprezentat pe 32 de biþi iar unul lung pe 64 de biþi.
12356L234871234567890l
Pentru a exprima un literal întreg în baza 16 trebuie sã definim cifrele de la 10 la 15. Convenþia va fi
urmãtoarea:
10 - a, A13 - d, D
11 - b, B14 - e, E
12 - c, C15 - f, F
În plus, pentru cã un literal întreg în baza 16 poate începe cu o literã, vom adãuga prefixul 0x sau 0X.
Dacã nu am adãuga acest sufix, compilatorul ar considera cã este vorba despre un identificator.
0xa340X1230x2c45L0xde123abccdL
În fine, pentru a reprezenta un literal întreg în baza 8, îl vom preceda cu cifra 0. Restul cifrelor pot fi
oricare între 0 ºi 7. Cifrele 8 ºi 9 nu sunt admise în literalii întregi în baza 8.
0234500123001234567712345677L
Valoarea maximã a unui literal întreg normal este de 2147483647 (231-1), scrisã în baza 10. În
baza 16, cel mai mare literal pozitiv se scrie ca 0x7fffffff iar în baza 8 ca 017777777777. Toate
trei scrierile reprezintã de fapt aceeaºi valoare, doar cã aceasta este exprimatã în baze diferite.
Cea mai micã valoare a unui literal întreg normal este -2147483648 (-231), respectiv 0x80000000 ºi
020000000000. Valorile 0xffffffff ºi 037777777777 reprezintã amândouã valoarea -1.
Specificarea în sursã a unui literal întreg normal care depãºeºte aceste limite reprezintã o eroare de
compilare. Cu alte cuvinte, dacã folosim în sursã numãrul: 21474836470 de exemplu, fãrã sã punem
sufixul de numãr lung dupã el, compilatorul va genera o eroare la analiza sursei.
Valoarea maximã a unui literal întreg lung este, în baza 10, 9223372036854775807L (263-1). În
octal, asta înseamnã 0777777777777777777777L iar în baza 16 0x7fffffffffffffffL. În
mod asemãnãtor, valoarea minimã a unui literal întreg lung este -9223372036854775808L (-263-
1), în octal aceastã valoare este 0400000000000000000000L iar în baza 16 este
0x8000000000000000L.
La fel ca ºi la literalii întregi normali, depãºirea acestor limite este o eroare de compilare.
Literalii flotanþi reprezintã numere reale. Ei sunt formaþi dintr-o parte întreagã, o parte fracþionarã, un
exponent ºi un sufix de tip. Exponentul, dacã existã, este introdus de litera e sau E urmatã opþional de un
semn al exponentului.
Este obligatoriu sã existe mãcar o cifrã fie în partea întreagã fie în partea zecimalã ºi punctul zecimal sau
litera e pentru exponent.
Sufixul care indicã tipul flotantului poate fi f sau F în cazul în care avem o valoare flotantã normalã ºi d
sau D dacã avem o valoare flotantã dublã. Dacã nu este specificat nici un sufix, valoarea este implicit
dublã.
Valoarea maximã a unui literal flotant normal este 3.40282347e+38f iar valoarea cea mai micã
La fel ca ºi la literalii întregi, este o eroare sã avem exprimat în sursã un literal mai mare decât valoarea
maximã reprezentabilã sau mai mic decât cea mai micã valoare reprezentabilã.
1.0e45f-3.456f0..01e-3
Primele douã exemple reprezintã literali flotanþi normali, iar celelalte literali flotanþi dubli.
Literalii booleeni nu pot fi decât true sau false, primul reprezentând valoarea booleanã de adevãr iar
celãlalt valoarea booleanã de fals. True ºi false nu sunt cuvinte rezervate ale limbajului Java, dar nu
veþi putea folosi aceste cuvinte ca identificatori.
Un literal de tip caracter este utilizat pentru a exprima caracterele codului Unicode. Reprezentarea se
face fie folosind o literã, fie o secvenþã escape. Secvenþele escape ne permit reprezentarea caracterelor
care nu au reprezentare graficã ºi reprezentarea unor caractere speciale precum backslash ºi însãºi
caracterul apostrof.
Pentru restul caracterelor Unicode trebuie sã folosim secvenþe escape. Dintre acestea, câteva sunt
predefinite în Java, ºi anume:
'\o' '\oo''\too'
Nu este corect sã folosiþi ca valori pentru literale caracter secvenþa '\u000d' (caracterul ASCII CR),
sau altele care reprezintã caractere speciale, pentru cã acestea fiind secvenþe escape Unicode sunt
transformate foarte devreme în timpul procesãrii sursei în caractere CR ºi sunt interpretate ca terminatori
de linie.
'\n''\u23a''\34'
În acest moment secvenþele escape Unicode au fost deja înlocuite cu caractere Unicode native. Dacã u
apare dupã \, se semnaleazã o eroare de compilare.
Un literal ºir de caractere este format din zero sau mai multe caractere între ghilimele. Caracterele care
formeazã ºirul de caractere pot fi caractere grafice sau secvenþe escape ca cele definite la literalii
caracter.
Dacã un literal ºir de caractere conþine în interior un caracter terminator de linie va fi semnalatã o eroare
de compilare. Cu alte cuvinte, nu putem avea în sursã ceva de forma:
"Acesta este
greºit!”
chiar dacã aparent exprimarea ar reprezenta un ºir format din caracterele A, c, e, s, t, a, spaþiu, e, s, t, e,
linie nouã, g, r, e, º, i, t, !. Dacã dorim sã introducem astfel de caractere terminatoare de linie într-un ºir
va trebui sã folosim secvenþe escape ca în:
“Acesta este\ngreºit”
Dacã ºirul de caractere este prea lung, putem sã-l spargem în bucãþi mai mici pe care sã le concatenãm
cu operatorul +.
Fiecare ºir de caractere este în fapt o instanþã a clasei de obiecte String declaratã standard în pachetul
java.lang.
Primul ºir de caractere din exemplu nu conþine nici un caracter ºi se numeºte ºirul vid. Ultimul exemplu
este format din douã ºiruri distincte concatenate.
4.2.4 Separatori
Un separator este un caracter care indicã sfârºitul unei unitãþi lexicale ºi începutul alteia. Separatorii
sunt necesari atunci când unitãþi lexicale diferite sunt scrise fãrã spaþii între ele. Acestea se pot totuºi
separa dacã unele dintre ele conþin caractere separatori. În Java separatorii sunt urmãtorii:
( ) { } [ ] ; , .
Exemple de separare:
a[i]sin(56)
În primul exemplu nu avem o singurã unitate lexicalã ci patru: a, [, i, ]. Separatorii [ ºi ] ne dau aceastã
Atenþie, separatorii participã în acelaºi timp ºi la construcþia sintaxei limbajului. Ei nu sunt identici cu
spaþiile deºi, ca ºi acestea, separã unitãþi lexicale diferite.
4.2.5 Operatori
Operatorii reprezintã simboluri grafice pentru operaþiile elementare definite de limbajul Java. Despre
operatori vom discuta mai mult atunci când vom prezenta expresiile. Deocamdatã, iatã lista tuturor
operatorilor limbajului Java:
=><!~?:
==<=>=!=&&||++--
+-*/&|^%<<>> >>>
+=-=*=/=&=|=^=%=<<=>>=>>>=
Sã mai precizãm deocamdatã cã toþi operatorii joacã ºi rol de separatori. Cu alte cuvinte, din secvenþa de
caractere:
vasile+gheorghe
4.3 Comentarii
Un comentariu este o secvenþã de caractere existentã în fiºierul sursã dar care serveºte doar pentru
explicarea sau documentarea sursei ºi nu afecteazã în nici un fel semantica programelor.
● Comentarii pe mai multe linii, închise între /* ºi */. Toate caracterele dintre cele douã secvenþe
sunt ignorate.
● Comentarii pe mai multe linii care þin de documentaþie, închise între /** ºi */. Textul dintre cele
douã secvenþe este automat mutat în documentaþia aplicaþiei de cãtre generatorul automat de
documentaþie.
● Comentarii pe o singurã linie care încep cu //. Toate caracterele care urmeazã acestei secvenþe
pânã la primul caracter sfârºit de linie sunt ignorate.
În Java, nu putem sã scriem comentarii în interiorul altor comentarii. La fel, nu putem introduce
comentarii în interiorul literalilor caracter sau ºir de caractere. Secvenþele /* ºi */ pot sã aparã pe o linie
dupã secvenþa // dar îºi pierd semnificaþia. La fel se întâmplã cu secvenþa // în comentarii care încep cu /
* sau /**.
[cuprins]
Capitolul V
Componente de bazã
ale programelor Java
5.1 Variabile
5.2 Expresii
5.3 Instrucþiuni
[cuprins]
file:///C|/Documents%20and%20Settings/Luminita/Desktop/...JAVA%20in%20romana/Carte%20JAVA%20in%20romana/cap5.html12.01.2006 23:07:47
Capitolul VI -- Obiecte Java
Capitolul VI
Obiecte Java
6.1 Declaraþia unei noi clase de obiecte
6.2.1 Modificatori
6.2.2 Protecþie
6.2.3 Accesarea unei variabile
6.2.4 Vizibilitate
6.2.5 Variabile predefinite: this ºi super
6.3.5 Vizibilitate
6.5.1 constructori
6.5.2 Finalizatori
6.5.3 Crearea instanþelor
În primul rând sã observãm cã, atunci când scriem programe în Java nu facem altceva decât sã definim noi ºi
noi clase de obiecte. Dintre acestea, unele vor reprezenta însãºi aplicaþia noastrã în timp ce altele vor fi
necesare pentru rezolvarea problemei la care lucrãm. Ulterior, atunci când dorim sã lansãm aplicaþia în
execuþie nu va trebui decât sã instanþiem un obiect reprezentând aplicaþia în sine ºi sã apelãm o metodã de
pornire definitã de cãtre aplicaþie, metodã care de obicei are un nume ºi un set de parametri bine fixate.
Totuºi, numele acestei metode depinde de contextul în care este lansatã aplicaþia noastrã.
Aceastã abordare a construcþiei unei aplicaþii ne spune printre altele cã vom putea lansa oricâte instanþe ale
aplicaþiei noastre dorim, pentru cã fiecare dintre acestea va reprezenta un obiect în memorie având propriile
lui valori pentru variabile. Execuþia instanþelor diferite ale aplicaþiei va urma desigur cãi diferite în funcþie
de interacþiunea fiecãreia cu un utilizator, eventual acelaºi, ºi în funcþie de unii parametri pe care îi putem
defini în momentul creãrii fiecãrei instanþe.
Sã vedem ce trebuie sã definim atunci când dorim sã creãm o nouã clasã de obiecte. În primul rând trebuie sã
stabilim care este conceptul care este reprezentat de cãtre noua clasã de obiecte ºi sã definim informaþiile
memorate în obiect ºi modul de utilizare a acestuia. Acest pas este cel mai important din tot procesul de
definire al unei noi clase de obiecte. Este necesar sã încercaþi sã respectaþi douã reguli oarecum antagonice.
Una dintre ele spune cã nu trebuiesc create mai multe clase de obiecte decât este nevoie, pentru a nu face
dificilã înþelegerea modului de lucru al aplicaþiei la care lucraþi. Cea de-a doua regulã spune cã nu este bine
sã mixaþi într-un singur obiect funcþionalitãþi care nu au nimic în comun, creând astfel clase care corespund
la douã concepte diferite.
Medierea celor douã reguli nu este întotdeauna foarte uºoarã. Oricum, vã va fi mai uºor dacã pãstraþi în
minte faptul cã fiecare clasã pe care o definiþi trebuie sã corespundã unui concept real bine definit, necesar la
rezolvarea problemei la care lucraþi. ªi mai pãstraþi în minte ºi faptul cã este inutil sã lucraþi cu concepte
foarte generale atunci când aplicaþia dumneavoastrã nu are nevoie decât de o particularizare a acestora.
Riscaþi sã pierdeþi controlul dezvoltãrii acestor clase de obiecte prea generale ºi sã îngreunaþi dezvoltarea
aplicaþiei.
Dupã ce aþi stabilit exact ce doriþi de la noua clasã de obiecte, sunteþi în mãsurã sã gãsiþi un nume pentru
noua clasã, nume care trebuie sã urmeze regulile de construcþie ale identificatorilor limbajului Java definite
în capitolul anterior.
Stabilirea unui nume potrivit pentru o nouã clasã nu este întotdeauna un lucru foarte uºor. Problema este cã
acest nume nu trebuie sã fie exagerat de lung dar trebuie sã exprime suficient de bine destinaþia clasei.
Regulile de denumire ale claselor sunt rezultatul experienþei fiecãruia sau al unor convenþii de numire
stabilite anterior. De obicei, numele de clase este bine sã înceapã cu o literã majusculã. Dacã numele clasei
conþine în interior mai multe cuvinte, aceste cuvinte trebuie de asemenea începute cu literã majusculã. Restul
caracterelor vor fi litere minuscule.
De exemplu, dacã dorim sã definim o clasã de obiecte care implementeazã conceptul de motor Otto vom
folosi un nume ca MotorOtto pentru noua clasã ce trebuie creatã. La fel, vom defini clasa MotorDiesel
sau MotorCuReacþie. Dacã însã avem nevoie sã definim o clasã separatã pentru un motor Otto cu cilindri
în V ºi carburator, denumirea clasei ca MotorOttoCuCilindriÎnVªiCarburator nu este poate cea
mai bunã soluþie. Poate cã în acest caz este preferabilã o prescurtare de forma MotorOttoVC. Desigur,
acestea sunt doar câteva remarci la adresa acestei probleme ºi este în continuare necesar ca în timp sã vã
creaþi propria convenþie de denumire a claselor pe care le creaþi.
În cazul în care aþi definit deja o parte din funcþionalitatea de care aveþi nevoie într-o altã superclasã, puteþi
sã derivaþi noua clasã de obiecte din clasa deja existentã. Dacã nu existã o astfel de clasã, noua clasã va fi
automat derivatã din clasa de obiecte predefinitã numitã Object. În Java, clasa Object este superclasã direct
sau indirect pentru orice altã clasã de obiecte definitã de utilizator.
Alegerea superclasei din care derivaþi noua clasã de obiecte este foarte importantã pentru cã vã ajutã sã
refolosiþi codul deja existent. Totuºi, nu alegeþi cu uºurinþã superclasa unui obiect pentru cã astfel puteþi
încãrca obiectele cu o funcþionalitate inutilã, existentã în superclasã. Dacã nu existã o clasã care sã vã ofere
doar funcþionalitatea de care aveþi nevoie, este preferabil sã derivaþi noua clasã direct din clasa Object ºi
sã apelaþi indirect funcþionalitatea pe care o doriþi.
Stabilirea acestor interfeþe are dublu scop. În primul rând ele instruiesc compilatorul sã verifice dacã noua
clasã respectã cu adevãrat toate interfeþele pe care le-a declarat, cu alte cuvinte defineºte toate metodele
declarate în aceste interfeþe. A doua finalitate este aceea de a permite compilatorului sã foloseascã instanþele
noii clase oriunde aplicaþia declarã cã este nevoie de un obiect care implementeazã interfeþele declarate.
În unele cazuri trebuie sã oferim compilatorului informaþii suplimentare relative la modul în care vom folosi
clasa nou creatã pentru ca acesta sã poatã executa verificãri suplimentare asupra descrierii clasei. În acest
scop, putem defini o clasã ca fiind abstractã, finalã sau publicã folosindu-ne de o serie de cuvinte rezervate
numite modificatori. Modificatorii pentru tipurile de clase de mai sus sunt respectiv: abstract, final ºi
public.
În cazul în care declarãm o clasã de obiecte ca fiind abstractã, compilatorul va interzice instanþierea acestei
clase. Dacã o clasã este declaratã finalã, compilatorul va avea grijã sã nu putem deriva noi subclase din
aceastã clasã. În cazul în care declarãm în acelaºi timp o clasã de obiecte ca fiind abstractã ºi finalã, eroarea
va fi semnalatã încã din timpul compilãrii pentru cã cei doi modificatori se exclud.
Pentru ca o clasã sã poatã fi folositã ºi în exteriorul contextului în care a fost declaratã ea trebuie sã fie
declaratã publicã. Mai mult despre acest aspect în paragraful referitor la structura programelor. Pânã atunci,
sã spunem cã orice clasã de obiecte care va fi instanþiatã ca o aplicaþie trebuie declaratã publicã.
În sfârºit, dupã ce toþi ceilalþi paºi au fost efectuaþi, putem trece la scrierea corpului declaraþiei de clasã. În
principal, aici vom descrie variabilele clasei împreunã cu metodele care lucreazã cu acestea. Tot aici putem
preciza ºi gradele de protejare pentru fiecare dintre elementele declaraþiei. Uneori numim variabilele ºi
metodele unei clase la un loc ca fiind câmpurile clasei. Subcapitolele urmãtoare vor descrie în amãnunt
corpul unei declaraþii.
[ extends NumeSuperclasã ]
{ [ CâmpClasã ]* }
ele trebuie sã aibã un tip, un nume ºi poate avea iniþializatori. În afarã de aceste elemente, pe care le-am
prezentat deja în secþiunea în care am prezentat variabilele, variabilele definite în interiorul unei clase pot
avea definiþi o serie de modificatori care altereazã comportarea variabilei în interiorul clasei, ºi o specificaþie
de protecþie care defineºte cine are dreptul sã acceseze variabila respectivã.
6.2.1 Modificatori
Modificatorii sunt cuvinte rezervate Java care precizeazã sensul unei declaraþii. Iatã lista acestora:
static
final
transient
volatile
Dintre aceºtia, transient nu este utilizat în versiunea curentã a limbajului Java. Pe viitor va fi folosit
pentru a specifica variabile care nu conþin informaþii care trebuie sã rãmânã persistente la terminarea
programului.
Modificatorul volatile specificã faptul cã variabila respectivã poate fi modificatã asincron cu rularea
aplicaþiei. În aceste cazuri, compilatorul trebuie sã-ºi ia mãsuri suplimentare în cazul generãrii ºi optimizãrii
codului care se adreseazã acestei variabile.
Modificatorul final este folosit pentru a specifica o variabilã a cãrei valoare nu poate fi modificatã.
Variabila respectivã trebuie sã primeascã o valoare de iniþializare chiar în momentul declaraþiei. Altfel, ea
nu va mai putea fi iniþializatã în viitor. Orice încercare ulterioarã de a seta valori la aceastã variabilã va fi
semnalatã ca eroare de compilare.
Modificatorul static este folosit pentru a specifica faptul cã variabila are o singurã valoare comunã tuturor
instanþelor clasei în care este declaratã. Modificarea valorii acestei variabile din interiorul unui obiect face ca
modificarea sã fie vizibilã din celelalte obiecte. Variabilele statice sunt iniþializate la încãrcarea codului
specific unei clase ºi existã chiar ºi dacã nu existã nici o instanþã a clasei respective. Din aceastã cauzã, ele
pot fi folosite de metodele statice.
6.2.2 Protecþie
În Java existã patru grade de protecþie pentru o variabilã aparþinând unei clase:
● privatã
● protejatã
● publicã
● prietenoasã
O variabilã publicã este accesibilã oriunde este accesibil numele clasei. Cuvântul rezervat este public.
O variabilã protejatã este accesibilã în orice clasã din pachetul cãreia îi aparþine clasa în care este declaratã.
În acelaºi timp, variabila este accesibilã în toate subclasele clasei date, chiar dacã ele aparþin altor pachete.
Cuvântul rezervat este protected.
O variabilã privatã este accesibilã doar în interiorul clasei în care a fost declaratã. Cuvântul rezervat este
private.
O variabilã care nu are nici o declaraþie relativã la gradul de protecþie este automat o variabilã prietenoasã.
O variabilã prietenoasã este accesibilã în pachetul din care face parte clasa în interiorul cãreia a fost
declaratã, la fel ca ºi o variabilã protejatã. Dar, spre deosebire de variabilele protejate, o variabilã prietenoasã
nu este accesibilã în subclasele clasei date dacã aceste sunt declarate ca aparþinând unui alt pachet. Nu existã
un cuvânt rezervat pentru specificarea explicitã a variabilelor prietenoase.
O variabilã nu poate avea declarate mai multe grade de protecþie în acelaºi timp. O astfel de declaraþie este
semnalatã ca eroare de compilare.
Accesarea unei variabile declarate în interiorul unei clasei se face folosindu-ne de o expresie de forma:
ReferinþãInstanþã.NumeVariabilã
Referinþa cãtre o instanþã trebuie sã fie referinþã cãtre clasa care conþine variabila. Referinþa poate fi
valoarea unei expresii mai complicate, ca de exemplu un element dintr-un tablou de referinþe.
În cazul în care avem o variabilã staticã, aceasta poate fi accesatã ºi fãrã sã deþinem o referinþã cãtre o
instanþã a clasei. Sintaxa este, în acest caz:
NumeClasã.NumeVariabilã
6.2.4 Vizibilitate
O variabilã poate fi ascunsã de declaraþia unei alte variabile cu acelaºi nume. De exemplu, dacã într-o clasã
avem declaratã o variabilã cu numele unu ºi într-o subclasã a acesteia avem declaratã o variabilã cu acelaºi
nume, atunci variabila din superclasã este ascunsã de cea din clasã. Totuºi, variabila din superclasã existã
încã ºi poate fi accesatã în mod explicit. Expresia de referire este, în acest caz:
NumeSuperClasã.NumeVariabilã
sau
super.NumeVariabilã
La fel, o variabilã a unei clase poate fi ascunsã de o declaraþie de variabilã dintr-un bloc de instrucþiuni.
Orice referinþã la ea va trebui fãcutã în mod explicit. Expresia de referire este, în acest caz:
this.NumeVariabilã
În interiorul fiecãrei metode non-statice dintr-o clasã existã predefinite douã variabile cu semnificaþie
specialã. Cele douã variabile sunt de tip referinþã ºi au aceeaºi valoare ºi anume o referinþã cãtre obiectul
curent. Diferenþa dintre ele este tipul.
Prima dintre acestea este variabila this care are tipul referinþã cãtre clasa în interiorul cãreia apare metoda.
A doua este variabila super al cãrei tip este o referinþã cãtre superclasa imediatã a clasei în care apare
metoda. În interiorul obiectelor din clasa Object nu se poate folosi referinþa super pentru cã nu existã
nici o superclasã a clasei de obiecte Object.
În cazul în care super este folositã la apelul unui constructor sau al unei metode, ea acþioneazã ca un cast
cãtre superclasa imediatã.
În plus, o clasã îºi poate defini metode de construcþie a obiectelor ºi metode de eliberare a acestora. Metodele
de construcþie sunt apelate ori de câte ori este alocat un nou obiect din clasa respectivã. Putem declara mai
multe metode de construcþie, ele diferind prin parametrii din care trebuie construit obiectul.
Metodele de eliberare a obiectului sunt cele care elibereazã resursele ocupate de obiect în momentul în care
acesta este distrus de cãtre mecanismul automat de colectare de gunoaie. Fiecare clasã are o singurã metodã
de eliberare, numitã ºi finalizator. Apelarea acestei metode se face de cãtre sistem ºi nu existã nici o cale de
control al momentului în care se produce acest apel.
Pentru a declara o metodã, este necesar sã declarãm numele acesteia, tipul de valoare pe care o întoarce,
parametrii metodei precum ºi un bloc în care sã descriem instrucþiunile care trebuiesc executate atunci când
metoda este apelatã. În plus, orice metodã are un numãr de modificatori care descriu proprietãþile metodei ºi
Recunoaºterea unei anumite metode se face dupã numele ºi tipul parametrilor sãi. Pot exista metode cu
acelaºi nume dar având parametri diferiþi. Acest fenomen poartã numele de supraîncãrcarea numelui unei
metode.
Numele metodei este un identificator Java. Avem toatã libertatea în a alege numele pe care îl dorim pentru
metodele noastre, dar în general este preferabil sã alegem nume care sugereazã utilizarea metodei.
Numele unei metode începe de obicei cu literã micã. Dacã acesta este format din mai multe cuvinte, litera de
început a fiecãrui cuvânt va fi majusculã. În acest mod numele unei metode este foarte uºor de citit ºi de
depistat în sursã.
Parametrii metodei sunt în realitate niºte variabile care sunt iniþializate în momentul apelului cu valori care
controleazã modul ulterior de execuþie. Aceste variabile existã pe toatã perioada execuþiei metodei. Se pot
scrie metode care sã nu aibã nici un parametru.
Fiind o variabilã, fiecare parametru are un tip ºi un nume. Numele trebuie sã fie un identificator Java. Deºi
avem libertatea sã alegem orice nume dorim, din nou este preferabil sã alegem nume care sã sugereze scopul
la care va fi utilizat parametrul respectiv.
Tipul unui parametru este oricare dintre tipurile valide în Java. Acestea poate fi fie un tip primitiv, fie un tip
referinþã cãtre obiect, interfaþã sau tablou.
În momentul apelului unei metode, compilatorul încearcã sã gãseascã o metodã în interiorul clasei care sã
aibã acelaºi nume cu cel apelat ºi acelaºi numãr de parametri ca ºi apelul. Mai mult, tipurile parametrilor de
apel trebuie sã corespundã cu tipurile parametrilor declaraþi ai metodei gãsite sau sã poatã fi convertiþi la
aceºtia.
Dacã o astfel de metodã este gãsitã în declaraþia clasei sau în superclasele acesteia, parametrii de apel sunt
convertiþi cãtre tipurile declarate ºi se genereazã apelul cãtre metoda respectivã.
Este o eroare de compilare sã declarãm douã metode cu acelaºi nume, acelaºi numãr de parametri ºi acelaºi
tip pentru parametrii corespunzãtori. Într-o asemenea situaþie, compilatorul n-ar mai ºti care metodã trebuie
apelatã la un moment dat.
De asemenea, este o eroare de compilare sã existe douã metode care se potrivesc la acelaºi apel. Acest lucru
se întâmplã când nici una dintre metodele existente nu se potriveºte exact ºi când existã douã metode cu
acelaºi nume ºi acelaºi numãr de parametri ºi, în plus, parametrii de apel se pot converti cãtre parametrii
declaraþi ai ambelor metode.
Rezolvarea unei astfel de probleme se face prin conversia explicitã (cast) de cãtre programator a valorilor de
apel spre tipurile exacte ale parametrilor metodei pe care dorim sã o apelãm în realitate.
[,TipParametru NumeParametru]* )
Modificatorii sunt cuvinte cheie ale limbajului Java care specificã proprietãþi suplimentare pentru o metodã.
Iatã lista completã a acestora în cazul metodelor:
În mod normal, o metodã a unei clase se poate apela numai printr-o instanþã a clasei respective sau printr-o
instanþã a unei subclase. Acest lucru se datoreazã faptului cã metoda face apel la o serie de variabile ale
clasei care sunt memorate în interiorul instanþei ºi care au valori diferite în instanþe diferite. Astfel de
metode se numesc metode ale instanþelor clasei.
Dupã cum ºtim deja, existã ºi un alt tip de variabile, ºi anume variabilele de clasã sau variabilele statice care
sunt comune tuturor instanþelor clasei respective ºi existã pe toatã perioada de timp în care clasa este
încãrcatã în memorie. Aceste variabile pot fi accesate fãrã sã avem nevoie de o instanþã a clasei respective.
În mod similar existã ºi metode statice. Aceste metode nu au nevoie de o instanþã a clasei sau a unei subclase
pentru a putea fi apelate pentru cã ele nu au voie sã foloseascã variabile care sunt memorate în interiorul
instanþelor. În schimb, aceste metode pot sã foloseascã variabilele statice declarate în interiorul clasei.
Metodele abstracte sunt metode care nu au corp de implementare. Ele sunt declarate numai pentru a forþa
subclasele care vor sã aibã instanþe sã implementeze metodele respective.
Metodele abstracte trebuie declarate numai în interiorul claselor care au fost declarate abstracte. Altfel
compilatorul va semnala o eroare de compilare. Orice subclasã a claselor abstracte care nu este declaratã
abstractã trebuie sã ofere o implementare a acestor metode, altfel va fi generatã o eroare de compilare.
Prin acest mecanism ne asigurãm cã toate instanþele care pot fi convertite cãtre clasa care conþine definiþia
unei metode abstracte au implementatã metoda respectivã dar, în acelaºi timp, nu este nevoie sã
implementãm în nici un fel metoda chiar în clasa care o declarã pentru cã nu ºtim pe moment cum va fi
implementatã.
O metodã staticã nu poate fi declaratã ºi abstractã pentru cã o metodã staticã este implicit finalã ºi nu poate fi
rescrisã.
O metodã finalã este o metodã care nu poate fi rescrisã în subclasele clasei în care a fost declaratã. O metodã
este rescrisã într-o subclasã dacã aceasta implementeazã o metodã cu acelaºi nume ºi acelaºi numãr ºi tip de
parametri ca ºi metoda din superclasã.
Declararea metodelor finale este utilã în primul rând compilatorului care poate genera metodele respective
direct în codul rezultat fiind sigur cã metoda nu va avea nici o altã implementare în subclase.
Metodele native sunt metode care sunt implementate pe o cale specificã unei anumite platforme. De obicei
aceste metode sunt implementate în C sau în limbaj de asamblare. Metoda propriu-zisã nu poate avea corp de
implementare pentru cã implementarea nu este fãcutã în Java.
În rest, metodele native sunt exact ca orice altã metodã Java. Ele pot fi moºtenite, pot fi statice sau nu, pot fi
finale sau nu, pot sã rescrie o metodã din superclasã ºi pot fi la rândul lor rescrise în subclase.
O metodã sincronizatã este o metodã care conþine cod critic pentru un anumit obiect sau clasã ºi nu poate fi
rulatã în paralel cu nici o altã metodã criticã sau cu o instrucþiune synchronized referitoare la acelaºi
obiect sau clasã.
Înainte de execuþia metodei, obiectul sau clasa respectivã sunt blocate. La terminarea metodei, acestea sunt
deblocate.
Dacã metoda este staticã atunci este blocatã o întreagã clasã, clasa din care face parte metoda. Altfel, este
blocatã doar instanþa în contextul cãreia este apelatã metoda.
Accesul la metodele unei clase este protejat în acelaºi fel ca ºi accesul la variabilele clasei. În Java existã
patru grade de protecþie pentru o metodã aparþinând unei clase:
● privatã
● protejatã
● publicã
● prietenoasã
O metodã declaratã publicã este accesibilã oriunde este accesibil numele clasei. Cuvântul rezervat este
public.
O metodã declaratã protejatã este accesibilã în orice clasã din pachetul cãreia îi aparþine clasa în care este
declaratã. În acelaºi timp, metoda este accesibilã în toate subclasele clasei date, chiar dacã ele aparþin altor
pachete. Cuvântul rezervat este protected.
O metodã declaratã privatã este accesibilã doar în interiorul clasei în care a fost declaratã. Cuvântul rezervat
este private.
O metodã care nu are nici o declaraþie relativã la gradul de protecþie este automat o metodã prietenoasã. O
metodã prietenoasã este accesibilã în pachetul din care face parte clasa în interiorul cãreia a fost declaratã la
fel ca ºi o metodã protejatã. Dar, spre deosebire de metodele protejate, o metodã prietenoasã nu este
accesibilã în subclasele clasei date dacã aceste sunt declarate ca aparþinând unui alt pachet. Nu existã un
cuvânt rezervat pentru specificarea explicitã a metodelor prietenoase.
O metodã nu poate avea declarate mai multe grade de protecþie în acelaºi timp. O astfel de declaraþie este
semnalatã ca eroare de compilare.
Dacã o metodã poate arunca o excepþie, adicã sã apeleze instrucþiunea throw, ea trebuie sã declare tipul
acestor excepþii într-o clauzã throws. Sintaxa acesteia este:
Numele de tipuri specificate în clauza throws trebuie sã fie accesibile ºi sã fie asignabile la tipul de clasã
Throwable. Dacã o metodã conþine o clauzã throws, este o eroare de compilare ca metoda sã arunce un
obiect care nu este asignabil la compilare la tipurile de clase Error, RunTimeException sau la tipurile
de clase specificate în clauza throws.
Dacã o metodã nu are o clauzã throws, este o eroare de compilare ca aceasta sã poatã arunca o excepþie
normalã din interiorul corpului ei.
Pentru a apela o metodã a unei clase este necesar sã dispunem de o cale de acces la metoda respectivã. În
plus, trebuie sã dispunem de drepturile necesare apelului metodei.
CaleDeAcces.Metodã( Parametri )
În cazul în care metoda este staticã, pentru a specifica o cale de acces este suficient sã furnizãm numele
clasei în care a fost declaratã metoda. Accesul la numele clasei se poate obþine fie importând clasa sau întreg
pachetul din care face parte clasa fie specificând în clar numele clasei ºi drumul de acces cãtre aceasta.
De exemplu, pentru a accesa metoda random definitã static în clasa Math aparþinând pachetului java.
lang putem scrie:
sau, alternativ:
În cazul claselor definite în pachetul java.lang nu este necesar nici un import pentru cã acestea sunt
implicit importate de cãtre compilator.
Cea de-a doua cale de acces este existenþa unei instanþe a clasei respective. Prin aceastã instanþã putem
accesa metodele care nu sunt declarate statice, numite uneori ºi metode ale instanþelor clasei. Aceste metode
au nevoie de o instanþã a clasei pentru a putea lucra, pentru cã folosesc variabile non-statice ale clasei sau
apeleazã alte metode non-statice. Metodele primesc acest obiect ca pe un parametru ascuns.
De exemplu, având o instanþã a clasei Object sau a unei subclase a acesteia, putem obþine o reprezentare
sub formã de ºir de caractere prin:
În cazul în care apelãm o metodã a clasei din care face parte ºi metoda apelantã putem sã renunþãm la calea
de acces în cazul metodelor statice, scriind doar numele metodei ºi parametrii. Pentru metodele specifice
instanþelor, putem renunþa la calea de acces, dar în acest caz metoda acceseazã aceeaºi instanþã ca ºi metoda
apelantã. În cazul în care metoda apelantã este staticã, specificarea unei instanþe este obligatorie în cazul
metodelor de instanþã.
Parametrii de apel servesc împreunã cu numele la identificarea metodei pe care dorim sã o apelãm. Înainte de
a fi transmiºi, aceºtia sunt convertiþi cãtre tipurile declarate de parametri ai metodei, dupã cum este descris
mai sus.
Specificarea parametrilor de apel se face separându-i prin virgulã. Dupã ultimul parametru nu se mai pune
virgulã. Dacã metoda nu are nici un parametru, parantezele rotunde sunt în continuare necesare. Exemple de
apel de metode cu parametri:
O metodã trebuie sã-ºi declare tipul valorii pe care o întoarce. În cazul în care metoda doreºte sã specifice
explicit cã nu întoarce nici o valoare, ea trebuie sã declare ca tip de retur tipul void ca în exemplul:
void a() { … }
În caz general, o metodã întoarce o valoare primitivã sau un tip referinþã. Putem declara acest tip ca în:
Thread cautaFirulCurent() { … }
long abs( int valoare ) { … }
Pentru a returna o valoare ca rezultat al execuþiei unei metode, trebuie sã folosim instrucþiunea return, aºa
cum s-a arãtat în secþiunea dedicatã instrucþiunilor. Instrucþiunea return trebuie sã conþinã o expresie a
cãrei valoare sã poatã fi convertitã la tipul declarat al valorii de retur a metodei.
De exemplu:
Metoda staticã abs din clasa Math care primeºte un parametru întreg returneazã tot un întreg. În exemplul
nostru, instrucþiunea return este corectã pentru cã existã o cale de conversie de la întreg la întreg lung,
conversie care este apelatã automat de compilator înainte de ieºirea din metodã.
compilatorul va genera o eroare de compilare pentru cã metoda staticã abs din clasa Math care primeºte ca
parametru un întreg lung întoarce tot un întreg lung, iar un întreg lung nu poate fi convertit sigur la un întreg
normal pentru cã existã riscul deteriorãrii valorii, la fel ca la atribuire.
În cazul în care o metodã este declaratã void, putem sã ne întoarcem din ea folosind instrucþiunea return
fãrã nici o expresie. De exemplu:
void metoda() {
…
if( … )
return;
…
}
Specificarea unei expresii în acest caz duce la o eroare de compilare. La fel ºi în cazul în care folosim
instrucþiunea return fãrã nici o expresie în interiorul unei metode care nu este declaratã void.
6.3.5 Vizibilitate
O metodã este vizibilã dacã este declaratã în clasa prin care este apelatã sau într-una din superclasele
acesteia. De exemplu, dacã avem urmãtoarea declaraþie:
class A {
…
void a() { … }
}
class B extends A {
void b() {
a();
c();
…
}
void c() { .. }
…
}
Apelul metodei a în interiorul metodei b din clasa B este permis pentru cã metoda a este declaratã în
interiorul clasei A care este superclasã pentru clasa B. Apelul metodei c în aceeaºi metodã b este permis
pentru cã metoda c este declaratã în aceeaºi clasã ca ºi metoda a.
Uneori, o subclasã rescrie o metodã dintr-o superclasã a sa. În acest caz, apelul metodei respective în
interiorul subclasei duce automat la apelul metodei din subclasã. Dacã totuºi dorim sã apelãm metoda aºa
cum a fost ea definitã în superclasã, putem prefixa apelul cu numele superclasei. De exemplu:
class A {
…
void a() { … }
}
class B extends A {
void a() { .. }
void c() {
a();// metoda a din clasa B
A.a();// metoda a din clasa A
…
}
…
}
Desigur, pentru a "vedea" o metodã ºi a o putea apela, este nevoie sã avem drepturile necesare.
static BlocDeInstrucþiuni
Blocul de instrucþiuni este executat automat la încãrcarea clasei. De exemplu, putem defini un iniþializator
static în felul urmãtor:
class A {
static double a;
static int b;
static {
a = Math.random();
// numãr dublu între 0.0 ºi 1.0
b = ( int )( a * 500 );
// numãr întreg între 0 ºi 500
}
…
}
Declaraþiile de variabile statice ºi iniþializatorii statici sunt executate în ordinea în care apar în clasã. De
exemplu, dacã avem urmãtoarea declaraþie de clasã:
class A {
static int i = 11;
static {
i += 100;
i %= 55;
}
static int j = i + 1;
}
6.5.1 constructori
La crearea unei noi instanþe a unei clase sistemul alocã automat memoria necesarã instanþei ºi o iniþializeazã
cu valorile iniþiale specificate sau implicite. Dacã dorim sã facem iniþializãri suplimentare în interiorul
acestei memorii sau în altã parte putem descrie metode speciale numite constructori ai clasei.
Putem avea mai mulþi constructori pentru aceeaºi clasã, aceºtia diferind doar prin parametrii pe care îi
primesc. Numele tuturor constructorilor este acelaºi ºi este identic cu numele clasei.
Declaraþia unui constructor este asemãnãtoare cu declaraþia unei metode oarecare, cu diferenþa cã nu putem
specifica o valoare de retur ºi nu putem specifica nici un fel de modificatori. Dacã dorim sã returnãm dintr-un
constructor, trebuie sã folosim instrucþiunea return fãrã nici o expresie. Putem însã sã specificãm gradul
de protecþie al unui constructor ca fiind public, privat, protejat sau prietenos.
Dacã o clasã nu are constructori, compilatorul va crea automat un constructor implicit care nu ia nici un
parametru ºi care iniþializeazã toate variabilele clasei ºi apeleazã constructorul superclasei fãrã argumente
prin super(). Dacã superclasa nu are un constructor care ia zero argumente, se va genera o eroare de
compilare.
Dacã o clasã are cel puþin un constructor, constructorul implicit nu mai este creat de cãtre compilator.
Când construim corpul unui constructor avem posibilitatea de a apela, pe prima linie a blocului de
instrucþiuni care reprezintã corpul constructorului, un constructor explicit. Constructorul explicit poate avea
douã forme:
this( [Parametri] );
super( [Parametri] );
Cu aceastã sintaxã apelãm unul dintre constructorii superclasei sau unul dintre ceilalþi constructori din
aceeaºi clasã. Aceste linii nu pot apãrea decât pe prima poziþie în corpul constructorului. Dacã nu apar acolo,
compilatorul considerã implicit cã prima instrucþiune din corpul constructorului este:
super();
ªi în acest caz se va genera o eroare de compilare dacã nu existã un constructor în superclasã care sã lucreze
fãrã nici un parametru.
Dupã apelul explicit al unui constructor din superclasã cu sintaxa super( … ) este executatã în mod implicit
iniþializarea tuturor variabilelor de instanþã (non-statice) care au iniþializatori expliciþi. Dupã apelul unui
constructor din aceeaºi clasã cu sintaxa this( … ) nu existã nici o altã acþiune implicitã, deci nu vor fi
iniþializate nici un fel de variabile. Aceasta datoritã faptului cã iniþializarea s-a produs deja în constructorul
apelat.
Iatã ºi un exemplu:
class A extends B {
String valoare;
A( String val ) {
// aici existã apel implicit
// al lui super(), adicã B()
valoare = val;
}
A( int val ) {
this( String.valueOf( val ) );// alt constructor
}
} ^
6.5.2 Finalizatori
În Java nu este nevoie sã apelãm în mod explicit distrugerea unei instanþe atunci când nu mai este nevoie de
ea. Sistemul oferã un mecanism de colectare a gunoaielor care recunoaºte situaþia în care o instanþã de
obiect sau un tablou nu mai sunt referite de nimeni ºi le distruge în mod automat.
Acest mecanism de colectare a gunoaielor ruleazã pe un fir de execuþie separat, de prioritate micã. Nu avem
nici o posibilitate sã aflãm exact care este momentul în care va fi distrusã o instanþã. Totuºi, putem specifica
o funcþie care sã fie apelatã automat în momentul în care colectorul de gunoaie încearcã sã distrugã obiectul.
Aceastã funcþie are nume, numãr de parametri ºi tip de valoare de retur fixe:
void finalize()
Dupã apelul metodei de finalizare (numitã ºi finalizator), instanþa nu este încã distrusã pânã la o nouã
verificare din partea colectorului de gunoaie. Aceastã comportare este necesarã pentru cã instanþa poate fi
revitalizatã prin crearea unei referinþe cãtre ea în interiorul finalizatorului.
Totuºi, finalizatorul nu este apelat decât o singurã datã. Dacã obiectul revitalizat redevine candidat la
colectorul de gunoaie, acesta este distrus fãrã a i se mai apela finalizatorul. Cu alte cuvinte, un obiect nu
poate fi revitalizat decât o singurã datã.
Dacã în timpul finalizãrii apare o excepþie, ea este ignoratã ºi finalizatorul nu va mai fi apelat din nou.
O instanþã este creatã folosind o expresie de alocare care foloseºte cuvântul rezervat new. Iatã care sunt paºii
care sunt executaþi la apelul acestei expresii:
● Se creeazã o nouã instanþã de tipul specificat. Toate variabilele instanþei sunt iniþializate pe valorile
lor implicite.
● Se apeleazã constructorul corespunzãtor în funcþie de parametrii care sunt transmiºi în expresia de
alocare. Dacã instanþa este creatã prin apelul metodei newInstance, se apeleazã constructorul care
nu ia nici un argument.
● Dupã creare, expresia de alocare returneazã o referinþã cãtre instanþa nou creatã.
Exemple de creare:
A o1 = new A();
B o2 = new B();
class C extends B {
String valoare;
C( String val ) {
O altã cale de creare a unui obiect este apelul metodei newInstance declarate în clasa Class. Iatã paºii
de creare în acest caz:
● Se creeazã o nouã instanþã de acelaºi tip cu tipul clasei pentru care a fost apelatã metoda
newInstance. Toate variabilele instanþei sunt iniþializate pe valorile lor implicite.
● Este apelat constructorul obiectului care nu ia nici un argument.
● Dupã creare referinþa cãtre obiectul nou creat este returnatã ca
valoare a metodei newInstance. Tipul acestei referinþe va fi Object în timpul compilãrii ºi tipul clasei reale
în timpul execuþiei.
O clasã poate fi derivatã dintr-o singurã altã clasã, cu alte cuvinte o clasã poate avea o singurã superclasã
imediatã.
Clasa derivatã moºteneºte toate variabilele ºi metodele superclasei sale. Totuºi, ea nu poate accesa decât acele
variabile ºi metode care nu sunt declarate private.
Putem rescrie o metodã a superclasei declarând o metodã în noua clasã având acelaºi nume ºi aceiaºi
parametri. La fel, putem declara o variabilã care are acelaºi nume cu o variabilã din superclasã. În acest caz,
noul nume ascunde vechea variabilã, substituindu-i-se. Putem în continuare sã ne referim la variabila ascunsã
din superclasã specificând numele superclasei sau folosindu-ne de variabila super.
Exemplu:
class A {
int a = 1;
void unu() {
System.out.println( a );
}
}
class B extends A {
double a = Math.PI;
void unu() {
System.out.println( a );
}
void doi() {
System.out.println( A.a );
}
void trei() {
unu();
super.unu();
}
}
Dacã apelãm metoda unu din clasa A, aceasta va afiºa la consolã numãrul 1. Acest apel se va face cu
instrucþiunea:
Dacã apelãm metoda unu din clasa B, aceasta va afiºa la consolã numãrul PI. Apelul îl putem face de
exemplu cu instrucþiunea:
Observaþi cã în metoda unu din clasa B, variabila referitã este variabila a din clasa B. Variabila a din clasa A
este ascunsã. Putem însã sã o referim prin sintaxa A.a ca în metoda doi din clasa B.
În interiorul clasei B, apelul metodei unu fãrã nici o altã specificaþie duce automat la apelul metodei unu
definite în interiorul clasei B. Metoda unu din clasa B rescrie metoda unu din clasa A. Vechea metodã este
accesibilã pentru a o referi în mod explicit ca în metoda trei din clasa B. Apelul acestei metode va afiºa mai
întâi numãrul PI ºi apoi numãrul 1.
Dacã avem declaratã o variabilã de tip referinþã cãtre o instanþã a clasei A, aceastã variabilã poate sã conþinã
în timpul execuþiei ºi o referinþã cãtre o instanþã a clasei B. Invers, afirmaþia nu este valabilã.
În clipa în care apelãm metoda unu pentru o variabilã referinþã cãtre clasa A, sistemul va apela metoda unu a
clasei A sau B în funcþie de adevãratul tip al referinþei din timpul execuþiei. Cu alte cuvinte, urmãtoarea
secvenþã de instrucþiuni:
va afiºa douã numere diferite, mai întâi 1 ºi apoi PI. Aceasta din cauzã cã cel de-al doilea element din tablou
este, în timpul execuþiei, de tip referinþã la o instanþã a clasei B chiar dacã la compilare este de tipul
referinþã la o instanþã a clasei A.
Acest mecanism se numeºte legare târzie, ºi înseamnã cã metoda care va fi efectiv apelatã este stabilitã doar
în timpul execuþiei ºi nu la compilare.
Dacã nu declarãm nici o superclasã în definiþia unei clase, atunci se considerã automat cã noua clasã derivã
direct din clasa Object, moºtenind toate metodele ºi variabilele acesteia.
6.7 Interfeþe
O interfaþã este în esenþã o declaraþie de tip ce constã dintr-un set de metode ºi constante pentru care nu s-a
specificat nici o implementare. Programele Java folosesc interfeþele pentru a suplini lipsa moºtenirii
multiple, adicã a claselor de obiecte care derivã din douã sau mai multe alte clase.
Corp
Modificatorii unei interfeþe pot fi doar cuvintele rezervate public ºi abstract. O interfaþã care este
publicã poate fi accesatã ºi de cãtre alte pachete decât cel care o defineºte. În plus, fiecare interfaþã este în
mod implicit abstractã. Modificatorul abstract este permis dar nu obligatoriu.
Numele interfeþelor trebuie sã fie identificatori Java. Convenþiile de numire a interfeþelor le urmeazã în
general pe cele de numire a claselor de obiecte.
Interfeþele, la fel ca ºi clasele de obiecte, pot avea subinterfeþe. Subinterfeþele moºtenesc toate constantele ºi
declaraþiile de metode ale interfeþei din care derivã ºi pot defini în plus noi elemente. Pentru a defini o
subinterfaþã, folosim o clauzã extends. Aceste clauze specificã superinterfaþa unei interfeþe. O interfaþã
poate avea mai multe superinterfeþe care se declarã separate prin virgulã dupã cuvântul rezervat extends.
Circularitatea definiþiei subinterfeþelor nu este permisã.
În cazul interfeþelor nu existã o rãdãcinã comunã a arborelui de derivare aºa cum existã pentru arborele de
clase, clasa Object.
În corpul unei declaraþii de interfaþã pot sã aparã declaraþii de variabile ºi declaraþii de metode. Variabilele
sunt implicit statice ºi finale. Din cauza faptului cã variabilele sunt finale, este obligatoriu sã fie specificatã o
valoare iniþialã pentru aceste variabile. În plus, aceastã valoare iniþialã trebuie sã fie constantã (sã nu
depindã de alte variabile).
Dacã interfaþa este declaratã publicã, toate variabilele din corpul sãu sunt implicit declarate publice.
În ceea ce priveºte metodele declarate în interiorul corpului unei interfeþe, acestea sunt implicit declarate
abstracte. În plus, dacã interfaþa este declaratã publicã, metodele din interior sunt implicit declarate publice.
Cele douã interfeþe definesc comportamentul unui obiect spaþial respectiv al unui obiect spaþio-temporal.
Un obiect spaþial are o greutate, un volum ºi o razã a sferei minime în care se poate înscrie. În plus, putem
defini tipul unui obiect folosindu-ne de o serie de valori constante predefinite precum ar fi SFERA sau CUB.
Un obiect spaþio-temporal este un obiect spaþial care are în plus o poziþie pe axa timpului. Pentru un astfel
de obiect, în afarã de proprietãþile deja descrise pentru obiectele spaþiale, trebuie sã avem în plus un moment
iniþial, de apariþie, pe axa timpului ºi un moment final. Obiectul nostru nu existã în afara acestui interval de
timp. În plus, pentru un astfel de obiect putem afla poziþia centrului sãu de greutate în fiecare moment aflat
în intervalul de existenþã.
Pentru a putea lucra cu obiecte spaþiale ºi spaþio-temporale este nevoie sã definim diverse clase care sã
implementeze aceste interfeþe. Acest lucru se face specificând clauza implements în declaraþia de clasã.
O clasã poate implementa mai multe interfeþe. Dacã o clasã declarã cã implementeazã o anumitã interfaþã,
ea este obligatoriu sã implementeze toate metodele declarate în interfaþa respectivã.
De exemplu, putem spune cã o minge este un obiect spaþial de tip sferã. În plus, mingea are o poziþie în
funcþie de timp ºi un interval de existenþã. Cu alte cuvinte, mingea este chiar un obiect spaþio-temporal.
Desigur, în afarã de proprietãþile spaþio-temporale mingea mai are ºi alte proprietãþi precum culoarea,
proprietarul sau preþul de cumpãrare.
import java.awt.Color;
class Minge extends Jucarie implements ObiectSpatioTemporal {
int culoare = Color.red;
double pret = 10000.0;
double raza = 0.25;
long nastere;
long moarte;
// metodele din ObiectSpatial
double greutate() {
return raza * 0.5;
}
double raza() {
return raza;
}
double volum() {
return ( 4.0 / 3.0 ) * Math.PI * raza * raza * raza;
}
int tip() {
return SFERA;
}
return nastere;
}
long momentFinal() {
return moarte;
}
int ceCuloare() {
return culoare;
}
double cePret() {
return pret;
}
}
Sã presupunem în continuare cã avem ºi o altã clasã, Rezervor, care este tot un obiect spaþio-temporal, dar
de formã cubicã. Declaraþia acestei clase ar arãta ca:
…
}
Desigur, toate metodele din interfeþele de mai sus trebuiesc implementate, plus alte metode specifice.
Sã mai observãm cã cele douã obiecte derivã din clase diferite: Mingea din Jucãrii iar Rezervorul din
Construcþii. Dacã am putea deriva o clasã din douã alte clase, am putea deriva Minge din Jucarie ºi
ObiectSpatioTemporal iar Rezervor din Constructie ºi ObiectSpaþioTemporal. Într-o
astfel de situaþie, nu ar mai fi necesar ca ObiectSpaþioTemporal sã fie o interfaþã, ci ar fi suficient ca
acesta sã fie o altã clasã.
Din pãcate, în Java, o clasã nu poate deriva decât dintr-o singurã altã clasã, aºa cã este obligatoriu în astfel de
situaþii sã folosim interfeþele. Dacã ObiectSpaþioTemporal ar fi putut fi o clasã, am fi avut avantajul
cã puteam implementa acolo metodele cu funcþionare identicã din cele douã clase discutate, acestea fiind
automat moºtenite fãrã a mai fi nevoie de definirea lor de douã ori în fiecare clasã în parte.
Putem crea în continuare metode care sã lucreze cu obiecte spaþio-temporale, de exemplu o metodã care sã
afle distanþa unui corp spaþio-temporal faþã de un punct dat la momentul sãu iniþial. O astfel de metodã se
poate scrie o singurã datã, ºi poate lucra cu toate clasele care implementeazã interfaþa noastrã. De exemplu:
…
double distanta( double punct[], ObiectSpatioTemporal obiect ) {
Putem apela metoda atât cu un obiect din clasa Minge cât ºi cu un obiect din clasa Rezervor.
Compilatorul nu se va plânge pentru cã el ºtie cã ambele clase implementeazã interfaþa
ObiectSpaþioTemporal, aºa cã metodele apelate în interiorul calculului distanþei (momentInitial ºi
centruDeGreutate) sunt cu siguranþã implementate în ambele clase. Deci, putem scrie:
Minge minge;
Rezervor rezervor;
double punct[] = { 10.0, 45.0, 23.0 };
distanþa( punct, minge );
distanþa( punct, rezervor );
Desigur, în mod normal ar fi trebuit sã proiectãm ºi un constructor sau mai mulþi care sã iniþializeze
obiectele noastre cu valori rezonabile. Aceºti constructori ar fi stat cu siguranþã în definiþia claselor ºi nu în
definiþia interfeþelor. Nu avem aici nici o cale de a forþa definirea unui anumit constructor cu ajutorul
interfeþei.
[cuprins]
Capitolul VII
Structura programelor
● 8.1 Pachete de clase
● 8.2 Importul claselor
● 8.3 Fiºiere sursã
● 8.4 Compilare ºi execuþie
[NumePachet.]* NumeComponentãPachet
Java. De obicei, aceste nume urmeazã structura de directoare în care sunt memorate clasele compilate.
Rãdãcina arborelui de directoare în care
sunt memorate clasele este indicatã de o variabilã sistem CLASSPATH. În DOS aceasta se seteazã în
felul urmãtor:
set CLASSPATH=.;c:\java\lib
dacã lucraþi cu bash . Din aceastã rãdãcinã, fiecare pachet are propriul director. În director existã codul
binar pentru componentele pachetului respectiv. Dacã pachetul conþine subpachete, atunci acestea sunt
memorate într-un subdirector în interiorul directorului pachetului.
Creatorii Java recomandã folosirea unei reguli unice de numire a pachetelor, astfel încât sã nu aparã
conflicte. Convenþia recomandatã de ei este aceea de a folosi numele domeniului Internet aparþinând
producãtorului claselor. Astfel, numele de pachete ar putea arãta ca în:
COM.Microsoft.OLE
COM.Apple.quicktime.v2
import numeClasã ;
unde numele clasei include ºi pachetul din care aceasta face parte. De exemplu:
import java.awt.Graphics;
import java.applet.Applet;
Se poate importa ºi un pachet întreg, adicã toate clasele aparþinând acelui pachet, printr-o instrucþiune
de forma:
import numePachet.*;
De exemplu:
import java.awt.*;
Dacã dorim sã înregistrãm codul clasei într-un anumit pachet, putem sã includem la începutul fiºierului
sursã o declaraþie de forma:
package numePachet;
dacã aceastã declaraþie lipseºte, clasa va fi plasatã în pachetul implicit, care nu are nume.
Comanda aceasta, ca ºi celelalte descrise în acest paragraf este specificã mediului de dezvoltare Java pus
la dispoziþie de Sun, numit JDK (Java Development Kit). În viitor este probabil sã aparã multe alte
medii de dezvoltare care vor avea propriile lor compilatoare ºi interpretoare ºi, posibil, propriile linii de
comandã.
La compilare, variabila sistem CLASSPATH trebuie sã fie deja setatã pentru cã însuºi compilatorul Java
actual este scris în Java.
java NumeClasã
unde numele clasei este numele aplicaþiei care conþine metoda main . Interpretorul va cãuta un fiºier cu
numele NumeClasã.class ºi va încerca sã instanþieze clasa respectivã.
Pentru lansarea unui aplet veþi avea nevoie de un document HTML care conþine tagul APPLET ºi ca
parametru al acesteia
name=NumeClasã.class
La lansarea unui aplet, clasele care sunt apelate de clasa principalã sunt mai întâi cãutate pe sistemul pe
care ruleazã navigatorul. Dacã nu sunt acolo, ele vor fi transferate în reþea. Asta înseamnã cã transferul
de cod este relativ mic, trebuie transferat doar codul specific aplicaþiei.
[cuprins]
Capitolul IX
Fire de execuþie ºi sincronizare
● 9.1 Crearea firelor de execuþie
● 9.2 Stãrile unui fir de execuþie
● 9.3 Prioritatea firelor de execuþie
● 9.4 Grupuri de fire de execuþie
● 9.5 Enumerarea firelor de execuþie
● 9.6 Sincronizare
● 9.7 Un exemplu
● 9.8 Un exemplu Runnable
O aplicaþie Java ruleazã în interiorul unui proces al sistemului de operare. Acest proces constã din segmente de cod ºi
segmente de date mapate într-un spaþiu virtual de adresare. Fiecare proces deþine un numãr de resurse alocate de cãtre
sistemul de operare, cum ar fi fiºiere deschise, regiuni de memorie alocate dinamic, sau fire de execuþie. Toate aceste
resurse deþinute de cãtre un proces sunt eliberate la terminarea procesului de cãtre sistemul de operare.
Un fir de execuþie este unitatea de execuþie a unui proces. Fiecare fir de execuþie are asociate o secvenþã de
instrucþiuni, un set de regiºitri CPU ºi o stivã. Atenþie, un proces nu executã nici un fel de instrucþiuni. El este de fapt
un spaþiu de adresare comun pentru unul sau mai multe fire de execuþie. Execuþia instrucþiunilor cade în
responsabilitatea firelor de execuþie. În cele ce urmeazã vom prescurta uneori denumirea firelor de execuþie, numindu-
le pur ºi simplu fire .
În cazul aplicaþiilor Java interpretate, procesul deþine în principal codul interpretorului iar codul binar Java este tratat
ca o zonã de date de cãtre interpretor. Dar, chiar ºi în aceastã situaþie, o aplicaþie Java poate avea mai multe fire de
execuþie, create de cãtre interpretor ºi care executã, seturi distincte de instrucþiuni binare Java.
Fiecare dintre aceste fire de execuþie poate rula în paralel pe un procesor separat dacã maºina pe care ruleazã aplicaþia
este o maºinã cu mai multe procesoare. Pe maºinile monoprocesor, senzaþia de execuþie în paralel a firelor de execuþie
este creatã prin rotirea acestora pe rând la controlul unitãþii centrale, câte o cuantã de timp fiecare. Algoritmul de rotire
al firelor de execuþie este de tip round-robin.
Mediul de execuþie Java executã propriul sãu control asupra firelor de execuþie. Algoritmul pentru planificarea firelor
de execuþie, prioritãþile ºi stãrile în care se pot afla acestea sunt specifice aplicaþiilor Java ºi implementate identic pe
toate platformele pe care a fost portat mediul de execuþie Java. Totuºi, acest mediu ºtie sã profite de resursele
sistemului pe care lucreazã. Dacã sistemul gazdã lucreazã cu mai multe procesoare, Java va folosi toate aceste
procesoare pentru a-ºi planifica firele de execuþie. Dacã sistemul oferã multitasking preemptiv, multitaskingul Java va
fi de asemenea preemptiv, etc.
În cazul maºinilor multiprocesor, mediul de execuþie Java ºi sistemul de operare sunt responsabile cu repartizarea
firelor de execuþie pe un procesor sau altul. Pentru programator, acest mecanism este complet transparent, neexistând
nici o diferenþã între scrierea unei aplicaþii cu mai multe fire pentru o maºinã cu un singur procesor sau cu mai multe.
Desigur, existã însã diferenþe în cazul scrierii aplicaþiilor pe mai multe fire de execuþie faþã de acelea cu un singur fir
de execuþie, diferenþe care provin în principal din cauza necesitãþii de sincronizare între firele de execuþie aparþinând
aceluiaºi proces.
Sincronizarea firelor de execuþie înseamnã cã acestea se aºteaptã unul pe celãlalt pentru completarea anumitor operaþii
care nu se pot executa în paralel sau care trebuie executate într-o anumitã ordine. Java oferã ºi în acest caz mecanismele
sale proprii de sincronizare, extrem de uºor de utilizat ºi înglobate în chiar sintaxa de bazã a limbajului.
La lansarea în execuþie a unei aplicaþii Java este creat automat ºi un prim fir de execuþie, numit firul principal. Acesta
poate ulterior sã creeze alte fire de execuþie care la rândul lor pot crea alte fire, ºi aºa mai departe. Firele de execuþie
dintr-o aplicaþie Java pot fi grupate în grupuri pentru a fi manipulate în comun.
În afarã de firele normale de execuþie, Java oferã ºi fire de execuþie cu prioritate micã care lucreazã în fundalul
aplicaþiei atunci când nici un alt fir de execuþie nu poate fi rulat. Aceste fire de fundal se numesc demoni ºi executã
operaþii costisitoare în timp ºi independente de celelalte fire de execuþie. De exemplu, în Java colectorul de gunoaie
lucreazã pe un fir de execuþie separat, cu proprietãþi de demon. În acelaºi fel poate fi gândit un fir de execuþie care
executã operaþii de încãrcare a unor imagini din reþea.
O aplicaþie Java se terminã atunci când se terminã toate firele de execuþie din interiorul ei sau când nu mai existã decât
fire demon. Terminarea firului principal de execuþie nu duce la terminarea automatã a aplicaþiei.
În primul caz, noua clasã moºteneºte toate metodele ºi variabilele clasei Thread care implementeazã în mod standard,
în Java, funcþionalitatea de lucru cu fire de execuþie. Singurul lucru pe care trebuie sã-l facã noua clasã este sã
reimplementeze metoda run care este apelatã automat de cãtre mediul de execuþie la lansarea unui nou fir. În plus,
noua clasã ar putea avea nevoie sã implementeze un constructor care sã permitã atribuirea unei denumiri firului de
execuþie.
Dacã firul are un nume, acesta poate fi obþinut cu metoda getName care returneazã un obiect de tip String .
Dacã vom crea un nou obiect de tip FirNou ºi îl lansãm în execuþie acesta va afiºa la infinit mesajul "Tastaþi ^C".
Întreruperea execuþiei se poate face într-adevãr prin tastarea caracterului ^C, caz în care întreaga aplicaþie este
terminatã. Atâta timp însã cât noul obiect nu va fi întrerupt din exterior, aplicaþia va continua sã se execute pentru cã
mai existã încã fire de execuþie active ºi indiferent de faptul cã firul de execuþie principal s-a terminat sau nu.
public TestFirNou {
public static void main( String[] ) {
new FirNou( "Primul" ).start();
}
}
Metoda start , predefinitã în obiectul Thread lanseazã execuþia propriu-zisã a firului. Desigur existã ºi cãi de a opri
execuþia la nesfârºit a firului creat fie prin apelul metodei stop , prezentatã mai jos, fie prin rescrierea funcþiei run în
aºa fel încât execuþia sa sã se termine dupã un interval finit de timp.
A doua cale de definiþie a unui fir de execuþie este implementarea interfeþei Runnable într-o anumitã clasã de
obiecte. Aceastã cale este cea care trebuie aleasã atunci când clasa pe care o creãm nu se poate deriva din clasa
Thread pentru cã este important sã fie derivatã din altã clasã. Desigur, moºtenirea multiplã ar rezolva aceastã
problemã, dar Java nu are moºtenire multiplã.
class Oclasa {
…
}
class FirNou extends Oclasa implements Runnable {
public void run() {
for( int i = 0; i < 100; i++ ) {
System.out.println( "pasul " + i );
}
}
…
}
public class TestFirNou {
public static void main( String argumente[] ) {
new Thread( new FirNou() ).start();
// Obiectele sunt create ºi folosite imediat
// La terminarea instrucþiunii, ele sunt automat
// eliberate nefiind referite de nimic
}
}
Dupã cum observaþi, clasa Thread are ºi un constructor care primeºte ca argument o instanþã a unei clase care
implementeazã interfaþa Runnable . În acest caz, la lansarea în execuþie a noului fir, cu metoda start , se apeleazã
metoda run din acest obiect ºi nu din instanþa a clasei Thread .
Atunci când dorim sã creãm un aplet care sã ruleze pe un fir de execuþie separat faþã de pagina de navigator în care
ruleazã pentru a putea executa operaþii în fereastra apletului ºi în acelaºi timp sã putem folosi în continuare navigatorul,
suntem obligaþi sã alegem cea de-a doua cale de implementare. Aceasta pentru cã apletul nostru trebuie sã fie derivat
din clasa standard Applet . Singura alternativã care ne rãmâne este aceea de a implementa în aplet interfaþa
Runnable .
Atunci când este creat, dar înainte de apelul metodei start, firul se gãseºte într-o stare pe care o vom numi Fir Nou
Creat . În aceastã stare, singurele metode care se pot apela pentru firul de execuþie sunt metodele start ºi stop .
Metoda start lanseazã firul în execuþie prin apelul metodei run . Metoda stop omoarã firul de execuþie încã
înainte de a fi lansat. Orice altã metodã apelatã în aceastã stare provoacã terminarea firului de execuþie prin generarea
unei excepþii de tip IllegalThreadStateException .
Dacã apelãm metoda start pentru un Fir Nou Creat firul de execuþie va trece în starea Ruleazã . În aceastã
stare, instrucþiunile din corpul metodei run se executã una dupã alta. Execuþia poate fi opritã temporar prin apelul
metodei sleep care primeºte ca argument un numãr de milisecunde care reprezintã intervalul de timp în care firul
trebuie sã fie oprit. Dupã trecerea intervalului, firul de execuþie va porni din nou.
În timpul în care se scurge intervalul specificat de sleep , obiectul nu poate fi repornit prin metode obiºnuite. Singura
cale de a ieºi din aceastã stare este aceea de a apela metoda interrupt . Aceastã metodã aruncã o excepþie de tip
InterruptedException care nu este prinsã de sleep dar care trebuie prinsã obligatoriu de metoda care a apelat
metoda sleep . De aceea, modul standard în care se apeleazã metoda sleep este urmãtorul:
…
try {
sleep( 1000 ); // o secundã
} catch( InterruptedException ) {
…
}
…
Dacã dorim oprirea firului de execuþie pe timp nedefinit, putem apela metoda suspend . Aceasta trece firul de
execuþie într-o nouã stare, numitã Nu Ruleazã . Aceeaºi stare este folositã ºi pentru oprirea temporarã cu sleep .
În cazul apelului suspend însã, execuþia nu va putea fi reluatã decât printr-un apel al metodei resume . Dupã acest
apel, firul va intra din nou în starea Ruleazã .
Pe timpul în care firul de execuþie se gãseºte în starea Nu Ruleazã , acesta nu este planificat niciodatã la controlul
unitãþii centrale, aceasta fiind cedatã celorlalte fire de execuþie din aplicaþie.
Firul de execuþie poate intra în starea Nu Ruleazã ºi din alte motive. De exemplu se poate întâmpla ca firul sã
aºtepte pentru terminarea unei operaþii de intrare/ieºire de lungã duratã caz în care firul va intra din nou în starea
Ruleazã doar dupã terminarea operaþiei.
O altã cale de a ajunge în starea Nu Ruleazã este aceea de a apela o metodã sau o secvenþã de instrucþiuni
sincronizatã dupã un obiect. În acest caz, dacã obiectul este deja blocat, firul de execuþie va fi oprit pânã în clipa în
care obiectul cu pricina apeleazã metoda notify sau notifyAll .
În fine, atunci când metoda run ºi-a terminat execuþia, obiectul intrã în starea Mort . Aceastã stare este pãstratã pânã
în clipa în care obiectul
este eliminat din memorie de mecanismul de colectare a gunoaielor. O altã posibilitate de a intra în starea Mort este
aceea de a apela metoda stop .
Atunci când se apeleazã metoda stop , aceasta aruncã cu o instrucþiune throw o eroare numitã ThreadDeath .
Aceasta poate fi prinsã de cãtre cod pentru a efectua curãþenia necesarã. Codul necesar este urmãtorul:
…
try {
firDeExecutie.start();
…
} catch( ThreadDeath td ) {
… // curãþenie
throw td; // se aruncã obiectul mai departe
// pentru a servi la distrugerea
// firului de execuþie
}
Desigur, firul de execuþie poate fi terminat ºi pe alte cãi, caz în care metoda stop nu este apelatã ºi eroarea
ThreadDeath nu este aruncatã. În aceste situaþii este preferabil sã ne folosim de o clauzã finally ca în:
…
try {
firDeExecutie.start();
…
} finally {
..// curãþenie
}
În fine, dacã nu se mai poate face nimic pentru cã firul de execuþie nu mai rãspunde la comenzi, puteþi apela la calea
disperatã a metodei destroy . Din pãcate, metoda destroy terminã firul de execuþie fãrã a proceda la curãþirile
necesare în memorie.
Atunci când un fir de execuþie este oprit cu comanda stop , mai este nevoie de un timp pânã când sistemul efectueazã
toate operaþiile necesare opririi. Din aceastã cauzã, este preferabil sã aºteptãm în mod explicit terminarea firului prin
apelul metodei join :
firDeExecutie.stop()
try {
firDeExecutie.join();
} catch( InterruptedException e ) {
…
}
Excepþia de întrerupere trebuie prinsã obligatoriu. Dacã nu apelãm metoda join pentru a aºtepta terminarea ºi metoda
stop este de exemplu apelatã pe ultima linie a funcþiei main , existã ºansa ca sistemul sã creadã cã firul auxiliar de
execuþie este încã în viaþã ºi aplicaþia Java sã nu se mai termine rãmânând într-o stare de aºteptare. O puteþi desigur
termina tastând ^C.
Mediul de execuþie Java planificã firele de execuþie la controlul unitãþii centrale în funcþie de prioritatea lor. Dacã
existã mai multe fire cu prioritate maximã, acestea sunt planificate dupã un algoritm round-robin. Firele de prioritate
mai micã intrã în calcul doar atunci când toate firele de prioritate mare sunt în starea Nu Ruleazã .
Prioritatea unui fir de execuþie se poate interoga cu metoda getPriority care întoarce un numãr întreg care
reprezintã prioritatea curentã a firului de execuþie. Pentru a seta prioritatea, se foloseºte metoda setPriority care
primeºte ca parametru un numãr întreg care reprezintã prioritatea doritã.
Schimbarea prioritãþii unui fir de execuþie este o treabã periculoasã dacã metoda cu prioritate mare nu se terminã
foarte repede sau dacã nu are opriri dese. În caz contrar, celelalte metode nu vor mai putea primi controlul unitãþii
centrale.
Existã însã situaþii în care putem schimba aceastã prioritate fãrã pericol, de exemplu când avem un fir de execuþie care
nu face altceva decât sã citeascã caractere de la utilizator ºi sã le memoreze într-o zonã temporarã. În acest caz, firul de
execuþie este în cea mai mare parte a timpului în starea Nu Ruleazã din cauzã cã aºteaptã terminarea unei
operaþii de intrare/ieºire. În clipa în care utilizatorul tasteazã un caracter, firul va ieºi din starea de aºteptare ºi va fi
primul planificat la execuþie din cauza prioritãþii sale ridicate. În acest fel utilizatorul are senzaþia cã aplicaþia
rãspunde foarte repede la comenzile sale.
În alte situaþii, avem de executat o sarcinã cu prioritate micã. În aceste cazuri, putem seta pentru firul de execuþie care
executã aceste sarcini o prioritate redusã.
Alternativ, putem defini firul respectiv de execuþie ca un demon. Dezavantajul în aceastã situaþie este faptul cã
aplicaþia va fi terminatã atunci când existã doar demoni în lucru ºi existã posibilitatea pierderii de date. Pentru a declara
un fir de execuþie ca demon, putem apela metoda setDaemon. Aceastã metodã primeºte ca parametru o valoare
booleanã care dacã este true firul este fãcut demon ºi dacã nu este adus înapoi la starea normalã. Putem testa faptul cã
un fir de execuþie este demon sau nu cu metoda isDemon .
La pornirea unei aplicaþii Java, se creeazã automat un prim grup de fire de execuþie, numit grupul principal, main .
Firul principal de execuþie
face parte din acest grup. În continuare, ori de câte ori creãm un nou fir de execuþie, acesta va face parte din acelaºi
grup de fire de execuþie ca ºi firul de execuþie din interiorul cãruia a fost creat, în afarã de cazurile în care în
constructorul firului specificãm explicit altceva.
Într-un grup de fire de execuþie putem defini nu numai fire dar ºi alte grupuri de execuþie. Se creeazã astfel o
arborescenþã a cãrei rãdãcinã este grupul principal de fire de execuþie.
Pentru a specifica pentru un fir un nou grup de fire de execuþie, putem apela constructorii obiºnuiþi dar introducând un
prim parametru suplimentar de tip ThreadGroup . De exemplu, putem folosi urmãtorul cod:
Acest nou fir de execuþie va face parte dintr-un alt grup de fire decât firul principal. Putem afla grupul de fire de
execuþie din care face parte un anumit fir apelând metoda getThreadGroup , ca în secvenþa:
Operaþiile definite pentru un grup de fire de execuþie sunt clasificabile în operaþii care acþioneazã la nivelul grupului,
cum ar fi aflarea numelui, setarea unei prioritãþi maxime, etc., ºi operaþii care acþioneazã asupra fiecãrui fir de
execuþie din grup, cum ar fi stop , suspend sau resume . Unele dintre aceste operaþii necesitã aprobarea
controloarelor de securitate acest lucru fãcându-se printr-o metodã numitã checkAccess . De exemplu, nu puteþi
seta prioritatea unui fir de execuþie decât dacã aveþi drepturile de acces necesare.
Pentru a afla câte fire active sunt în grupul respectiv la un moment dat, putem apela metoda activeCount din clasa
ThreadGroup . De exemplu:
public listeazaFire {
ThreadGroup grup = Thread.currentThread().getThreadGroup();
int numarFire = grup.activeCount();
Thread fire[] = new Thread[numarFire];
grup.enumerate( fire );
for( int i = 0; i < numar; i++ ) {
System.out.println( fire[i].toString() );
}
}
Metoda enumerate întoarce numãrul de fire memorate în tablou, care este identic cu numãrul de fire active.
9.6 Sincronizare
În unele situaþii se poate întâmpla ca mai multe fire de execuþie sã vrea sã acceseze aceeaºi variabilã. În astfel de
situaþii, se pot produce încurcãturi dacã în timpul unuia dintre accese un alt fir de execuþie modificã valoarea variabilei.
Limbajul Java oferã în mod nativ suport pentru protejarea acestor variabile. Suportul este construit de fapt cu
granulaþie mai mare decât o singurã variabilã, protecþia fãcându-se la nivelul obiectelor. Putem defini metode, în
cadrul claselor, care sunt sincronizate.
Pe o instanþã de clasã, la un moment dat, poate lucra o singurã metodã sincronizatã. Dacã un alt fir de execuþie
încearcã sã apeleze aceeaºi metodã pe aceeaºi instanþã sau o altã metodã a clasei de asemenea declaratã sincronizatã,
acest al doilea apel va trebui sã aºtepte înainte de execuþie eliberarea instanþei de cãtre cealaltã metodã.
În afarã de sincronizarea metodelor, se pot sincroniza ºi doar blocuri de instrucþiuni. Aceste sincronizãri se fac tot în
legãturã cu o anumitã instanþã a unei clase. Aceste blocuri de instrucþiuni sincronizate se pot executa doar când
instanþa este liberã. Se poate întâmpla ca cele douã tipuri de sincronizãri sã se amestece, în sensul cã obiectul poate fi
blocat de un bloc de instrucþiuni ºi toate metodele sincronizate sã aºtepte, sau invers.
synchronize ( Instanþã ) {
Instrucþiuni
iar declararea unei metode sincronizate se face prin folosirea modificatorului synchronize la implementarea
metodei.
9.7 Un exemplu
Exemplul urmãtor implementeazã soluþia urmãtoarei probleme: Într-o þarã foarte îndepãrtatã trãiau trei înþelepþi
filozofi. Aceºti trei înþelepþi îºi pierdeau o mare parte din energie certându-se între ei pentru a afla care este cel mai
înþelept. Pentru a tranºa problema o datã pentru totdeauna, cei trei înþelepþi au pornit la drum cãtre un al patrulea
înþelept pe care cu toþii îl recunoºteau cã ar fi mai bun decât ei.
Când au ajuns la acesta, cei trei i-au cerut sã le spunã care dintre ei este cel mai înþelept. Acesta, a scos cinci pãlãrii,
trei negre ºi douã albe, ºi li le-a arãtat explicându-le cã îi va lega la ochi ºi le va pune în cap câte o pãlãrie, cele douã
rãmase ascunzându-le. Dupã aceea, le va dezlega ochii, ºi fiecare dintre ei va vedea culoarea pãlãriei celorlalþi dar nu ºi-
o va putea vedea pe a sa. Cel care îºi va da primul seama ce culoare are propria pãlãrie, acela va fi cel mai înþelept.
Dupã explicaþie, înþeleptul i-a legat la ochi, le-a pus la fiecare câte o pãlãrie neagrã ºi le-a ascuns pe celelalte douã.
Problema este aceea de a descoperi care a fost raþionamentul celui care a ghicit primul cã pãlãria lui este neagrã.
Programul urmãtor rezolvã problema datã în felul urmãtor: Fiecare înþelept priveºte pãlãriile celorlalþi doi. Dacã
ambele sunt albe, problema este rezolvatã, a lui nu poate fi decât neagrã. Dacã vede o pãlãrie albã ºi una neagrã, atunci
el va trebui sã aºtepte puþin sã vadã ce spune cel cu pãlãria neagrã. Dacã acesta nu gãseºte soluþia, înseamnã cã el nu
vede douã pãlãrii albe, altfel ar fi gãsit imediat rãspunsul. Dupã un scurt timp de aºteptare, înþeleptul poate sã fie sigur
cã pãlãria lui este neagrã.
În fine, dacã ambele pãlãrii pe care le vede sunt negre, va trebui sã aºtepte un timp ceva mai lung pentru a vedea dacã
unul dintre concurenþii sãi nu ghiceºte pãlãria. Dacã dupã scurgerea timpului nici unul nu spune nimic, înseamnã cã
nici unul nu vede o pãlãrie albã ºi una neagrã. Înseamnã cã propria pãlãrie este neagrã.
Desigur, raþionamentul pleacã de la ideea cã ne putem baza pe faptul cã toþi înþelepþii gândesc ºi pot rezolva probleme
uºoare. Cel care câºtigã a gândit doar un pic mai repede. Putem simula viteza de gândiri cu un interval aleator de
aºteptare pânã la luarea deciziilor. În realitate, intervalul nu este aleator ci dictat de viteza de gândire a fiecãrui înþelept.
Cei trei înþelepþi sunt implementaþi identic sub formã de fire de execuþie. Nu câºtigã la fiecare rulare acelaºi din cauza
caracterului aleator al implementãrii. Înþeleptul cel mare este firul de execuþie principal care controleazã activitatea
celorlalte fire ºi le serveºte cu date, culoarea pãlãriilor, doar în mãsura în care aceste date trebuie sã fie accesibile. Adicã
nu se poate cere propria culoare de pãlãrie.
import java.awt.Color;
// clasa Filozof implementeazã comportamentul
// unui concurent
class Filozof extends Thread {
// pãrerea concurentului despre culoarea
// pãlãriei sale. Null dacã încã nu ºi-a
// format o pãrere.
Color parere = null;
Filozof( String nume ) {
super( nume );
}
public void run() {
// concurentii firului curent
Filozof concurenti[] = new Filozof[2];
// temporar
Thread fire[] = new Thread[10];
int numarFire = enumerate( fire );
for( int i = 0, j = 0; i < numarFire && j < 2; i++ ) {
if( fire[i] instanceof Filozof &&
fire[i] != this ) {
concurenti[j++] = (Filozof)fire[i];
}
}
while( true ) {
Color primaCuloare = Concurs.culoare( this,
concurenti[0] );
Color adouaCuloare =
Concurs.culoare( this, concurenti[1] );
if( primaCuloare == Color.white &&
adouaCuloare == Color.white ) {
synchronized( this ) {
parere = Color.black;
}
} else if( primaCuloare == Color.white ){
try{
sleep( 500 );
} catch( InterruptedException e ){
};
if( Concurs.culoare( this, concurenti[1]) != concurenti[1].aGhicit()) {
synchronized( this ) {
parere = Color.black;
};
}
} else if( adouaCuloare == Color.white ) {
try{
sleep( (int)( Math.random()*500));
} catch( InterruptedException e ) {
};
if( Concurs.culoare(this, concurenti[0] ) != concurenti[0].aGhicit()) {
synchronized( this ) {
parere = Color.black;
};
}
} else {
try {
sleep( (int)( Math.random()*500)+500 );
} catch( InterruptedException e ) {
};
if( Concurs.culoare(this, concurenti[0]) != concurenti[0].aGhicit() &&
Concurs.culoare( this,
concurenti[1] ) !=
concurenti[1].aGhicit() ) {
synchronized( this ) {
parere = Color.black;
};
}
}
}
}
public synchronized Color aGhicit() {
return parere;
}
}
public class Concurs {
private static Color palarii[] = {
Color.black, Color.black, Color.black
};
private static Filozof filozofi[] = new Filozof[3];
public static void main( String args[] ) {
for( int i = 0; i < args.length && i < 3; i++ ) {
if( args[i].equalsIgnoreCase( "alb" ) ) {
palarii[i] = Color.white;
} else if(args[i].equalsIgnoreCase("negru")) {
palarii[i] = Color.black;
}
}
Soluþia de faþã extinde problema la o tablã de NxN cãsuþe ºi la N regine. Parametrul N este citit din tagul HTML
asociat apletului.
import java.awt.*;
import java.applet.Applet;
public
class QueensRunner extends Applet implements Runnable {
int n;
int regine[];
int linie;
Image queenImage;
Thread myThread;
public void start() {
if( myThread == null ) {
myThread = new Thread( this, "Queens" );
myThread.start();
}
}
public void stop() {
myThread.stop();
myThread = null;
}
public void run() {
while( myThread != null ) {
nextSolution();
repaint();
try {
myThread.sleep( 1000 );
} catch ( InterruptedException e ){
}
}
}
boolean isGood() {
for( int i = 0; i < linie; i++ ) {
if( regine[linie] == regine[i] ||
Math.abs( regine[i] -
regine[linie] ) == Math.abs( i - linie ) ) {
return false;
}
}
return true;
}
void nextSolution() {
while( true ) {
if( linie < 0 ) {
linie = 0;
}
regine[linie]++;
if( regine[linie] > n ) {
regine[linie] = 0;
linie--;
} else {
if( isGood() ) {
linie++;
if( linie >= n ) {
break;
}
}
}
}
}
[cuprins]
Bibliografie
1. Dumitru Rãdoiu,
HTML - Publicaþii Web, Computer Press Agora SRL
2. James Gostling, Henry McGilton,
The Java Language Environment, A white paper, Sun Microsystems, Inc.
3. David Flanagan,
Java in a Nutshell: A Desktop Quick Reference for Java Programmers,
O'Reilly & Associates, Inc.
4. Ed Anuff,
The Java Sourcebook , Wiley Computer Publishing
5. Mike Daconta,
Java for C/C++ Programmers, Wiley Computer Publishing
6. Arthur van Hoff, Sami Shaio, Orca Starbuck,
Hooked on Java, Addison-Wesley Publishing Company
7. Laura Lemay, Charles L. Perkins,
Teach your self Java in 21 days, Sams.net Publishing
8. *** - The Java Language Specification, Sun Microsystems, Inc.
[cuprins]
file:///C|/Documents%20and%20Settings/Luminita/Desktop/bu...0in%20romana/Carte%20JAVA%20in%20romana/bibliografie.html12.01.2006 23:07:51
Capitolul V -- 1. Variabile
Capitolul V
5.1 Variabile
5.1.1 Declaraþii de variabile
5.1.6 Conversii
O variabilã în limbajul Java este o locaþie de memorie care poate pãstra o valoare de un anumit tip. În
ciuda denumirii, existã variabile care îºi pot modifica valoarea ºi variabile care nu ºi-o pot modifica,
numite în Java variabile finale. Orice variabilã trebuie sã fie declaratã pentru a putea fi folositã. Aceastã
declaraþie trebuie sã conþinã un tip de valori care pot fi memorate în locaþia rezervatã variabilei ºi un
nume pentru variabila declaratã. În funcþie de locul în sursa programului în care a fost declaratã
variabila, aceasta primeºte o clasã de memorare localã sau staticã. Aceastã clasã de memorare defineºte
intervalul de existenþã al variabilei în timpul execuþiei.
În forma cea mai simplã, declaraþia unei variabile aratã în felul urmãtor:
Tipul unei variabile poate fi fie unul dintre tipurile primitive definite de limbajul Java fie o referinþã.
Creatorii limbajului Java au avut grijã sã
defineascã foarte exact care sunt caracteristicile fiecãrui tip primitiv în parte ºi care este setul de valori
care se poate memora în variabilele care au tipuri primitive. În plus, a fost exact definitã ºi modalitatea
de reprezentare a acestor tipuri primitive în memorie. În acest fel, variabilele Java devin independente de
În acelaºi spirit, Java defineºte o valoare implicitã pentru fiecare tip de datã, în cazul în care aceasta nu a
primit nici o valoare de la utilizator. În acest fel, ºtim întotdeauna care este valoarea cu care o variabilã
intrã în calcul. Este o practicã bunã însã aceea ca programele sã nu depindã niciodatã de aceste
iniþializãri implicite.
Numele variabilei poate fi orice identificator Java. Convenþia nescrisã de formare a numelor variabilelor
este aceea cã orice variabilã care nu este finalã are un nume care începe cu literã minusculã în timp ce
variabilele finale au nume care conþin numai majuscule. Dacã numele unei variabile care nu este finalã
conþine mai multe cuvinte, cuvintele începând cu cel de-al doilea se scriu cu litere minuscule dar cu
prima literã majusculã. Exemple de nume de variabile care nu sunt finale ar putea fi:
PORTOCALIUVERDEALBASTRUDESCHIS
Limbajul Java permite iniþializarea valorilor variabilelor chiar în momentul declarãrii acestora. Sintaxa
este urmãtoarea:
Desigur, valoarea iniþialã trebuie sã fie de acelaºi tip cu tipul variabilei sau sã poatã fi convertitã într-o
valoare de acest tip.
Deºi limbajul Java ne asigurã cã toate variabilele au o valoare iniþialã bine precizatã, este preferabil sã
executãm aceastã iniþializare în mod explicit pentru fiecare declaraþie. În acest fel mãrim claritatea
propriului cod.
Tipul boolean este folosit pentru memorarea unei valori de adevãr. Pentru acest scop, sunt suficiente
doar douã valori: adevãrat ºi fals. În Java aceste douã valori le vom nota prin literalii true ºi respectiv
false.
Aceste valori pot fi reprezentate în memorie folosindu-ne de o singurã cifrã binarã, adicã pe un bit.
Valorile booleene sunt foarte importante în limbajul Java pentru cã ele sunt valorile care se folosesc în
condiþiile care controleazã instrucþiunile repetitive sau cele condiþionale. Pentru a exprima o condiþie
este suficient sã scriem o expresie al cãrui rezultat este o valoare booleanã, adevãrat sau fals.
Valorile de tip boolean nu se pot transforma în valori de alt tip în mod nativ. La fel, nu existã
transformare nativã dinspre celelalte valori înspre tipul boolean. Cu alte cuvinte, având o variabilã de tip
boolean nu putem memora în interiorul acesteia o valoare întreagã pentru cã limbajul Java nu face pentru
noi nici un fel de presupunere legatã de ce înseamnã o anumitã valoare întreagã din punctul de vedere al
valorii de adevãr. La fel, dacã avem o variabilã întreagã, nu îi putem atribui o valoare de tip boolean.
Orice variabilã booleanã nou creatã primeºte automat valoarea implicitã false. Putem modifica aceastã
comportare specificând în mod explicit o valoare iniþialã true dupã modelul pe care îl vom descrie mai
târziu.
Pentru a declara o variabilã de tip boolean, în Java vom folosi cuvântul rezervat boolean ca în
exemplele de mai jos:
boolean terminat;
boolean areDreptate;
Rândurile de mai sus reprezintã declaraþia a douã variabile de tip boolean numite terminatrespectiv
areDreptate. Cele douã variabile au, dupã declaraþie, valoarea false. Adicã nu e terminat dar nici
n-are dreptate.
Orice limbaj de programare ne oferã într-un fel sau altul posibilitatea de a lucra cu caractere grafice care
sã reprezinte litere, cifre, semne de punctuaþie, etc. În cazul limbajului Java acest lucru se poate face
folosind tipul primitiv numit tip caracter.
O variabilã de tip caracter poate avea ca valoare coduri Unicode reprezentate pe 16 biþi, adicã doi octeþi.
Codurile reprezentabile astfel sunt foarte multe, putând acoperi caracterele de bazã din toate limbile
scrise existente.
În Java putem combina mai multe caractere pentru a forma cuvinte sau ºiruri de caractere mai lungi.
Totuºi, trebuie sã precizãm cã aceste ºiruri de caractere nu trebuiesc confundate cu tablourile de caractere
pentru cã ele conþin în plus informaþii legate de lungimea ºirului.
Codul nu este altceva decât o corespondenþã între numere ºi caractere fapt care permite conversii între
variabile întregi ºi caractere în ambele sensuri. O parte din aceste transformãri pot sã altereze valoarea
originalã din cauza dimensiunilor diferite ale zonelor în care sunt memorate cele douã tipuri de valori.
Convertirea caracterelor în numere ºi invers poate sã fie utilã la prelucrarea în bloc a caracterelor, cum ar
fi trecerea tuturor literelor minuscule în majuscule ºi invers.
Atunci când declarãm un caracter fãrã sã specificãm o valoare iniþialã, el va primi automat ca valoare
implicitã caracterul nullal codului Unicode, \u0000?.
Pentru a declara o variabilã de tip caracter folosim cuvântul rezervat char ca în exemplele urmãtoare:
char primaLiterã;
char prima, ultima;
În cele douã linii de cod am declarat trei variabile de tip caracter care au fost automat iniþializate cu
caracterul null. În continuare, vom folosi interschimbabil denumirea de tip caracter cu denumirea de
tip char, care are avantajul cã este mai aproape de declaraþiile Java.
Între tipurile întregi, acest tip ocupã un singur octet de memorie, adicã opt cifre binare. Într-o variabilã
de tip octet sunt reprezentate întotdeauna valori cu semn, ca de altfel în toate variabilele de tip întreg
definite în limbajul Java. Aceastã convenþie simplificã schema de tipuri primitive care, în cazul altor
limbaje include separat tipuri întregi cu semn ºi fãrã.
Fiind vorba de numere cu semn, este nevoie de o convenþie de reprezentare a semnului. Convenþia
folositã de Java este reprezentarea în complement faþã de doi. Aceastã reprezentare este de altfel folositã
de majoritatea limbajelor actuale ºi permite memorarea, pe 8 biþi a 256 de numere începând de la -128
pânã la 127. Dacã aveþi nevoie de numere mai mari în valoare absolutã, apelaþi la alte tipuri întregi.
Valoarea implicitã pentru o variabilã neiniþializatã de tip octet este valoarea 0 reprezentatã pe un octet.
Iatã ºi câteva exemple de declaraþii care folosesc cuvântul Java rezervat byte:
byte octet;
byte eleviPeClasa;
În continuare vom folosi interschimbabil denumirea de tip octet cu cea de tip byte.
Tipul întreg scurt este similar cu tipul octet dar valorile sunt reprezentate pe doi octeþi, adicã 16 biþi. La
fel ca ºi la tipul octet, valorile
sunt întotdeauna cu semn ºi se foloseºte reprezentarea în complement faþã de doi. Valorile de întregi
scurþi reprezentabile sunt de la -32768 la 32767 iar valoarea implicitã este 0 reprezentat pe doi octeþi.
Pentru declararea variabilelor de tip întreg scurt în Java se foloseºte cuvântul rezervat short, ca în
exemplele urmãtoare:
short i, j;
short valoareNuPreaMare;
În continuare vom folosi interschimbabil denumirea de tip întreg scurt ºi cea de tip short.
Singura diferenþã dintre tipul întreg ºi tipurile precedente este faptul cã valorile sunt reprezentate pe
patru octeþi adicã 32 biþi. Valorile reprezentabile sunt de la -2147483648 la 2147483647 valoarea
implicitã fiind 0. Cuvântul rezervat este int ca în:
int salariu;
În continuare vom folosi interschimbabil denumirea de tip întreg ºi cea de tip int.
În fine, pentru cei care vor sã reprezinte numerele întregi cu semn pe 8 octeþi, 64 de biþi, existã tipul
întreg lung. Valorile reprezentabile sunt de la -9223372036854775808 la
9223372036854775807 iar valoarea implicitã este 0L.
Pentru cei care nu au calculatoare care lucreazã pe 64 de biþi este bine de precizat faptul cã folosirea
acestui tip duce la operaþii lente pentru cã nu existã operaþii native ale procesorului care sã lucreze cu
numere aºa de mari.
Declaraþia se face cu cuvântul rezervat long. În continuare vom folosi interschimbabil denumirea de tip
întreg lung cu cea de tip long.
Acest tip este folosit pentru reprezentarea numerelor reale sub formã de exponent ºi cifre semnificative.
Reprezentarea se face pe patru octeþi, 32 biþi, aºa cum specificã standardul IEEE 754.
sm2e
unde s este semnul +1 sau -1, m este partea care specificã cifrele reprezentative ale numãrului, numitã ºi
mantisã, un întreg pozitiv mai mic decât 224 iar e este un exponent întreg între -149 ºi 104.
Valoarea implicitã pentru variabilele flotante este 0.0f. Pentru declararea unui numãr flotant, Java
defineºte cuvântul rezervat float. Declaraþiile se fac ca în exemplele urmãtoare:
float procent;
float noi, ei;
În continuare vom folosi interschimbabil denumirea de tip flotant ºi cea de tip float.
Dacã valorile reprezentabile în variabile flotante nu sunt destul de precise sau destul de mari, puteþi
folosi tipul flotant dublu care foloseºte opt octeþi pentru reprezentare, urmând acelaºi standard IEEE 754
sm2e
unde s este semnul +1 sau -1, m este mantisa, un întreg pozitiv mai mic decât 253 iar e este un exponent
întreg între -1045 ºi 1000. Valoarea implicitã în acest caz este 0.0d.
Pentru a declara flotanþi dubli, Java defineºte cuvântul rezervat double ca în:
double distanþaPânãLaLunã;
În continuare vom folosi interschimbabil denumirea de tip flotant dublu ºi cea de tip double.
În afarã de valorile definite pânã acum, standardul IEEE defineºte câteva valori speciale reprezentabile
Prima dintre acestea este NaN (Not a Number), valoare care se obþine atunci când efectuãm o operaþie a
cãrei rezultat nu este definit, de exemplu 0.0 / 0.0.
În plus, standardul IEEE defineºte douã valori pe care le putem folosi pe post de infinit pozitiv ºi negativ.
ªi aceste valori pot rezulta în urma unor calcule.
Aceste valori sunt definite sub formã de constante ºi în ierarhia standard Java, mai precis în clasa java.
lang.Float ºi respectiv în java.lang.Double. Numele constantelor este POSITIVE_INFINITY,
NEGATIVE_INFINITY, NaN.
În plus, pentru tipurile întregi ºi întregi lungi ºi pentru tipurile flotante existã definite clase în ierarhia
standard Java care se numesc respectiv java.lang.Integer, java.lang.Long, java.lang.Float ºi java.lang.
Double. În fiecare dintre aceste clase numerice sunt definite douã constante care reprezintã valorile
minime ºi maxime care se pot reprezenta în tipurile respective. Aceste douã constante se numesc în mod
uniform MIN_VALUE ºi MAX_VALUE.
Tipurile referinþã sunt folosite pentru a referi un obiect din interiorul unui alt obiect. În acest mod putem
înlãnþui informaþiile aflate în memorie.
Tipurile referinþã au, la fel ca ºi toate celelalte tipuri o valoare implicitã care este atribuitã automat
oricãrei variabile de tip referinþã care nu a fost iniþializatã. Aceastã valoare implicitã este definitã de
cãtre limbajul Java prin cuvântul rezervat null.
Puteþi înþelege semnificaþia referinþei nule ca o referinþã care nu trimite nicãieri, a cãrei destinaþie nu a
fost încã fixatã.
Simpla declaraþie a unei referinþe nu duce automat la rezervarea spaþiului de memorie pentru obiectul
referit. Singura rezervare care se face este aceea a spaþiului necesar memorãrii referinþei în sine.
Rezervarea obiectului trebuie fãcutã explicit în program printr-o expresie de alocare care foloseºte
cuvântul rezervat new.
O variabilã de tip referinþã nu trebuie sã trimitã pe tot timpul existenþei sale cãtre acelaºi obiect în
memorie. Cu alte cuvinte, variabila îºi poate schimba locaþia referitã în timpul execuþiei.
Tipul referinþã cãtre o clasã este un tip referinþã care trimite cãtre o instanþã a unei clasei de obiecte.
Clasa instanþei referite poate fi oricare clasã validã definitã de limbaj sau de utilizator.
Clasa de obiecte care pot fi referite de o anumitã variabilã de tip referinþã la clasã trebuie declaratã
explicit. De exemplu, pentru a declara o referinþã cãtre o instanþã a clasei Minge, trebuie sã folosim
urmãtoarea sintaxã:
Minge mingeaMea;
Din acest moment, variabila referinþã de clasã numitã mingeaMea va putea pãstra doar referinþe cãtre
obiecte de tip Minge sau cãtre obiecte aparþinând unor clase derivate din clasa Minge. De exemplu,
dacã avem o altã clasã, derivatã din Minge, numitã MingeDeBaschet, putem memora în referinþa
mingeaMea ºi o trimitere cãtre o instanþã a clasei MingeDeBaschet.
În mod general însã, nu se pot pãstra în variabila mingeaMea referinþe cãtre alte clase de obiecte. Dacã
se încercã acest lucru, eroarea va fi semnalatã chiar în momentul compilãrii, atunci când sursa
programului este examinatã pentru a fi transformatã în instrucþiuni ale maºinii virtuale Java.
Sã mai observãm cã o referinþã cãtre clasa de obiecte Object, rãdãcina ierarhiei de clase Java, poate
pãstra ºi o referinþã cãtre un tablou. Mai multe lãmuriri asupra acestei afirmaþii mai târziu.
Tipul referinþã cãtre o interfaþã permite pãstrarea unor referinþe cãtre obiecte care respectã o anumitã
interfaþã. Clasa obiectelor referite poate fi oricare, atâta timp cât clasa respectivã implementeazã
interfaþa cerutã.
ObiectSpaþioTemporal mingeaLuiVasile;
în care tipul este chiar numele interfeþei cerute. Dacã clasa de obiecte Minge declarã cã implementeazã
aceastã interfaþã, atunci variabila referinþã mingeaLuiVasile poate lua ca valoare referinþa cãtre o
instanþã a clasei Minge sau a clasei MingeDeBaschet.
Prin intermediul unei variabile referinþã cãtre o interfaþã nu se poate apela decât la funcþionalitatea
cerutã în interfaþa respectivã, chiar dacã obiectele reale oferã ºi alte facilitãþi, ele aparþinând unor clase
mai bogate în metode.
Tipul referinþã cãtre un tablou este un tip referinþã care poate pãstra o trimitere cãtre locaþia din
memorie a unui tablou de elemente. Prin intermediul acestei referinþe putem accesa elementele tabloului
furnizând indexul elementului dorit.
Tablourile de elemente nu existã în general ci ele sunt tablouri formate din elemente de un tip bine
precizat. Din aceastã cauzã, atunci când declarãm o referinþã cãtre un tablou, trebuie sã precizãm ºi de ce
tip sunt elementele din tabloul respectiv.
La declaraþia referinþei cãtre tablou nu trebuie sã precizãm ºi numãrul de elemente din tablou.
long numere[];
Numele variabilei este numere. Un alt exemplu de declaraþie de referinþã cãtre un tablou:
Minge echipament[];
Declaraþia de mai sus construieºte o referinþã cãtre un tablou care pãstreazã elemente de tip referinþã
cãtre o instanþã a clasei Minge. Numele variabilei referinþã este echipament. Parantezele drepte de
dupã numele variabilei specificã faptul cã este vorba despre un tablou.
Fiecare variabilã trebuie sã aibã o anumitã clasã de memorare. Aceastã clasã ne permite sã aflãm care
este intervalul de existenþã ºi vizibilitatea unei variabile în contextul execuþiei unui program.
Este important sã înþelegem exact aceastã noþiune pentru cã altfel vom încerca sã referim variabile
înainte ca acestea sã fi fost create sau dupã ce au fost distruse sau sã referim variabile care nu sunt
vizibile din zona de program în care le apelãm. Soluþia simplã de existenþã a tuturor variabilelor pe tot
timpul execuþiei este desigur afarã din discuþie atât din punct de vedere al eficienþei cât ºi a eleganþei ºi
stabilitãþii codului.
Aceste variabile nu au importanþã prea mare în contextul întregii aplicaþii, ele servind la rezolvarea unor
probleme locale. Variabilele locale sunt declarate, rezervate în memorie ºi utilizate doar în interiorul
unor blocuri de instrucþiuni, fiind distruse automat la ieºirea din aceste blocuri. Aceste variabile sunt
vizibile doar în interiorul blocului în care au fost create ºi în subblocurile acestuia.
Variabilele statice sunt în general legate de funcþionalitatea anumitor clase de obiecte ale cãror instanþe
folosesc în comun aceste variabile. Variabilele statice sunt create atunci când codul specific clasei în care
au fost declarate este încãrcat în memorie ºi nu sunt distruse decât atunci când acest cod este eliminat din
memorie.
Valorile memorate în variabile statice au importanþã mult mai mare în aplicaþie decât cele locale, ele
pãstrând informaþii care nu trebuie sã se piardã la dispariþia unei instanþe a clasei. De exemplu,
variabila în care este memorat numãrul de picioare al obiectelor din clasa Om nu trebuie sã fie distrusã la
dispariþia unei instanþe din aceastã clasã. Aceasta din cauzã cã ºi celelalte instanþe ale clasei folosesc
aceeaºi valoare. ªi chiar dacã la un moment dat nu mai existã nici o instanþã a acestei clase, numãrul de
picioare ale unui Om trebuie sã fie accesibil în continuare pentru interogare de cãtre celelalte clase.
Variabilele statice nu se pot declara decât ca variabile ale unor clase ºi conþin în declaraþie cuvântul
rezervat static. Din cauza faptului cã ele aparþin clasei ºi nu unei anumite instanþe a clasei,
variabilele statice se mai numesc uneori ºi variabile de clasã.
Un alt tip de variabile sunt variabilele a cãror perioadã de existenþã este stabilitã de cãtre programator.
Aceste variabile pot fi alocate la cerere, dinamic, în orice moment al execuþiei programului. Ele vor fi
distruse doar atunci când nu mai sunt referite de nicãieri.
La alocarea unei variabile dinamice, este obligatoriu sã pãstrãm o referinþã cãtre ea într-o variabilã de tip
referinþã. Altfel, nu vom putea accesa în viitor variabila dinamicã. În momentul în care nici o referinþã
nu mai trimite cãtre variabila dinamicã, de exemplu pentru cã referinþa a fost o variabilã localã ºi blocul
în care a fost declaratã ºi-a terminat execuþia, variabila dinamicã este distrusã automat de cãtre sistem
printr-un mecanism numit colector de gunoaie.
Colectorul de gunoaie poate porni din iniþiativa sistemului sau din iniþiativa programatorului la
momente bine precizate ale execuþiei.
Pentru a rezerva spaþiu pentru o variabilã dinamicã este nevoie sã apelãm la o expresie de alocare care
foloseºte cuvântul rezervat new. Aceastã expresie alocã spaþiul necesar pentru un anumit tip de valoare.
De exemplu, pentru a rezerva spaþiul necesar unui obiect de tip Minge, putem apela la sintaxa:
iar pentru a rezerva spaþiul necesar unui tablou de referinþe cãtre obiecte de tip Minge putem folosi
declaraþia:
Am alocat astfel spaþiu pentru un tablou care conþine 5 referinþe cãtre obiecte de tip Minge. Pentru
alocarea tablourilor conþinând tipuri primitive se foloseºte aceeaºi sintaxã. De exemplu, urmãtoarea linie
de program alocã spaþiul necesar unui tablou cu 10 întregi, creând în acelaºi timp ºi o variabilã referinþã
spre acest tablou, numitã numere:
Tablourile servesc, dupã cum spuneam, la memorarea secvenþelor de elemente de acelaºi tip. Tablourile
unidimensionale au semnificaþia vectorilor de elemente. Se poate întâmpla sã lucrãm ºi cu tablouri de
referinþe cãtre tablouri, în acest caz modelul fiind acela al unei matrici bidimensionale. În fine, putem
extinde definiþia ºi pentru mai mult de douã dimensiuni.
Pentru a declara variabile de tip tablou, trebuie sã specificãm tipul elementelor care vor umple tabloul ºi
un nume pentru variabila referinþã care va pãstra trimiterea cãtre zona de memorie în care sunt
memorate elementele tabloului.
Deºi putem declara variabile referinþã cãtre tablou ºi separat, de obicei declaraþia este fãcutã în acelaºi
timp cu alocarea spaþiului ca în exemplele din paragraful anterior.
Sintaxa Java permite plasarea parantezelor drepte care specificã tipul tablou înainte sau dupã numele
variabilei. Astfel, urmãtoarele douã declaraþii sunt echivalente:
int[] numere;
int numere[];
Dacã doriþi sã folosiþi tablouri cu douã dimensiuni ca matricile, puteþi sã declaraþi un tablou de
referinþe cãtre tablouri cu una dintre urmãtoarele trei sintaxe echivalente:
float[][] matrice;
float[] matrice[];
float matrice[][];
De precizat cã ºi în cazul dimensiunilor multiple, declaraþiile de mai sus nu fac nimic altceva decât sã
rezerve loc pentru o referinþã ºi sã precizeze numãrul de dimensiuni. Alocarea spaþiului pentru
elementele tabloului trebuie fãcutã explicit.
Despre rezervarea spaþiului pentru tablourile cu o singurã dimensiune am vorbit deja. Pentru tablourile
cu mai multe dimensiuni, rezervarea spaþiului se poate face cu urmãtoarea sintaxã:
În expresia de alocare sunt specificate în clar numãrul elementelor pentru fiecare dimensiune a tabloului.
Limbajul Java permite ºi o sintaxã pentru iniþializarea elementelor unui tablou. Într-un astfel de caz este
rezervat automat ºi spaþiul de memorie necesar memorãrii valorilor iniþiale. Sintaxa folositã în astfel de
cazuri este urmãtoarea:
char []caractere = { a, b, c, d };
Acest prim exemplu alocã spaþiu pentru patru elemente de tip caracter ºi iniþializeazã aceste elemente cu
valorile dintre acolade. Dupã aceea, creeazã variabila de tip referinþã numitã caractere ºi o iniþializeazã
cu referinþa la zona de memorie care pãstreazã cele patru valori.
int [][]numere = {
{ 1, 3, 4, 5 },
{ 2, 4, 5 },
{ 1, 2, 3, 4, 5 }
};
double [][][]reali = {
{ { 0.0, -1.0 }, { 4.5 } },
{ { 2.5, 3.0 } }
};
Dupã cum observaþi numãrul iniþializatorilor nu trebuie sã fie acelaºi pentru fiecare element.
Tablourile Java sunt alocate dinamic, ceea ce înseamnã cã ele îºi pot schimba dimensiunile pe parcursul
execuþiei. Pentru a afla numãrul de elemente dintr-un tablou, putem apela la urmãtoarea sintaxã:
sau
Elementele unui tablou se pot referi prin numele referinþei tabloului ºi indexul elementului pe care dorim
sã-l referim. În Java, primul element din tablou este elementul cu numãrul 0, al doilea este elementul
numãrul 1 ºi aºa mai departe.
Sintaxa de referire foloseºte parantezele pãtrate [ ºi ]. Între ele trebuie specificat indexul elementului pe
care dorim sã-l referim. Indexul nu trebuie sã fie constant, el putând fi o expresie de complexitate
oarecare.
În cazul tablourilor cu mai multe dimensiuni, avem în realitate tablouri de referinþe la tablouri. Asta
înseamnã cã dacã considerãm urmãtoarea declaraþie:
Figura 5.1 Elementele tabloului sunt de tip referinþã, iniþializate implicit la valoarea null.
expresii de alocare:
Figura 5.2 Noile tablouri sunt referite din interiorul tabloului original. Elementele noilor tablouri
sunt caractere.
Figura 5.3 Variabilele de tip referinþã caractere[0] ºi tablouDeCaractere trimit spre acelaºi
tablou rezervat în memorie.
Variabila tablouDeCaractere trimite cãtre acelaºi tablou de caractere ca ºi cel referit de primul element al
tabloului referit de variabila caractere.
Sã mai precizãm cã referirea unui element de tablou printr-un index mai mare sau egal cu lungimea
tabloului duce la oprirea execuþiei programului cu un mesaj de eroare de execuþie corespunzãtor.
Despre alocarea tablourilor am spus deja destul de multe. În cazul în care nu avem iniþializatori,
variabilele sunt iniþializate cu valorile implicite definite de limbaj pentru tipul corespunzãtor. Aceasta
înseamnã cã, pentru tablourile cu mai multe dimensiuni, referinþele sunt iniþializate cu null.
Pentru eliberarea memoriei ocupate de un tablou, este suficient sã tãiem toate referinþele cãtre tablou.
Sistemul va sesiza automat cã tabloul nu mai este referit ºi mecanismul colector de gunoaie va elibera
zona. Pentru a tãia o referinþã cãtre un tablou dãm o altã valoare variabilei care referã tabloul. Valoarea
poate fi null sau o referinþã cãtre un alt tablou.
De exemplu:
5.1.6 Conversii
Operaþiile definite în limbajul Java au un tip bine precizat de argumente. Din pãcate, existã situaþii în
care nu putem transmite la apelul acestora exact tipul pe care compilatorul Java îl aºteaptã. În asemenea
situaþii, compilatorul are douã alternative: fie respinge orice operaþie cu argumente greºite, fie încearcã
sã converteascã argumentele cãtre tipurile necesare. Desigur, în cazul în care conversia nu este posibilã,
singura alternativã rãmâne prima.
În multe situaþii însã, conversia este posibilã. Sã luãm de exemplu tipurile întregi. Putem sã convertim
întotdeauna un întreg scurt la un întreg. Valoarea rezultatã va fi exact aceeaºi. Conversia inversã însã,
poate pune probleme dacã valoarea memoratã în întreg depãºeºte capacitatea de memorare a unui întreg
scurt.
În afarã de conversiile implicite, pe care compilatorul le hotãrãºte de unul singur, existã ºi conversii
explicite, pe care programatorul le poate forþa la nevoie. Aceste conversii efectueazã de obicei operaþii
în care existã pericolul sã se piardã o parte din informaþii. Compilatorul nu poate hotãrî de unul singur în
aceste situaþii.
Conversiile implicite pot fi un pericol pentru stabilitatea aplicaþiei dacã pot sã ducã la pierderi de
informaþii fãrã avertizarea programatorului. Aceste erori sunt de obicei extrem de greu de depistat.
În fiecare limbaj care lucreazã cu tipuri fixe pentru datele sale existã conversii imposibile, conversii
periculoase ºi conversii sigure. Conversiile imposibile sunt conversiile pe care limbajul nu le permite
pentru cã nu ºtie cum sã le execute sau pentru cã operaþia este prea periculoasã. De exemplu, Java refuzã
sã converteascã un tip primitiv cãtre un tip referinþã. Deºi s-ar putea imagina o astfel de conversie bazatã
pe faptul cã o adresã este în cele din urmã un numãr natural, acest tip de conversii
sunt extrem de periculoase, chiar ºi atunci când programatorul cere explicit aceastã conversie.
În aceste conversii valoarea se reprezintã într-o zonã mai mare fãrã sã se piardã nici un fel de informaþii.
Iatã conversiile de extindere pe tipuri primitive:
Sã mai precizãm totuºi cã, într-o parte din aceste cazuri, putem pierde din precizie. Aceastã situaþie
apare de exemplu la conversia unui long într-un float, caz în care se pierd o parte din cifrele
semnificative pãstrându-se însã ordinul de mãrime. De altfel aceastã observaþie este evidentã dacã þinem
cont de faptul cã un long este reprezentat pe 64 de biþi în timp ce un float este reprezentat doar pe
32 de biþi.
Precizia se pierde chiar ºi în cazul conversiei long la double sau int la float pentru cã, deºi
dimensiunea zonei alocatã pentru cele douã tipuri este aceeaºi, numerele flotante au nevoie de o parte din
aceastã zonã pentru a reprezenta exponentul.
Convenþiile de trunchiere a valorii pot produce pierderi de informaþie pentru cã ele convertesc tipuri
mai bogate în informaþii cãtre tipuri mai sãrace. Conversiile de trunchiere pe tipurile elementare sunt
urmãtoarele:
● byte la char
În cazul conversiilor de trunchiere la numerele cu semn, este posibil sã se schimbe semnul pentru cã, în
timpul conversiei, se îndepãrteazã pur ºi simplu octeþii care nu mai încap ºi poate rãmâne primul bit
diferit de vechiul prim bit. Copierea se face începând cu octeþii mai puþin semnificativi iar trunchierea
se face la octeþii cei mai semnificativi.
Prin octeþii cei mai semnificativi ne referim la octeþii în care sunt reprezentate cifrele cele mai
semnificative. Cifrele cele mai semnificative sunt cifrele care dau ordinul de mãrime al numãrului. De
exemplu, la numãrul 123456, cifrele cele mai semnificative sunt primele, adicã: 1, 2, etc. La acelaºi
numãr, cifrele cele mai puþin semnificative sunt ultimele, adicã: 6, 5, etc.
Conversiile tipurilor referinþã nu pun probleme pentru modul în care trebuie executatã operaþia din
cauzã cã, referinþa fiind o adresã, în timpul conversiei nu trebuie afectatã în nici un fel aceastã adresã. În
schimb, se pun probleme legate de corectitudinea logicã a conversiei. De exemplu, dacã avem o
referinþã la un obiect care nu este tablou, este absurd sã încercãm sã convertim aceastã referinþã la o
referinþã de tablou.
Limbajul Java defineºte extrem de strict conversiile posibile în cazul tipurilor referinþã pentru a salva
programatorul de eventualele necazuri care pot apare în timpul execuþiei. Iatã conversiile posibile:
● O referinþã cãtre un obiect aparþinând unei clase C poate fi convertit la o referinþã cãtre un
obiect aparþinând clasei S doar în cazul în care C este chiar S sau C este derivatã direct sau
indirect din S.
● O referinþã cãtre un obiect aparþinând unei clase C poate fi convertit cãtre o referinþã de
interfaþã I numai dacã clasa C implementeazã interfaþa I.
● O referinþã cãtre un tablou poate fi convertitã la o referinþã cãtre o clasã numai dacã clasa
respectivã este clasa Object.
● O referinþã cãtre un tablou de elemente ale cãrui elemente sunt de tipul T1 poate fi convertitã la o
referinþã cãtre un tablou de elemente de tip T2 numai dacã T1 ºi T2 reprezintã acelaºi tip primitiv
sau T2 este un tip referinþã ºi T1 poate fi convertit cãtre T2.
Conversiile pe care limbajul Java le executã implicit la atribuire sunt foarte puþine. Mai exact, sunt
executate doar acele conversii care nu necesitã validare în timpul execuþiei ºi care nu pot pierde
informaþii în cazul tipurilor primitive.
În cazul valorilor aparþinând tipurilor primitive, urmãtorul tabel aratã conversiile posibile. Pe coloane
avem tipul de valoare care se atribuie iar pe linii avem tipurile de variabile la care se atribuie:
boolean Da Nu Nu Nu Nu Nu Nu Nu
char Nu Da Da Da Nu Nu Nu Nu
byte Nu Da Da Nu Nu Nu Nu Nu
short Nu Da Da Da Nu Nu Nu Nu
int Nu Da Da Da Da Nu Nu Nu
long Nu Da Da Da Da Da Nu Nu
float Nu Da Da Da Da Da Da Nu
double Nu Da Da Da Da Da Da Da
Tabloul 5.1 Conversiile posibile într-o operaþie de atribuire cu tipuri primitive. Coloanele
reprezintã tipurile care se atribuie iar liniile reprezintã tipul de variabilã cãtre care se face
atribuirea.
Dupã cum observaþi, tipul boolean nu poate fi atribuit la o variabilã de alt tip.
Valorile de tip primitiv nu pot fi atribuite variabilelor de tip referinþã. La fel, valorile de tip referinþã nu
pot fi memorate în variabile de tip primitiv. În ceea ce priveºte tipurile referinþã între ele, urmãtorul tabel
defineºte situaþiile în care conversiile sunt posibile la atribuirea unei valori de tipul T la o variabilã de
tipul S:
T este o clasã care T este o clasã care T este o interfaþã T = B[] este un
nu este finalã este finalã tablou cu
elemente de
tipul B
Tabloul 5.2 Conversiile posibile la atribuirea unei valori de tipul T la o variabilã de tipul S.
Conversiile de tip cast, sau casturile, sunt apelate de cãtre programator în mod explicit. Sintaxa pentru
construcþia unui cast este scrierea tipului cãtre care dorim sã convertim în paranteze în faþa valorii pe
care dorim sã o convertim. Forma generalã este:
( Tip ) Valoare
Conversiile posibile în acest caz sunt mai multe decât conversiile implicite la atribuire pentru cã în acest
caz programatorul este prevenit de eventuale pierderi de date el trebuind sã apeleze conversia explicit.
Dar, continuã sã existe conversii care nu se pot apela nici mãcar în mod explicit, dupã cum am explicat
înainte.
În cazul conversiilor de tip cast, orice valoare numericã poate fi convertitã la orice valoare numericã.
În cazul conversiilor dintr-un tip referinþã într-altul putem separa douã cazuri. Dacã compilatorul poate
decide în timpul compilãrii dacã conversia este corectã sau nu, o va decide. În cazul în care compilatorul
nu poate decide pe loc, se va efectua o verificare a conversiei în timpul execuþiei. Dacã conversia se
dovedeºte greºitã, va apare o eroare de execuþie ºi programul va fi întrerupt.
Iatã un exemplu de situaþie în care compilatorul nu poate decide dacã conversia este posibilã sau nu:
Minge mingeaMea;
?
MingeDeBaschet mingeaMeaDeBaschet;
// MingeDeBaschet este o clasã
// derivatã din clasa Minge
mingeaMeaDeBaschet=(MingeDeBaschet)mingeaMea;
În acest caz, compilatorul nu poate fi sigur dacã referinþa memoratã în variabila mingeaMea este de tip
MingeDeBaschet sau nu pentru cã variabilei de tip Minge i se pot atribui ºi referinþe cãtre instanþe
de tip Minge în general, care nu respectã întru totul definiþia clasei MingeDeBaschet sau chiar
referinþã cãtre alte tipuri de minge derivate din clasa Minge, de exemplu MingeDePolo care
implementeazã proprietãþi ºi operaþii diferite faþã de clasa MingeDeBaschet.
Minge mingeaMea;
MingeDeBaschet mingeaMeaDeBaschet;
?
mingeaMea = ( Minge ) mingeaMeaDeBaschet;
MingeDeBaschet mingeaMeaDeBaschet;
MingeDePolo mingeaMeaDePolo;
?
mingeaMeaDePolo = ( MingeDePolo ) mingeaMeaDeBaschet;
În fine, tabelul urmãtor aratã conversiile de tip cast a cãror corectitudine poate fi stabilitã în timpul
compilãrii. Conversia încearcã sã transforme printr-un cast o referinþã de tip T într-o referinþã de tip S.
S este o clasã T trebuie sã fie T trebuie sã fie o Totdeauna corectã la S trebuie sã fie
care nu este subclasã a lui S subclasã a lui S compilare Object
finalã
S = A[] este T trebuie sã fie eroare la compilare eroare la compilare A sau B sunt
un tablou cu Object acelaºi tip
elemente de primitiv sau A
tipul A este un tip
referinþã ºi B
poate fi
convertit cu un
cast la A
Tabloul 5.3 Cazurile posibile la convertirea unei referinþe de tip T într-o referinþã de tip S.
Promovarea aritmeticã se aplicã în cazul unor formule în care operanzii pe care se aplicã un operator
sunt de tipuri diferite. În aceste cazuri,
compilatorul încearcã sã promoveze unul sau chiar amândoi operanzii la acelaºi tip pentru a putea fi
executatã operaþia.
În cazul promovãrii aritmetice unare, existã un singur operand care în cazul cã este byte sau short
este transformat la int altfel rãmâne nemodificat.
De exemplu, în urmãtoarea operaþie amândoi operanzii vor fi convertiþi la float prin promovare
aritmeticã binarã:
float f;
double i = f + 3;
short s, r;
?
int min = ( r < -s ) ? r : s;
În expresia condiþionalã, operandul -s se traduce de fapt prin aplicarea operatorului unar - la variabila s
care este de tip short. În acest caz, se va produce automat promovarea aritmeticã unarã de la short la
int, apoi se va continua evaluarea expresiei.
[capitolul V]
[cuprins]
Capitolul V
5.2 Expresii
5.2.1 Valoarea ºi tipul unei expresii
5.2.2 Ordinea de evaluare
5.2.3 Operatori
Fiecare expresie a limbajului Java are un rezultat ºi un tip. Rezultatul poate fi:
● o valoare
● o variabilã
● nimic
În cazul în care valoarea unei expresii are ca rezultat o variabilã, expresia poate apare în stânga unei
operaþii de atribuire. De exemplu expresia:
tablou[i]
este o expresie care are ca rezultat o variabilã ºi anume locaþia elementului cu indexul i din tabloul de
elemente numit tablou.
O expresie nu produce nimic doar în cazul în care este un apel de metodã ºi acest apel nu produce nici un
rezultat (este declarat de tip void).
Fiecare expresie are în timpul compilãrii un tip cunoscut. Acest tip poate fi o valoare primitivã sau o
referinþã. În cazul expresiilor care au tip referinþã, valoarea expresiei poate sã fie ºi referinþa
neiniþializatã, null.
În Java operanzii sunt evaluaþi întotdeauna de la stânga spre dreapta. Acest lucru nu trebuie sã ne
îndemne sã scriem cod care sã depindã de ordinea de evaluare pentru cã, în unele situaþii, codul rezultat
este greu de citit. Regula este introdusã doar pentru a asigura generarea uniformã a codului binar,
independent de compilatorul folosit.
● în cazul unui operator binar, operandul din stânga este întotdeauna complet evaluat atunci când se
trece la evaluarea operandului din dreapta. De exemplu, în expresia:
( i++ ) + i
dacã i avea valoarea iniþialã 2, toatã expresia va avea valoarea 5 pentru cã valoarea celui
de-al doilea i este luatã dupã ce s-a executat incrementarea i++.
● în cazul unei referinþe de tablou, expresia care numeºte tabloul este complet evaluatã înainte de a
se trece la evaluarea expresiei care dã indexul. De exemplu, în expresia:
( a = b )[i]
● indexul va fi aplicat dupã ce s-a executat atribuirea valorii referinþã b la variabila referinþã de
tablou a. Cu alte cuvinte, rezultatul va fi al i-lea element din tabloul b.
● în cazul apelului unei metode, expresia care numeºte obiectul este complet evaluatã atunci când se
new int[i++][i]
5.2.3 Operatori
Operatorii unari se aplicã întotdeauna unui singur operand. Aceºti operatori sunt, în general exprimaþi
înaintea operatorului asupra cãruia se aplicã. Existã însã ºi douã excepþii, operatorii de incrementare ºi
decrementare care pot apare ºi înainte ºi dupã operator, cu semnificaþii diferite.
● ++ preincrement
● -- predecrement
● ++ postincrement
● -- postdecrement
● + unar
● - unar
● ~ complementare
● ! negaþie logicã
● cast
În cazul operatorului prefix, valoarea rezultat a acestei expresii este valoarea variabilei dupã
incrementare în timp ce, la operatorul postfix, valoarea rezultat a expresiei este valoarea de dinainte de
incrementare. De exemplu, dupã execuþia urmãtoarei secvenþe de instrucþiuni:
int i = 5;
int j = i++;
valoarea lui j este 5, în timp ce, dupã execuþia urmãtoarei secvenþe de instrucþiuni:
int i = 5;
int j = ++i;
În cazul operatorilor -? predecrement ºi postdecrement, sunt valabile aceleaºi consideraþii ca mai sus, cu
diferenþa cã valoarea variabilei asupra cãreia se aplicã operandul va fi decrementatã cu 1. De exemplu,
urmãtoarele instrucþiuni:
int i = 5;
int j = i--;
int i = 5;
int j = --i;
fac ca j sã aibã valoarea finalã 4. În ambele cazuri, valoarea finalã a lui i este 4.
double f = 5.6;
double g = ++f;
Operatorul + unar se aplicã asupra oricãrei valori primitive aritmetice. Valoarea rãmâne neschimbatã.
Operatorul - unar se aplicã asupra oricãrei valori primitive aritmetice. Rezultatul aplicãrii acestui
operand este negarea aritmeticã a valorii. În cazul valorilor întregi, acest lucru este echivalent cu
int i = 5;
int j = -i;
îi dau lui j valoarea -5 . În cazul valorilor speciale definite de standardul IEEE pentru reprezentarea
numerelor flotante, se aplicã urmãtoarele reguli:
● Dacã operandul este NaN rezultatul negãrii aritmetice este tot NaN pentru cã NaN nu are semn.
● Dacã operandul este unul dintre infiniþi, rezultatul este infinitul opus ca semn.
● Dacã operandul este zero de un anumit semn, rezultatul este zero de semn diferit. ^
Operatorul de complementare ~ se aplicã asupra valorilor primitive de tip întreg. Rezultatul aplicãrii
operandului este complementarea bit cu bit a valorii originale. De exemplu, dacã operandul era de tip
byte având valoarea, în binar, 00110001, rezultatul va fi 11001110. În realitate, înainte de
complementare se face ºi extinderea valorii la un întreg, deci rezultatul va fi de fapt: 11111111
11111111 11111111 11001110.
Operatorul de negare logicã ! se aplicã în exclusivitate valorilor de tip boolean. În cazul în care valoarea
iniþialã a operandului este true rezultatul va fi false ºi invers.
5.2.3.1.6 Casturi
Casturile sunt expresii de conversie dintr-un tip într-altul, aºa cum deja am arãtat la paragraful destinat
conversiilor. Rezultatul unui cast este valoarea operandului convertitã la noul tip de valoare exprimat de
cast. De exemplu, la instrucþiunile:
double f = 5.6;
int i = ( int )f;
double g = -5.6;
int j = ( int )g;
valoarea variabilei f este convertitã la o valoare întreagã, anume 5, ºi noua valoare este atribuitã
variabilei i. La fel, j primeºte valoarea -5.
Sã mai precizãm cã nu toate casturile sunt valide în Java. De exemplu, nu putem converti o valoare
întreagã într-o valoare de tip referinþã.
● Operatori multiplicativi: *, /, %
● Operatori aditivi: +, -, + (concatenare) pentru ºiruri de caractere
● Operatori de ºiftare: >>, <<, >>>
● Operatori relaþionali: <, >, <=, >=, instanceof
● Operatori de egalitate: ==, !=
● Operatori la nivel de bit: &, |, ^
● Operatori logici: &&, ||
Operatorii multiplicativi reprezintã respectiv operaþiile de înmulþire (*), împãrþire (/) ºi restul
împãrþirii (%). Prioritatea acestor operaþii este mai mare relativ la operaþiile aditive, deci aceºti operatori
se vor executa mai întâi. Exemple:
10 * 5 == 50
10.3 * 5.0 == 51.5
10 / 2.5 == 4.0// împãrþire realã
3 / 2 == 1// împãrþire întreagã
7 % 2 == 1// restul împãrþirii întregi
123.5 % 4 == 3.5 // 4 * 30 + 3.5
123.5 % 4.5 == 2.0 // 4.5 * 27 + 2.0
Dupã cum observaþi, operanzii sunt convertiþi mai întâi la tipul cel mai puternic, prin promovare
aritmeticã, ºi apoi se executã operaþia. Rezultatul este de acelaºi tip cu tipul cel mai puternic.
În cazul operatorului pentru restul împãrþirii, dacã lucrãm cu numere flotante, rezultatul se calculeazã în
felul urmãtor: se calculeazã de câte ori este cuprins cel de-al doilea operand în primul (un numãr întreg
de ori) dupã care rezultatul este diferenþa care mai rãmâne, întotdeauna mai micã strict decât al doilea
operand.
Operatorii aditivi reprezintã operaþiile de adunare (+), scãdere (-) ºi concatenare (+) de ºiruri.
Observaþiile despre conversia tipurilor fãcute la operatorii multiplicativi rãmân valabile. Exemple:
2 + 3 == 5
2.34 + 3 == 5.34
34.5 - 23.1 == 11.4
La concatenarea ºirurilor de caractere, lungimea ºirului rezultat este suma lungimii ºirurilor care intrã în
operaþie. Caracterele din ºirul rezultat sunt caracterele din primul ºir, urmate de cele dintr-al doilea ºir în
ordine.
Dacã cel de-al doilea operand nu este de tip String ci este de tip referinþã, se va apela metoda sa
toString, ºi apoi se va folosi în operaþie rezultatul. Metoda toString este definitã în clasa
Object ºi este moºtenitã de toate celelalte clase.
Dacã cel de-al doilea operand este un tip primitiv, acesta este convertit la un ºir rezonabil de caractere
care sã reprezinte valoarea operandului.
Operatorii de ºiftare se pot aplica doar pe valori primitive întregi. Ei reprezintã respectiv operaþiile de
ºiftare cu semn stânga (<<) ºi dreapta (>>) ºi operaþia de ºiftare fãrã semn spre dreapta (>>>).
ªiftãrile cu semn lucreazã la nivel de cifre binare. Cifrele binare din locaþia de memorie implicatã sunt
mutate cu mai multe poziþii spre stânga sau spre dreapta. Poziþia binarã care reprezintã semnul rãmâne
neschimbatã. Numãrul de poziþii cu care se efectueazã mutarea este dat de al doilea operand. Locaþia de
memorie în care se executã operaþia este locaþia în care este memorat primul operand.
ªiftarea cu semn la stânga reprezintã o operaþie identicã cu înmulþirea cu 2 de n ori, unde n este al doilea
operand. ªiftarea cu semn la dreapta reprezintã împãrþirea întreagã. În acest caz, semnul este copiat în
mod repetat în locurile rãmase goale. Iatã câteva exemple:
ªiftarea fãrã semn la dreapta, mutã cifrele binare din operand completând spaþiul rãmas cu zerouri:
Operatorii relaþionali întorc valori booleene de adevãrat sau fals. Ei reprezintã testele de mai mic (<),
mai mare (>), mai mic sau egal (<=), mai mare sau egal (>=) ºi testul care ne spune dacã un anumit
obiect este sau nu instanþã a unei anumite clase (instanceof). Iatã câteva exemple:
Aceºti operatori testeazã egalitatea sau inegalitatea dintre douã valori. Ei reprezintã testul de egalitate
(==) ºi de inegalitate (!=). Rezultatul aplicãrii acestor operatori este o valoare booleanã.
Exemple:
( 1 == 1.0 ) == true
( 2 != 2 ) == false
Object o = new Object();
String s1 = "vasile";
String s2 = s1;
String s4 = "e";
String s3 = "vasil" + s4;
( o == s1 ) == false
( s1 == s2 ) == true // acelaºi obiect referit
( s3 == s1 ) == false // acelaºi ºir de caractere
// dar obiecte diferite
Sã observãm cã egalitatea a douã obiecte de tip String reprezintã egalitatea a douã referinþe de obiecte
ºi nu egalitatea conþinutului ºirului de caractere. Douã referinþe sunt egale dacã referã exact acelaºi
obiect, nu dacã obiectele pe care le referã sunt egale între ele. Egalitatea conþinutului a douã ºiruri de
caractere se testeazã folosind metoda equals, definitã în clasa String.
Operatorii la nivel de bit reprezintã operaþiile logice obiºnuite, dacã considerãm cã 1 ar reprezenta
adevãrul ºi 0 falsul. Operatorii la nivel de bit, considerã cei doi operanzi ca pe douã ºiruri de cifre binare
ºi fac operaþiile pentru fiecare dintre perechile de cifre binare corespunzãtoare în parte. Rezultatul este
un nou ºir de cifre binare. De exemplu, operaþia de ºi (&) logic are urmãtorul tabel de adevãr:
1 & 1 == 1
1 & 0 == 0
0 & 1 == 0
0 & 0 == 0
00101111
01110110
rezultatul este:
00100110
Primul numãr reprezintã cifra 47, al doilea 118 iar rezultatul 38, deci:
47 & 118 == 38
În mod asemãnãtor, tabela de adevãr pentru operaþia logicã sau (|) este:
1 | 1 == 1
1 | 0 == 1
0 | 1 == 1
0 | 0 == 0
iar tabela de adevãr pentru operaþia logicã de sau exclusiv (^) este:
1 ^ 1 == 0
1 ^ 0 == 1
0 ^ 1 == 1
0 ^ 0 == 0
Operatorii logici se pot aplica doar asupra unor operanzi de tip boolean. Rezultatul aplicãrii lor este tot
boolean ºi reprezintã operaþia logicã de ºi (&&) sau operaþia logicã de sau (||) între cele douã valori
booleene. Iatã toate posibilitãþile de combinare:
În cazul operatorului && este evaluat mai întâi operandul din stânga. Dacã acesta este fals, operandul din
dreapta nu mai este evaluat, pentru cã oricum rezultatul este fals. Acest lucru ne permite sã testãm
condiþiile absolut necesare pentru corectitudinea unor operaþii ºi sã nu executãm operaþia decât dacã
aceste condiþii sunt îndeplinite.
De exemplu, dacã avem o referinþã ºi dorim sã citim o valoare de variabilã din obiectul referit, trebuie sã
ne asigurãm cã referinþa este diferitã de null. În acest caz, putem scrie:
În cazul în care s este null, a doua operaþie nu are sens ºi nici nu va fi executatã.
În mod similar, la operatorul ||, se evalueazã mai întâi primul operand. Dacã acesta este adevãrat, nu
se mai evalueazã ºi cel de-al doilea operand pentru cã rezultatul este oricum adevãrat. Faptul se poate
folosi în mod similar ca mai sus:
Este singurul operator definit de limbajul Java care acceptã trei operanzi. Operatorul primeºte o expresie
condiþionalã booleanã pe care o evalueazã ºi alte douã expresii care vor fi rezultatul aplicãrii
operandului. Care dintre cele douã expresii este rezultatul adevãrat depinde de valoarea rezultat a
expresiei booleene. Forma generalã a operatorului este:
Dacã valoarea expresiei condiþionale este true, valoarea operaþiei este valoarea expresiei 1. Altfel,
valoarea operaþiei este valoarea expresiei 2.
Cele douã expresii trebuie sã fie amândouã aritmetice sau amândouã booleene sau amândouã de tip
referinþã.
Iatã ºi un exemplu:
int i = 5;
int j = 4;
double f = ( i < j ) ? 100.5 : 100.4;
Dupã execuþia instrucþiunilor de mai sus, valoarea lui f este 100.4. Iar dupã:
int a[] = { 1, 2, 3, 4, 5 };
int b[] = { 10, 20, 30 };
int k = ( ( a.length < b.length ) ? a : b )[0];
Dacã amândoi operanzii unei operator sunt întregi atunci întreaga operaþie este întreagã.
Dacã unul dintre operanzi este întreg lung, operaþia se va face cu precizia de 64 de biþi. Operanzii sunt
eventual convertiþi. Rezultatul este un întreg lung sau o valoare booleanã dacã operatorul este un
operator condiþional.
Dacã nici unul dintre operanzi nu e întreg lung, operaþia se face întotdeauna pe 32 de biþi, chiar dacã cei
doi operanzi sunt întregi scurþi sau octeþi. Rezultatul este întreg sau boolean.
Dacã valoarea obþinutã este mai mare sau mai micã decât se poate reprezenta pe tipul rezultat, nu este
semnalatã nici o eroare de execuþie, dar rezultatul este trunchiat.
Dacã un operand al unei operaþii este flotant, atunci întreaga operaþie este flotantã. Dacã unul dintre
operanzi este flotant dublu, operaþia este pe flotanþi dubli. Rezultatul este un flotant dublu sau boolean.
În caz de operaþii eronate nu se genereazã erori. În loc de aceasta se obþine rezultatul NaN. Dacã într-o
operaþie participã un NaN rezultatul este de obicei NaN.
NaN == NaN
are întotdeauna rezultatul fals pentru cã un NaN nu este egal cu nimic. La fel, expresia:
NaN != NaN
-0.0 == +0.0
este adevãratã, unde +0.0 este zeroul pozitiv iar -0.0 este zeroul negativ. Cele douã valori sunt definite în
standardul IEEE 754.
În alte operaþii însã, zero pozitiv diferã de zero negativ. De exemplu 1.0 / 0.0 este infinit pozitiv iar 1.0 /
-0.0 este infinit negativ.
La apelul unei metode, valorile întregi nu sunt automat extinse la întreg sau la întreg lung ca la operaþii.
Asta înseamnã cã, dacã un operand este transmis ca întreg scurt de exemplu, el rãmâne întreg scurt ºi în
interiorul metodei apelate.
[capitolul V]
[cuprins]
Capitolul V
5.3 Instrucþiuni
5.3.1 Blocuri de instrucþiuni
5.3.2.3.1 Instrucþiunea if
5.3.2.3.2 Instrucþiunea switch
Un bloc de instrucþiuni este o secvenþã, eventual vidã, de instrucþiuni ºi declaraþii de variabile locale.
Aceste instrucþiuni se executã în ordinea în care apar în interiorul blocului. Sintactic, blocurile de
instrucþiuni sunt delimitate în sursã de caracterele { ºi }.
În limbajul Java, regula generalã este aceea cã oriunde putem pune o instrucþiune putem pune ºi un bloc
de instrucþiuni, cu câteva excepþii pe
care le vom sesiza la momentul potrivit, specificând în acest fel cã instrucþiunile din interiorul blocului
trebuiesc privite în mod unitar ºi tratate ca o singurã instrucþiune.
O declaraþie de variabilã localã introduce o nouã variabilã care poate fi folositã doar în interiorul
blocului în care a fost definitã. Declaraþia trebuie sã conþinã un nume ºi un tip. În plus, într-o declaraþie
putem specifica o valoare iniþialã în cazul în care valoarea implicitã a tipului variabilei, definitã standard
de limbajul Java, nu ne satisface.
Numele variabilei este un identificator Java. Acest nume trebuie sã fie diferit de numele celorlalte
variabile locale definite în blocul respectiv ºi de eventualii parametri ai metodei în interiorul cãreia este
declarat blocul.
De exemplu, este o eroare de compilare sã declarãm cea de-a doua variabilã x în blocul:
{
int x = 3;
…
{
int x = 5;
…
}
}
Compilatorul va semnala faptul cã deja existã o variabilã cu acest nume în interiorul metodei. Eroarea de
compilare apare indiferent dacã cele douã variabile sunt de acelaºi tip sau nu.
Nu acelaºi lucru se întâmplã însã dacã variabilele sunt declarate în douã blocuri de instrucþiuni complet
disjuncte, care nu se includ unul pe celãlalt. De exemplu, declaraþiile urmãtoare sunt perfect valide:
{
{
int x = 3;
…
}
…
{
int x = 5;
…
}
}
Practic, fiecare bloc de instrucþiuni defineºte un domeniu de existenþã a variabilelor locale declarate în
interior. Dacã un bloc are subblocuri declarate în interiorul lui, variabilele din aceste subblocuri trebuie
sã fie distincte ca nume faþã de variabilele din superbloc. Aceste domenii se pot reprezenta grafic ca în
figura urmãtoare:
Figura 5.4 Reprezentarea graficã a domeniilor de existenþã a variabilelor incluse unul într-altul.
Figura reprezintã situaþia din primul exemplu. Observaþi cã domeniul blocului interior este complet
inclus în domeniul blocului exterior. Din aceastã cauzã, x apare ca fiind definitã de douã ori. În cel de-al
doilea exemplu, reprezentarea graficã este urmãtoarea:
În acest caz, domeniile în care sunt definite cele douã variabile sunt complet disjuncte ºi compilatorul nu
va avea nici o dificultate în a identifica la fiecare referire a unei variabile cu numele x despre care
variabilã este vorba. Sã mai observãm cã, în cel de-al doilea exemplu, referirea variabilelor x în blocul
mare, în afara celor douã subblocuri este o eroare de compilare.
În cazul în care blocul mare este blocul de implementare al unei metode ºi metoda respectivã are
parametri, aceºti parametri sunt luaþi în considerare la fel ca niºte declaraþii de variabile care apar chiar
la începutul blocului. Acest lucru face ca numele parametrilor sã nu poatã fi folosit în nici o declaraþie
de variabilã localã din blocul de implementare sau subblocuri ale acestuia.
În realitate, o variabilã localã poate fi referitã în interiorul unui bloc abia dupã declaraþia ei. Cu alte
cuvinte, domeniul de existenþã al unei variabile locale începe din punctul de declaraþie ºi continuã pânã
la terminarea blocului.
{
x = 4;
int x = 3;
}
{
{
int x = 4;
}
int x = 3;
}
pentru cã, în momentul declaraþiei din interiorul subblocului, variabila x din exterior nu este încã
definitã. La ieºirea din subbloc, prima declaraþie îºi terminã domeniul, aºa cã se poate defini fãrã
probleme o nouã variabilã cu acelaºi nume.
O atribuire este o setare de valoare într-o locaþie de memorie. Forma generalã a unei astfel de
instrucþiuni este:
Locaþie = valoare ;
Specificarea locaþiei se poate face în mai multe feluri. Cel mai simplu este sã specificãm un nume de
variabilã localã. Alte alternative sunt acelea de a specifica un element dintr-un tablou de elemente sau o
variabilã care nu este declaratã finalã din interiorul unei clase sau numele unui parametru al unei metode.
Valoarea care trebuie atribuitã poate fi un literal sau rezultatul evaluãrii unei expresii.
Instrucþiunea de atribuire are ca rezultat chiar valoarea atribuitã. Din aceastã cauzã, la partea de valoare
a unei operaþii de atribuire putem avea chiar o altã operaþie de atribuire ca în exemplul urmãtor:
int x = 5;
int y = 6;
x = ( y = y / 2 );
Valoarea finalã a lui x va fi identicã cu valoarea lui y ºi va fi egalã cu 3. Parantezele de mai sus sunt puse
doar pentru claritatea codului, pentru a înþelege exact care este ordinea în care se executã atribuirile. În
realitate, ele pot sã lipseascã pentru cã, în mod implicit, Java va executa întâi atribuirea din dreapta.
Deci, ultima linie se poate rescrie ca:
x = y = y / 2;
( x = y ) = y / 2;
Aceastã instrucþiune va genera o eroare de compilare pentru cã, dupã executarea atribuirii din paranteze,
rezultatul este valoarea 6 iar unei valori nu i se poate atribui o altã valoare.
În momentul atribuirii, dacã valoarea din partea dreaptã a operaþiei nu este de acelaºi tip cu locaþia din
partea stângã, compilatorul va încerca conversia valorii la tipul locaþiei în modul pe care l-am discutat în
paragraful referitor la conversii. Aceastã conversie trebuie sã fie posibilã, altfel compilatorul va semnala
o eroare.
Pentru anumite conversii, eroarea s-ar putea sã nu poatã fi depistatã decât în timpul execuþiei. În aceste
cazuri, compilatorul nu va semnala eroare dar va fi semnalatã o eroare în timpul execuþiei ºi rularea
programului va fi abandonatã.
Se întâmplã deseori ca valoarea care se atribuie unei locaþii sã depindã de vechea valoare care era
memoratã în locaþia respectivã. De exemplu, în instrucþiunea:
int x = 3;
x = x * 4;
vechea valoare a lui x este înmulþitã cu 4 ºi rezultatul înmulþirii este memorat înapoi în locaþia destinatã
lui x. Într-o astfel de instrucþiune, calculul adresei locaþiei lui x este efectuat de douã ori, o datã pentru a
lua vechea valoare ºi încã o datã pentru a memora noua valoare. În realitate, acest calcul nu ar trebui
executat de douã ori pentru cã locaþia variabilei x nu se schimbã în timpul instrucþiunii.
Pentru a ajuta compilatorul la generarea unui cod eficient, care sã calculeze locaþia lui x o singurã datã,
în limbajul Java au fost introduse instrucþiuni mixte de calcul combinat cu atribuire. În cazul nostru,
noua formã de scriere, mai eficientã, este:
int x = 3;
x *= 4;
Eficienþa acestei exprimãri este cu atât mai mare cu cât calculul locaþiei este mai complicat. De
exemplu, în secvenþa urmãtoare:
int x = 3, y = 5;
double valori[] = new double[10];
valori[( x + y ) / 2] += 3.5;
calculul locaþiei de unde se va lua o valoare la care se va aduna 3.5 ºi unde se va memora înapoi
rezultatul acestei operaþii este efectuat o singurã datã. În acest exemplu, calcului locaþiei presupune
execuþia expresiei:
( x + y ) / 2
ºi indexarea tabloului numit valori cu valoarea rezultatã. Valoarea din partea dreaptã poate fi o expresie
arbitrar de complicatã, ca în:
valori[x] += 7.0 * ( y * 5 );
*=, /=, %=, +=, -=, <<=, >>=, >>>=, &=, |=, ^= ^
Sã mai observãm cã, în cazul folosirii operatorilor de incrementare ºi decrementare se face ºi o atribuire
implicitã pentru cã pe lângã valoarea rezultatã în urma operaþiei, se modificã ºi valoarea memoratã la
locaþia pe care se aplicã operatorul. La fel ca ºi la operaþiile mixte de atribuire, calculul locaþiei asupra
cãreia se aplicã operatorul se efectueazã o singurã datã.
Unele dintre instrucþiunile din interiorul unui bloc de instrucþiuni trebuie referite din altã parte a
programului pentru a se putea direcþiona execuþia spre aceste instrucþiuni. Pentru referirea acestor
instrucþiuni, este necesar ca ele sã aibã o etichetã asociatã. O etichetã este deci un nume dat unei
instrucþiuni din program prin care instrucþiunea respectivã poate fi referitã din alte pãrþi ale
programului.
Eticheta cea mai simplã este un simplu identificator urmat de caracterul : ºi de instrucþiunea pe care
dorim sã o etichetãm:
Etichetã: Instrucþiune
ca în exemplul urmãtor:
int a = 5;
Eticheta: a = 3;
Pentru instrucþiunea switch, descrisã mai jos, sunt definite douã moduri diferite de etichetare. Acestea
au forma:
default: Instrucþiune
Instrucþiunile condiþionale sunt instrucþiuni care selecteazã pentru execuþie o instrucþiune dintr-un set
de instrucþiuni în funcþie de o anumitã condiþie.
5.3.2.3.1 Instrucþiunea if
Instrucþiunea if primeºte o expresie a cãrei valoare este obligatoriu de tipul boolean. Evaluarea acestei
expresii poate duce la doar douã valori: adevãrat sau fals. În funcþie de valoarea rezultatã din aceastã
evaluare, se executã unul din douã seturi de instrucþiuni distincte, specificate de instrucþiunea if.
Dupã evaluarea expresiei booleene, dacã valoarea rezultatã este true, se executã instrucþiunea 1.
Restul instrucþiunii este opþional, cu alte cuvinte partea care începe cu cuvântul rezervat else poate sã
lipseascã. În cazul în care aceastã parte nu lipseºte, dacã rezultatul evaluãrii expresiei este false se va
executa instrucþiunea 2.
Indiferent de instrucþiunea care a fost executatã în interiorul unui if, dupã terminarea acesteia execuþia
continuã cu instrucþiunea de dupã instrucþiunea if, în afarã de cazul în care instrucþiunile executate
conþin în interior o instrucþiune de salt.
Sã mai observãm cã este posibil ca într-o instrucþiune if sã nu se execute nici o instrucþiune în afarã de
evaluarea expresiei în cazul în care expresia este falsã iar partea else din instrucþiunea if lipseºte.
Expresia booleanã va fi întotdeauna evaluatã.
int x = 3;
if( x == 3 )
x *= 7;
int x = 5;
if( x % 2 == 0 )
x = 100;
else
x = 1000;
Instrucþiunea 1 nu poate lipsi niciodatã. Dacã totuºi pe ramura de adevãr a instrucþiunii condiþionale nu
dorim sã executãm nimic, putem folosi o instrucþiune vidã, dupã cum este arãtat puþin mai departe.
În cazul în care dorim sã executãm mai multe instrucþiuni pe una dintre ramurile instrucþiunii if, putem
sã în locuim instrucþiunea 1 sau 2 sau pe amândouã cu blocuri de instrucþiuni, ca în exemplul urmãtor:
int x = 5;
if( x == 0 ) {
x = 3;
y = x * 5;
} else {
x = 5;
y = x * 7;
}
int x = 3;
int y = 5;
if( y != 0 && x / y == 2 )
x = 4;
În acest caz, aºa cum deja s-a specificat la descrierea operatorului &&, evaluarea expresiei nu este
terminatã în toate situaþiile. În exemplul nostru, dacã y este egal cu 0, evaluarea expresiei este opritã ºi
rezultatul este fals. Aceastã comportare este corectã pentru cã, dacã un termen al unei operaþii logice de
conjuncþie este fals, atunci întreaga conjuncþie este falsã.
În plus, acest mod de execuþie ne permite sã evitãm unele operaþii cu rezultat incert, în cazul nostru o
împãrþire prin 0. Pentru mai multe informaþii relative la operatorii && ºi || citiþi secþiunea destinatã
operatorilor.
Instrucþiunea switch ne permite saltul la o anumitã instrucþiune etichetatã în funcþie de valoarea unei
expresii. Putem sã specificãm câte o etichetã pentru fiecare valoare particularã a expresiei pe care dorim
sã o diferenþiem. În plus, putem specifica o etichetã la care sã se facã saltul implicit, dacã expresia nu ia
nici una dintre valorile particulare specificate.
switch( Expresie ) {
[default: InstrucþiuniImplicite;]
Execuþia unei instrucþiuni switch începe întotdeauna prin evaluarea expresiei dintre parantezele
rotunde. Aceastã expresie trebuie sã aibã tipul caracter, octet, întreg scurt sau întreg. Dupã evaluarea
expresiei se trece la compararea valorii rezultate cu valorile particulare specificate în etichetele case din
interiorul blocului de instrucþiuni. Dacã una dintre valorile particulare este egalã cu valoarea expresiei,
se executã instrucþiunile începând de la eticheta case corespunzãtoare acelei valori în jos, pânã la
capãtul blocului. Dacã nici una dintre valorile particulare specificate nu este egalã cu valoarea expresiei,
se executã instrucþiunile care încep cu eticheta default, dacã aceasta existã.
int x = 4;
…
int y = 0;
switch( x + 1 ) {
case 3:
x += 2;
y++;
case 5:
x = 11;
y++;
default:
x = 4;
y += 3;
}
Dacã valoarea lui x în timpul evaluãrii expresiei este 2 atunci expresia va avea valoarea 3 ºi
instrucþiunile vor fi executate una dupã alta începând cu cea etichetatã cu case 3. În ordine, x va
deveni 4, y va deveni 1, x va deveni 11, y va deveni 2, x va deveni 4 ºi y va deveni 5.
Dacã valoarea lui x în timpul evaluãrii expresiei este 4 atunci expresia va avea valoarea 5 ºi
instrucþiunile vor fi executate pornind de la cea etichetatã cu case 5. În ordine, x va deveni 11, y va
deveni 1, x va deveni 4 ºi y va deveni 4.
În fine, dacã valoarea lui x în timpul evaluãrii expresiei este diferitã de 2 ºi 4, se vor executa
instrucþiunile începând cu cea etichetatã cu default. În ordine, x va deveni 4 ºi y va deveni 3.
Eticheta default poate lipsi, caz în care, dacã nici una dintre valorile particulare nu este egalã cu
valoarea expresiei, nu se va executa nici o instrucþiune din bloc.
În cele mai multe cazuri, aceastã comportare a instrucþiunii switch nu ne convine, din cauza faptului
cã instrucþiunile de dupã cea etichetatã cu case 5 se vor executa ºi dacã valoarea este 3. La fel,
instrucþiunile de dupã cea etichetatã cu default se executã întotdeauna. Pentru a schimba aceastã
comportare trebuie sã folosim una dintre instrucþiunile de salt care sã opreascã execuþia instrucþiunilor
din bloc înainte de întâlnirea unei noi instrucþiuni etichetate.
Putem de exemplu folosi instrucþiunea de salt break care va opri execuþia instrucþiunilor din blocul
switch. De exemplu:
char c = '\t';
…
String mesaj = "nimic";
switch( c ) {
case '\t':
mesaj = "tab";
break;
case '\n':
mesaj = "linie noua";
break;
case '\r':
mesaj = "retur";
default:
mesaj = mesaj + " de";
mesaj = mesaj + " car";
}
În acest caz, dacã c este egal cu caracterul tab, la terminarea instrucþiunii switch, mesaj va avea
valoarea "tab". În cazul în care c are valoarea CR, mesaj va deveni mai întâi "retur" apoi "retur
de" ºi apoi "retur de car". Lipsa lui break dupã instrucþiunea
mesaj = "retur";
Instrucþiunile de ciclare (sau ciclurile, sau buclele) sunt necesare atunci când dorim sã executãm de mai
multe ori aceeaºi instrucþiune sau acelaºi bloc de instrucþiuni. Necesitatea acestui lucru este evidentã
dacã ne gândim cã programele trebuie sã poatã reprezenta acþiuni de forma: executã 10 întoarceri,
executã 7 genoflexiuni, executã flotãri pânã ai obosit, etc.
Desigur, sintaxa instrucþiunilor care se executã trebuie sã fie aceeaºi, pentru cã ele vor fi în realitate
scrise în Java o singurã datã. Totuºi, instrucþiunile nu sunt neapãrat aceleaºi. De exemplu, dacã executãm
în mod repetat instrucþiunea:
în realitate se va memora valoarea 0 în locaþii diferite pentru cã variabila care participã la calculul
locaþiei îºi modificã la fiecare iteraþie valoarea. La primul pas, se va face 0 primul element din tablou ºi
în acelaºi timp i va primi valoarea 1. La al doilea pas, se va face 0 al doilea element din tablou ºi i va
primi valoarea 2, ºi aºa mai departe.
Lucrurile par ºi mai clare dacã ne gândim cã instrucþiunea executatã în mod repetat poate fi o
instrucþiune if. În acest caz, În funcþie de expresia condiþionalã din if se poate executa o ramurã sau
alta a instrucþiunii.
int i = 0;
if( i++ % 2 == 0 )
…
else
…
În acest caz, i este când par când impar ºi se executã alternativ cele douã ramuri din if. Desigur,
comportarea descrisã este valabilã dacã valoarea lui i nu este modificatã în interiorul uneia dintre ramuri.
Aceastã instrucþiune de buclare se foloseºte atunci când vrem sã executãm o instrucþiune atâta timp cât
o anumitã expresie condiþionalã rãmâne adevãratã. Expresia condiþionalã se evalueazã ºi testeazã înainte
de execuþia instrucþiunii, astfel cã, dacã expresia era de la început falsã, instrucþiunea nu se mai executa
niciodatã.
Test este o expresie booleanã iar Corp este o instrucþiune normalã, eventual vidã. Dacã avem nevoie sã
repetãm mai multe instrucþiuni, putem înlocui corpul buclei cu un bloc de instrucþiuni.
Iatã ºi un exemplu:
int i = 0;
int tablou[] = new int[20];
while( i < 10 )
tablou[i++] = 1;
Bucla while de mai sus se executã de 10 ori primele 10 elemente din tablou fiind iniþializate cu 1. În
treacãt fie spus, celelalte rãmân la valoarea 0 care este valoarea implicitã pentru întregi. Dupã cei 10 paºi
iterativi, i devine 10 ºi testul devine fals (10 < 10).
int i = 3;
while( i < 3 )
i++;
while( true )
;
Întreruperea execuþiei unui ciclu infinit se poate face introducând în corpul ciclului o instrucþiune de
salt.
5.3.2.4.2 Instrucþiunea do
Buclele do se folosesc atunci când testul de terminare a buclei trebuie fãcut dupã execuþia corpului
buclei. Sintaxa de descriere a instrucþiuni do este:
Test este o expresie booleanã iar Corp este o instrucþiune sau un bloc de instrucþiuni. Execuþia acestei
instrucþiuni înseamnã execuþia corpului în mod repetat atâta timp cât expresia Test are valoarea
adevãrat. Testul se evalueazã dupã execuþia corpului, deci corpul se executã cel puþin o datã.
De exemplu, în instrucþiunea:
int i = 3;
do
i++;
while( false );
valoarea finalã a lui i este 4, pentru cã instrucþiunea i++ care formeazã corpul buclei se executã o datã
chiar dacã testul este întotdeauna fals.
În instrucþiunea:
int i = 1;
do {
tablou[i] = 0;
i += 2;
} while( i < 5 );
sunt setate pe 0 elementele 1 ºi 3 din tablou. Dupã a doua iteraþie, i devine 5 ºi testul eºueazã cauzând
terminarea iteraþiei.
Instrucþiunea for se foloseºte atunci când putem identifica foarte clar o parte de iniþializare a buclei,
testul de terminare a buclei, o parte de reluare a buclei ºi un corp pentru buclã. În acest caz, putem folosi
sintaxa:
Corp ºi Iniþializare sunt instrucþiuni normale. Test este o expresie booleanã iar Reluare este o
instrucþiune cãreia îi lipseºte caracterul ; final.
Execuþia unei bucle for începe cu execuþia instrucþiunii de iniþializare. Aceastã instrucþiune stabileºte
de obicei niºte valori pentru variabilele care controleazã bucla. Putem chiar declara aici noi variabile.
Aceste variabile existã doar în interiorul corpului buclei ºi în instrucþiunile de test ºi reluare ale buclei.
În partea de iniþializare nu putem scrie decât o singurã instrucþiune fie ea declaraþie sau instrucþiune
normalã. În acest caz instrucþiunea nu se poate înlocui cu un bloc de instrucþiuni. Putem însã sã
int i = 0, j = 1;
Dupã execuþia pãrþii de iniþializare se porneºte bucla propriu-zisã. Aceasta constã din trei instrucþiuni
diferite executate în mod repetat. Cele trei instrucþiuni sunt testul, corpul buclei ºi instrucþiunea de
reluare. Testul trebuie sã fie o expresie booleanã. Dacã aceasta este evaluatã la valoarea adevãrat, bucla
continuã cu execuþia corpului, a instrucþiunii de reluare ºi din nou a testului. În clipa în care testul are
valoarea fals, bucla este opritã fãrã sã mai fie executat corpul sau reluarea.
Iatã un exemplu:
int x = 0;
for( int i = 3; i < 30; i += 10 )
x += i;
În aceastã buclã se executã mai întâi crearea variabilei i ºi iniþializarea acesteia cu 3. Dupã aceea se
testeazã variabila i dacã are o valoare mai micã decât 30. Testul are rezultat adevãrat (i este 0 < 30) ºi se
trece la execuþia corpului unde x primeºte valoarea 3 (0 + 3). În continuare se executã partea de reluare
în care i este crescut cu 10, devenind 13. Se terminã astfel primul pas al buclei ºi aceasta este reluatã
începând cu testul care este în continuare adevãrat (13 < 30). Se executã corpul, x devenind 16 (3 + 13),
ºi reluarea, unde x devine 23 (13 + 10). Se reia bucla de la test care este în continuare adevãrat (23 < 30).
Se executã corpul unde x devine 39 (16 + 23) ºi reluarea unde i devine 33 (23 + 10). Se reia testul care
în acest caz devine fals (33 < 30) ºi se pãrãseºte bucla, continuându-se execuþia cu prima instrucþiune de
dupã buclã.
Dupã ieºirea din buclã, variabila i nu mai existã, deci nu se mai poate folosi ºi nu putem vorbi despre
valoarea cu care iese din buclã, iar variabila x rãmâne cu valoarea 39.
Pentru a putea declara variabila i în instrucþiunea de iniþializare a buclei for este necesar ca în blocurile
superioare instrucþiunii for sã nu existe o altã variabilã i, sã nu existe un parametru numit i ºi nici o
etichetã cu acest nume.
Dacã dorim un corp care sã conþinã mai multe instrucþiuni, putem folosi un bloc. Nu putem face acelaºi
lucru în partea de iniþializare sau în partea de reluare.
Oricare dintre pãrþile buclei for în afarã de iniþializare poate sã lipseascã. Dacã aceste pãrþi lipsesc, se
considerã cã ele sunt reprezentate de instrucþiunea vidã. Dacã nu dorim iniþializare trebuie totuºi sã
specificãm implicit instrucþiunea vidã. Putem de exemplu sã scriem o buclã infinitã prin:
int x;
for( ;; )
x = 0;
Putem specifica funcþionarea instrucþiunii for folosindu-ne de o instrucþiune while în felul urmãtor:
Iniþializare
while( Test ) {
Corp
Reluare ;
Instrucþiunile de salt provoacã întreruperea forþatã a unei bucle, a unui bloc de instrucþiuni sau ieºirea
dintr-o metodã. Instrucþiunile de salt sunt destul de periculoase pentru cã perturbã curgerea uniformã a
programului ceea ce poate duce la o citire ºi înþelegere eronatã a codului rezultat.
Instrucþiunea break produce întreruperea unei bucle sau a unui bloc switch. Controlul este dat
instrucþiunii care urmeazã imediat dupã bucla întreruptã sau dupã blocul instrucþiunii switch.
De exemplu în secvenþa:
int i = 1, j = 3;
int tablou[] = new int[10];
while( i < 10 ) {
tablou[i++] = 0;
if( i == j )
break;
}
i += 2;
sunt setate pe 0 elementele 1 ºi 2 ale tabloului. Dupã a doua iteraþie i devine 3 ºi testul i == j are
valoarea adevãrat, producându-se execuþia instrucþiunii break.
Instrucþiunea break cauzeazã ieºirea forþatã din buclã, chiar dacã testul buclei i < 10 este în
continuare valid. La terminarea tuturor instrucþiunilor de mai sus, i are valoarea 5.
În exemplul urmãtor:
int i, j = 3;
int tablou[] = new tablou[10];
do {
tablou[i] = 0;
if( i++ >= j )
break;
} while( i < 10 );
sunt setate pe 0 elementele 0, 1, 2 ºi 3 din tablou. Dacã vã întrebaþi la ce mai foloseºte testul buclei,
atunci sã spunem cã este o mãsurã suplimentarã de siguranþã. Dacã cumva j intrã cu o valoare greºitã,
testul ne asigurã în continuare cã nu vom încerca sã setãm un element care nu existã al tabloului. De fapt,
pericolul existã încã, dacã valoarea iniþialã a lui i este greºitã, deci testul ar trebui mutat la început ºi
bucla transformatã într-un while, ca cel de mai sus.
În cazul buclelor for, sã mai precizãm faptul cã la o ieºire forþatã nu se mai executã instrucþiunea de
reluare.
break Identificator;
În acest caz, identificatorul trebuie sã fie eticheta unei instrucþiuni care sã includã instrucþiunea break.
Prin faptul cã instrucþiunea include instrucþiunea break, înþelegem cã instrucþiunea break apare în
corpul instrucþiunii care o include sau în corpul unei instrucþiuni care se gãseºte în interiorul corpului
instrucþiunii care include instrucþiunea break. Controlul revine instrucþiunii de dupã instrucþiunea
etichetatã cu identificatorul specificat.
De exemplu, în instrucþiunile:
int i, j;
asta: while( i < 3 ) {
do {
i = j + 1;
if( i == 3 )
break asta;
} while( j++ < 3 );
}
j = 10;
instrucþiunea break terminã bucla do ºi bucla while controlul fiind dat instrucþiunii de dupã while,
ºi anume atribuirea:
j = 10;
Instrucþiunea continue permite reluarea unei bucle fãrã a mai termina execuþia corpului acesteia.
Reluarea se face ca ºi cum corpul buclei tocmai a fost terminat de executat. Bucla nu este pãrãsitã.
De exemplu, în instrucþiunea:
int i;
while( i < 10 ) {
i++;
continue;
i++;
}
corpul buclei se executã de 10 ori, pentru cã a doua incrementare a lui i nu se executã niciodatã.
Execuþia începe cu testul ºi apoi urmeazã cu prima incrementare. Dupã aceasta se executã instrucþiunea
continue care duce la reluarea buclei, pornind de la test ºi urmând cu incrementarea ºi din nou
instrucþiunea continue.
În exemplul urmãtor:
int i;
do {
i++;
continue;
i++;
} while( i < 10 );
corpul se executã de 10 ori la fel ca ºi mai sus. Instrucþiunea continue duce la evitarea celei de-a doua
incrementãri, dar nu ºi la evitarea testului de sfârºit de buclã.
corpul se executã tot de 10 ori, ceea ce înseamnã cã reluarea buclei duce la execuþia instrucþiunii de
reluare a buclei for ºi apoi a testului. Doar ceea ce este în interiorul corpului este evitat.
Instrucþiunea continue poate avea, la fel ca ºi instrucþiunea break, un identificator opþional care
specificã eticheta buclei care trebuie continuatã. Dacã existã mai multe bucle imbricate una în cealaltã
buclele interioare celei referite de etichetã sunt abandonate.
De exemplu, în secvenþa:
instrucþiunea continue provoacã abandonarea buclei while ºi reluarea buclei for cu partea de
reluare ºi apoi testul.
Instrucþiunea return provoacã pãrãsirea corpului unei metode. În cazul în care return este urmatã
de o expresie, valoarea expresiei este folositã ca valoare de retur a metodei. Aceastã valoare poate fi
eventual convertitã cãtre tipul de valoare de retur declarat al metodei, dacã acest lucru este posibil. Dacã
Este o eroare de compilare specificarea unei valori de retur într-o instrucþiune return din interiorul
unei metode care este declaratã void, cu alte cuvinte care nu întoarce nici o valoare.
Instrucþiunea return fãrã valoare de retur poate fi folositã ºi pentru a pãrãsi execuþia unui iniþializator
static.
Exemple de instrucþiuni return veþi gãsi în secþiunea care trateazã metodele unei clase de obiecte.
Instrucþiunea throw este folositã pentru a semnaliza o excepþie de execuþie. Aceastã instrucþiune
trebuie sã aibã un argument ºi acesta trebuie sã fie un tip obiect, de obicei dintr-o subclasã a clasei de
obiecte Exception.
La execuþia instrucþiunii throw, fluxul normal de execuþie este pãrãsit ºi se terminã toate
instrucþiunile în curs pânã la prima instrucþiune try care specificã într-o clauzã catch un argument
formal de acelaºi tip cu obiectul aruncat sau o superclasã a acestuia.
Aceste instrucþiuni sunt necesare pentru tratarea erorilor ºi a excepþiilor precum ºi pentru sincronizarea
unor secvenþe de cod care nu pot rula în paralel.
Instrucþiunea try iniþiazã un context de tratare a excepþiilor. În orice punct ulterior iniþializãrii acestui
context ºi înainte de terminarea acestuia, o excepþie semnalatã prin execuþia unei instrucþiuni throw va
returna controlul la nivelul instrucþiunii try, abandonându-se în totalitate restul instrucþiunilor din
corpul acestuia.
La semnalarea unei excepþii aceasta poate fi prinsã de o clauzã catch ºi, în funcþie de tipul obiectului
aruncat, se pot executa unele instrucþiuni care repun programul într-o stare stabilã. De obicei, o excepþie
este generatã atunci când s-a produs o eroare majorã ºi continuarea instrucþiunilor din contextul curent
nu mai are sens.
În finalul instrucþiunii, se poate specifica ºi un bloc de instrucþiuni care se executã imediat dupã blocul
try ºi blocurile catch indiferent cum s-a terminat execuþia acestora. Pentru specificarea acestor
instrucþiuni, trebuie folositã o clauzã finally.
Dacã, undeva în interiorul blocului 1 sau în metodele apelate din interiorul acestuia, pe oricâte nivele,
este apelatã o instrucþiune throw, execuþia blocului ºi a metodelor în curs este abandonatã ºi se revine
în instrucþiunea try. În continuare, obiectul aruncat de throw este comparat cu argumentele
specificate în clauzele catch. Dacã unul dintre aceste argumente este instanþã a aceleiaºi clase sau a
unei superclase a clasei obiectului aruncat, se executã blocul de instrucþiuni corespunzãtor clauzei
catch respective. Dacã nici una dintre clauzele catch nu se potriveºte, obiectul este aruncat mai
departe. Dacã excepþia nu este nicãieri prinsã în program, acesta se terminã cu o eroare de execuþie.
Indiferent dacã a apãrut o excepþie sau nu, indiferent dacã s-a executat blocul unei clauze catch sau
nu, în finalul execuþiei instrucþiunii try se executã blocul specificat în clauza finally, dacã aceasta
existã.
Clauza finally se executã chiar ºi dacã în interiorul blocului 1 s-a executat o instrucþiune throw care
a aruncat un obiect care nu poate fi prins de clauzele catch ale acestei instrucþiuni try. În astfel de
situaþii, execuþia instrucþiunii throw se opreºte temporar, se executã blocul finally ºi apoi se
aruncã mai departe excepþia.
Gândiþi-vã, de exemplu, ce s-ar întâmpla dacã mai multe pãrþi ale programului ar încerca sã
incrementeze în acelaºi timp valoarea unei variabile. Una dintre ele ar citi vechea valoare a variabilei, sã
spunem 5, ar incrementa-o la 6 ºi, când sã o scrie înapoi, sã presupunem cã ar fi întreruptã de o altã parte
a programului care ar incrementa-o la 6. La revenirea în prima parte, aceasta ar termina prima
incrementare prin scrierea valorii 6 înapoi în variabilã. Valoarea finalã a variabilei ar fi 6 în loc sã fie 7
aºa cum ne-am aºtepta dacã cele douã incrementãri s-ar face pe rând.
Spunem cã cele douã regiuni în care se face incrementarea aceleiaºi variabile sunt regiuni critice. Înainte
ca una dintre ele sã se execute, ar trebui sã ne asigurãm cã cealaltã regiune criticã nu ruleazã deja. Cea
mai simplã cale de a face acest lucru este sã punem o condiþie de blocare chiar pe variabila
incrementatã. Ori de câte ori o regiune criticã va încerca sã lucreze, va verifica dacã variabila noastrã
este liberã sau nu.
Instrucþiunea synchronized îºi blocheazã obiectul pe care îl primeºte ca parametru ºi apoi executã
secvenþa criticã. La sfârºitul acesteia obiectul este deblocat înapoi. Dacã instrucþiunea nu poate bloca
imediat obiectul pentru cã acesta este blocat de o altã instrucþiune, aºteaptã pânã când obiectul este
deblocat.
Mai mult despre aceastã instrucþiune precum ºi exemple de utilizare veþi gãsi în partea a treia, capitolul
9. Pânã atunci, iatã sintaxa generalã a acestei instrucþiuni:
Expresia trebuie sã aibã ca valoare o referinþã cãtre un obiect sau un tablou care va servi drept dispozitiv
de blocare. Instrucþiunea poate fi o instrucþiune simplã sau un bloc de instrucþiuni.
Instrucþiunea vidã este o instrucþiune care nu executã nimic. Ea este folositã uneori, atunci când este
obligatoriu sã avem o instrucþiune, dar nu dorim sã executãm nimic în acea instrucþiune. De exemplu, în
cazul unei instrucþiuni if, este obligatoriu sã avem o instrucþiune pe ramura de adevãr. Dacã însã nu
dorim sã executãm nimic acolo, putem folosi un bloc vid sau o instrucþiune vidã.
Iatã ºi un exemplu:
int x = 3;
if( x == 5 ) ; else x = 5;
Caracterul ; care apare dupã condiþia din if reprezintã o instrucþiune vidã care specificã faptul cã, în
cazul în care x are valoarea 5 nu trebuie sã se execute nimic.
[capitolul V]
[cuprins]