Tot despre clase si obiecte |
Simboluri speciale: null si this |
Asa cum am vazut in
capitolele precedente, numele unei clase poate fi utilizat la fel ca numele
unui tip predefinit, pentru a declara variabile. In Java variabilele al caror
tip este o clasa sunt reprezentate ca referinte (adrese) spre
obiecte ale clasei.
Ca sa putem initializa o
asemenea referinta trebuie sa generam un obiect nou sau sa folosim o alta
referinta la un obiect deja existent. De exemplu, folosind clasa Punct
definita in modulul 2.1, putem scrie:
Punct p1 = new Punct( ); // se creaza un obiect nou
Punct p2 = p1; // p2 va
referi acelasi obiect ca si p1
In legatura cu referintele exista o valoare speciala, null, care poate fi atribuita unei variabile-referinta. Cand o referinta are valoarea null, aceasta inseamna ca ea nu refera nici un obiect. Cu alte cuvinte, null este echivalent cu "nici-o-adresa". In programe se poate testa daca o variabila-referinta are valoarea null:
Punct p2 = null;
// alte prelucrari
asupra lui p2
if ( p2 == null )
FaCeva( );
else FaAltceva( ); // de
exemplu p2.move(...)
Observatie: valoarea null nu este
atribuita automat tuturor variabilelor referinta la declararea lor. Regula
este urmatoarea: daca referinta este o data-membru a unei clase si ea nu este
initializata in nici un fel (vezi capitolul despre initializari), la crearea
unui obiect al clasei respective referinta va primi implicit valoarea null.
Daca insa referinta este o variabila locala a unei metode initializarea
implicita nu mai functioneaza. De aceea se recomanda ca programatorul sa
realizeze INTOTDEAUNA o initializare explicita a variabilelor.
class OClasa class AltaClasa
|
Simbolul this
Am vazut
pana acum ca obiectele unei clase pot fi accesate de clientii lor prin
intermediul referintelor. Se pune problema cum putem obtine o referinta la un
obiect din INTERIORUL lui insusi, mai precis din interiorul functiilor
membru ale obiectului. Pentru aceasta exista simbolul this.
Simbolul this
este o referinta care poate fi utilizata doar in cadrul functiilor membru
non-statice ale unei clase. this este de fapt, din punctul de
vedere al unei functii membru, referinta spre obiectul receptor,
"posesor" al acelei functii. Se poate spune ca this
reprezinta intr-un fel "constiinta de sine" a unui obiect, in sensul
ca obiectul isi cunoaste adresa la care este localizat in memorie.
Practic orice referire a unui
membru non-static v in interiorul unei metode apartinand
aceleiasi clase ca si v poate fi considerata ca echivalenta cu this.v.
De exemplu, metoda move din clasa Punct definita in
lucrarea precedenta poate fi scrisa sub forma:
class Punct |
In realitate toate referirile la membrii non-statici ai unui obiect, din interiorul unei metode non-statice sunt considerate de catre compilatorul de Java ca si cum ar fi prefixate de this.
Cand FOLOSIM explicit referinta this
Ea este necesara in primul rand acolo unde ar putea exista conflicte de nume cu datele membru ale unui obiect. De exemplu:
class Punct |
A. O metoda trebuie sa returneze ca rezultat o referinta la obiectul ei receptor:
class Rational public Rational aduna(int n) // exemplu de
utilizare a clasei |
In exemplul de mai sus apelul in lant al metodei adauga se desfasoara astfel: mai intai se executa primul apel, avand ca obiect receptor obiectul indicat de referinta a; rezultatul obtinut este returnarea unei referinte spre un obiect al clasei Rational; acest obiect va fi in continuare obiectul receptor pentru al doilea apel al 17217t1924r metodei adauga.
B. Referinta la obiectul receptor
trebuie transmisa ca parametru la apelul unei alte metode:
class Context public int Calcul() public int getX() class Algoritm /* exemplu de
utilizare a claselor */ |
In exemplul de mai sus se
observa ca acelasi obiect Algoritm deserveste 2 obiecte Context.
De aceea, fiecare dintre obiectele c1 si c2 se va
pasa pe sine ca parametru la apelul metodei Calcul din Algoritm.
In fine, simbolul this
reprezinta mijlocul prin care poate fi apelat un constructor al unei clase din
interiorul altui constructor al aceleiasi clase:
class Punct public Punct(Punct p) |
Atentie:
Cum NU TREBUIE SA FOLOSIM referinta this
Referinta this nu poate fi modificata (nu poate aparea ca membru stang intr-o atribuire:
class
OClasa |
Ceea ce vrea exemplul de mai sus sa scoata in evidenta este faptul ca un obiect nu-si poate muta singur locul in memorie. Intuitiv, o atribuire ca cea din exemplu seamana cu isprava baronului Munchausen care se lauda ca odata s-a salvat dintr-o mlastina tragandu-se singur in sus de propriile urechi (chestie care contravine legii conservarii impulsului :-)).
Referinta this
nu poate sa apara in afara corpului de instructiuni al metodelor non-statice
ale unei clase sau in afara blocurilor de initializare asociate cu
variabilele membru ale unei clase. Referinta this NU poate fi
utilizata in interiorul unei functii-membru statice. DE CE oare ? Putem comenta
in Conferinta Software Consulting.
Colectorul de reziduuri (Garbage Collector) |
In legatura cu obiectele
claselor stim acum ca ele se creaza dinamic, folosind operatorul new.
Nu am spus insa nimic referitor la eliberarea memoriei ocupate de
obiecte.
Si in limbajele Pascal si C
exista posibilitatea alocarii dinamice a memoriei, dar programatorul are
obligativitatea de a gestiona singur aceasta memorie, in sensul ca, la un
moment dat mai trebuie sa si elibereze ceea ce a alocat.
In Java programatorul este
scutit de sarcina de a face curat in memoria dinamica. De acest lucru se ocupa
o componenta a masinii virtuale (interpreterului) numita Garbage Collector
(GC). Principiul de lucru al GC-ului este urmatorul:
Daca spre un anumit obiect nu mai exista nici o referinta externa, in nici o functie activa, acel obiect ramane "orfan" si devine candidat la eliminare din memoria heap.
Nu este
obligatoriu ca un obiect sa fie sters imediat dupa ce dispare si ultima
referinta spre el. Este posibil ca obiectul sa ramana alocat pana cand, in
decursul executiei programului, se ajunge in situatia ca spatiul liber din heap
sa nu mai fie suficient. De asemenea, este posibil ca un program sa nu aiba
nevoie de memorie multa si ca urmare, sa nu trebuiasca sa se colecteze
reziduurile. In cazul programelor didactice, in care numarul total al
obiectelor create nu depaseste cateva zeci, practic GC-ul nici nu
intervine.
In exemplul de mai jos sunt
ilustrate situatiile in care un anumit obiect ar putea ajunge sa fie candidat
la stergere:
public void oMetoda( ) public void altaMetoda() |
Pentru utilizator actiunea
GC-ului este transparenta. Singurul efect care ar putea fi sesizat in unele
programe ar fi o anumita incetinire a lor.
Pentru a vizualiza insa
activitatea GC-ului, vom incerca sa simulam o situatie de umplere a memoriei
heap cu obiecte orfane, astfel incat sa-l fortam sa intre in actiune. Se pune
problema cum vom vedea noi ca intr-adevar GC-ul elimina obiecte ? Aici ne bazam
pe faptul ca GC nu actioneaza "fara preaviz", ci, inainte de a
elimina un obiect din memorie, el incearca sa apeleze o anumita metoda speciala
detinuta de obiectul respectiv.
Aceasta metoda are antetul
urmator:
protected void finalize() throws Throwable;
si ea va fi executata chiar inainte ca obiectul sa dispara (incercati sa ignorati cuvantul protected , precum si clauza throws Throwable, pentru ca despre ele vom invata la capitolul cu mostenirea, respectiv cu tratarea exceptiilor).
Am putea de exemplu sa
definim metoda finalize astfel incat aceasta sa afiseze pe ecran
un anumit mesaj. Atunci cand vom vedea pe ecran mesajul respectiv vom sti ca in
momentul acela un obiect a fost eliminat de catre GC din memorie. Daca prevedem
si in constructor afisarea unui mesaj, putem urmari practic traseul vietii unui
obiect. In secventa de mai jos este propus un program in care sa surprindem
interventiile GC-ului:
import java.io.*; class OClasa protected void finalize() throws Throwable class ClientOClasa public
static void main(String[] arg) oMetoda();
|
Pentru a putea analiza in liniste mesajele afisate prin executia programului de mai sus, se recomanda ca in comanda de lansare in executie sa se faca o redirectare a iesirii spre un fisier care apoi va putea fi vizualizat cu un editor oarecare. Pentru aceasta, comanda de executie se va da astfel:
java ClientOClasa > fis.txt
Metode |
Asa cum am aratat cand am
vorbit despre componenta unei clase, functiile care fac parte dintr-o clasa se
mai numesc metode sau operatii.
Definitia unei metode respecta
in general urmatoarea sintaxa:
tip_returnat nume_metoda (lista_parametri_formali)
unde tip_returnat
poate fi un tip primitiv, o referinta de obiect sau void. In
acest din urma caz metoda nu returneaza de fapt rezultate.
In Java metodele nu pot sa
apara decat in interiorul claselor.
In ceea ce priveste
transmiterea parametrilor, aceasta se face NUMAI prin VALOARE. La transmiterea
prin valoare metoda primeste o copie a parametrilor de apel. Ca urmare, orice
modificare efectuata asupra parametrilor in interiorul metodei se produce
asupra copiei respective, care dispare cand metoda isi incheie executia.
Astfel, la revenirea in apelant modificarile nu se mai "vad".
Exemplu:
class OClasa public static void metoda2() |
In exemplul dat parametrul
metodei a fost de tip primitiv. Aspecte mai interesante apar cand parametrul
este o referinta la un obiect. Practic si in acest caz transmisia se face prin
valoare, dar cea care se transmite asa este referinta, adica adresa obiectului.
Prin urmare, daca in interiorul metodei modificam valoarea referintei,
modificarea nu va fi resimtita in apelant. In schimb, daca modificam interiorul
obiectului indicat de referinta, modificarea respectiva ramane:
class Masina public void schimbaCuloare(String c) public String ceCuloare() public static void metoda1(Masina m) public static void metoda2(Masina m) public static void metoda3() |
Ca sa intelegem mai bine ce se intampla si de ce, intr-un caz ca cel din exemplul de mai sus, sa ne uitam putin pe figura urmatoare:
In figura este surprins momentul in care s-a intrat in metoda metoda1, inainte de a se efectua prima instructiune din ea. Se observa ca parametrul m al metodei este o copie a parametrului de apel, a, ceea ce inseamna ca ambele referinte indica spre acelasi obiect. Asa se explica de ce modificarea adusa de metoda metoda1 asupra interiorului obiectului se vede si in metoda3, la revenirea din apel.
Sa vedem acum figura urmatoare:
Aici este surprins momentul de dupa executia primei instructiuni din metoda metoda2, adica modificarea referintei m. Acum m indica alt obiect decat a. La revenirea din metoda2 se pierd variabilele locale ale acesteia, deci si m. Ca urmare, in metoda metoda3 lucrurile se regasesc asa cum au fost lasate. E adevarat ca in heap a ramas obiectul creat de metoda metoda2, dar spre el nu mai exista nici o referinta vizibila si ca atare, acest obiect va constitui un candidat la stergere de catre colectorul de reziduuri.
Setul de instructiuni |
In cele expuse pana acum am
considerat o abordare cumva intuitiva a instructiunilor din metode, in ideea ca
participantii la acest curs au deja notiuni elementare de programare (si chiar
mai mult :-)) si prin urmare nu intampina probleme in a intelege o instructiune
de atribuire, o instructiune de test sau o instructiune ciclica. Am accentuat
mai mult aspectele legate strict de lucrul cu obiecte, care se presupune a fi
ceva nou pentru cursanti.
E bine insa sa dedicam un
paragraf si prezentarii sistematice a setului de instructiuni Java. El se
bazeaza pe setul de instructiuni din limbajele C/C++, fiind format din
urmatoarele categorii principale:
Sintaxa:
variabila = expresie;
Semantica: se evalueaza expresia din membrul drept, iar rezultatul se depune la variabila specificata in membrul stang. In general este necesar ca cei 2 membri sa aiba acelasi tip. Expresiile din membrul drept pot fi:
conditie ? expresie1 : expresie2
unde conditie este o expresie logica sau de
comparare, iar expresie1 si expresie2 sunt expresii
oarecare.
Rezultatul expresiei de
selectie este expresie1 daca evaluarea conditiei da rezultatul
true, respectiv expresie2 in caz contrar. Practic, o atribuire in
care membrul drept este expresia de selectie poate fi inlocuita cu o
instructiune de testare de forma:
if(conditie)
variabila=expresie1;
else
variabila=expresie2;
Operanzii din expresii pot fi variabile, constante ale tipurilor primitive sau apeluri de metode, cu conditia ca acestea sa nu aiba void ca tip returnat.
Sintaxa:
if(conditie)
instructiune1;
sau
if(conditie)
instructiune1;
else
instructiune2;
In loc de instructiune1, respectiv instructiune2, putem avea secvente formate din mai multe instructiuni, terminate prin caracterul ;, dar atunci secventele respective trebuie incluse intre acolade.
Semantica instructiunilor de test: se evalueaza conditia si daca rezultatul obtinut este true, se executa ramura instructiune1; in caz contrar se executa instructiunea ce urmeaza dupa instructiunea de test (cazul fara ramura esle) sau ramura instructiune2 (daca avem ramura else).
In Java avem 3 tipuri de instructiuni de ciclare:
Sintaxa:
while(conditie)
instructiune1;
sau
while(conditie)
Semantica: se evalueaza conditia si daca rezultatul este
true, se executa instructiunile din corpul ciclului, dupa care se reia
evaluarea conditiei; cand rezultatul evaluarii conditiei este false se trece la
instructiunea care urmeaza instructiunii while. De aici deducem
ca exista situatii in care o bucla while poate sa nu se execute
niciodata, anume atunci cand conditia da false inca de la prima evaluare.
Sintaxa:
do
instructiune1;
while(conditie);
sau
dowhile(conditie);
Semantica: se executa instructiunile din corpul ciclului,
apoi se evalueaza conditia si daca rezultatul este true se reia corpul
ciclului; cand rezultatul evaluarii conditiei este false se trece la
instructiunea care urmeaza instructiunii do. De aici deducem o
bucla do se executa cel putin o data.
Asa cum vom vedea in
continuare, ciclul cu contor din Java este de fapt o generalizare a ceea ce se
intelege de obicei prin ciclu cu contor, in sensul ca executia lui nu este
neaparat controlata de valoarea unui contor. Denumirea provine insa de la
faptul ca acest tip de ciclu este cel mai adesea folosit in contextul cu
contor.
Sintaxa:
for(instr1;conditie;instr2)
instructiune3;
sau
for(instr1;conditie;instr2)
Semantica: se executa instructiunea
instr1, apoi se evalueaza conditia; daca rezultatul este true, se
executa corpul ciclului, apoi se executa instr2 si se reia
evaluarea conditiei; cand rezultatul conditiei este false se trece la
instructiunea de dupa ciclu. Se observa ca si aici, ca si in cazul buclei while
pot exista situatii cand bucla nu se executa nici o data.
In particular, oricare dintre elementele instr1, conditie
si instr2 pot sa lipseasca. Daca lipseste conditie,
se considera ca e ca si cum am avea acolo tot timpul rezultat true.
Exemple:
/*
caz clasic de folosire a buclei for */ System.out.println(arg[i]); /* se afiseaza elementul de index i al tabloului arg */ /*
varianta echivalenta cu cea de mai sus */ /* caz in care
bucla for nu are contor */ |
Sunt instructiuni care pot sa apara in corpul unui ciclu.
Sintaxa:
break;
Semantica: executia acestei instructiuni presupune parasirea
ciclului si trecerea la instructiunea urmatoare celei de buclare. Asa cum vom
vedea imediat, instructiunea break poate sa apara si intr-o
structura switch.
Un exemplu tipic de folosire a
lui break putem vedea in exemplul de mai sus, in cadrul
instructiunii for.
Sintaxa:
continue;
Semantica: executia acestei instructiuni presupune neglijarea eventualelor instructiuni de dupa continue si trecerea la evaluarea conditiei, apoi, in functie de rezultatul evaluarii reluarea obisnuita a ciclului sau parasirea lui.
Exemplu:
/* presupunem ca avem o bucla in care trebuie sa citim 10 numere cuprinse in intervalul [1,100] si sa calculam media lor aritmetica; in cazul in care utilizatorul introduce o valoare eronata, este invitat sa mai tasteze o data; pentru citire vom pune in comentariu secventa, pentru ca nu stim inca sa citim de la tastatura; presupunem ca cele 10 valori trebuie depuse intr-un tablou tab declarat in prealabil */ double
med=0;
|
Sa vedem cum interpretam secventa de mai sus: in cazul in
care utilizatorul introduce un numar eronat, i se afiseaza un mesaj
corespunzator, dupa care instructiunea continue determina ca
executia sa se reia de la testul conditiei, adica i<10,
neglijandu-se portiunea din bucla care cuprinde actualizarea variabilei med,
precum si incrementarea contorului i (va aduceti aminte ca la
bucla de tip for, ultima instructiune din corpul buclei este de
fapt instructiunea care apare ca al treilea element intre parantezele ce
urmeaza dupa cuvantul for). Deci contorul ramane la aceeasi
valoare, ceea ce inseamna ca se va citi acelasi element al tabloului tab
pana cand se va tasta o valoare corecta. Abia atunci ramura instructiunii if
va fi "sarita", executia continuand cu actualizarea lui med
si a lui i.
Sintaxa:
return expresie;
sau
return;
Prima forma
este admisa doar in metodele al caror tip returnat NU este void,
iar tipul expresiei trebuie sa fie compatibil cu tipul returnat.
A doua forma este admisa doar
in metodele care au ca tip returnat void, adica acele metode care
nu returneaza de fapt rezultate.
Semantica: executia unei
instructiuni return are ca efect parasirea metodei curente si
revenirea in apelant. Pentru metodele care returneaza rezultat, are loc
copierea in stiva, pe spatiul apelantului a rezultatului returnat, care poate
fi astfel utilizat in calcule sau pentru atribuire.
Sunt o categorie aparte de
atribuiri, care utilizeaza urmatorii operatori: +=,-=,*=,/=
si %=, in loc de obisnuitul =. Exemplu:
a+=expr;
O asemenea instructiune este echivalenta cu:
a=a+expr;
Similar se interpreteaza si ceilalti operatori speciali de
atribuire.
Presupun utilizarea
operatorilor ++, respectiv --.
Cu operatorul ++ ne-am
mai intalnit in exemplele de pana acum si l-am preluat "din mers", in
mod intuitiv. Acum i-a venit randul la o explicatie mai detaliata.
In primul rand trebuie spus ca
acesti operatori sunt operatori unari (au un singur operand) si se pot aplica
in 2 moduri: prefix si postfix. Cu ajutorul unor exemple vom
vedea care este diferenta dintre cele 2 moduri. Inainte de aceasta, insa, vom
preciza ca efectul acestor operatori consta in cresterea/scaderea valorii
operandului lor cu valoarea 1.
Sa consideram ca in cele ce
urmeaza vom aplica operatorul de incrementare asupra unei variabile i
care initial are valoarea 5.
In varianta prefix sintaxa
operatiei este:
++i
iar in varianta postfix:
i++
Operatia de incrementare poate
constitui o instructiune in sine, daca este urmata de caracterul ;.
In acest caz diferenta dintre forma postfix si cea prefix nu este
sesizabila.
Daca insa incrementarea apare
ca operand intr-o expresie, atunci cele 2 forme au efecte diferite. Fie
instructiunea:
a=++i;
In acest caz efectul este: se mareste valoarea lui i
cu 1, deci se ajunge la 6 si apoi aceasta valoare se atribuie lui a.
Daca insa avem:
a=i++;
lucrurile se petrec asa: mai intai se ia valoarea veche a lui i, adica 5 si se atribuie lui a si dupa aceea se mareste valoarea lui i. Deci in ambele situatii i va avea in final valoarea 6, dar a va avea valori diferite.
Lucrul cu tablouri in Java |
Ca si alte limbaje de programare de nivel inalt, limbajul Java ofera ca tip predefinit tipul tablou, cu ajutorul caruia se pot crea colectii de variabile de acelasi tip. Componentele tabloului pot fi accesate cu ajutorul unui index. Acesta trebuie sa apartina unui tip intreg, iar numerotarea elementelor intr-un tablou incepe intotdeauna de la 0. Pentru a declara un tablou se utilizeaza notatia:
tip_element[ ] nume_tablou;
O simpla declaratie ca aceasta nu presupune alocare de spatiu. In Java elementele unui tablou se aloca DINAMIC, specificandu-se dimensiunea tabloului:
tip_element[ ] nume_tablou = new tip_element[numar_elemente];
In Java numele unui tablou
este considerat ca o REFERINTA DE OBIECT, iar tabloul propriu-zis este
un obiect. Dupa ce s-a rezervat spatiu pentru elemente NU mai putem modifica
dimensiunea tabloului, decat daca cream un alt tablou, asa ca in exemplul de
mai jos:
int [ ] tab = new int[10]; // am creat un tablou de 10 intregi /* acum ii
completez cumva elementele si constat ca imi trebuie un tablou mai mare
int [ ] tab1 =
new int[20]; /* dupa care
actualizez referinta tab pt. a indica spre noul tablou */ |
La crearea unui tablou se
memoreaza dimensiunile sale, programatorul putand in orice moment sa le
cunoasca, prin intermediul atributului length:
static void oFunctie(int[ ] tabp) public static void main(String[ ] arg) |
Din acest exemplu se
observa ca dimensiunea unui tablou este cunoscuta oriunde in program, chiar si
cand tabloul este transmis ca parametru.
Putem sa ne imaginam ca un
obiect de tip tablou arata asa ca in figura de mai jos:
Faptul ca pentru fiecare
tablou se memoreaza dimensiunea permite ca la executie sa se detecteze orice
tentativa de depasire a limitelor tablourilor. Asemenea tentative se
sanctioneaza prin emiterea cate unei exceptii IndexOutOfBoundsException.
In Java tipul tablou este
considerat ca o clasa, iar in aceasta calitate, asa ca oricare alta clasa Java,
el mosteneste implicit clasa predefinita Object (mai multe despre
acest subiect in modulul cu mostenirea).
Ceea ce deosebeste un tablou fata de alte obiecte este, in esenta, posibilitatea de a aplica asupra lui operatorul de indexare pentru a-i accesa elementele. Nici o alta clasa din Java nu are definita operatia de indexare.
Daca
elementele unui tablou sunt la randul lor de tip clasa, aceasta inseamna ca de
fapt elementele respective vor fi referinte la obiecte ale acelei clase. La
crearea tabloului se rezerva spatiu DOAR pentru acele referinte si ele sunt
initializate cu valoarea null. Daca dorim ca acele referinte sa
indice spre obiecte propriu-zise, trebuie fie sa cream obiectele in mod
dinamic, asa cum am vazut in modulele precedente, fie sa atribuim elementelor
tabloului valoarea altor referinte nenule:
Punct
[ ] tab = new Punct[10]; // am creat un tablou de 10 REFERINTE la Punct
tab[i] = new Punct( ); // acum creez si OBIECTELE de tip Punct |
Tablouri bidimensionale
Java accepta si declaratii de tablouri bidimensionale:
tip_element[ ] [ ] nume_matrice;
Un tablou bidimensional este considerat de fapt ca un tablou de tablouri. Deci, elementele unui tablou bidimensional sunt referinte la tablouri unidimensionale. Rezervarea de spatiu pentru elementele unui tablou bidimensional se poate face in urmatoarele variante:
nume_matrice = new tip_element[nr_linii][nr_coloane];
nume_matrice = new tip_element[nr_linii][ ];
for(int i = 0; i<
nume_matrice.length; i++)
nume_matrice[i] = new tip_element[i+2];
in aceasta secventa s-a creat
o matrice in care prima linie are 2 elemente, a 2-a linie 3 elemente, s.a.m.d;
in continuare, pentru a prelucra elementele matricii vom proceda ca in
secventa:
for(int i=0; i < nume_matrice.length; i++)
for(int j=0; j < nume_matrice[i].length; j++)
// prelucreaza nume_matrice[ i ][ j ]
In secventele prezentate nume_matrice.length ne da numarul de linii, iar nume_matrice[i].length ne da numarul de elemente de pe linia i.
Lucrul cu siruri de caractere: clasa String |
Clasa String
este una dintre clasele predefinite fundamentale, care modeleaza lucrul cu
siruri de caractere. Modul cel mai simplu de manipulare a obiectelor de tip
String il constituie constantele String, care sunt siruri de caractere
incadradate de ghilimele (").
Orice referire a unei asemenea
constante in cadrul unui program Java are ca efect crearea unui obiect String care
contine secventa de caractere respectiva. Exemplu:
String s1 = "un sir";
Efectul frazei de mai sus este urmatorul :
Putem crea un obiect String si prin aplicarea unui constructor:
String s2 = new String(s1);
In acest exemplu obiectul indicat de s2 va avea acelasi continut ca si cel indicat de s1.
Intrebare: ce ar fi trebuit sa facem pentru ca s1 si s2 sa indice unul si acelasi obiect?
Alti constructori ai clasei String mai sunt:
String svid = new String( );
char[ ] tc = ;
String stc = new
String(tc);
Sirurile de caractere se pot obtine si prin diverse prelucrari ale unor siruri existente. In acest scop clasa String ofera un set de metode dintre care enumeram:
String toUpperCase();
String
toLowerCase();
String
substring(int);
String substring(int,
int);
Metodele clasei String includ si operatii care permit consultarea stringurilor, respectiv obtinerea de informatii cu privire la continutul lor:
int length();
char charAt(int);
int indexOf(char);
int
indexOf(String);
int
lastIndexOf(char);
int lastIndexOf(String);
Semnificatia acestor metode, precum si alte informatii legate de clasa String se gasesc in documentatia API si cursantii sunt rugati sa cerceteze aceasta documentatie (a se vedea adresa in rubrica Linkuri).
In afara de cele enumerate mai sus, clasa String mai este dotata si cu operatii de uz general, existente si in alte clase de altfel, printre care amintim: operatiile de comparare si cele de conversie. Despre acestea vom vorbi in modulul urmator.
O proprietate foarte importanta a metodelor clasei String este aceea ca ele nu modifica NICIODATA obiectul receptor, ci, atunci cand este necesar, creaza obiecte noi in care este inclus sirul de caractere modificat. Fie spre exemplu urmatoarea secventa:
String s1 = "un sir";
String s2 =
s1.toUpperCase();
Desi am fi tentati sa credem ca efectul acestei secvente consta in transformarea sirului continut in obiectul indicat de s1, astfel incat toate literele sa devina majuscule, in realitate ceea ce se intampla este ilustrat in figura urmatoare:
Deci obiectul indicat de s1 si-a pastrat continutul nemodificat. Metoda toUpperCase a creat un obiect NOU in care a plasat sirul cu litere mari si a returnat ca rezultat o referinta la acest nou obiect.
In
concluzie, putem spune ca un obiect String odata creat nu mai poate fi
modificat in nici un fel.
Concatenarea stringurilor
In Java aceasta operatie se realizeaza cu ajutorul operatorului + . Efectul este crearea unui obiect nou, al carui continut va fi sirul rezultat prin concatenare. Exemplu:
String s1 = "codul ";
String s2 =
"penal";
String s3 = s1 +
s2;
Efectul secventei de mai sus
este ilustrat in urmatoarea figura:
Sa rescriem acum exemplul de mai sus sub forma:
String s1 = "codul ";
String s2 = s1 +
"penal";
Efectul acestei secvente este
ilustrat mai jos:
Deosebirea fata de cazul anterior o constituie faptul ca obiectul care contine sirul "penal" nu este indicat de nici o referinta. Acest obiect a fost creat ca urmare a regulii ca orice constanta sir de caractere dintr-un program Java presupune crearea unui obiect nou, dar adresa lui nu a fost memorata explicit in nici o variabila a utilizatorului. Ca atare, dupa evaluarea expresiei de concatenare, obiectul ramane izolat in heap si nu mai poate fi accesat. Asemenea obiecte "orfane" vor fi eliminate din memorie la prima "razie" a colectorului de reziduuri.
In
legatura cu operatia de concatenare a stringurilor, trebuie facute urmatoarele
observatii:
Clasa String este singura asupra careia se poate aplica operatorul +. In afara de ea, doar tipurile primitive numerice si tipul char mai au definit acest operator (cu alta semnificatie insa).
Intr-o expresie de concatenare a stringurilor pot sa apara, pe langa obiecte string, si valori ale tipurilor primitive, acestea fiind automat convertite la string:
int a = 6;
String s1 =
"tab[" + a + "]="; /* s1 va contine sirul
"tab[6]=" */
String s2 = s1+8.5; /*
s2 va contine sirul "tab[6]=8.5" */
Artificiu: o metoda foarte
simpla de a converti o valoare primitiva in string este aceea de a o concatena
cu sirul vid; de exemplu:
String s1 = "" + a;
Intr-o
expresie de concatenare putem sa avem ca operanzi si obiecte ale altor clase
decat String, acestea fiind convertite la String, dar intr-un mod mai special
decat valorile primitive. In capitolul despre conversii de date vom vedea cum.
Cu alte cuvinte, operatorul + folosit pentru obiecte are un singur sens:
concatenare de siruri.
Stringurile si tablourile de caractere
Desi aparent un string si un tablou de caractere (tipul char[ ] ) modeleaza acelasi lucru, adica o secventa de caractere, cele doua tipuri se comporta diferit. In primul rand, asa cum am vazut, unui obiect String nu i se poate modifica continutul. Unui tablou de caractere i se poate modifica oricare dintre elemente:
String vs;
char[ ] tc;
tc[pos] = 'x';
//corect
//pentru vs nu avem o
posibilitate similara
Pentru a obtine caracterul aflat la o anumita pozitie pos intr-un String, vs de exemplu, nu putem sa folosim notatia vs[pos] ca în cazul tablourilor, ci trebuie sa utilizam metoda charAt(pos):
String vs;
char[ ] tc;
char c1 = tc[pos];
//corect
char c2 =
vs.charAt(pos); //corect
char c3 =
tc.charAt(pos); //eroare
char c4 = vs[pos];
//eroare
Atributul length aplicabil pentru tablouri nu are tocmai aceeasi semnificatie cu metoda length( ) de la stringuri, asa dupa cum va rezulta din urmatoarea secventa:
String
vs = "ababu"; tc[i] = vs.charAt(i); /* am pus in tc aceleasi caractere ca si in vs */ System.out.println("Lungimea
lui vs = "+vs.length()); |
Rezultatul afisat de acest program va fi:
Lungimea lui vs = 5
Lungimea lui tc = 10
Cu alte cuvinte, nu putem sa aflam cate caractere semnificative se afla intr-un tablou de caractere, decat daca parcurgem tabloul element cu element si le testam.
Intre tipurile String si
char[ ] exista totusi o legatura, in sensul ca limbajul Java ofera metode de
conversie prin care putem sa tranformam un String în tablou de caractere si
viceversa:
String
vs="blabla"; |
Metoda toCharArray
creaza un tablou ale carui elemente sunt caracterele stringului sursa.
Operatia inversa se realizeaza
cu ajutorul unui constructor al clasei String. Acesta în principiu poate
initializa un string preluand un subsir din tabloul de caractere. Parametrii
necesari sunt: referinta tabloului, indicele primului caracter din subsirul
dorit si lungimea acestui subsir. In exemplul nostru subsirul cuprinde tot
tabloul.
Aplicatii |
/** * Aplicatia afiseaza tripletele Pitagora in care numerele sunt <= o valoare maxima * Valorile in triplete vor fi in ordine crescatoare * Se face observatia ca nu exista triplete cu doua valori egale */ // Modificati programul astfel incat valoarea maxima sa se citeasca si // la sfarsit sa se afiseze numarul de solutii import javax.swing.JOptionPane; public class NumerePitagora // if contor } // if relatie // iesire for-uri incuibate if (contor != 0 ) // mai sunt solutii netiparite JOptionPane.showMessageDialog(null,mesaj, "Numere Pitagora",JOptionPane.INFORMATION_MESSAGE); System.exit(0); } } |
Alte informatii |
Tema |
1. Rulati aplicatiile din modul. Vizitati resursele propuse la Alte informatii..
Observatii, comentarii sunt binevenite in Conferinta Software Consulting.
2. Sa se scrie un program care: primeste ca argument la lansarea in executie un sir reprezentand un numar intreg si converteste numarul respectiv la tipul int.
Pentru conversie se parcurge sirul cifra cu cifra, de la stanga la dreapta aplicand formula de sintetizare a unui numar in baza 10:
valoare = valoare * 10 +
cifra_curenta
, unde valoare este initial
0.
3. Sa se scrie un program care genereaza primele N numere Fibonacci, memorandu-le intr-un tablou. Elementele tabloului se vor afisa apoi unul sub altul, iar valorile pare vor fi marcate in dreapta cu cate un asterisc. Valoarea N va fi furnizata ca argument la lansarea in executie a programului si pentru conversia ei la tipul int se va folosi metoda de la punctul anterior. ( Varianta: se citeste N folosind ferestre de dialog si se converteste folosind metoda parseInt ).
Indicatie: sirul Fibonacci are ca prime 2 elemente valorile 1 si 1; apoi, fiecare element este suma precedentelor doua. Astfel, elementele al 3-lea, al 4-lea si al 5-lea sunt: 2 , 3 si 5.
4. Se cere sa se testeze practic programul dat in paragraful Garbage Collector pentru diferite valori ale lui nr_ob.
5. Sa se scrie un program care, primind ca argumente un sir de caractere si un caracter, va afisa numarul de pozitii aflate intre prima si respectiv ultima aparitie in cadrul sirului a caracterului dat ca parametru. De exemplu, la lansarea in executie cu
comanda: java nume_program pompieristicomilitienesc m
rezultatul afisat va trebui sa fie: 10 pozitii.
6. Se citesc numere
intregi, pana la tastarea lui 0, care nu se prelucreaza.
Pentru fiecare numar sa se tipareasca:
Exemplu:
Pentru numarul 1231 se va tipari:
1 2 3 1
Nu este palindrom
Pentru numarul 75657 se va tipari:
7 5 6 5 7
Este palindrom
|