Home » Resurse Web Design » Despre Java

 
 
 

Despre Java

Generalitati

Pentru Internet firma  SUN Microcomputers a propus, implementat si utilizeaza un nou limbaj  – Java care ar putea sa devina „limbajul” acceptat pentru acest domeniu. În afara de calitatile recunoscute ale acestui limbaj pe care le vom discuta si noi,

faptul ca acest limbaj a fost acceptat de catre firmele Microsoft si Netscape par sa constituie argumente importante în acest sens. În noiembrie 1995 firma Borland a anuntat ca va cumpara licenta pentru Java si ca în prima jumatate a anului 1996 va lansa o serie de instrumente pentru dezvoltarea aplicatiilor Java. Principalele tipuri de instrumente avute în vedere sunt:

  • un mediu pentru dezvolatarea rapida de aplicatii grafice (visual RAD – rapid application development) pentru WWW si Internet. Produsul se va numi Latte.
  • compilatoare cu performante ridicate
  • acces la baze de date distribuite

Firma Lotus a anuntat ca  în 1996 va lansa  Notes Release 4 server care va utiliza sI tehnologia Java.

Ce aduce nou Java ?

Una dintre problemele „delicate” legate de sistemele distribuite o constituie eterogenitatea masinilor care constituie un astfel de sistem. Este perfect posibil ca în cadrul

aceluiasi sistem distribuit sa existe procesoare SPARC, PENTIUM, ’86, etc. ªi atunci un program care a început sa se execute pe un procesor din sistemul distribuit nu poate sa îsi continuie executia pe un procesor de alt tip. Dar daca executia este interpretata ? . Ineficient – dar cat de grava este situatia ?. O combinatie de interpretare si generare de cod ar putea sa constituie o solutie. Ideia de la care s-a plecat de fapt este legata de fapt de World Wide Web.  Programele de tip   Archie, Mosaic sau Netscape care realizeaza cautarea de informatii în serverele din WWW stiu (sau stiau) sa aduca numai informatie statica: texte, poze eventual foarte colorate dar atat. Atata timp cat „clientii” WWW erau numai cercetatori stiintifici pentru care gasirea ultimelor rapoarte stiintifice despre un anumit subiect constituia maxima bucurie cautata si satisfacuta de accesul la Web, acest tip de prezentare a informatiei era perfect. Dar  Web-ul devine tot mai mult sursa de informatii pentru multe alte categorii de  „clientii”. Astfel ca pe serverele din WWW au aparut reclame, a aparut  posibilitatea tranzactilor comerciale, învatamantului la distanta, a jocurilor  executate la distanta, accesul la stiri, etc.

Corespunzator forma de prezentare a informatiilor devine cel putin la fel de importanta ca si viteza cu care informatiile sunt regasite.  Este nevoie de animatie, sunete, etc. Dar daca o prezentare de produs care presupune animatie ar trebui sa se executa pe un server  Web, aflat eventual pe un alt continent calitatea rezultatului ar fi foarte proasta.  În loc de asta ceea ce se aduce de pe server este un program care se va executa pe statia locala. Simplu, doar ca apar o serie de probleme:

  • daca programul este în format binar executabil, atunci ar trebui sa existe cate un compilator pentru  fiecare tip de procesor care poate sa fie utilizat pentru accesul la Internet. Solutia – existenta unui factor comun – o masina virtuala simpla pentru care sa existe interpretoare (sau generatoare de cod nativ) pentru fiecare tip de procesor. Executia programelor pentru diferite arhitecturi se face prin interpretarea codului de catre un simulator al masinii virtuale.
  • daca un limbaj de programare urmeaza sa aiba o utilizare pe o scara atat de mare cum este Internet-ul este, ciclul de viata obisnuit pentru un compilator  – se distribuie binar o prima versiune, urmeaza versiuni noi, corectii, etc. nu poate sa fie realizat pastrand sincrone toate variantele care exista. Solutia – nu este noua  – si compilatorul este scris în noul limbaj. Deci orice noua varianta se propaga simplu pe baza unei versiuni care functioneaza
  • accesul la Internet este liber, este foarte posibil (si s-a constatat de multe ori chiar ca este asa) ca persoane rau intentionate sa  produca programe care sa  contina virusi.  Executia unui program care nu are un autor (origine) cunoscuta pe statia proprie sperie pe oricine. Solutia – gasirea unor mecanisme care sa poata sa verifice ca un program nu contine cod „rau”. Existenta unor mecanisme care sa verifice codul înainte de a fi executat permite ca simularea masinii virtuale sa se poata face foarte rapid pentru ca numarul de teste necesare în faza de executie este redus.
  • în mediul  industrial, comercial, reactia obisnuita în fata unui nou limbaj de programare este de respingere (spre deosebire de mediul academic care adora experimentele – cu cat mai ciudate cu atat mai bine). Solutia – un limbaj care sa preia conceptele unui limbaj larg utilizat, eventual restrangandu-l  si la care sa adauge un numar minim de elemente pastrand  însa „atmosfera” limbajului, ceea ce permite programatorilor sa se adapteze rapid la noul limbaj.
  • chiar daca se accepta ideia de limbaj de programare nou, se pune problema în ce masura se poate utiliza în cadrul unui program scris în acest limbaj nou, cod scris în alte limbaje de programare. Solutia – sa se prevada de la început în limbaj un mecanism corespunzator utilizarii de clase sau metode „native”.
  • multe din limbajele utilizate în prezent pentru programarea aplicatiilor paralele sau distribuite utilizeaza biblioteci de functii care implementeaza aspectele legate de utilizarea a mai multor fire de executie (thread-uri). În acest caz, compilatorul nu „stie” sa faca nimic special – inclusiv sa optimizeze codul pentru aspectele legate de programarea firelor de executie. Solutia – integrarea în limbaj a aspectelor care tin de programarea firelor de executie.
  • evolutia aplicatiilor în Internet devine pe zice trece mai rapida. Corespunzator viteza de modificare a programelor existente în Internet este foarte mare. Sa presupunem ca un program disponibil pe un server A invoca clase de pe un alt server B. În acest  caz la modificarea claselor din B în mod normal trebuie sa se recompileze programul din A. Solutia – încarcarea dinamica a claselor. Adica, clasele se încarca în momentul în care sunt necesare, eventual din retea.

  • existenta unei documentatii bune si mai ales „la zi” pentru apelurile API (Applicaton Programming Interface) este o conditie foarte importanta pentru succesul unei platforme de programare.  Sa nu uitam ca pentru MS-DOS si MS-Windows s-au scris multe carti despre aspectele nedocumentate sau prost documentate.  ªi asta mai ales în faza de început, de stabilire a platformei,  cand  au loc schimbari multe la intervale scurte de timp.  Solutia – generarea documentatiei direct  pornind de la sursele bibliotecilor care implementeaza API. Deoarece se considera ca documentatia va fi disponibila pe WWW, generarea se face în format HTML. În acest scop au fost definite niste reguli foarte stricte de scriere a comentarilor pentru interfata de programare pentru Java. Pe baza acestor comentarii un program numit javadoc realizeaza generarea documentatiei. În acest mod este foarte simplu de pastrat sicronizarea dintre surse si documentatie.

ªi uite asa a aparut limbajul Java. Ca limbaj de plecare a fost ales limbajul C++ din care  s-au eliminat o serie de aspecte „toxice” ca de exemplu:

  • Java nu adminte pointeri. De ce a fost luata aceasta decizie – este destul de clar.  În acest  mod este mult mai usor de controlat codul care se executa si care va accesa numai zonele de memorie la care interpretorul îi da acces. Transferul argumentelor de tip vectori sau obiecte se face prin referinta, dar programatorul nu  are acces la valorile respective decat ca sa acceseze elementele vectorilor sau obiectelor.
  • Java nu utilizeaza tipul union sau structure. Existenta claselor face inutila utilizarea acestora si în plus verificarile codului sunt mai simple.
  • Pentru siruri de caractere în Java se utilizeaza o clasa String continuta în pachetul standard de clase cu care lucreaza orice program. În implementarea clasei String toate verificarile corespunzatoare sunt realizate implicit., iar operatiile de tip – comparatie, copiere se fac utilizand operatii standard.
    • pentru toate tipurile de date reprezentarea este fixata prin definitia limbajului :
tip dimensiune descriere
byte 1 octet un numar întreg cu semn cu valori între -128 si 127
short 2 octeti un numar întreg cu semn cu valori între -215 si 215-1
int 4 octeti un numar întreg cu semn cu valori între -231 si 231-1
long 8 octeti un numar întreg cu semn cu valori între -263 si 263-1
float 4 octeti numar real în format IEEE
double 8 octeti numar real în format IEEE
boolean 1 bit numai true si false
char 2 octeti caracter reprezentat în codul Unicode

pentru caractere se utilizeaza formatul Unicode care permite reprezentarea unui numar mare de caractere.

  • în afara de instructiunile „clasice” : atribuire, if, switch, for, while, do, break, return, continue în Java exista  si instructiunile : throwtry si synchronized. Instructiunile throw si try sunt utilizate pentru tratarea exceptilor. În Java exista o serie de exceptii  standard, ca de exemplu – exceptia referitoare la împartirea cu zero. Forma generala pentru instructiunea throw este:

throw Expresie.

argumentul pentru throw este un obiect derivat din clasa Exception. Dupa executia instructiuni throw se abandoneaza executia instructiunilor ce contin aceasta instructiune (de exemplu cicluri) si nu sunt try. Exceptia se propaga pana cand se ajunge la o instructiune try care are prevazuta o clauza catch cu un tip de exceptie ce constituie o superclasa a clasei obiectului care apare în instructiune throw. Instructiunea try are una dintre urmatoarele forme generale:

try {

} catch( Argument) { …}

try {

} catch( Argument) { …} … catch( Argument) { …}

try {

} finally { …}

try {

} catch( Argument) { …} finally { … }

O instructiune try precizeaza modul de tratare al exceptiilor care pot sa apara în executia listei de instructiuni care formeaza blocul atasat acesteia. Daca apare o exceptie atunci tipul acesteia se compara cu tipurile argumetelor care apar în lista catch. Daca se gaseste printre argumente un tip caruia poate sa i se atribuie p valoare de  tipul exceptiei atunci se va executa codul asociat. Acest cod reprezinta domeniul de valabilitate pentru argumentul care a primit ca valoare obiectul exceptie. Dupa ce se executa acest cod se va executa codul specificat cu clauza finally (daca exista).  Instructiunea synchronized este utilizata pentru stabilirea regiunilor critice (pentru care se asigura excluderea mutuala). Se prefera însa utilizarea unor metode cu atributul  syncronized în loc sa se defineasca portiuni de cod ca find sectiuni critice.

  • Java nu permite utilizarea unui numar variabil de argumente pentru functii (mecanismul vararg), ceea ce simplifica verificarea programelor.
  • în Java se pot definii metode sau constante ca find finale. Definitia unei  metode sau constante care a fost declarata finala nu poate sa mai fie inlocuita la extinderea clasei.
  • Java nu admite mostenirea multipla, concept greu de implementat si în acelasi timp greu de utilizat.

Java este implementat utilizand cateva componente. Exista un compilator care genereaza cod intermediar,  care este cod obiect pentru o masina virtuala orientata stiva. Executia programelor Java se face prin interpretarea codului intermediar. Textul în limbaj intermediar este adus de catre un program de cautare în retea (browoser). Un browser care „stie” Java contine un interpretor pentru masina virtuala  Java.  În cazul în care este necesara o viteza mare de lucru se poate utiliza si un compilator care sa genereze cod nativ din codul intermediar. În momentul de fata exista doua browswer-e care stiu sa „trateze” cod Java : Netscape si HotJava.  Primul este  larg cunoscut si face parte din categoria browser-elor „clasice”. Un asfel de browser este un interpretor pentru limbajul HTML care contine module pentru diferite protocoale ca: HHTP, FTP, SMTP, NNTP, Gopher, Telnet, etc. sau „stiu” sa vizualizeze imagini descrise de diferite formate ca: PostScript, GIF, JPEG, etc. Pentru fiecare protocol tratarea informatiilor se face în mod diferit. Pentru Netscape protocolul pentru Java nu reprezinta altceva decat un protocol care aduce sI trateaza alt tip de informatii. Orice format sau protocol nou presupune extinderea browser-elor existente cu module corespunzatoare. HotJava este un browser orientat Java. Adica este scris în Java si stie sa trateze formatul intermediar Java. Pentru fiecare protocol de transmitere de informatie sau format de reprezentare a imaginilor exista un program scris în Java. Un tip nou de protocol sau de format de reprezentare al imaginilor înseamna scrierea unui nou program în Java. Cand HotJava întalneste un format pe care nu îl întelege va solicita server-ului sursa pentru fisierul respectiv si programul Java care stie sa trateze protocolul sau formatul respectiv. În acest mod propagarea unor noi protocoale sau formate se face în mod dinamic pe masura ce acestea se raspandesc.

Java utilizeaza o serie de biblioteci (pachete) standard :

  • java.lang – colectia claselor corespunzatoare tipurilor de baza. Contine radacina ierarhiei de clase (Object), definitiile pentru toate tipurile de baza, exceptii, fire de executie.
  • java.io – colectia de clase pentru accesul la fisiere
  • java.net – colectia de clase pentru accesul la retea. Contine suport pentru diferite protocoale – socket-uri, interfata telnet, URL.
  • java.util – un numar mare de clase necesare pentru orice aplicatie
    • java.awt  – o biblioteca care ofera suportul pentru grafica (AWT = Abstract Windowing Toolkit). Realizeaza o interfata abstracta care se implementeaza usor pe diferite sisteme de ferestre. Trateaza – evenimente, culori, fonte, controale, etc.

Java si HotJava reprezinta un exemplu tipic de inovatie tehnologica aparuta la un moment favorabil  suportata în mod inteligent de o mare putere financiara. Ideile prezentate de Java nu sunt noi. Compilatoare portabile pe baza unei masini virtuale se scriu de zeci de ani, ideea de a transmite programe pentru executie prin retea de asemenea nu este noua. Toti ce care si-au propus asa ceva au tratat si problemele de securitate. Firma Sun nu este singura care si-a propus o solutie de tip Java. De exemplu firma Nombas a propus un limbaj numit Cmm (C minus minus). Spre deosebire de Java care a fost proiectat pornind de la C**, Cmm a fost proiectat pornind de la C. În ambele cazuri au fost eliminate aspectele periculoase si se utilizeaza  o solutie interpretata. Ceea ce este interesant este faptul ca Cmm a aparut ca produs comercial în 1993. În prezent este la versiunea 2.1 în timp ce Java care a devenit deja atat de faimos este înca la versiunea beta si probabil va apare ca produs comercial în 1996. Dar cine a auzit de firma Nombas ? Cine a vazut o versiune free de Cmm ? Lansarea Java a fost facuta cu un efort de marketing important. ªi în acelasi timp s-a luat o decizie foarte importanta. Toate informatiile necesare pentru dezvoltarea de aplicatii legate de Java sunt disponibile pe Internet. Este cunoscuta complet arhitectura masinii virtuale, interfata de aplicatii este bine documentata, probabil ca si sursele pentru HotJava vor fi facute disponibile, etc. Ce se întampla cu Java se aseamana pe undeva cu ce s-a întamplat cand firma IBM a propus micro calculatoarele din familia PC. Din punct de vedere tehnic acestea nu erau cu nimic mai bune decat alte solutii existente la momentul respectiv. Dar, firma IBM a luat o decizie de marketing foarte importanta si anume a facut publice atat schemele cat si listingul BIOS-ului. În acest mod numeroase firme au putut sa produca rapid calculatoare compatbile IBM. Aceasta decizie a facut ca arhitectura PC sa devina un standard al industriei de microcalculatoare.

Structura unui program Java

Sursele Java sunt organizate în biblioteci (pachete). Numele bibliotecilor care urmeaza sa fie larg utilizate trebuie sa fie unice si sa permita regasirea sursei. Construirea unui astfel de nume se face pe baza domeniului în care au fost create. Astfel o biblioteca nou creata  la Catedra de Calculatoare din UPB ar putea sa aiba numele: RO.pub.cs.apolo.nou. Se observa ca numele a fost prefixat cu un sir de caractere obtinut prin inversarea numelui domeniului si masinii pe care a fost creat. Prima componenta a numelui se scrie cu litere mari. Daca prima componenta a numelui este scrisa cu litere mici înseamna ca este voraba de o biblioteca care va fi utilizata numai local. Pe masina care contine codul sursa apolo (în cazul nostru), biblioteca respectiva se va gasi într-un subdirector cu numele: RO/pub/cs/apolo/nou. Fiecare biblioteca contine un numar de unitati de compilare. O unitate de compilare declara una sau mai multe clase sau interfete.  Cel mult una dintre ele este declarata public, numele fisierului care contine unitatea de compilare coincide cu numele acestui tip. Astfel ca daca trebuie sa se localizeze fisierul care contine definitia pentru untip nou declarat public atunci se stie ca sursa se gaseste în fisierul nou.java iar codul obiect pentru masina virtuala în nou.class.

O unitate de compilare creaza un spatiu de nume care contine toate numele declarate, importate si declarate în toate unitatiile de compilare din aceeasi biblioteca. O unitate de compilare poate sa importe tipuri sau biblioteci. Orice unitate de compilare importa în mod automat toate tipurile definite în biblioteca standard java.lang.

O declaratie de import poate sa aiba una dintre formele :

import NumeBiblioteca

import NumeBiblioteca.NumeTip

import NumeBiblioteca.*

În primul caz toate tipurile din biblioteca respectiva pot sa fie referite utilizand ultima componenta a numelui bibliotecii. De exemplu pentru  :

import java.io;

referirile la tipurile din java.io se fac sub forma : io.nume. A doua forma de declaratie de import  se refera la un tip declarat public în biblioteca respectiva si care va putea sa fie referit direct prin nume (fara a mai utiliza si numele bibliotecii). Ultima forma defineste importul la cerere. Astfel, orice tip declarat public în biblioteca respectiva poate sa fie importat pe baza acestei declaratii  atunci cand este referit.

Obiecte,  clase si  interfete în Java

Deoarece Java a fost definit pornind de la C++ majoritatea conceptelor legate de obiecte si clase sunt similare cu cele din C++. Se regasesc notiunile de: incapsulare, polimorfism, mostenire si legare dinamica. Cu exceptia variabilelor definite cu tipuri primitive orice altceva este definit ca obiect în Java. Pentru a defini un obiect se defineste întai o clasa care descrie comportarea obiectului respectiv.  O clasa are un continut – variabilele care alcatuiesc obiectul si o serie de metode care descriu comportarea acestuia. Ca si în alte limbaje orientate obiect clasele formeaza o ierarhie. Radacina acestei ierarhii se numeste Object. Pornind de la  o definitie de clasa se pot construi clase noi prin extinderea acesteia. O noua clasa obtinuta prin extinderea unei clase se numeste subclasa. Clasa care a fost extinsa se numeste superclasa.  Extinderea unei clase înseamna definirea variabilelor care se adauga la cele din definitia clasei care se extinde, definirea de noi metode si eventual înlocuirea unor metode existente în definitia care se extinde (metode care nu au fost declarate finale si care deci pot sa fie extinse). Clasa Object contine o serie de metode finale care vor fi mostenite de orice clasa definita în Java.  Majoritatea metodelor clasei Object nu sunt scrise în Java.

metoda se poate înlocui ? este  scrisa în Java ? Descriere
public final nativeClass getClass(); nu nu rezultatul este un descriptor al clasei
public native int hashCode(); da nu rezultatul este numarul pe care fiecare obiect îl primeste în timpul executiei
public boolean equals(Object obj) da da rezultatul este true daca cele doua obiecte sunt egale
protected native voidcopy(Object src); da nu copiaza valorile variabilelor din obiectul transmis ca argument
protected native Object clone(); da nu creaza un obiect nou cu aceleasi valori pentru variabile ca si obiectul curent
public nativeString toString(); da nu rezultatul este un sir de caractere care reprezinta valoarea obiectului. Orice sublasa trebuie sa redefineasca aceasta metoda
public final native void notify(); nu nu anunta un fir de executie blocat (prin executia unei metode wait()) ca a aparut o modificare.
public final native void notifyAll(); nu nu anunta toate firele de executie blocate ca a aparut o modificare.
public final native voidwait(long timeout) throws InterruptedException; nu nu produce blocarea  firului de executie din care se executa pana cand un alt fir de executie executa notify sau a trecut intervalul de timp fixat
public final void wait(long timeout, int nanos) throws InterruptedException {…

}

nu da produce blocarea firului de executie din care se excuta pana cand un alt fir de executie executa notify sau a trecut intervalul de timp fixat
public final void wait() throws InterruptedException {…

}

produce blocarea firului de executie din care se excuta pana cand un alt fir de executie executa notify

Sa consideram un exemplu de clasa care extinde clasa Object (exemplul este preluat din documentatia Java)

class Point extends Object {

public double x;

public double y;

}

Point este o subclasa a clasei Object. Utilizand aceasta declaratie pentru clasa Point, se poate declara o variabila în care se poate memora o referinta la un obiect de tip Point :

Point punct;

ca efect al acestei declaratii se face numai o rezervare pentru o variabila care poate sa contina o referinta la un obiect de tip Point. Dar un astfel de obiect nu exista înca. Pentru a crea un obiect se va executa:

punct = new Point();

Dupa executia acestei instructiuni se pot referi componentele obiectului prin intermediul variabilei punct:

punct.x  = 5;

Pentru a referi componentele obiectului în cadrul metodelor din clasa respectiva se utilizeaza obiectul generic this, care desemneaza obiectul curent. Astfel pentru a referi componenta x din interiorul obiectului se poate folosii numele x sau notatia this.x atunci cand exista pericol de confuzie. Dintr-un obiect se pot  referii variabile corespunzatoare superclasei corespunzatoare utilizand explicit numele clasei sau utilizand notatia super.  În exemplul considerat componentele obiectului pot sa fie referite explicit din exteriorul obiectului deoarece numele respective au fost declarate ca find publice.

La crearea obiectului punct nu au fost necesare prelucrari speciale, din acest motiv nu a fost specificat un constructor. Daca astfel de prelucrari sunt necesare  – de exemplu trebuie sa se initializeze componentele obiectului, atunci  trebuie sa fie specificat un constructor. Un constructor apare în definirea unei clase ca o metoda care are acelasi nume cu clasa. În acest mod programatorul nu poate sa refere direct o metoda de tip constructor. De exemplu:

class Point extends Object {

public double x;

public double y;

Point(){

x = 0.0;

y = 0.0;

}

Point(double x, double y){

this.x = x;

this.y = y;

}

}

Au fost definiti doi constructori, care difera prin numarul de argumente. În functie de apel se va utiliza un constructor sau altul. Astfel, daca se executa o instructiune ca:

punct = new Point();

atunci se va utiliza primul constructor, deci componentele se vor initializa la zero. Daca se executa instructiunea:

punct = new Point(2.0, 5.0);

atunci se va utiliza al doilea constructor. Într-un constructor se poate referi ca prima instructiune un constructor  al clasei curente sau un constructor al clasei pe care clasa curenta o extinde. Un astfel de apel apare sub forma ;

this (ListaDeArgumente); /*constructor

din aceasi clasa

*/

respectiv

super(ListaDeArgumente); /*constructor

din super clasa

*/

Daca nu se prevede explicit un constructor (si nu este vorba de clasa Object) atunci se considera implicit ca se foloseste constructorul fara argumente al  superclasei respective (daca un astfel de constructor nu exista se va semnala o eroare de compilare) dupa care se initializeaza variabilele instantierii respective.

A fost posibil sa se defineasca doi constructori pentru clasa Point deoarece limbajul Java permite „overloading”, adica existenta mai multor metode cu acelasi nume dar cu semnaturi (lista de parametrii) diferite.

Pentru tipul Point cele doua variabile componente au fost declarate publice, cu alte cuvinte ele pot sa fie modificate explicit. Dar, astfel de prelucrari nu sunt de fapt în spiritul programarii orientate obiect. Ideea ar fi ca variabilele continute în obiecte sa fie ascunse si modificarea lor sa se poata face numai prin intermediul metodelor definite o data cu clasa respectiva. Sa consideram o alta declaratie pentru clasa Point.

class Point extends Object {

private double x;

private double y;

Point(){

x = 0.0;

y = 0.0;

}

Point(double x, double z){

this.x = x;

this.y = y;

}

public void setX(double x) {

this.x = x;

}

public void setY(double y) {

this.y = y;

}

public double getX() {

return x;

}

public double getY() {

return y;

}

}

Variabilele x si y nu mai sunt accesibile din exterior decat prin intermediul metodelor de acces.     Atributele public si private descriu nivelul de acces la variabilele respective, ele nu sunt singurele posibile. Java permite utilizarea a patru nivele de acces la variabile sI metode :

  • public – pentru o variabila sau metoda înseamna ca accesul este permis din orice clasa, de oriunde
  • protected – pentru o variabila sau metoda înseamna ca accesul este posibil numai din clasa respectiva sau subclasele sale.
  • private – pentru o variabila sau metoda înseamna ca accesul este posibil numai din clasa în care au fost declarate. Nici macar subclasele clasei în care s-a facut definirea nu au acces la variabilele respective.
  • daca nu se utilizeaza un atribut se considera ca variabila sau metoda respectiva sunt accesibile din orice clasa care face parte din aceeasi biblioteca ca si clasa respectiva.

Daca  pentru o clasa se defineste cel putin un constructor (pentru a nu fi considerat un constructor implicit) si toti constructorii sunt declarati cu atributul private atunci  nu se pot crea instante ale clasei din afara sa.

Daca la crearea unui obiect se pot executa initializari, se pune problema cum se pot asocia operatii cu distrugerea unui obiect. Astfel de operatii sunt necesare de exemplu pentru a închide fisiere care au fost deschise în timpul executiei obiectului care se distruge.

protected void finalize(){

try {

file.close();

} catch (Exception e) {}

}

Metoda finalize() va fi activata atunci cand mecanismul de recuperare a memorie disponibile (garbage collection)  elibereaza spatiul de memorie ocupat de obiectul respectiv.

Putem sa continuam cu extinderea ierarhiei, pornind de la o clasa care descrie un punct în plan:

class Point extends Object {

protected double x;

protected double y;

Point(){

x = 0.0;

y = 0.0;

}

Point(double x, double y){

this.x = x;

this.y = y;

}

}

putem sa definim un punct în spatiu :

class Point3 extends Point {

protected double z;

Point3(){

x = 0.0;

y = 0.0;

z = 0.0;

}

Point3(double x, double y, double z){

this.x = x;

this.y = y;

this.y = y;

}

}

Se observa ca pentru variabile s-a utilizat atributul protected. În acest mod se permite accesul subclasei Point3 la variabilele x, y din clasa Point. S-a adaugat o noua variabila z si s-a completat definitia constructorilor în mod corespunzator. Cele doua componente vechi (x, y)  raman cu definitiile initiale.

Daca într-o clasa se defineste o metoda care are aceeasi semnatura cu o metoda din superclasa ambele metode pot sa fie referite dintr-o alta metoda din clasa. Metoda din clasa curenta prin numele sau iar super clasa utilizand prefixul super..

Pornind de la o definitie de clasa se pot obtine instantieri pentru mai multe obiecte. Fiecare dintre aceste obiecte contine cate un exemplar din fiecare variabila pe baza carora a fost definita clasa. Ce se întampla daca sunt necesare variabile globale, adica variabile care sa poata sa fie referite de catre orice obiect din clasa respectiva ?. Limbajul permite pentru astfel de cazuri declararea unor variabile statice. O astfel de variabila este locala unei clase, adica ea este creata o singura data la crearea primului obiect de tipul respectiv. ªi o  metoda poate sa fie declarata cu atributul static. În acest caz metoda este si finala în mod implicit. O astfel de metoda este considerata ca apartine clasei si nu instantierilor clasei. O metoda statica poate sa refere numai variabile statice.

De exemplu:

class Aplicatie extends Object{

static final int versiune = 3;

}

A fost declarata o clasa Aplicatie. Orice obiect instantiat din clasa Aplicatie va avea acces la variabila globala versiune. S-a folosit si atributul final, dar acest atribut este în acest caz  redundant.

Java opereaza cu o ierarhie de clase,  orice clasa este o subclasa a unei alte clase (cu exceptia radacinii Object).  Limbajul permite însa si definirea unor clase abstracte pornind de la care se pot crea noi ierarhii. O clasa abstracta reprezinta un „model” de clasa, pentru care se precizeaza o parte din metode si se fixeaza numele si modul de utilizare pentru alte metode. În cadrul unei clase abstracte se pot defini metode abstracte sau „obisnuite”.

abstract class Aplicatie  {

protected double x;

protected double y;

public void setXY(double x, double y) {

this.x = x;

this.y = y;

}

abstract void prelucrare();

}

Definitia clasei Aplicatie contine metoda prelucrare pentru care nu s-a precizat decat modul de apel. Prin extinderea acestei clase se pot construi noi subclase care vor mosteni tot ce este declarat  public sau protected în Aplicatie si vor  substitui metodele abstracte cu implementari concrete.

Pentru a permite utilizarea unor metode scrise în alte limbaje de programare (de exemplu  C sau limbaj de asamblare) limbajul Java permite utilizarea atributului native pentru metode. În acest caz în definirea clasei apare numai modelul (semnatura) metodei.

Limbajul Java permite utilizarea notiunii de interfata. O interfata defineste un set de constante si metode pentru care se specifica numai semnatura (forma de apel) fara a se preciza modul de implementare. Sa consideram de exemplu urmatoarea definitie din java.util:

/*

* @(#)Enumeration.java      1.9 95/08/13

*

package java.util;

/**

* The Enumeration interface specifies a set of methods that may be used

* to enumerate, or count through, a set of values. The enumeration is

* consumed by use; its values may only be counted once.<p>

*

* For example, to print all elements of a Vector v:

* <pre>

*          for (Enumeration e = v.elements() ; e.hasMoreElements() 😉 {

*              System.out.println(e.nextElement());

*          }

* </pre>

* @see Vector

* @see Hashtable

* @version        1.9, 08/13/95

* @author         Lee Boynton

*/

public interface Enumeration {

/**

* Returns true if the enumeration contains more

* elements;  false

* if its empty.

*/

boolean hasMoreElements();

/**

* Returns the next element of the enumeration. Calls to

* this method will enumerate successive elements.

* @exception NoSuchElementException If no more elements

* exist.

*/

Object nextElement();

}

Interfata prevede doua metode: hasMoreElements si nextElement. Aceasta interfata indica modul de apel al metodelor fara sa precizeze nimic despre modul de implementare. O impementare posibila este continuta în  Vector.java.

final

class VectorEnumerator implements Enumeration {

Vector vector;

int count;

VectorEnumerator(Vector v) {

vector = v;

count = 0;

}

public boolean hasMoreElements() {

return count < vector.elementCount;

}

public Object nextElement() {

synchronized (vector) {

if (count < vector.elementCount) {

return vector.elementData[count++];

}

}

throw new NoSuchElementException(„VectorEnumerator”);

}

Se observa ca noua clasa realizeaza implementarea interfetei Enumeration. Obiectele care instantiaza aceasta clasa (VectorEnumerator) contin vectori pentru care accesul se face pentru citire, într-o parcurgere secventiala.

O clasa poate sa implementeze mai mult dcat o singura interfata.  Adica sa defineasca  modul de implementare al metodelor al caror modele sunt continute în mai multe interfete. De  exemplu – interfetele: DataInput si DataOutput continute în java.io descriu metodele corespunzatoare citirii respectiv scrierii într-un format independent de masina. Aceste interfete sunt utilizate pentru definirea clasei RandomAccessFile :

public

class RandomAccessFile implements DataOutput, DataInput {

}

O interfata poate sa reprezine extensia unei interfete, adica sa adauge noi forme de metode la o interfata care exista.

O variabila poate sa fie declarata de tipul unei interfete. Valoarea unei astfel de variabile poate sa fie orice referinta la un obiect care apartine unei clase ce realizeaza implementarea interfetei respective sau extinde o astfel de clasa.

Aplicatii si applet-uri în Java

O aplicatie Java este un program scris în Java care se poate executa independent. Cea mai cunoscuta aplicatie Java este HotJava. Sa consideram cea mai simpla aplicatie posibila:

class HelloWorld extends Object {

public static void main( String args[] )

{

System.out.println( „Hello, world!” );

}

}

A fost definita o clasa care extinde direct clasa Object. Pentru aceasta clasa a fost definita o metoda numita main(). Semnificatia acestei metode este cea din C.  Metoda afiseaza bine cunoscutul text  „Hello, world!”.

Un applet este un program Java care se executa dintr-un browser care „stie” Java. O clasa care descrie un applet va fi o extensie a clasei Applet. Un applet este apelat cu o lista de parametrii. Fiecare parametru are un nume, o valoare si o descriere. O parte din metodele publice ale clasei Applet sunt descrise în tabelul urmator:

nume Descrire
public boolean isActive() Rezultatul este true daca applet-ul este activ. Un applet este marcat activ exact înainte de apelul metodei start
public URL getDocumentBase() Rezultatul este URL-ul documentului care contine applet-ul
public URL getCodeBase() Rezultatul este URL-ul applet-ului
public String getParameter(String name) Rezultatul este  parametrul applet-ului asociat sirului de caractere transmis ca argument
public AppletContext getAppletContext() Rezultatul este o referita catre contextul în care se executa applet-ul. Contextul este dat de browser sau de programul de vizualizare al applet-ului
public void resize(int width, int height) Se realizeaza redimensionarea applet-ului
public void showStatus(String msg) Se afiseaza un mesaj în contextul applet-ului
public Image getImage(URL url) Rezultatul este imaginea al carui URL se transmite ca argument. De fapt imaginea se va încarca atunci cand va fi referita.
public Image getImage(URL url, String name) Rezultatul este imaginea obtinuta pornind de la  URL care se transmite ca prim argument, utilizand numele transmis ca al doilea argument . De fapt imaginea se va încarca atunci cand va fi referita.
public AudioClip getAudioClip(URL url) Rezultatul este  AudioClip-ul al carui URL se transmite ca parametru
public AudioClip getAudioClip(URL url, String name) Rezultatul este  un AudioClip-ul care se obtine pornind de la  URL -ul transmis ca prim  parametru utilizand al doilea parametru ca nume
public String getAppletInfo() Rezultatul este un sir care descrie aplicatia
public String[][] getParameterInfo() Rezultatul este un vector de vectori de siruri de caractere care descriu parametrii aplicatiei. Fiecare vector are trei componente: nume, tip, descriere.
public void play(URL url) Se executa un AudioClip care se obtine pe baza URL-ului.
public void play(URL url, String name) Se executa un AudioClip care se obtine pe baza URL-ului si a numelui.
public void init() Initializarea applet-ului. Aceasta metoda este apelata automat de catre sistem la crearea applet-ului
public void start() Porneste executia applet-ului. Este apelata automat cand este vizitat documentul care contine applet-ul
public void stop() Opreste  applet-ul. Este apelata automat atunci cand documentul care contine applet-ul nu mai este vizibil.
public void destroy() Este utilizata pentru a elibera resurse care eventual sunt ocupate de catre un applet. Este sigur ca se va apela metoda stop înainte de a se executa aceasta metoda.
public void paint(Graphics g) Este utilizata pentru a afisa reprezentarea applet-ului în pagina afisata de browser
keyDown() Tratarea evenimentului – a fost apasata o tasta
mouseDown(), MouseUP() Tratarea evenimentului – a fost apasat /eliberat butonul mouse-ului
mouseEnter(), mouseExit() Tratarea evenimentului mouse+ul a intrat / iesit din zona afisata
mouseDrag() Tratarea evenimentului – mouse-ul a fost deplasat cu butonul apasat
mouseMove() Tratarea evenimentului – mouse-ul a fost miscat, fara sa fie un buton apasat

De exemplu sa consideram o parte dintr-un applet de demonstratie Java. Applet-ul afiseaza un desen la intervale de timp fixate.

/*

* Copyright (c) 1994 Sun Microsystems, Inc. All Rights Reserved.

*/

import java.awt.*;

import java.util.StringTokenizer;

/**

* I love blinking things.

*

* @author Arthur van Hoff

*/

public class Blink extends java.applet.Applet implements Runnable {

Thread blinker;

String lbl;

Font font;

int speed;

public void init() {

font = new java.awt.Font(„TimesRoman”, Font.PLAIN, 24);

String att = getParameter(„speed”);

speed = (att == null) ? 400 :

(1000 / Integer.valueOf(att).intValue());

att = getParameter(„lbl”);

lbl = (att == null) ? „Blink” : att;

}

public void paint(Graphics g) {

}

public void start() {

blinker = new Thread(this);

blinker.start();

}

public void stop() {

blinker.stop();

}

public void run() {

while (true) {

try {Thread.currentThread().sleep(speed);

} catch (InterruptedException e){}

repaint();

}

}

}

La crearea applet-ului (executia metodei init()) se preiau parametrii care sunt viteza de  repetare a redesenarii si un nume. Executia applet-ului  înseamna executia metodei run(). aceasta metoda este executata ca efect al executiei metodei start() a firului de executie pornit pentru metoda start() a applet-ului. Aceasta metoda este activata automat atunci cand este vizitat documentul care contine applet-ul. Metoda repaint() este o metoda mostenita de catre clasa Applet sI care va apela metoda paint.

În general pentru un applet trebuie sa se defineasca urmatoarele metode:

  • init() – este apelata automat la crearea applet-ului. În aceasta metoda de obicei se fixeaza atributele utilizate pentru executia applet-ului
  • start() – este apelata automat de fiecare data cand pagina caruia applet-ul îi este asociata este vizitata.
  • paint() – este apelata automat de fiecare data cand trebuie sa se afiseze desenul asociat applet-ului, de exemplu: cand applet-ul este vizibil prima data sau cand trebuie sa fie reafisat
  • stop() – este apelata automat cand este parasita pagina asociata applet-ului. Executia acestei metode trebuie sa opreasca executia applet-ului
  • destroy() – este apelata cand trebuie distrus applet-ul, de exemplu pentru ca urmeaza sa fie încarcat din nou. Metoda trebuie sa elibereze resursele pe care le detine.

Fire de executie în Java (thread-uri)

Deoarece aplicatiile Java trebuie sa permita executia „simultana” a unor prelucrari diferite: afisare de imagini, aducere de fisiere din retea, verificarea codului adus, controlul animatiei, controlul sunetelor, etc., în limbaj au fost prevazute mecanisme care sa permita programarea simpla a unor activitati „simultane”. Solutia adoptata: utilizarea unor fire de executie în cadrul unui program. Spre deosebire de procese, firele de executie care formeaza un program împart spatiul de adrese. Diferite fire de executie pot sa execute actiuni diferite. Avand acces la acelasi spatiu de adrese firele de executie pot sa comunice simplu prin variabile comune, de asemenea implementarea sincronizarilor se face simplu. În biblioteca java.lang exista definitia clasei Thread. Prin extensia acestei clase se pot defini clase noi care sa descrie obiecte de tip fire de executie. Pentru extinderea clasei Thread trebuie sa se specifice cel putin metoda run() care descrie codul care se executa ca fir de executie separat. În situatia în care un singura metoda a clasei Thread ce urmeaza sa fie înlocuita prin extensia clasei este metoda run() atunci se poate face o impementare a interfetei Runnable:

public

interface Runnable {

/**

* The method that is executed when a Runnable object is      * activated.The run() method is the „soul” of a Thread.

* It is in this method that all of the action of a

* Thread takes place.

* @see Thread#run

*/

public abstract void run();

}

Definirea interfetei Runnable a fost de fapt necesara pentru a permite executia ca fir de executie a codului continut în obiecte care nu reprezinta extensii ale clasei Thread. Un astfel de obiect ar trebui sa mosteneasca metode din doua origini. Deoarece Java nu admite mostenire multipla problema se rezolva extinzand o clasa si implementand în acelasi timp o interfata.

Sa consideram de exemplu ca dorim sa pornim un fir de executie pentru care se descrie numai codul corespunzator metodei run(). În primul rand trebuie sa definim o clasa noua. aceasta definire se poate face în doua:

class FirExecutie extends Thread {

public void run() {

}

}

sau

class Noua extends Object implements Runnable {

public void run() {

}

}

Pentru a porni executia firului de executie în primul caz se ve executa o secventa ca:

FirExecutie f = new FirExecutie();

f.start();

Adica se declara o variabila de tip FirExecutie. Aceasta variabila este initializata cu referinta la un obiect de tip FirExecutie care este creat utilizand un constructor cu acelasi nume. Constructorul este apelat fara parametrii. Utilizand referinta se poate initia executia firului prin apelarea metodei start(). Aceasta metoda va apela metoda run(). În al doilea caz se va executa:

Noua n = new Noua();

new Thread(p).start();

Se declara o variabila de tip Noua. Aceasta variabila este initializata cu o referinta la un obiect de tip Noua (care nu este un fir de executie). Pentru initializare se apeleaza un constructor fara parametrii.  Utilizand referinta obtinuta, se construieste un obiect de tip Thread prin apelul constructorului respectiv.  Constructorul apelat primeste ca argument un obiect care implementeaza interfata Runnable. Rezultatul apelului este un obiect  de tip Thread pentru care se va executa metoda start(). Ca efect se va începe executia metodei run() pentru clasa Noua. De obicei pentru clasele care implementeaza interfata Runnable se definesc  doua metode start() respectiv stop() dupa modelul celor ale clasei Thread.

class Noua extends Object implements Runnable {

Thread fir;

public void run() {

}

public void start() {

fir = new Thread(this);

fir.start();

}

public void stop() {

fir.stop();

}

}

Utilizand aceasta definitie pornirea unui fir de executie devine:

Noua n = new Noua();

n.start();

adica  o forma similara cu pornirea unui fir descris de un obiect de tip Thread.

Într-un program se pot crea mai multe fire de executie. Aceste fire pot sa formeze un grup. La randul lor grupurile de fire de executie pot sa faca parte din alte grupuri.  Sa mai consideram un exemplu inspirat din documentatia Java. Clasa SimpleThread implementeaza un fir de executie care executa repetat (de zece ori) urmatoarele actiuni: îsi afiseaza numele si doarme pentru un interval de timp (a carui durata este generata aleator).

class SimpleThread extends Thread{

public SimpleThread (String str){

super(str);

}

public void run(){

for(int i = 0; i < 10; i++){

System.out.println(i + “ “+ getName());

try {

sleep((int)(Math.random() * 1000));

} catch (InterruptedException e) {}

}

System.out.println(“DONE! “ + getName());

}

}

Prima metoda defineste un constructor care apeleaza constructorul superclasei Thread. Ca efect la crearea unui obiect SimpleThread acesta va primi un nume corespunzator sirului de caractere transmis ca argument. A doua metoda descrie modul în care se executa firul de executie. ªi anume de zece ori  se  afiseaza numarul iteratiei si  numele sau. Între doua afisari thread-ul va fi blocat pentru un interval de timp generat aleator. O clasa care sa testeze SimpleThread ar putea sa fie:

class TwoThreadTest{

public static void main (String args[]) {

new SimpleThread(“Bucuresti”).start();

new SimpleThread(“Brasov”).start();

}

}

Se vor crea doua obiecte de tip SimpleThread, argumentele transmise pentru constructori respectivi reprezinta numele firelor de executie create sI care vor fi tiparite pe parcursul executiei firelor de executie.

In tabelul urmator sunt continute metodele clasei  Thread cre pot sa fie apelate din afara clasei. Definitia clasei Thread este continuta în biblioteca java.lang.

Nume metoda Descriere
public Thread() Constructor , numele firului de executie va fi generat automat, trebuie sa se defineasca metoda run
public Thread(Runnable target) Constructor, argumentul trebuie sa fie un obiect care reprezinta o implementare pentru interfata Runnable. Numele firului de executiese va genera automat.
public Thread(String name) Constructor, numele firului de executie este transmis ca argument, trebuie sa se defineasca metoda run
public Thread(ThreadGroup group, Runnable target) Constructor, firul de executie va face parte din grupul de fire de executie transmis ca argument. pornirea executiei se face prin apelul  metodei run() pentru obiectul transmis ca argument.
public hread(ThreadGroup group,

String name

Constructor, firul de executie va face parte din grupul de fire de executie transmis ca argument, al doilea argument este numele firului de control
public Thread(ThreadGroup group, Runnable target, String name) Constructor, firul de executie va face parte din grupul de fire de executie transmis ca argument, al treilea  argument este numele firului de control, executia se lanseaza prin executia metodei run() pentru obiectul transmis ca al doilea argument .
public static Thread currentThread() Rezultatul este o referinta la firul de executie curent.
public static void yield() Firul de excutie curent  cedeaza controlul.
public static void sleep(int millis) Firul de executie curent este blocat pentru un numar de milisecunde corespunzator valorii argumentului.
public synchronized void start() Porneste executia firului de executie. De fapt va apela metoda run().
public void run() Codul corespunzator executiei firului de executie. Aceasta metoda trebuie sa fie redefinita pentru orice clasa care extinde clasa Thread.
public final void stop() Opreste firul de executie. Daca nu este înlocuita aceasta metoda transmite firului de executie un obiect  care este o instanta a clasei ThreadDeath.Daca se prevede un catch pentru acest obiect, codul de tatare trebuie sa execute un throw pentru a permite terminarea executiei firului de executie respectiv.
public final synchronized void stop(Object o) Opreste firul de executie. În mod normal pentru oprire se utilizeaza metoda fara argumente.
public void interrupt() Se transmite o întrerupere unui fir de executie
public static boolean interrupted() Test daca firul de executie a fost întrerupt
public static boolean interrupted() Se verifica daca alt fir de executie a fost întrerupt
public void destroy() Distruge un fir de executie. Metoda brutala, poate sa lase alte fire de executie blocate.
public final native boolean isAlive(); Verifica daca in fir de executie este activ
public final void suspend() Suspenda executia firului de executie
public final void resume() Reia executia unui fir de executie suspendat.
public final void setPriority(int newPriority) Seteaza prioritatea firului de executie
public final int getPriority() Rezultatul este nivelul d prioritate al firului de executie
public final void setName Modifica numele firului de executie
public final String getName() Rezultatul este numele firului de executie
public final ThreadGroup getThreadGroup() Rezultatul este o referinta la grupul din care face parte firul de control
public static int activeCount() Rezultatul este numarul de fire de executie active
public static int enumerate(Thread tarray[]) Copiaza în vectorul transmis ca argument referinte la firele de executie din grupul din care firul de executie face parte. Rezultatul este numarul de fire de executie
public native int countStackFrames(); Rezultatul este numarul de înregistrari în stiva pentru acest fir de executie. Cand se executa acest apel  firul de executie trebuie sa fie suspendat.
public final synchronized void join(long millis) throws InterruptedException Se asteapta terminarea firului de xecutie pentru un interval de timp transmis ca argument
public final synchronized void join(long millis, int nanos) throws InterruptedException Se asteapta terminarea firului de xecutie pentru un interval de timp transmis ca argument
public final void join() throws InterruptedException Se asteapta oricat terminarea unui fir de executie
public static void dumpStack() O metoda utila pentru depanare. Tipareste o lista a continutului stivei
public final void setDaemon(boolean on) Firul de executie curent devine daemon sau un fir de control obisnuit
public final boolean isDaemon() Verifica deca un fir de executie este daemon
public void checkAccess() Verifica daca firul de executie din care s-a apelat metoda unui alt fir de executie are dreptul sa faca modificari asupra acestuia. Este apelat în metode ca: stop, suspend, resume, setPriority, etc.
public String toString() Rezultatul este un sir de caractere  care descrie firul de executie contine: numele, nivelul de prioritate si numele grupului din care face parte

Dupa cum rezulta si din tabela firele de executie au nivele de prioritate. Executia este preemtiva (adica un fir cu prioritate mai mare poate sa întrerupa executia unui fir cu prioritate mai mica).

Fire de executie sincronizate

La nivelul limbajului este posibil sa se declare metode sincronizate. În acest caz metodele nu se pot executa concurent. Implementarea unei astfel de clase se face pe baza unui monitor. Pentru fiecare obiect instantiat  functioneaza un monitor. Sa consideram de exemplu metodele startAction() si stop() definite într-o clasa care implementeaza interfata Runnable.

Thread fir;

public synchronized void startAction() {

if (fir == null || !fir.isAlive()) {

fir = new Thread(this)

fir.start();

}

}

public synchronized void stop() {

if (fir != null) {

fir.stop();

fir = null;

}

}

Prima metoda verifica daca este prima executie a metodei (    (fir == null)) sau daca firul de executie a fost oprit (!fir.isAlive()) de exemplu pentru ca pagina în legatura cu care se executa nu a mai fost vizibila. În fiecare dintre aceste doua cazuri se va crea un fir de executie avand ca metoda run() metoda cu acelasi nume din  clasa curenta si se va initia executia firului respectiv. Daca variabila fir indica spre un fir de excutie activ atunci metoda nu are niciun efect. A doua metoda va executa oprirea firului de executie (daca el era activ) sI atribuie variabilei fir valoarea null. În acest mod spatiul ocupat de catre firul de executie va fi eliberat la prima activare a mecanismului de colectare a spatiului disponibil. Cele doua metode nu se pot executa simultan. În acest mod se pastreaza consistenta variabilei comune. Intrarea în executia unei metode sincronizate înseamna ocuparea monitorului. Implicit numai o singura metoda sincronizata poate sa fie în executie. Sa consideram un exemplu tipic:  problema producator / consumator.

class Producator extends Thread {

private ZonaTampon zonaTampon;

private int numar;

public Producator(ZonaTampon z, int numar) {

ZonaTampon = z;

this.numar = numar;

}

public void run() {

for (int i = 0; i < 10; i++) {

zonaTampon.put(i);

System.out.println(“Producator #” +

this.numar +

“ put: “+ i);

try{

sleep((int)(Math.random() * 100));

} catch (InterruptedException e){}

}

}

}

class Consumator extends Thread {

private ZonaTampon zonaTampon;

private int numar;

public Consumator(ZonaTampon z, int numar) {

ZonaTampon = z;

this.numar = numar;

}

public void run() {

int valoare = 0;

for (int i = 0; i < 10; i++) {

valoare = zonaTampon.get();

System.out.println(“Consumator #” +

this.numar +

“ got: “+ valoare);

}

}

}

Obiectele de tip Producator respectiv Consumator vor utiliza un obiect comun de tip ZonaTampon.  Se observa ca asa cum au fost definite cele doua clase nu s-a pus problema sincronizarii. Daca cele doua obiecte nu functioneaza cu aceasi viteza Consumatorul ar putea  sa piarda un numar sau sa citeasca acelasi numar de doua ori.  Corespunzator în definirea clasei ZonaTampon trebuie sa se asigure sincronizarea accesului. Rezulta o solutie pentru clasa ZonaTampon:

class ZonaTampon {

private int valoare;

private boolean disponibil = false;

public synchronized int get() {

while (disponibil == false) {

try{

wait();

} catch (InterruptedException e) {}

}

disponibil = false;

return valoare;

}

public synchronized void put(int val) {

valoare = val;

disponibil = true;

notify();

}

}

Variabila locala valoare pastreaza valoarea curenta memorata în  ZonaTampon. Valoarea variabilei locale disponibil indica existenta unei valori disponibile pentru consumator. Fiecare obiect pentru care s-a definit o metoda sincronizata are asociat un monitor. Deci va exista un monitor pentru fiecare obieect de tip ZonaTampon. La intrarea în executia unei metode declarate sincronizata se va ocupa monitorul, astfel ca pana la terminarea executiei metodei  nu se poate începe executia unei alte metode. Conform celor spuse anterior aparent în cazul în care se executa metoda get s-ar produce o situatie de deadlock deoarece daca se intra în executia metodei get si variabila disponibil nu are valoarea true metoda nu se poate termina mentinand ocupat monitorul. corespunzator metoda put nu se poate executa deci nu are cine sa modifice valorea variabilei disponibil.  De fapt cand se executa metoda wait() monitorul este eliberat find din nou ocupat cand se termina executia acestei metode.

Fire de executie daemoni

Orice fir de executie poate sa fie declarat daemon. Un daemon ofera servicii pentru alte thread-uri sau obiecte. De exemplu în HotJava exista un daemon numit BackgroundImageReader care citeste imagini dintr-un fisier sau le aduce prin retea. Metoda run pentru un astfel de thread contine un ciclu infinit.

Masina virtuala

Pentru a descrie arhitectura masinii vortuale Java trebuie sa specificam:

  • un set  de instrucsiuni
  • un set de registre
  • o stiva
  • o memorie pentru alocare dinamica (heap)
  • o zona pentru memorarea metodelor
  • tabele de simboli

Ca pentru orice masina virtuala în specificare nu se precizeaza nimic referitor la tehnologia de implementare. De asemenea pozitia zonelor de memorie implicate nu este semnificativa. Singura restrictie – lungimea cuvantului este de cel putin 32 biti. Corespunzator dimensiunea  memoriei adresabila este limitata la 4G.

Setul de instructiuni

Codul obiect generat de catre compilator utilizeaza instructiunile masinii virtuale. O instructiune este formata dintr-un cod de operatie si din zero sau mai multi parametrii. Codul operatiei ocupa un octet, parametrii pot sa fie de lungimi diferite (masina virtuala nu este RISC). Pentru codificarea instructiunilor se presupune ca numerele sunt reprezentate cu octetul cel mai semnificativ întai (ordine big-endian). Alinierea instructinilor în memorie se face la nivel de octet (ceea ce nu este eficient din punct de vedere al masinii virtuale, dar produce o reprezentare compacta a codului).

Tipuri de date

La nivelul masinii virtuale se pastreaza o parte din  tipurile primitive din limbajul sursa: integer, long, byte, short si simpla si dubla precizie pentru valori reale. Toate valorile numerice sunt cu semn. Tipul short este utilizat numai pentru reprezentarea caracterelor (reprezentare care utilizeaza codul Unicode). Exista si tipul object utilizat pentru reprezentarea obiectelor la nivelul masinii virtuale.

Tratarea tipurilor este rezolvata la momentul compilarii. De exemplu exista coduri de instructiuni diferite pentru adunarea a doua valori de tip integer (iadd) , long (long),  real simpla precizie (fadd), etc. Din acest motiv valorile cu care lucreaza masina virtuala nu au nevoie de indicatoare speciale de tip.

Setul de registre

Deoarece masina virtuala este orientata stiva,  setul de registre este relativ redus si contine numai registrele auxiliare referirii stivei. Principalele registre sunt:

  • pc – program counterregistrul adresei instructinii curente
  • optop – registru care contine adresa varfului stivei
  • frame – registru care contine adresa mediului pentru executia instructiunii curente
    • vars – registru care contine adresa de început a zonei de memorie care contine variabilele locale pentru metoda curenta

Fiecare registru este format din 32 de biti. Decizia utilizarii unei matini virtuale care nu are multe registre (cumva contrara abordarii RISC) face ca interpretarea codului sa se faca simplu (nu neaparat eficient) pe orice calculator.

Stiva

Dupa cum s-a mai spus masina virtuala este orientata stiva. Corespunzator orice instructiune presupune ca îsi va gasii valorile parametrilor în stiva si ca va depune rezultatul în stiva. Exista notiunea de frame – echivalenta cu înregistrarea de activare pentru o metoda. Înregistrarile de activare sunt memorate si ele pe stiva. Continutul unei înregistrari de activare este :

  • vectorul variabilelor locale
  • mediul de executie pentru metoda (environment)
  • stiva de operanzi

Dimensiunea spatiului necesar pentru variabilele locale si mediu este cunoscuta la momentul apelului metodei. Partea de stiva de operanzi variaza o data cu executia metodei.

Adresarea variabilelor locale se face relativ la registrul vars. Exista instructiuni care transfera valorile variabilelor locale între stiva de operanzi si vectorul variabilelor locale. Numarul de variabile locale este limitat  la 256.

Mediul de executie (environment) contine legaturi la înregistrarile de activare anterioare (pentru a implementa domeniul de viata) si legaturi la zona de variabile respectiv stiva. Executia instructiunilor presupune gasirea valorilor referite prin intermediul acestei zone cu pozitie cunoscuta. În vederea depanarii programelor se considera ca aceasta zona poate sa contina si alte tipuri de informatii.

Fiecare metoda Java are asociata o lista de exceptii prevazute. Pentru fiecare exceptie se specifica instructiunile la care se refera si care este codul care trebuie sa fie executat daca   apare exceptia respectiva. La aparitia unei exceptii se cauta identificarea unei clauze corespunzatoare pentru metoda curenta. Daca o astfel de clauza nu este gasita atunci se descarca stiva de înregistrarea de activare curenta si se cauta din nou o potrivire.

Stiva de operanzi curenta se gaseste în varful stivei masinii virtuale. Executia unei instructiuni cu operanzi presupune ca valorile operanzilor au fost memorate anterior în stiva. În urma executiei instructinii se vor extrage valorile respective din stiva si în schimb se memoreaza rezultatul operatiei.

Zona de memorie pentru alocare dinamica (heap)

Zona heap este utilizata pentru memorarea obiectelor. Referirea obiectelor se face prin intermediul  unor pointeri. Obiectele nu sunt dealocate explicit. În implementarea masinii virtuale se presupune ca functioneaza un algoritm de colectare a memoriei disponibile (garbage collection) care va recupera zonele de meorie corespunzatoare obiectelor care nu mai sunt accesibile.

Zona pentru memorarea metodelor

Aceasta zona contine codul corespunzator metodelor sub forma de cod pentru masina virtuala, tabele, etc..

Tabela de simboli

Fiecare clasa are asociata o tabela de simboli. Aceasta tabela contine numele tuturor campurilor, metodelor, etc.  Cand o clasa este adusa pentru prima data în memorie se determina doua campuri – nconstants si constant_info_constants_offset. Primul camp indica numarul de valori constante (maximum 32k). Al doilea camp indica pozitia fata de începutul clasei a tabelei de simboli.

Probleme de securitate

Executarea unui program adus din Internet dintr-o sursa neverificabila reprezinta un risc semnificativ. Sa presupunem de exemplu ca cineva implementeaza in Java un joc de mare succes. Dupa ce „vestea” se raspandeste, acelasiautor modifica codul si adauga o secventa care modifica toate fisierele de pe hard discul calculatorului pe care se executa. Mai ramane ca dupa aceasta operatie sa se afiseze un anunt referitor la costul programului care reface fisierele. Evident exista si alte exemple care ar putea sa produca cosmaruri – de exemplu un program care ar avea acces la fisierele de pe hard disc chiar numai pentru citire ar putea sa transmita prin Internet informatii corespunzatoare. Ca de exemplu identificarea unor copii pirat pentru software cu copyright. Sau un program care consuma resurse – aloca mii de obiecte, deschide mii de ferestre, etc.   Evident astfel de riscuri nu pot sa fie acceptat de catre nici unul dintre cautatorii în Internet. Corespunzator pentru ca solutia Java sa  fie luata în serios este necesar ca astfel de scenarii sa nu poata sa fie posibile.

Proiectantii Java au fost si ei constienti de astfel de pericole si au încercat sa prevada cateva nivele de protectie  pentru a asigura apararea împotriva unor atacuri care sa utilizeze programele Java.

protectii la nivelul limbajului :

  • programarea orientata pe obiecte realizeaza incapsularea datelor, astfel ca accesul este controlat, programatorul nu poate sa utilizeze pentru accesul la resurse decat metodele oferite de catre obiecte.  Pentru toate resursele sistemului pe care se executa programul accesul se face prin intermediul unor obiecte ale caror clase sunt descrise în biblioteci locale. Limbajul admite declararea unor variabile, clase sau metode ca „finale”. Adica, aplicatiile nu pot sa modifice valoarea unei variabile initializata ca finala, nu pot sa defineasca o clasa care sa mosteneasca metodele unei clase considerate finale obtinand în acest fel acces la operatiile interne, si nu pot sa înlocuiasca o metoda declarata finala.
  • limbajul a fost proiectat pentru a permite verificari complete de tipuri  (în compilare sau la executie) astfel încat greselile de programare sa nu poata sa reprezinte o sursa de risc. Se asigura compatibilitatea dintre tipurile considerate la compilare si cele utilizate la executie. Astfel, limbajul permite utilizarea de „cast-uri” numai între tipuri compatibile. În acest mod se evita situatia în care se forteaza accesul la un obiect utilizand metode definite pentru alt obiect.
  • valorile indicilor utilizati pentru accesul la elementele vectorilor sunt verificate în timpul executiei. Dupa ce un obiect de tip vector a fost creat,  lungimea sa nu se mai modifica. De asemenea sirurile de caractere sunt obiecte fixe. Utilizand clasa String se evita posibilitatea aparitiei de erori.
  • limbajul  nu admite utilizarea pointerilor, corespunzator nu este posibil sa se fabrice un pointer care sa acceseze o zona de memorie arbitrara. În acest mod nu se poate utiliza referinta la un obiect pentru a obtine accesul la metodele sale.
  • deorece nu exista pointeri  pentru Java se pot utiliza metode de „garbage collection”. În acest mod se evita o clasa importanta de erori,  si în acelasi timp se elimina posibilitatea urmatoarei secvente de operatii : – se creaza  un obiect de un tip definit de catre programator care are aceleasi dimensiuni cu un obiect  „de biblioteca”, se memoreaza valoarea pointerului respectiv, se distruge obiectul dupa care imediat se creaza un obiect de tipul  „de biblioteca „. Exista în acest  moment sanse importante ca metodele interne ale noului obiect sa devina accesibile utilizand pointerul memorat.
  • pentru a se evita situatia în care o clasa definita într-un program adus înlocuieste o clasa  „de biblioteca” sistemul garanteaza ca orice nume de clasa referit este întai cautat în biblioteca locala si numai daca nu este gasita se face cautarea în codul adus.

protectii la nivelul codului intermediar

  • încarcarea codului adus (în format de cod intermediar cod pentru masina virtuala) este precedata de o verificare a codului. Acest verificator  nu are cum sa stabileasca daca textul primit a fost generat de catre un compilator de Java si deci respecta conditiile anterioare sau  a fost creat de catre un program care permite obtinerea unui cod care sa încalce securitatea sistemului. Ca urmare,  încearca sa  faca verificari cat mai  complete. Astfel se verifica faptul ca nu exista pointeri, ca nu se violeza protectiile de acces, ca nu se încearca sa se utilizeze metodele într-un mod incorect , ca se respecta tipurile pentru toate constructiile, ca nici o variabila nu este utilizata înainte de a fi initializata, ca nu pot sa apara erori de tip „overflow ” sau „underflow ” pentru stiva, etc. Se refac o serie de  verificari facute de catre compilator. Codul intermediar pentru Java a fost proiectat astfel încat sa asigure tratarea diferita a tipurilor diferite. Astfel exista o instructine aload si o instructiune iload, utilizate respectiv pentru încarcarea unui pointer respectiv a unui întreg. Desi la nivelul codului nativ cele doua instructiuni probabil se vor executa la fel, nu se poate executa o operatie de tip aload asupra unei valori întregi. Operandul  pentru o instructiune ca getfield (instructiune care aduce în varful stivei valoarea unui camp dintr-un obiect) este o adresa în tabela de simboli asociata clasei. pe baza acestei adrese se va determina si tipul campului.
  • instructiunile matinii virtuale au fost alese astfel încat în orice moment tipul elementului din varful stivei sa fie cunoscut.  Adica dandu-se tipurile valorilor aflate în stiva înainte de executia unei instructiuni, în functie de instructiune rezulta în mod unic tipul valorilor dupa executia instructiunii. De exemplu pentru o secventa ca :

iload_1           se încarca valoarea unei variabile intregi

iconst 5  se incarca valoarea constanta 5

iadd      se executa suma a doua valori intregi

dupa executia secventei varful stivei va fii de tip întreg. Nu pentru orice masina virtuala

conditia anterioara este satisfacuta. De exemplu pentru masina virtuala PostScript operatia de adunare a doua valori întregi  va produce un rezulztat de tip întreg numai daca rezultatul este o valoare în domeniul numerelor întregi. Altfel rezultatul este o valoare reala. Autorii Java au impus însa  restrictia referitoare la posibilitatea de a determina în mod unic tipul unui rezultat pentru a facilita verificarile stricte de tip. O alta conditie asigurata de catre compilator este ca  daca într-un punct al executiei unui program se poate ajunge pe mai multe cai tipurile valorilor continute în stiva sunt aceleasi. Din nou aceasta conditie simplifica verificarile de tip.

protectii la nivelul claselor

  • un fir de executie (thread) care se executa poate sa fie interpretat ca o colectie de clase împartite în spatii de nume. Toate clasele încarcate din mediul local vor face parte dintr-un singur spatiu de nume. Pentru fiecare sursa din retea se creaza cate un spatiu de nume distinct. Corespunzator nu poate sa existe interferente între clasele provenite din surse diferite.
  • mediul de lucru Java contine un numar important de biblioteci de clase corespunzatoare accesului la resurse ca:  sistemul de fisiere, retea, ecran, tastatura, etc. Se presupune ca aceste clase sunt sigure. Ceea ce mai ramane de vazut.
  • încarcarea claselor se face de catre ClassLoader. Deoarece clasele se încarca dinamic pe masura ce se executa programul exista pericolul ca un program sa îbcerce sa încarce o clasa care sa înlocuiasca o clasa sigura de sistem cu una proprie. Pentru a evita asa ceva pentru încarcarea unei noi clase se face întai o cautare între clasele locale si numai daca nu se gaseste o clasa cu numele cautat se va cauta între clasele programului adus. De asemenea datorita separarii spatiilor de nume o clasa locala nu poate sa acceseze din gresala o clasa importata. Un astfel de acces se poate face numai explicit. Pentru fiecare spatiu de nume se poate determina care este server-ul din care provin clasele respective. În acest mod se poate face diferenta între serverele „de încredere” si celelalte servere.

protectii la nivelul executiei programelor

  • mediul Java contine si o clasa globala numita SecurityManager. Metodele acestui obiect sunt utilizate pentru a realiza verificari în timpul executiei.  Definind o subclasa a acestei clase se pot implementa diferite politici de protectie. Utilizarea metodelor oferite de catre SecurityManager se face în implementarea metodelor publice care acceseaza resursele critice. Pentru accesul la fisiere exista o lista care controleaza drepturile de citire / scriere. Initial aceste liste sunt goale. În mod explicit prin configurarea interpretorului pentru cod intermediar se pot introduce nume de directoare sau fisiere ce pot sa fie citite sau / si scrise.  De exemplu:

public boolean mkdir(String path) throws IOException {

SecurityManager security = System.getSecurityManager();

if (security != null) {

security.checkWrite(path);

}

return mkdir0();

}

Se observa ca metoda publica mkdir va verifica întai ca exista dreptul de scriere pe

calea transmisa ca argument. Numai daca nu se semnaleaza o eroare (o exceptie) se       va executa metoda privata. Desigur, faptul ca în principiu exista un mecanism pentru a se obtine acces la fisierele sistemului pe care se executa un program constituie un pericol în sine. Este relativ usor ca un program adus din retea sa convinga un utilizator mai putin avizat sa îi ofere acces la toate resursele locale.

  • programele Java nu au acces la informatii din sistem ca de exemplu – directorul în care este instalat mediul Java, numele utilizatorului, numele diectorului standard pentru utilizator, etc. Obtinerea informatiilor la care programula are acces – ca de exemplu tipul sistemului de operare, versiunea de Java care se utilizeaza, etc. se face apeland  metode locale ca de exemplu :

String s = System.getProperty(„os.name”);

Deoarece programatorul poate sa ofere programilui care se executa acces si la alte

informatii si în acest caz exista posibilitatea ca utilzatorul sa fie pacalit

  • programele Java pot sa stabileasca o conexiune prin retea numai cu server-ul de origine. Orice încercare cu o alta  adresa va fi tratata ca aparitia unei exceptii.
  • un program Java adus prin retea nu poate sa porneasca un alt program (nu poate sa execute o instructiune fork sau exec), nu pot sa termine executia interpretorului pentru masina virtuala.

protectii la nivelul programelor de cautare (browser)

Nivelele anterioare asigura o sansa pentru a asigura securitatea sistemelor pe care se executa programele (applet-urile) preluate din retea. Dar programul care realizeaza cautarea si aducerea din Internet implementeaza prin utilizarea ClassLoader-ului si a SecurityManager-ului  politica de protectie. De fapt programul include un interpretor de Java, o biblioteca de clase care contine si clasele pentru ClassLoader si SecurityManager. O parte din clasele pentru SecurityManager sunt proiectate o data cu browser-ul pentru ca depind de sistemul pe care se executa. În consecinta reprezinta o componenta foarte delicata.

Evident problemele de securitate legate de Java nu au fost epuizate. Suntem de abia la început. Chiar daca proiectarea ar fi corecta este  foarte posibil  sa mai existe numeroase erori în cod. Pe de alta parte se pot adauga la Java tehnicile de semnatura digitala pe baza carora sa se confirme ca un program provine dintr-o sursa sigura.  Pe masura ce tehnicile legate de Java se vor raspandi programele care se executa vor avea nevoie de tot mai multe resurse. Evident, în acest mod pericolele referitoare la securitate vor creste.

Limbajul Java. Specificatii

Una dintre principalele caracteristici ale limbajului java consta din faptul ca nu exista nici o parte care sa depinda de implementare. Se considera ca toate aspectele sunt definite în specifcatii. În acest mod prin respectarea specificatiilor orice implementare este portabila si orice program scris în Java poate sa fie execuat în orice mediu Java.

Alfabetul Java este Unicode,  un super set pentru setul ASCII care utilizeaza doi octeti pentru reprezentarea fiecarui caracter. Corespunzator se pot reprezenta 32768 de caractere. De obicei sursa programelor se reprezinta în ASCII sI numai pentru caracterele care nu fac parte din setul ASCII se utilizeaza o notatie de forma \un, unde  n reprezinta o valoare reprezentata în baza 16 reprezentand valoarea codului respectiv.

Setul de cuvinte rezervate este format din civintele rezervate ale limbajului C++ la care se adauga  cateva cuvinte noi:

abstract do import protected throws
boolean double instanceof public transient
break else int return try
bzte extends interface short void
case final long static volatile
catch finally native super while
char float new switch
class for null synchronized
continue if package this
default implements private throw

true si false reprezinta constante booleene.

Identificatorii sunt secvente de lungime nelimitata de caractere Unicode. Primul caracter trebuie sa fie o litera.

Constante

În Java exista constante numerice (întregi, reale), booleene, caracter si sir de caractere. Constantele de tip întreg pot sa fie reprezentate utilizand baza zece, opt sau sasesprezece.  Reprezentarea unei constante în baza zece începe cu una dintre cifrele [1-9],  reprezentarea unei constante în baza opt începe cu 0, iar reprezentarea unei constante în baza sasesperzece începe cu 0x. O constanta  de tip întreg este considerata de tip int daca nu este urmata de litera L. De exemplu:

0  012 12 0x12 0xffffffff 037777777777

reprezinta valorile în baza zece: 0, 10, 12, 18, -1, -1   reprezentate utilizand  4 octeti iar:

0L  012L  12L 0x12L

reprezinta valorile în baza zece: 0, 10, 12, 18 reprezentate însa utilizand 8 octeti.

Constantele de tip real sunt considerate ca find reprezentate ca valori de tip double daca nu sunt urmate de litera f sau F. Pentru reprezentarea constantelor reale se poate utiliza notasia cu exponent. De exemplu:

1.1f 1. 1.1e12f 1.e-9

Constantele boolene se reprezinta ca: true sI false. Constantele caracter se reprezinta ca un caracter  între ghilimele simple sau ca o notatie  de tip ‘\u‘.  De exemplu:

‘a’ \t’ ‘\\’ ‘\u15e’

Constantele sir de caractere se reprezinta ca siruri între ghilimele duble. De exemplu:

” acesta este un sir”     „\””

Cel de al doilea sir reprezinta un tir care contine un singur caracter: „.

operatori

Operatorii utilizati în Java sunt practic cei din C sau C++:

= > < ! ~ :
== <= >= != && || ++
+ * / & | ^ % << >> >>>
+= -= *= /= &= |= ^= %= <<= >>= >>>=

Tipuri  si valori

Se considera ca exista patru categorii de tipuri de date: primitive, clase, interfete, vectori. Pentru fiecare variabila în timpul compilarii se stabileste categoria careia îi apartine.

Variabilele declarate de un tip primitiv pot sa îsi modifice valorile numai prin executia unor operatii directe asupra lor. Pentru celelalte categorii, variabilele contin referinte la valori, este posibil sa existe mai multe referinte pentru aceasi valoare si corespunzator este posibil ca prin modificarea valorii referite de catre o variabila, efectul sa poata sa fie observat prin intermediul altei variabile.

tipuri primitive

În Java tipurile primitive sunt:

  • aritmetice:
    • întregi : byte ( un octet), short (doi octeti), int (4 octeti), long (8 octeti)
    • reale: float (patru octeti), double (opt octeti)
    • caracter: char (doi octeti)
    • logic : boolean (1 bit)

Orice valoare de tip întreg sau real poate sa fie convertita la orice tip aritmetic. Se pot face conversii între tipurile char si întregi sau reali. Modul de executie a operatiilor aritmetice este cel obisnuit. În cazul împartiri cu zero pentru întregi va apare o excepîie de tip ArithmeticException. În cazul  valori reale exista o valoare speciala NaN (Not-a-Number) care reprezinta rezultatul unei îmartiri cu zero. Majoritatea operatilor care au un operand NaN vor avea un rezultat de tip NaN. Compararea unei valori NaN cu orice valoare produce un rezultat false.

Asupra tipurilor primitive se pot realiza operatii de conversie prin : „casting”, atribuiri sau apeluri de metoda. Conversiile pot sa aiba loc prin largire (fara pierdere de informatie, cel mult la conversia de la long la double se poate pierde precizie) între urmatoarele tipuri:

byte short int long float double
byte x x x x x
short x x x x
char x x x x
int x x x
long x x
float x

sau prin îngustare (în acest caz se poate pierde si informatie si precizie).

byte short char int long float double
byte x
short x x
char x x
int x x x
long x x x x
float x x x x x
double x x x x x x

Cu toate ca pentru conversia între tipurile primitive pot sa apara depasiri, nu se semnaleaza exceptii.

( adunate de pe net )

Eticheta:

Comentariile sunt inchise

Ne pare rau, dar nu poti lasa un comentariu pentru acest post.

Resurse web design