Capitolul V
5.1 Variabile

5.1.1 Declarații de variabile

5.1.1.1 Tipul unei variabile
5.1.1.2 Numele variabilelor
5.1.1.3 Inițializarea variabilelor

5.1.2 Tipuri primitive

5.1.2.1 Tipul boolean
5.1.2.2 Tipul caracter
5.1.2.3 Tipuri întregi

5.1.2.3.1 Tipul octet
5.1.2.3.2 Tipul întreg scurt
5.1.2.3.3 Tipul întreg
5.1.2.3.4 Tipul întreg lung

5.1.2.4 Tipuri flotante

5.1.2.4.1 Tipul flotant
5.1.2.4.2 Tipul flotant dublu
5.1.2.4.3 Reali speciali definiți de IEEE

5.1.3 Tipuri referință

5.1.3.1 Tipul referință către o clasă
5.1.3.2 Tipul referință către o interfață
5.1.3.3 Tipul referință către un tablou

5.1.4 Clasa de memorare

5.1.4.1 Variabile locale
5.1.4.2 Variabile statice
5.1.4.3 Variabile dinamice

5.1.5 Tablouri de variabile

5.1.5.1 Declarația variabilelor de tip tablou
5.1.5.2 Inițializarea tablourilor.
5.1.5.3 Lungimea tablourilor
5.1.5.4 Referirea elementelor din tablou
5.1.5.5 Alocarea și eliberarea tablourilor

5.1.6 Conversii

5.1.6.1 Conversii de extindere a valorii
5.1.6.2 Conversii de trunchiere a valorii
5.1.6.3 Conversii pe tipuri referință
5.1.6.4 Conversii la operația de atribuire
5.1.6.5 Conversii explicite
5.1.6.6 Conversii de promovare aritmetică

5.1.1 Declarații de variabile

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:

Tip NumeVariabilă [, NumeVariabilă]*; ^

5.1.1.1 Tipul unei variabile

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 platforma hardware și software pe care lucrează.

Î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. ^

5.1.1.2 Numele variabilelor

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:

culoarea numărulDePași următorulElement

Variabilele finale ar putea avea nume precum:

PORTOCALIUVERDEALBASTRUDESCHIS ^

5.1.1.3 Inițializarea variabilelor

Limbajul Java permite inițializarea valorilor variabilelor chiar în momentul declarării acestora. Sintaxa este următoarea:

Tip NumeVariabilă = ValoareInițială;

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.

Regula ar fi deci următoarea: nici o declarație fără inițializare. ^

5.1.2 Tipuri primitive

5.1.2.1 Tipul boolean

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. ^

5.1.2.2 Tipul caracter

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. ^

5.1.2.3 Tipuri întregi

5.1.2.3.1 Tipul octet

Î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. ^

5.1.2.3.2 Tipul întreg scurt

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. ^

5.1.2.3.3 Tipul întreg

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. ^

5.1.2.3.4 Tipul întreg lung

Î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. ^

5.1.2.4 Tipuri flotante

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. ^

5.1.2.4.1 Tipul flotant

Valorile finite reprezentabile într-o variabilă de tip flotant sunt de forma:

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. ^

5.1.2.4.2 Tipul flotant dublu

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

Valorile finite reprezentabile cu flotanți dubli sunt de forma:

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 pe un flotant sau un flotant dublu. ^

5.1.2.4.3 Reali speciali definiți de IEEE

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. ^

5.1.3 Tipuri referință

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. ^

5.1.3.1 Tipul referință către o clasă

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. ^

5.1.3.2 Tipul referință către o interfață

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ă.

Declarația se face cu următoarea sintaxă:

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. ^

5.1.3.3 Tipul referință către un tablou

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.

Iată cum se declară o referință către un tablou de întregi lungi:

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.

Mai multe despre tablouri într-un paragraf următor. ^

5.1.4 Clasa de memorare

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. ^

5.1.4.1 Variabile locale

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. ^

5.1.4.2 Variabile statice

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ă. ^

5.1.4.3 Variabile dinamice

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:

Minge mingeaMea = new Minge();

iar pentru a rezerva spațiul necesar unui tablou de referințe către obiecte de tip Minge putem folosi declarația:

Minge echipament[] = new Minge[5];

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:

int numere[] = new int[10]; ^

5.1.5 Tablouri de variabile

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. ^

5.1.5.1 Declarația variabilelor de tip tablou

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ă:

byte [][]octeti = new byte[23][5];

În expresia de alocare sunt specificate în clar numărul elementelor pentru fiecare dimensiune a tabloului. ^

5.1.5.2 Inițializarea tablourilor.

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.

Inițializarea funcționează și la tablouri cu mai multe dimensiuni ca în exemplele următoare:

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. ^

5.1.5.3 Lungimea tablourilor

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ă:

float []tablou = new float[25];
int dimensiune = tablou.length;
// dimensiune primește valoarea 25

sau

float [][]multiTablou = new float[3][4];
int dimensiune1 = multiTablou[2].length;
// dimensiune1 primește valoarea 4
int dimensiune2 = multiTablou.length;
// dimensiune2 primește valoarea 3 ^

5.1.5.4 Referirea elementelor din tablou

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.

Iată câteva exemple:

int []tablou = new int[10];
tablou[3] = 1;
// al patrulea element primește valoarea 1
float [][]reali = new float[3][4];
reali[2][3] = 1.0f;
// al patrulea element din al treilea tablou
// primește valoarea 1

Î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:

char [][]caractere = new char [5][];

Figura 5.1 Elementele tabloului sunt de tip referință, inițializate implicit la valoarea null.

Variabila referință numită caractere conține deocamdată un tablou de 5 referințe la tablouri de caractere. Cele cinci referințe sunt inițializate cu null. Putem inițializa aceste tablouri prin atribuiri de expresii de alocare:

caractere[0] = new char [3];
caractere[4] = new char [5];

Figura 5.2 Noile tablouri sunt referite din interiorul tabloului original. Elementele noilor tablouri sunt caractere.

La fel, putem scrie:

char []tablouDeCaractere = caractere[0];

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. ^

5.1.5.5 Alocarea și eliberarea tablourilor

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:

float []reali = new float[10];
?
reali = null; // eliberarea tabloului
sau
reali = new float[15]; // eliberarea în alt fel
sau
{
float []reali = new float[10];
?
}// eliberare automată, variabila reali a fost
// distrusă la ieșirea din blocul în care a
// fost declarată, iar tabloul de 10 flotanți
// nu mai este referit ^

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. ^

5.1.6.1 Conversii de extindere a valorii

Î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:

  • byte la short, int, long, float sau double
  • short la int, long, float sau double
  • char la int, long, float sau double
  • int la long, float sau double
  • long la float sau double
  • float la double

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.

În aceste situații, se va produce o rotunjire a numerelor reprezentate. ^

5.1.6.2 Conversii de trunchiere a valorii

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
  • short la byte sau char
  • char la byte sau short
  • int la byte, short sau char
  • long la byte, short char, sau int
  • float la byte, short, char, int sau long
  • double la byte, short, char, int, long sau float.

Î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. ^

5.1.6.3 Conversii pe tipuri referință

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. ^

5.1.6.4 Conversii la operația de atribuire

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

char

byte

short

int

long

float

double

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 nu este finală

T este o clasă care este finală

T este o interfață

T = B[] este un tablou cu elemente de tipul B

S este o clasă care nu este finală

T trebuie să fie subclasă a lui S

T trebuie să fie o subclasă a lui S

eroare la compilare

S trebuie să fie Object

S este o clasă care este finală

T trebuie să fie aceeași clasă ca și S

T trebuie să fie aceeași clasă ca și S

eroare la compilare

eroare la compilare

S este o interfață

T trebuie să implementeze interfața S

T trebuie să implementeze interfața S

T trebuie să fie o subinterfață a lui S

eroare la compilare

S = A[] este un tablou cu elemente de tipul A

eroare la compilare

eroare la compilare

eroare la compilare

A sau B sunt același tip primitiv sau A este un tip referință și B poate fi atribuit lui A

Tabloul 5.2 Conversiile posibile la atribuirea unei valori de tipul T la o variabilă de tipul S. ^

5.1.6.5 Conversii explicite

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 continuare, valorile de tip boolean nu pot fi convertite la nici un alt tip.

Nu există conversii între valorile de tip referință și valorile de tip primitiv.

Î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.

Iată și un exemplu de conversie care poate fi decisă în timpul compilării:

Minge mingeaMea;
MingeDeBaschet mingeaMeaDeBaschet;
?
mingeaMea = ( Minge ) mingeaMeaDeBaschet;

În următorul exemplu însă, se poate decide în timpul compilării imposibilitatea conversiei:

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.

 

T este o clasă care nu este finală

T este o clasă care este finală

T este o interfață

T = B[] este un tablou cu elemente de tipul B

S este o clasă care nu este finală

T trebuie să fie subclasă a lui S

T trebuie să fie o subclasă a lui S

Totdeauna corectă la compilare

S trebuie să fie Object

S este o clasă care este finală

S trebuie să fie subclasă a lui T

T trebuie să fie aceeași clasă ca și S

S trebuie să implementeze interfața T

eroare la compilare

S este o interfață

Totdeauna corectă la compilare

T trebuie să implementeze interfața S

Totdeauna corectă la compilare

eroare la compilare

S = A[] este un tablou cu elemente de tipul A

T trebuie să fie Object

eroare la compilare

eroare la compilare

A sau B sunt același tip primitiv sau 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. ^

5.1.6.6 Conversii de promovare aritmetică

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.

Există două tipuri de promovare, promovare aritmetică unară și binară.

Î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.

La promovarea aritmetică binară se aplică următorul algoritm:

  1. Dacă un operand este double, celălalt este convertit la double.
  2. Altfel, dacă un operand este de tip float, celălalt operand este convertit la float.
  3. Altfel, dacă un operand este de tip long, celălalt este convertit la long
  4. Altfel, amândoi operanzii sunt convertiți la int.

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;

După efectuarea operației, valoarea obținută va fi convertită implicit la double.

În următorul exemplu, se produce o promovare unară la int de la short.

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]
(C) IntegraSoft 1996-1998