Tablouri

Conceptul de tablou

Tabloul (în engleză Array) este o structură de date de acelasi tip, numite componente ale tabloului, care sunt specificate prin indici. În programare, tabloul poate fi privit ca o colecție indexată de variabile de același tip.
 
Exemple tipice de tablouri sunt vectorul și matricea din matematică. Un vector este o colecție indexată de componente cu un singur indice. De exemplu  x=[x0, x1, ... , xn-1] este un vector cu n componente. Componentele au același "nume" cu vectorul, dar se disting prin indici, care specifică poziția componentei respective în cadrul tabloului. În limbajul Java, la fel ca în limbajele C/C++, indicii încep de la 0. Întrucât componentele vectorului sunt dispuse pe o singură direcție în spațiu, spunem că este un tablou unidimensional. La nivel conceptual, se consideră că tabloul ocupă o zonă compactă de memorie, în care componentele sale sunt așezate în ordinea crescătoare a indicilor, din care cauză mai este numit și masiv.

Matricea este un tablou bidimensional. Componentele matricei sunt ordonate pe două direcții în spațiu, iar poziția fiecărei componente este indicată prin doi indici: primul specifică linia, iar al doilea coloana în care se găsește componenta respectivă. Iată un exemplu de matrice cu 4 linii și 5 coloane:
 

a00
a01
a02
a03
a04
a10
a11
a12
a13
a14
a20
a21
a22
a23
a24
a30
a31
a32
a33
a34

În acest exemplu, numele matricei, ca și numele fiecărui element al ei, este a. Poziția componentei în cadrul matricei este specificată prin cei doi indici. Conform convenției de indexare din limbajul Java, indicii incep de la zero.

Pot exista și tablouri cu mai mult de două dimensiuni. Astfel, un tablou tridimensional poate fi imaginat ca un volum (o carte), având mai multe pagini, fiecare pagină fiind un tablou bidimensional (o matrice). În acest caz, primul indice specifică linia, al doilea - coloana, iar al treilea - pagina în care se găsește componenta respectivă. În mod similar, un tablou cu patru dimensiuni poate fi privit ca o serie de volume; fiecare componentă, în acest caz, are patru indici, cel de al patrulea fiind numărul volumului în cadrul seriei. Putem, desigur, continua raționamentul și pentru tablouri cu mai mulți indici.

 

Tablourile în limbajul Java

În limbajul Java, tablourile (engl.: Arrays) sunt considerate obiecte care aparțin unor clase descendente din clasa  Object. În consecință, variabilele care au ca valori tablouri sunt variabile referință, iar alocarea tablourilor în memorie se face dinamic, prin operatorul  new,  la fel ca în cazul celorlalte obiecte. Mai mult, unor variabile referință la  Object li se pot da ca valori referințe la tablouri, deoarece clasa  Object este superclasa oricărei alte clase, deci și a oricarui tip de tablou.

Tipul tabloului coincide cu tipul componentelor sale. Componentele pot aparține unor tipuri de date primitive, sau unor clase.

Tablouri cu un singur indice

Aceste tablouri corespund conceptului matematic de vector. Nu le vom numi totuși astfel, pentru a nu face confuzie cu obiectele clasei Vector din pachetul java.util.

Tabloul unidimensional este constituit dintr-un ansamblu de componente indexate (cu un singur indice), căruia i se asociază și o variabila de tip int numita  length, care reprezintă lungimea tabloului (numărul de componente). Indicii elementelor de tablou sunt cuprinși în intervalul  [0, length-1]. Utilizarea unui indice situat în afara acestui interval generează o excepție.

Întrucât tablourile sunt obiecte, pentru indicarea lor în program  se folosesc variabile referință.

Declararea și inițializarea tablourilor cu un singur indice

Variabilele referință la tablouri cu un singur indice pot fi declarate în două moduri:
    a/ într-o declarație de variabile se pune simbolul [] (o pereche de paranteze drepte) după numele variabilei referință la tablou. Ca exemplu, să considerăm declarațiile următoare:
    int a, b, c[], d, e[];
    String s1, ts1[], s2;
În aceste declarații, a, b, și d sunt variabile simple de tip double, deci ele pot primi valori simple de acest tip, iar s1 și s2 sunt variabile referință la  obiecte din clasa String (la șiruri de caractere). În schimb, c[] și e[] sunt variabile referință la tablouri de tip int (tablouri la care toate componentele sunt de tip int), iar ts1[] este o variabilă referință la un tablou cu componente din clasa String.
    b/ Parantezele se pun după numele tipului de date sau al clasei, în care caz toate variabilele din declarația respectiva sunt considerate drept referințe la tablouri. De exemplu, în declarațiile
    int[] i, j;
    long [] k, m;
    float []u, v;
    String[] ww;
variabilele i, j, k, m, u, v, ww sunt referințe la tablouri cu componente de tipuri corespunzătoare fiecarei declarații. Remarcăm că nu are importanță dacă parantezele sunt puse imediat după numele tipului sau clasei (fară spațiu liber) sau între acestea există unul sau mai multe spații.

Inițializarea tablourilor unidimensionale se poate face, de asemenea,  în două moduri:
    a/ indicând valorile componentelor tabloului, separate prin virgule și cuprinse între acolade, ca în exemplele următoare:
    int a=27, b=-15, c[]={-3,72,-21},d=-5,e[]={231,-98};
    String s1="un sir", ts1[]={"sirul 0", "sirul 1", "sirul 2"}, s2="alt sir";
    float[] u={-1.24076f, 0.03254f, 27.16f}, v={2.7698E-12f, -3.876e7f};
Remarcăm că în ultima declarație nu s-au mai pus paranteze după numele variabilelor, deoarece ele apar după numele tipului.
    b/ folosind operatorul new, urmat de numele tipului sau al clasei, însoțit de dimensiunea tabloului (numărul de elemente din tablou) scrisă între paranteze drepte, ca în exemplele următoare:
    double aa[]=new double[3];
    String str[]=new String[2];
În primul caz, se alocă în memorie spațiu pentru un tablou cu 3 componente de tip double, iar variabilei aa i se dă ca valoare referința la acest tablou. În al doilea caz, se alocă în memorie un tablou de variabile referință la obiecte din clasa String, iar variabilei str i se dă ca valoare referința la acest tablou.

Inițializarea unei variabile referință la tablou cu componente aparținând unei anumite clase se poate face atât cu tablouri din clasa respectivă, cât și din clase descendente ale acesteia. De exemplu, în declarația
    Object tab1[]=new Object[2], tab2[]=new String[3], tab3[]={"aaa","bbb","ccc"};
variabila tab1 este initializata cu o referință la un tablou de componente din clasa Object, în timp ce variabilele  tab2 și tab3 sunt inițializate cu referințe la tablouri de șiruri, clasa String(ca orice alta clasă) fiind descendentă a clasei Object.

În programul din fișierul InitTab1.java se testează declarațiile și inițializările de mai sus și altele similare și se afișează valorile componentelor tablourilor inițializate.  Se observă că, în cazul folosirii operatorului new pentru alocarea de tablouri, componentele acestora se inițializează la valorile lor implicite: 0 pentru date numerice și null pentru obiecte.

Atribuirea de valori variabilelor referință la tablou

Unei variabile referință la tablou i se poate atribui ca valoare o referință la un tablou de acelasi tip, sau dintr-o clasă descendentă a acestuia. Această referință poate fi obținută prin operatorul new (în care caz se alocă în memorie un nou tablou), fie printr-o expresie care are ca valoare o referință la un tablou de tip corespunzator deja existent în memorie.  În programul din fișierul Tab1.java se dau astfel de exemple.
 

Utilizarea tablourilor

Componentele tablourilor pot fi utilizate ca orice variabile simple. Referința la o componentă de tablou se face prin numele tabloului, insoțit de indicele componentei pus între paranteze drepte. De exemplu, u[3] este componenta de indice 3 a tabloului u, iar aa[i] este componenta de indice i a tabloului aa. Indicele poate fi orice expresie de tip întreg, cu condiția ca valoarea acesteia să nu iasa din cadrul domeniului de indici admis pentru tabloul respectiv.

Un exemplu de utilizare a variabilelor indexate s-a dat deja în programul din fișierul  InitTab1.java , când au fost afișate valorile componentelor tablourilor. Alte exemple se dau în programul din fișierul  Tab1.java. Este instructiv să urmărim în figurile următoare cum evoluează datele din memorie în timpul executării acestui program.

- Fig. 1 -

În figura 1 sunt reprezentate datele din memorie după executarea instrucțiunilor
    double a[]={3.276, -5.83, 12.8}, b[];
    String ts1[]={"aa","bb"}, ts2[];
    b=a;
    ts2=ts1;
Prin prima declarație s-au creat în memorie variabilele referință la tablouri de tip double a[] și b[], s-a creat, de asemenea, un tablou cu trei componente de tip double și s-a dat ca valoare variabilei  a referința la acest tablou. Prin a doua declaratie, s-au creat variabilele referință la tablouri de tip String  ts1[] si ts2[] și un tablou de tip String cu două componente, iar variabilei ts1 i s-a dat ca valoare o referință la acest tablou. Prin instrucțiunea  b=a  i s-a atribuit variabilei b[] aceeași valoare-referință ca cea din a[], iar prin ultima instrucțiune s-a atribuit variabilei ts2[] acceași valoare-referință ca a lui ts1[].

Remarcăm deosebirea importantă dintre tabloul de tip double și cel de tip String. Primul dintre ele are drept componente date de tip primitiv. În consecință, "celulele" tabloului conțin chiar valorile de tip double ale componentelor corespunzătoare.  În schimb, cel de al doilea este un tablou de obiecte din clasa String, deci componentele lui sunt, de fapt, variabile referință la obiecte String, iar aceste obiecte sunt reprezentate în memorie separat. În ambele cazuri, componentele tabloului sunt tratate ca niște variabile al căror tip este corespunzător declarației. Știm însă că variabilele de tipuri primitive au ca valori chiar date primitive, în timp ce pentru obiecte se folosesc variabile referință.

În figura 2 este reprezentată situația creeată după ce s-au executat instrucțiunile de atribuire
    b[0]=-12.7; b[1]=283.6;

- Fig. 2 -

Întrucât variabilele referință a[] și b[] indică același tablou, este normal ca valorile componentelor a[i] sunt și acum aceleași cu ale componentelor b[i], ceeace se constată și din afișarea prin program a datelor respective.

În figura 3 este reprezentată situația creată după executarea instrucțiunii  b=new double[4].

- Fig. 3 -

Prin operatorul  new s-a alocat în memorie un nou tablou cu 4 componente  double, iar lui b[] i s-a dat ca valoare  referința la acest tablou. Imediat după inițializare componentele noului tablou au valoarea zero, deoarece aceasta este valoarea implicită pentru tipurile de date numerice. În schimb, valoarea variabilei referință a[] a ramas aceeași, pe care a avut-o anterior. Acestor componente putem sa le dăm acum valori prin program.

În același program se testează și situația în care se încearcă accesul la tabloul b[] cu un indice care iese din domeniul admis [0 ... b.lenght-1]. Se constată că se produce excepția java.lang.ArrayIndexOutOfBoundsException.

Conversii de tip pentru referințe la tablouri

Tablourile cu componente aparținând unor tipuri de date primitive sunt considerate că sunt obiecte ale unor clase cu numele
    tip[]
De exemplu, un tablou cu componente  double (deci care a fost declarat ca  double[]), aparține clasei  double[], care este descendentă a clasei  Object (nu a clasei  Object[], care conține tablourile de obiecte). În mod asemanător, un tablou cu componente dintr-o anumită Clasa este considerat ca aparținând clasei Clasa[] care, de asemenea, este descendenta a clasei Object. De exemplu, daca s-au făcut declarațiile
    int a[]={54, 23, -17}, b[];
    String str1[]={"abc","def"}, str2[], str3[];
    Object ob1, ob3, tob1[];
sunt permise fara conversie explicită atribuiri de forma:
    tob1=str1;
    ob1=a;
    ob3=str1;
În primul caz,  tob1 este o referință la un tablou cu componente din clasa  Object, iar  str1 este referință la un tablou cu componente din clasa  String, care este descendenta a clasei Object. În următoarele două cazuri, ob1 si ob3 sunt referințe la Object, iar tablourile a[] și str1[] aparțin claselor int[] și, respectiv, String[], care sunt și ele descendente ale clasei Object.

În schimb, atribuirile următoare necesită conversie explicită (prin cast), deoarece se fac de la superclasă la clasă:
    str2=(String[])tob1;
    b=(int[])ob1;
    str3=(String[])ob3;

Pentru a face referință la componente din tablourile referite de variabilele ob1 sau ob3 este necesară, de asemenea, conversie explicită, deoarece ob1 siob3 nu au fost declarate ca tablouri. Se va scrie deci: ((int[])ob1).length, ((int[])ob1)[k], ((String[])ob3).length, ((String[])ob3)[j].

Nu trebuie, insa, facuta conversie explicită în cazul componentelor tabloului referit prin tob1[], deoarece vsrisbila tob1 a fost declarată ca referință la tablou, iar clasa Object este superclasă a clasei String. Este, deci permisă referința tob1[k].

Exemplele de mai sus, si altele, sunt testate în programul din fișierul ConvTip1.java. În același program, se testează și numele claselor-tablouri întoarse de metoda getName() a clasei Class. Explicarea codificărilor respective este dată în documentația java API, la descrierea acestei metode.
 



© Copyright 2000 - Severin BUMBARU, Universitatea "Dunărea de Jos" din Galați